gomc-rest is a small REST API server and HTTP gateway for Mitsubishi Electric PLCs using MC protocol 3E or 4E frames. It sits between HTTP clients and a PLC, brokering reads and writes for device strings such as D100.0, W100, and M0 while returning JSON values with automatic conversion: word devices become integers and bit devices become booleans.
The PLC transport is provided by gomcprotocol. The server uses only the Go standard library for HTTP handling.
A Python client library with no extra dependencies is also available: gomc-rest-client on PyPI
A lightweight debugging GUI is also available: gomc-rest-gui — a standalone HTTP client (Wails + React) for exercising gomc-rest's endpoints from a simple screen instead of curl.
- Read word and bit devices through a simple
/readendpoint. - Write integer arrays or boolean arrays through
/write. - Run, stop, pause, latch-clear, and reset the PLC remotely.
- Configure by command-line flags, with environment variables as defaults.
- Start in read-only mode to block write and remote-control endpoints.
- Serialize PLC communication through a single in-process worker queue.
- Keep a simple health endpoint that reports the current connection state.
- Retry the PLC connection on demand when startup connection fails or a previous connection was cleared.
This server is intended only for FA local networks, such as an isolated factory LAN, a trusted machine network, or localhost access from an operator PC. The API can read, write, run, stop, pause, latch-clear, and reset a PLC, and it does not provide authentication, authorization, TLS, or access control.
Recommended deployment:
- Run it inside an isolated FA network or on
localhostonly. - Restrict access with network segmentation, firewall rules, or host-level controls.
- Use
-listen 127.0.0.1:8080when only local access is required. - Do not place it behind a public reverse proxy or expose it through port forwarding.
Download the latest gomc-rest.exe from the Releases page.
Published releases provide the Windows binary as gomc-rest.exe. Source builds use the output name from the build command below.
Create start-gomc-rest.bat in the same folder as gomc-rest.exe and edit the values at the top to match your environment:
@echo off
REM ============================================================
REM Edit these values to match your environment
REM ============================================================
set PLC_HOST=192.168.0.1
set PLC_PORT=5007
set LISTEN_PORT=8080
REM ============================================================
gomc-rest.exe -host %PLC_HOST% -port %PLC_PORT% -listen %LISTEN_PORT%
pauseDouble-click the batch file to start the server. To stop it, press Ctrl+C or close the window. The pause line keeps the window open after the server exits so you can read any error messages.
Open a browser and go to:
http://localhost:8080/health
You should see:
{"plc_status":"ok","connected":true}If the PLC is not reachable yet, connected will be false. The server still starts and will retry the connection on the first PLC operation (/read, /write, or /remote/*).
To block all write and remote-control operations (for monitoring only):
@echo off
set PLC_HOST=192.168.0.1
set PLC_PORT=5007
set LISTEN_PORT=8080
gomc-rest.exe -host %PLC_HOST% -port %PLC_PORT% -listen %LISTEN_PORT% -readonly
pause@echo off
set PLC_HOST=192.168.0.1
set PLC_PORT=5007
set LISTEN_PORT=8080
set LOG_FILE=C:\gomc-rest.log
gomc-rest.exe -host %PLC_HOST% -port %PLC_PORT% -listen %LISTEN_PORT% -log-file %LOG_FILE%
pauseLogs are written to both the console and the file. The directory must already exist; the server does not create missing parent directories.
Remote-control endpoints (/remote/run, /remote/stop, etc.) are disabled by default. Add -enable-remote to turn them on:
gomc-rest.exe -host %PLC_HOST% -port %PLC_PORT% -listen %LISTEN_PORT% -enable-remote| Symptom | Likely cause | Fix |
|---|---|---|
{"connected":false} on /health |
PLC is off or IP/port is wrong | Check PLC_HOST and PLC_PORT in the batch file |
403 forbidden on /write |
Server started with -readonly |
Remove -readonly from the batch file |
403 forbidden on /remote/* |
-enable-remote is not set |
Add -enable-remote to the batch file |
503 busy |
Too many simultaneous requests | Increase -queue-size (default: 32) |
| Port already in use | Another process is using the port | Change LISTEN_PORT to a free port |
| Window closes immediately | Startup error | Run from Command Prompt to see the error message |
For the Windows release binary:
.\gomc-rest.exe -host 192.168.0.1 -port 5007 -mode binary -listen :8080For source builds or non-Windows environments:
./gomc-rest -host 192.168.0.1 -port 5007 -frame 3e -transport tcp -mode binary -queue-size 32 -timeout 5s -listen :8080For read-only operation, add -readonly. In read-only mode, /health and /read remain available, while POST operations on /write and /remote/* return 403 forbidden.
.\gomc-rest.exe -host 192.168.0.1 -port 5007 -mode binary -listen 127.0.0.1:8080 -readonlyOn startup, the server attempts to connect to the PLC. If the PLC is not reachable, startup continues and the server retries on the first PLC request.
git clone https://github.com/moge800/gomc-rest
cd gomc-rest
go build -o gomc-rest .Flags take priority. Environment variables provide the default values for those flags.
| Flag | Environment variable | Default | Notes |
|---|---|---|---|
-host |
GOMCR_HOST |
192.168.0.1 |
PLC host or IP address |
-port |
GOMCR_PORT |
5007 |
PLC port, 1 to 65535 |
-frame |
GOMCR_FRAME |
3e |
MC protocol frame, 3e or 4e |
-transport |
GOMCR_TRANSPORT |
tcp |
tcp or udp; 4e supports tcp only |
-mode |
GOMCR_MODE |
binary |
binary or ascii |
-queue-size |
GOMCR_QUEUE_SIZE |
32 |
Number of PLC requests that can wait while one request is active |
-timeout |
GOMCR_TIMEOUT |
5s |
PLC connect and I/O timeout |
-listen |
GOMCR_LISTEN |
:8080 |
HTTP listen address |
-readonly |
GOMCR_READONLY |
false |
Set to true to reject POST operations on /write and /remote/* |
-enable-remote |
GOMCR_ENABLE_REMOTE |
false |
Set to true to enable remote-control endpoints (/remote/*) |
-log-file |
GOMCR_LOG_FILE |
(none) | Path to log file; if set, logs are written to both the file and stderr |
-log-level |
GOMCR_LOG_LEVEL |
info |
Terminal log level: debug, info, warn, or error |
-log-file-level |
GOMCR_LOG_FILE_LEVEL |
warn |
File log level: debug, info, warn, or error; only used with -log-file |
All successful write and remote-control operations return:
{"ok":true}| Method | Path | Parameters / body | Response |
|---|---|---|---|
GET |
/openapi.yaml |
none | OpenAPI 3.1 specification (YAML) |
GET |
/version |
none | {"version":"v0.9.0"} or {"version":"dev"} for local builds |
GET |
/info |
none | {"version":"v0.9.0","gomcprotocol_version":"v0.3.0","host":"192.168.0.1","port":5007,"frame":"3e","transport":"tcp","mode":"binary","listen_addrs":["192.168.1.10:8080"],"readonly":false,"enable_remote":false} |
GET |
/metrics |
none | {"client_request_count":0,"busy_count":0,"client_avg_latency_ms":0,"client_recent_avg_latency_ms":0,"queue_length":0,"request_count":0,"reconnect_count":0,"timeout_count":0,"plc_error_count":0,"avg_latency_ms":0,"recent_avg_latency_ms":0} |
GET |
/health |
none | {"plc_status":"ok","connected":true} or {"plc_status":"disconnected","connected":false} |
GET |
/read |
query: addr required, count optional and defaults to 1, dword optional and defaults to false, sint optional and defaults to false |
{"values":[100,200]} or {"values":[true,false]} |
POST |
/write |
query: addr required, dword optional and defaults to false, sint optional and defaults to false; body: {"values":[1,2,3]} or {"values":[true,false]} |
{"ok":true} |
POST |
/random-read |
body: {"words":["D100","D200"],"dwords":["D300"]} |
{"words":[100,200],"dwords":[300]} |
POST |
/random-write |
body: {"words":[{"addr":"D100","value":1}],"dwords":[{"addr":"D300","value":65536}],"bits":[{"addr":"M0","value":true}]} |
{"ok":true} |
POST |
/remote/run |
requires -enable-remote; query: clear=0/1/2 optional, force=true/false optional |
{"ok":true} |
POST |
/remote/stop |
requires -enable-remote; none |
{"ok":true} |
POST |
/remote/pause |
requires -enable-remote; query: force=true/false optional |
{"ok":true} |
POST |
/remote/latch-clear |
requires -enable-remote; none |
{"ok":true} |
POST |
/remote/reset |
requires -enable-remote; none |
{"ok":true} |
Notes:
countmust be between1and1024.valuesmust be present and contain between1and1024items.- The
/writerequest body must be 1 MiB or smaller. - Word devices require integer values in the range
0..65535. Bit devices require boolean values. - When
dword=true, each value is an unsigned 32-bit integer in the range0..4294967295. The low 16 bits are stored in the register ataddrand the high 16 bits in the next register (addr+1). Only word devices supportdword=true. Withdword=true,countmust be512or less andvaluesmust contain512items or less (so that the actual word count sent to the PLC does not exceed1024). - When
sint=true, values are interpreted as signed integers. For word devices the range is-32768..32767; fordword=truethe range is-2147483648..2147483647. Only word devices supportsint=true. The PLC register bits are unchanged —sintonly affects how values are converted between JSON and the 16-bit register representation. /random-readacceptswordsanddwordsarrays of device address strings. Both default to empty; at least one must be non-empty. Only word devices (D,W,R, etc.) are allowed. Returns{"words":[...],"dwords":[...]}where word values are integers (0..65535) and dword values are unsigned 32-bit integers (0..4294967295)./random-writeacceptswords,dwords, andbitsarrays. Each entry is{"addr":"...","value":...}.wordsanddwordsrequire word devices;bitsrequires bit devices. Internally callsRandomWritefor words/dwords andRandomWriteBitsfor bits within one serialized job. Maximum 255 entries per array./remote/*endpoints are disabled by default and return403 forbiddenunless the server is started with-enable-remote. This is separate from read-only mode.- When read-only mode is enabled, POST operations on
/writeand/remote/*return403 forbidden. Read-only mode is a safety aid, not a replacement for network isolation, authentication, authorization, firewall rules, or PLC-side protection. - Boolean query flags (
dword,sint,force) accepttrueorfalse(case-insensitive, exactly once). Any other value (including empty or duplicated) returns400 bad_request. All endpoints reject unknown query parameters with400 bad_request(when the request passes earlier checks such as method, read-only, and remote-enabled validation). GET /healthalways returns HTTP200, even when the PLC is disconnected./remote/resetclears the TCP connection because the PLC closes it after reset./metricsPLC fields (request_count,avg_latency_ms,recent_avg_latency_ms) measure only the PLC wire time. Client fields (client_request_count,client_avg_latency_ms,client_recent_avg_latency_ms) measure the full round-trip including queue wait time.busy_countcounts requests rejected because the queue was full; these are excluded from the client latency averages.timeout_countcounts requests that returnedcontext.DeadlineExceeded(timed out before, during, or after queuing).
Device addresses are case-insensitive and may include surrounding whitespace. The device prefix determines whether the API reads or writes words or bits.
| Type | Devices | JSON value type | Examples |
|---|---|---|---|
| Word | D, W, R, ZR, TN, STN, CN, Z, SW, SD |
integer | D100, ZR512, TN10, CN5 |
| Bit | X, Y, M, L, B, F, V, SB, SM, S, DX, DY, TC, TS, STC, STS, CC, CS |
boolean | M0, TC10, CC5, STC3 |
Timer and counter contacts and coils use two-letter prefixes: TC (timer contact), TS (timer coil), CC (counter contact), CS (counter coil). The single-letter forms T and C are not valid device names and return 400 bad_request.
The numeric address must be a non-negative integer. Unknown devices, missing numbers, non-numeric numbers, and negative numbers return 400 bad_request. The devices X, Y, B, SB, W, SW, ZR, DX, and DY use hexadecimal address numbers (e.g. X4F, Y12D2, W1D).
Append .N (single hex digit, 0–F) to a word device address to read or write a single bit within the 16-bit register.
D3500.0 ← bit 0 (LSB) of D3500
D3500.F ← bit 15 (MSB) of D3500
W1D.7 ← bit 7 of W1D (hex address 0x1D)
- Read returns
{"values": [true]}or{"values": [false]}. - Write body must be
{"values": [true]}or{"values": [false]}(exactly one element). The server performs a read-modify-write internally. dword=trueorsint=truecombined with bit access returns400 bad_request.countmust be 1;count=2or higher returns400 bad_request.- Appending
.Nto a bit device (e.g.M0.0) returns400 bad_request.
Errors are returned as JSON. Every error response includes both the HTTP status code and a machine-readable code in the body.
| Scenario | HTTP | code |
Example |
|---|---|---|---|
| Invalid parameter, body, address, count, or method | 400 or 405 |
bad_request |
{"status":400,"error":"addr is required","code":"bad_request"} |
/remote/* endpoint called without -enable-remote |
403 |
forbidden |
{"status":403,"error":"remote-control operations are disabled (use -enable-remote to enable)","code":"forbidden"} |
| Operation rejected by read-only mode | 403 |
forbidden |
{"status":403,"error":"operation not allowed in read-only mode","code":"forbidden"} |
/write body is too large |
413 |
bad_request |
{"status":413,"error":"body must not be larger than 1048576 bytes","code":"bad_request"} |
| PLC MC protocol error with an end code | 502 |
plc_error |
{"status":502,"error":"MC error 0x4000","code":"plc_error","end_code":"0x4000"} |
| PLC connection error | 503 |
connection_error |
{"status":503,"error":"connect: refused","code":"connection_error"} |
| PLC communication queue is full | 503 |
busy |
{"status":503,"error":"PLC communication queue is full","code":"busy"} |
| PLC communication queue is closed during shutdown | 503 |
queue_closed |
{"status":503,"error":"PLC communication queue is closed","code":"queue_closed"} |
| HTTP request context was canceled before completion | 499 |
request_canceled |
{"status":499,"error":"request canceled","code":"request_canceled"} |
| HTTP request context deadline expired | 504 |
request_timeout |
{"status":504,"error":"request timed out","code":"request_timeout"} |
- PLC requests are serialized through one shared in-process worker queue and one client connection.
- The worker executes one PLC request at a time. HTTP handlers do not call the PLC client directly.
-queue-sizecontrols how many PLC requests can wait while one request is active. When the queue is full, the server immediately returns503 busywithRetry-After: 1.-timeoutcontrols the PLC connect and I/O deadline. It does not cancel HTTP request contexts that are already disconnected; queued requests are skipped before execution if their request context is canceled.- If initial connection fails, the HTTP server still starts.
- If there is no active connection, the next PLC request attempts to reconnect.
- Connection-level MC protocol errors clear the connection so a later request can reconnect.
The /health endpoint can be used without a PLC. The other examples require a reachable PLC.
curl http://localhost:8080/health
curl "http://localhost:8080/read?addr=D100&count=3"
curl "http://localhost:8080/read?addr=M0&count=4"
curl "http://localhost:8080/read?addr=D100&count=2&dword=true"
curl "http://localhost:8080/read?addr=D100&count=3&sint=true"
curl -X POST "http://localhost:8080/write?addr=D100" \
-H "Content-Type: application/json" \
-d '{"values":[10,20,30]}'
curl -X POST "http://localhost:8080/write?addr=D100&dword=true" \
-H "Content-Type: application/json" \
-d '{"values":[100000,200000]}'
curl -X POST "http://localhost:8080/write?addr=D100&sint=true" \
-H "Content-Type: application/json" \
-d '{"values":[-1,-32768,32767]}'
curl -X POST "http://localhost:8080/write?addr=M0" \
-H "Content-Type: application/json" \
-d '{"values":[true,false]}'
# Random read/write — multiple non-contiguous addresses in one request
curl -X POST "http://localhost:8080/random-read" \
-H "Content-Type: application/json" \
-d '{"words":["D100","D200"],"dwords":["D300"]}'
curl -X POST "http://localhost:8080/random-write" \
-H "Content-Type: application/json" \
-d '{"words":[{"addr":"D100","value":10},{"addr":"D200","value":20}],"bits":[{"addr":"M0","value":true}]}'
# Remote-control endpoints require -enable-remote at startup
curl -X POST "http://localhost:8080/remote/run?clear=0&force=false"
curl -X POST "http://localhost:8080/remote/stop"
curl -X POST "http://localhost:8080/remote/pause?force=false"
curl -X POST "http://localhost:8080/remote/latch-clear"
curl -X POST "http://localhost:8080/remote/reset"