SXapi / WebSocket
Experimental: The SXapi / WebSocket is a new interface that is still under
active development. The protocol and method set may change in future releases.
It is available in prerelease SWTools emGUI 4.0.
TLDR: The SXapi / WebSocket is a modern, full-duplex replacement for the SXapi / Server (HTTP REST) interface. It lets programs running on your PC or in a web browser talk to a connected siliXcon device over a single, persistent JSON-RPC 2.0 connection, with real-time server push.
The SXapi / WebSocket is provided by emGUI and uses JSON-RPC
2.0 over a WebSocket. It exposes the
same device operations as the older SXapi / Server (HTTP
REST) — search nodes, read/write variables,
execute commands, open dialogs — but over one connection that also carries
asynchronous events (task and node state changes).
Default endpoint: ws://localhost:29045
The HTTP REST server (default port 28945) and the WebSocket server (29045)
can run at the same time inside emGUI, so existing REST clients keep
working while new clients migrate to the WebSocket API.
Why a new API?
The HTTP REST + SSE server works well for atomic, request/response automation, but it has structural limits that the WebSocket API is designed to solve:
- One persistent, full-duplex connection. REST opens (or keeps alive) a socket per burst of requests and has no clean way to push data. The WebSocket stays open and carries requests, responses and server-initiated events on the same channel — ideal for high-frequency polling such as siliWatch.
- First-class real-time events. REST needed a separate long-lived
/eventsSSE endpoint for asynchronous task output. The WebSocket deliverstask.stateChangedandnode.stateChangedpush notifications over the very same connection — no second stream to manage. - Standard framing. JSON-RPC 2.0 gives request/response correlation via the
idfield, structurederrorobjects with numeric codes, and a well-defined notification shape. REST relied on ad-hoc URL query strings and a bespokeresultfield. - Safer node handles. REST returned the device node's raw pointer value as a hex string and accepted it back verbatim. The WebSocket instead hands out opaque integer handles from a per-connection registry, which are validated on every call and never expose internal addresses.
- Per-connection isolation. Each WebSocket connection runs on its own worker thread with its own handle registry; a blocking I/O operation on one client does not stall the others, and all state is cleaned up on disconnect.
- Namespaced, versioned protocol. Methods are grouped by scope
(
system.*,ui.*,node.*,task.*) and the wire protocol carries its own version (protocol) that evolves independently of the application version.
Protocol overview
All messages are JSON objects with a "jsonrpc":"2.0" field.
Request (client → server):
{ "jsonrpc": "2.0", "id": 1, "method": "node.readVariable", "params": { "handle": 1, "path": "/driver/temp" } }
Response (server → client):
{ "jsonrpc": "2.0", "id": 1, "result": { "result": 0, "value": "40.5640" } }
Error (server → client):
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Unknown handle: 7" } }
Notification (server → client, no id):
{ "jsonrpc": "2.0", "method": "task.stateChanged", "params": { "pid": "6", "state": 2 } }
The inner result object usually carries a device-level result integer where
0 means success and a negative value is an error code — this is distinct
from the JSON-RPC transport-level error object, which is only present on
protocol/validation failures.
Method namespaces
| Prefix | Scope |
|---|---|
system.* | Host/meta operations, no node context required. |
ui.* | Standalone GUI operations, no node context required. |
node.* | Per-node operations (require a handle) and node-state events. |
task.* | Asynchronous task lifecycle notifications (server → client only). |
Method reference
system.getVersion
Returns version and info about the server.
> {"jsonrpc":"2.0", "id":1, "method":"system.getVersion", "params":{}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":0, "protocol":1, "vendor":"siliXcon", "hostapp":"emGUI", "version":"4.0.0"}}
protocol— SXapi WebSocket protocol version (increments on any breaking change).version— application (SWTools) version.
ui.openDialog
Show a siliXcon tool with no node context.
| Param | Type | Notes |
|---|---|---|
command | string | Optional. Empty ⇒ show emGUI; or "{term}" / "{scope}" followed by optional tool arguments. |
node.searchNodes
Perform a search and wait for the result. Returns the number of nodes found.
| Param | Type | Notes |
|---|---|---|
flags | int | string[] | Optional. Integer bitmask, or an array of flag names. Default 0x7F. |
Flag names accepted in the array form:
| Name | Meaning |
|---|---|
dummy | Dummy search — ignores all other flags; returns the count of the current tree nodes. |
multiple | SF_MULTI |
sequential | SF_SEQ |
heartbeat | SF_ENHB |
auth | SF_AUTH |
fetch | SF_FETCH |
pull | SF_PULL |
recurse | SF_RECURSE |
reclaim | SF_RECLAIM |
Default flags are multiple, sequential, heartbeat, auth, fetch, pull, recurse.
The count returned by node.searchNodes is the number of searched nodes and
may differ from the total number of nodes. Do not use it as the maximum
order for node.resolveNode — instead increase order from 0 until an
error is returned.
node.resolveNode
Retrieve a single node and register a handle for it. Usually only one selector is provided.
| Param | Type | Notes |
|---|---|---|
order | int | If multiple nodes match, return the Nth one (0-based). |
address | string | Node address, e.g. "1". |
name | string | Node name, e.g. "SXmsr". May not be unique. |
Returns an opaque integer handle, plus ident (name, hwid, swid, sn, uuid,
class) and, when the node is live, a target object (state, inconsistent,
disabled, address).
Known limitation: node.resolveNode with address returns the first
node with a matching address even if that node is disconnected (result 0, no
target). After a node stops and a new search runs, a stale node with the same
address may shadow the live one. Prefer resolving by order when possible.
node.openDialog
Open an entry dialog / show a tool for a specific node.
| Param | Type | Notes |
|---|---|---|
handle | int | Required. From node.resolveNode. |
command | string | Entry path (e.g. /driver/supply/voltage) or "{term}" / "{scope}" with optional tool arguments. |
node.getVariableInfo
Retrieve variable meta-data.
| Param | Type | Notes |
|---|---|---|
handle | int | Required. |
path | string | Entry path, e.g. /driver/temp. |
> {"jsonrpc":"2.0", "id":1, "method":"node.getVariableInfo", "params":{"handle":1, "path":"/driver/temp"}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":0, "class":"state variable", "dimension":1, "inconsistent":false, "isSetableVariable":false, "type":"float"}}
node.readVariable
Read a variable value.
| Param | Type | Notes |
|---|---|---|
handle | int | Required. |
path | string | Entry path. |
mode | string | Optional I/O mode (see I/O modes). Default "wait". |
> {"jsonrpc":"2.0", "id":1, "method":"node.readVariable", "params":{"handle":1, "path":"/driver/temp"}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":0, "value":"40.5640"}}
node.writeVariable
Write a variable value.
| Param | Type | Notes |
|---|---|---|
handle | int | Required. |
path | string | Entry path. |
value | string | Required. Value to write. |
mode | string | Optional I/O mode (see I/O modes). Default "wait". |
node.executeCommand
Execute a command on a node, optionally streaming task progress.
| Param | Type | Notes |
|---|---|---|
handle | int | Required. |
command | string | Required. Command to execute, e.g. "plot". |
mode | string | "wait" (default) or "attempt" (only if no other I/O is pending). |
stream | bool | false (default) or true ⇒ push task.stateChanged notifications. |
timeout | int | Milliseconds; default 3000 for "attempt", otherwise 1000. |
arguments | string[] | Optional array of string arguments. |
With stream:false the response carries the device result and, when
timeout >= 0, the command's return value. With stream:true the server
emits task.stateChanged notifications during execution.
Known ordering quirk (stream:true): because execution is blocking, the
final response ({"result":{"result":0}}) is currently sent after the last
task.stateChanged notification. JSON-RPC convention would expect the response
to acknowledge submission early, with notifications carrying progress.
Server notifications
These are pushed by the server without a preceding request (no id).
task.stateChanged
Fired during a streamed node.executeCommand (stream:true).
{ "jsonrpc": "2.0", "method": "task.stateChanged", "params": { "pid": "6", "state": 2 } }
pid— task id (string).state— task state;0means finished (thenresultis included).
node.stateChanged
Fired when a node for which this connection holds a handle changes state.
{ "jsonrpc": "2.0", "method": "node.stateChanged", "params": { "handle": 4, "state": 1 } }
node.searchFinished
Fired when a search has finished and the node tree changed. Broadcast to every client, including the one that started the search — so an application can refresh when somebody else searched (another client, or the emGUI user) instead of polling for it.
{
"jsonrpc": "2.0",
"method": "node.searchFinished",
"params": { "count": 3, "flags": 127, "generation": 7 }
}
count— nodes in the tree now.flags— what the search ran with, so a client can tell whether the result covers what it would have asked for itself. Same values asnode.searchNodes.generation— tree revision; changes whenever nodes are added, dropped, or the tree is rebuilt.
Sent only when the tree actually changed, so an unchanged view is never
disturbed; a dummy search never triggers it. It is the coalesced form of
node.stateChanged, which fires per node during a search, against a
half-built tree — this fires once, when the tree has settled.
Receiving it means every handle held from before is void: re-run
node.resolveNode. There is no need to search again — the tree is already up to
date, so enumerating is enough and costs no bus I/O.
node.treeCleared
Fired when the node tree is discarded on its own — by the emGUI user, or by a client. The tree is now empty and every handle is void.
{ "jsonrpc": "2.0", "method": "node.treeCleared", "params": { "generation": 8 } }
generation— tree revision after the clear, so it can be ordered againstnode.searchFinished.
There is no count: the tree is empty by definition. Handle it the same way as
node.searchFinished — re-enumerate, do not search.
The clear that every search performs on its way in is not reported here.
Announcing it would have clients enumerate the empty tree the search is about to
fill; that case is reported once by node.searchFinished when the search
settles.
I/O modes
node.readVariable and node.writeVariable (and partially
node.executeCommand) accept a mode that controls how the value is exchanged
with the device vs. the emGUI cache:
mode | Behaviour |
|---|---|
none | Cache only — no device I/O (non-blocking). |
attempt | Perform I/O only if the device is not busy (non-blocking). |
schedule | Schedule the I/O; block while a previous operation is pending. |
wait | Full blocking I/O. Default. |
Equivalence with SXapi / Server (HTTP REST)
Every operation of the older SXapi / Server (HTTP REST)
has a direct equivalent in the WebSocket API. The main differences are the
transport (persistent WebSocket vs. per-request HTTP), the framing (JSON-RPC 2.0
vs. query string + result JSON), and node handles (opaque integers vs. raw
pointer hex strings).
| HTTP REST endpoint | WebSocket method | Notes |
|---|---|---|
GET / (HTML help page) | — | Replaced by this documentation page. |
GET /version | system.getVersion | WS adds a protocol field. |
GET /events (SSE stream) | (push notifications) | task.stateChanged / node.stateChanged / node.searchFinished / node.treeCleared over the same socket. |
GET /show [cmd] | ui.openDialog [command] | Show emGUI / a tool, no node. |
GET /search [flags] | node.searchNodes [flags] | WS also accepts flags as an array of names. |
GET /node [name] [addr] [order] | node.resolveNode [order] [address] [name] | WS returns an opaque integer handle; REST returned a pointer hex string. |
GET /open (handle) (path) | node.openDialog (handle) (command) | |
GET /var (handle) (path) | node.getVariableInfo (handle) (path) | |
GET /get (handle) (path) | node.readVariable mode:"wait" | Default. |
GET /cget … | node.readVariable mode:"none" | Cache only. |
GET /aget … | node.readVariable mode:"attempt" | |
GET /sget … | node.readVariable mode:"schedule" | |
GET /set (handle) (path) (value) | node.writeVariable mode:"wait" | Default. |
GET /cset … | node.writeVariable mode:"none" | |
GET /aset … | node.writeVariable mode:"attempt" | |
GET /sset … | node.writeVariable mode:"schedule" | |
GET /exec (handle) (path) [timeout] [args] | node.executeCommand mode:"wait", stream:false | |
GET /aexec … | node.executeCommand mode:"attempt", stream:false | |
GET /execio (handle) (path) [timeout] [args] | node.executeCommand stream:true | Progress via task.stateChanged instead of the /events SSE stream. |
GET /aexecio … | node.executeCommand mode:"attempt", stream:true |
The REST /get, /set and /exec families each collapsed their four/two
cache-vs-I/O flavours into distinct URLs. The WebSocket API keeps a single
method per operation and selects the flavour through the mode parameter,
which is easier to discover and extend.
Example session
Using wscat to talk to the server
interactively:
PS> wscat --connect ws://localhost:29045
> {"jsonrpc":"2.0", "id":1, "method":"node.searchNodes", "params":{}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":2}}
> {"jsonrpc":"2.0", "id":1, "method":"node.resolveNode", "params":{"order":0}}
< {"jsonrpc":"2.0", "id":1, "result":{"handle":1, "ident":{"class":"0D:SX", "hwid":"esc5-sx1e_62kla1060-A00", "name":"SXmsr", "sn":"702F4P081E4A", "swid":"VECTOR_epeklo_generic v0.6.9 May 10 2023", "uuid":"2037303246345008001E004A"}, "result":0, "target":{"address":"2", "disabled":0, "inconsistent":0, "state":1}}}
> {"jsonrpc":"2.0", "id":1, "method":"node.getVariableInfo", "params":{"handle":1, "path":"/driver/temp"}}
< {"jsonrpc":"2.0", "id":1, "result":{"class":"state variable", "dimension":1, "inconsistent":false, "isSetableVariable":false, "result":0, "type":"float"}}
> {"jsonrpc":"2.0", "id":1, "method":"node.readVariable", "params":{"handle":1, "path":"/driver/temp"}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":0, "value":"40.5640"}}
This documentation site uses the SXapi / WebSocket to power its interactive tools (for example siliDash).