SXapi / WebSocket
The SXapi / WebSocket is a new interface that is available in SWTools emGUI
4.0 and prerelease versions.
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
A second emGUI running at the same time scans upward from 29045. emGUI opens
its web applications with the port in the URL, so they find the right one.
Access
The server listens on your own PC only. Two things decide whether a client gets through:
-
Allow web applications in emGUI's Servers menu. Unticked, the server still listens and accepts connections, but answers that it is disabled (see Disabled). It is ticked by default; Servers → Remember settings keeps the choice for the next start;
--ws-offand--ws-onstart emGUI with it unticked or ticked, whatever was remembered. -
Where a web page comes from. Browsers let any website open a WebSocket to
localhost, so emGUI checks theOriginthe browser sends when the connection opens:Origin Accepted none (a program such as wscat, Python or Node)yes localhost,127.0.0.1,::1, any portyes the web applications' site ( --web-url, by default this one)yes each site given with --ws-origin=URLyes null(a page opened from a file)only when --web-urlis afile:URLany other site no: the connection is refused, and logged The check stops web pages, not programs: a program can send any
Originit likes.
Disabled
While web applications are not allowed:
system.getVersionis still answered, with"enabled": false.- Every other request fails with
-32004,data: {"reason": "disabled"}. node.*notifications are not sent.
The switch takes effect on open connections at once (a command already running
finishes), and every connected client receives
system.stateChanged. An older emGUI has no enabled
field: read a missing field as enabled.
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" } }
Error with an annotation (server → client, optional data):
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -100, "message": "Another search is already running", "data": { "reason": "busy", "retry": true } }
}
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.
Error codes
code | Meaning |
|---|---|
-32700 | Parse error: the message is not a JSON object. |
-32600 | Invalid request: method is missing. |
-32601 | Method not found. |
-32602 | Invalid params, see below. |
-32004 | Web applications are not allowed in emGUI, see Disabled. |
| other | The operation failed: the device or emGUI error code, with a readable message. |
-32602 is returned for a missing or unknown handle, an unknown mode, a
missing value or command, and for stdout together with stream or without
a positive timeout.
The data member is advisory: it may be absent, and a client must treat a
missing data as "unknown" and never depend on it. When data.retry is true
the request may be repeated after a pause; any other reason means the client
should stop rather than loop.
Node handles
node.resolveNode returns an opaque integer handle, which the other node.*
methods take. Handles belong to the connection and are not stable:
- A handle becomes invalid when the node tree is rebuilt (any search other
than
reclaim), when the tree is discarded, when the node is dropped in emGUI, and when the connection closes. - An invalid handle fails with
-32602Unknown handle. Handle numbers are never reused, so an old handle can never address a different node. - Resolving the same node again returns the same handle.
Run node.resolveNode again rather than keeping a handle across a search or a
reconnect. node.searchFinished and
node.treeCleared say when that is needed.
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":2, "vendor":"siliXcon", "hostapp":"emGUI", "version":"4.0.0", "enabled":true}}
Answered even while web applications are not allowed.
-
enabled—falsewhile web applications are not allowed in emGUI (see Disabled). Absent from an older emGUI, which means enabled. -
protocol— SXapi WebSocket protocol version. It changes independently of the application version. Version2added thestdoutcapture ofnode.executeCommandandsystem.getConnection. -
version— application (SWTools) version.
system.getConnection
Returns the interface emGUI reaches the devices through, and whether it is connected. No parameters, no device I/O.
> {"jsonrpc":"2.0", "id":1, "method":"system.getConnection", "params":{}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":0, "loaded":true, "connected":true, "config":{"name":"kvaser", "protocol":"...", "options":"...", "local_address":"7", "target_address":"1", "mesh_address":null, "credential":{"user":"admin", "password":true}, "service_token":false}}}
loaded— an interface driver is loaded.connected— the interface is connected.config— the interface settings, kept apart from the state:name— interface name, see Communication interfaces.protocol,options— protocol and the interface's option string (its format is in the interface's section of Communication interfaces).local_address,target_address,mesh_address— addresses.credential—useris the login user name;passwordonly says whether a password is set.service_token— whether a service token is set.
An unset string is null. The configuration cannot change while the interface
is connected: when connected is true the values are what the connection was
opened with, otherwise what the next connect will use. The credential is the
exception, the user may change it at any time in emGUI. The password and the
service token themselves are never returned.
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 default applies when flags is missing; an array is taken as it is, so an
empty array searches with no flags. Unknown names in the array are ignored.
> {"jsonrpc":"2.0", "id":1, "method":"node.searchNodes", "params":{"flags":["dummy"]}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":2}}
Only one search runs at a time. A request that arrives while another search is
running (a dummy one included, so that it does not report a half-built tree)
does not fail: it waits until that search ends and is answered with the
resulting node count. A request whose flags the running search does not cover
is queued, and its own search runs once the current one is done. Only if the
wait times out is busy returned.
On failure the error carries data.reason:
reason | Meaning |
|---|---|
busy | Another search is still running (retry: true): repeat after a pause. |
aborted | The connection is closing. |
notfound | No node was discovered. |
nointerface | The interface is not connected, see system.getConnection. |
failed | Any other failure; the message says more. |
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 (see Node handles), plus
ident (name, hwid, swid, sn, uuid, class; a field the node does not report is
left out) and, when the node is live, a target object (state, inconsistent,
disabled, address; inconsistent and disabled are 0 or 1). No match fails
with code -1 Node not found.
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. |
A dialog or tool that does not exist fails with Dialog not found; the same
applies to ui.openDialog.
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"}}
class is always present. type, dimension, isSetableVariable and
inconsistent are present only for a variable, not for a directory or a
command.
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"}}
The value is always returned as a string. An unknown mode fails with
-32602.
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". |
> {"jsonrpc":"2.0", "id":1, "method":"node.writeVariable", "params":{"handle":1, "path":"/driver/sampling_time", "value":"100"}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":0}}
The value is passed as a string, even for a number. An empty value fails with
-32602 Missing value, so an empty string cannot be written.
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. -1: do not wait for the return value. |
arguments | string[] | Optional array of string arguments. |
stdout | bool | false (default) or true ⇒ capture the command's output and return it. Not together with stream. |
stdout_limit | int | Byte limit of the capture; default 32768, at most 1048576. A value outside that range means the maximum. |
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. Any mode other than
"attempt" is taken as "wait".
Capturing the output
> {"jsonrpc":"2.0", "id":1, "method":"node.executeCommand", "params":{"handle":1, "command":"pr", "arguments":["-r","/driver"], "stdout":true}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":0, "return":3, "stdout":"List of available params:\n-> sampling_time uint8 : 100 (100), Interval for sending msg from driver API\n\nTotal 3 entries\n", "stdout_truncated":false}}
With stdout:true the response carries stdout (the captured text, possibly
empty) and stdout_truncated (true when stdout_limit cut it short). The
output is decoded as UTF-8, so binary output is not usefully readable this way.
- The capture is complete when the response arrives; there is no partial delivery while the command runs.
timeoutmust be positive;-1fails with-32602. It is an inactivity deadline, restarted by every task update and every completed read: it bounds a stalled command, not one that keeps printing.- Reaching
stdout_limitstops the copying, not the command: it still runs to the end, so the response is not delayed. - emGUI's main thread is blocked while the output is captured. The capture is
meant for short listing commands (
pr,st,ls,version,msgconf…); a long-running command freezes emGUI for as long as it runs. stdouttogether withstream:truefails with-32602: the streamed path has no capture yet.
Timeout and failure
A command abandoned on its timeout fails with code -3 and
data.reason: "timeout". The command may still be running on the device:
emGUI stopped waiting, it did not cancel anything. A command that fails or
times out may already have printed something; with stdout:true that output is
in the error's data.stdout and data.stdout_truncated. On a timeout it is a
partial capture, not the whole output. As with any data, it may be absent.
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).
system.stateChanged
Fired to every connected client when the operator allows or disallows web applications in emGUI.
{ "jsonrpc": "2.0", "method": "system.stateChanged", "params": { "enabled": false } }
enabled— the new state. Whilefalse, show the user that web applications are disabled in emGUI (Servers → Allow web applications) rather than retrying.
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. |
| — | system.getConnection | WebSocket only: interface configuration and state. |
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 | WS can also return the output (stdout:true). |
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 split 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":"system.getConnection", "params":{}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":0, "loaded":true, "connected":true, "config":{"name":"kvaser", ...}}}
> {"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"}}
> {"jsonrpc":"2.0", "id":1, "method":"node.executeCommand", "params":{"handle":1, "command":"pr", "arguments":["-r","/driver"], "stdout":true}}
< {"jsonrpc":"2.0", "id":1, "result":{"result":0, "return":3, "stdout":"List of available params:\n...", "stdout_truncated":false}}
This documentation site uses the SXapi / WebSocket to power its interactive tools (for example siliDash).