Skip to content

PiPLC WebSocket Protocol

The PiPLC editor and engine communicate over WebSocket using two frame types: JSON text frames for control messages and binary frames for memory snapshots.

Implementation: src/protocol/PlcProtocol.h / PlcProtocol.cpp (built as the piplc-protocol static library).

JSON Control Messages

All request/reply messages carry a seq number for correlation.

Category Messages
Handshake hellohello.reply (protocol version, capabilities, memory layout)
Engine control engine.start/stop/pause/resume/stepScan/stepRung/reset.reply
Program program.download (sends .plcproj XML as string), program.upload, program.clear
Memory force memory.forceBit, memory.unforceBit, memory.clearAllForces
Memory stream memory.subscribe (intervalMs) / memory.unsubscribe
Breakpoints breakpoint.set/remove/clearAll
I/O config ioconfig.load (sends IOProviderConfig JSON)
Queries engine.state, engine.statistics, engine.info
Signal routing route.sync (broadcasts full route table to server)
Context mgmt context.create, context.remove, context.list
Events (server→client) event.stateChanged, event.fault, event.breakpointHit, event.scanStats, event.ping

Message Examples

Handshake

// Client -> Server
{
    "type": "hello",
    "seq": 1,
    "protocolVersion": 1,
    "clientName": "PiPLC Editor",
    "clientVersion": "0.2.0"
}

// Server -> Client
{
    "type": "hello.reply",
    "seq": 1,
    "protocolVersion": 1,
    "serverName": "PiPLC Engine",
    "serverVersion": "0.2.0",
    "engineId": "uuid-string",
    "capabilities": ["gpio", "analog"],
    "memoryLayout": {
        "inputWords": 8,
        "outputWords": 8,
        "bitWords": 64,
        "integerWords": 256,
        "timerCount": 64,
        "counterCount": 64
    }
}

Engine Control

{"type": "engine.start", "seq": 2}
// Reply:
{"type": "engine.start.reply", "seq": 2, "success": true, "state": "Running"}

Program Download

{
    "type": "program.download",
    "seq": 10,
    "format": "plcproj-xml",
    "data": "<?xml version=\"1.0\" ...>...</PLCProject>"
}

Memory Force

{"type": "memory.forceBit", "seq": 20, "address": "I:0/3", "value": true}

Memory Stream Subscription

{"type": "memory.subscribe", "seq": 30, "intervalMs": 50}

Events (Server → Client, unsolicited)

{"type": "event.stateChanged", "state": "Running"}
{"type": "event.fault", "message": "Division by zero at rung 3"}
{"type": "event.breakpointHit", "rungIndex": 3}

Binary Memory Snapshot Format

Binary WebSocket frames for efficient memory monitoring (sent at the subscribed interval, default 20 Hz):

Offset  Size    Field
0       4       Magic: 0x504D454D ("PMEM")
4       4       Sequence (uint32 LE)
8       4       Timestamp ms (uint32 LE)
12      1       Engine state enum
13      3       Reserved
16      32      Input words (8 × uint32 LE)
48      32      Output words (8 × uint32 LE)
80      256     Bit words (64 × uint32 LE)
336     1024    Integers (256 × int32 LE)
1360    512     Timers (64 × 8 bytes: PRE:i16 ACC:i16 flags:u8 pad:3)
1872    512     Counters (64 × 8 bytes: PRE:i16 ACC:i16 flags:u8 pad:3)
2384    320     Force bitmap (80 words × uint32)
─────────────────
Total: 2704 bytes (~54 KB/s at 20Hz)

Timer Data Layout (8 bytes per timer)

Offset Size Field
0 2 PRE (int16 LE)
2 2 ACC (int16 LE)
4 1 Flags: bit 0 = EN, bit 1 = TT, bit 2 = DN
5 3 Reserved

Counter Data Layout (8 bytes per counter)

Offset Size Field
0 2 PRE (int16 LE)
2 2 ACC (int16 LE)
4 1 Flags: bit 0 = DN, bit 1 = OV, bit 2 = UN, bit 3 = CU, bit 4 = CD
5 3 Reserved

Force Bitmap

80 words (2560 bits) covering all addressable memory locations. A set bit indicates the corresponding address is forced. The bitmap layout mirrors the memory region order: inputs, outputs, bits, integers, timers, counters.

Connection Lifecycle

Client                              Server
  |                                    |
  |--- WebSocket connect ------------->|
  |--- hello ------------------------->|
  |<-- hello.reply --------------------|
  |--- memory.subscribe (50ms) ------->|
  |<-- binary snapshot (periodic) -----|
  |--- engine.start ------------------>|
  |<-- engine.start.reply -------------|
  |<-- event.stateChanged "Running" ---|
  |                                    |
  |  ... (runtime operations) ...      |
  |                                    |
  |<-- event.ping ---------------------|  (every 30s)
  |--- pong ------------------------->>|
  |                                    |
  |--- WebSocket close --------------->|

Auto-Reconnect Behavior

The RemoteEngineContext handles disconnections automatically:

  1. On disconnect: starts a reconnect timer (3s interval)
  2. On reconnect: re-handshakes, re-subscribes to memory stream, re-downloads program if previously loaded
  3. Pending replies timeout after 5s
  4. Connection state changes are emitted for UI updates (green/yellow/red indicators)

See Also