MS2710X "Sleepy Hollow" WebSocket API

Wire Format

The server writes and reads strictly-formatted JSON objects delmited by newlines, like:

{ "type": "join", "value": "name-of-the-room" }

or

{ "type": "foo", "value": ["bar", 2, {"baz": "quux"}] }

The wire format must enquote object keys, and have no newlines except to terminate an object. But, this document uses a relaxed notation where object keys are unquoted and newlines are not significant, such as:

{
    type: "join",
    value: "name-of-the-room"
}

Each object must have at least the two members type and value.

The client may send an ack member which the server will copy to the response object in the ack member. The client can check that it got the ack within some "reasonable" amount of time to detect timeouts.

If the client sends invalid traffic (such as poorly formatted objects, or objects with invalid values) the server will reply with an error member. Receiving these objects indicates that the client has a bug.

URL and Port

The API can be accessed by telnet, netcat, or socket(2) on port 4000. The port is also available via WebSockets at /json.ws and json6.ws. The payload traffic over the two interfaces is the same.

JavaScript WebSocket Client

See src/application/spa/web/json_tcp_client.js. It is recommended to create a stub test page, and use the Firefox developer console for interactive exploration.

<html>
    <script src=json_tcp_client.js></script>
</html>

Then, in the developer console:

var client = json_tcp_client('ws://172.26.201.67/json.ws')

The protocol must be ws:// not http:// or https://.

The name of the client is slightly misleading.

json_tcp_client().send()

client.send(room_name);
client.send(room_name, value);
client.send(room_name, callback);
client.send(room_name, value, callback);

The room_name string is required.

The value may be any plain JSON value (object, string, number, array, boolean) which is correctly serialized by JSON.stringify().

The callback may be a function which accepts a single argument, which is only the value received. It cannot access the type, error, or ack members of the received object.

For example:

client.send('echo',
    {
        it: 'is',
        my: ['test', 'object', 1]
    },
    function (response) {
        assert(response.it === 'is');
        assert(response.my[0] === 'test');
        assert(response.my[1] === 'object');
        assert(response.my[2] === 1);
    }
);

json_tcp_client().join()

client.join(room_name, callback);

Joins a room. Calls the callback whenever the server decides to do so. The callback is the same as in json_tcp_client().send().

json_tcp_client().leave()

client.leave(room_name);

Leaves a room. The server will no longer send these objects.

json_tcp_client().onconnect()

The onconnect method accepts one callback argument which is called with no arguments when the client connects. The client will automatically reconnect.

All other client methods must occur within an onconnect callback.

json_tcp_client().ondisconnect()

The onconnect method accepts one callback argument which is called with no arguments when the client disconnects. The client will automatically disconnect on certain timeouts.

Rooms

The server will send unsolicited objects to all clients who have joined that room.

client.join("some_room", function (room_message) {
    console.log("Server sent " + room_message + " to some_room");
});

The server will usually send some initial state immediately on joining, then more information will arrive over time. Each piece of initial state is the same as each successive object, meaning the client should make no distinction between initial state and updates which follow.

A useful analogy is IRC chat rooms. Objects may arrive at any time as long as the client is still joined to that room. At a console, try:

$ telnet 172.26.x.x 4000
{"type": "join", "value": "setting-value"}

Then, open that same IP address in a browser and change a setting. The values of the settings will be sent to the telnet client. Other rooms operate similarly.

scpi-log

{
    errors: [
        {
            num: <Number>,
            description: <Human readable string>
        }
    ],
    command: <Literal SCPI string as entered by the user>
    quiet: <Boolean. True if the command was used internally by the GUI>
}

setting-value

{
    id: <Number, non-unique>,
    command: <Shortest form SCPI command string>,
    value: <String, might have numeric meaning>
}

gps

{
    state: <Boolean, enabled if true>,
    good_fix: <Boolean, true if good fix>,
    timestamp: <Number, milliseconds since UNIX epoch>,
    latitude: <Number>,
    longitude: <Number>,
    altitude: <Number, meters>,
    num_satellites: <Number><
}

iq-capture-result

<Boolean, was the capture successful>

overheat-status

<Boolean, is the unit overheating>

fwupdate

{
    what: <String description, "error" or "started">,
    message: <String>,
    url: <String of where update is being downloaded from>,
}

limitFailure

{} <The empty object, no additional information>

Requests

echo

Input

Any JSON value

Output

Copies input to output

scpi

Provides a direct interface to the instrument's SCPI engine. It cheats the one-connection-only rule. Rejects compound commands, e.g. "SENS:FREQ:STAR 2ghz; SENS:FREQ:STAR?". Rejects commands related to occupied bandwidth and channel power, e.g. "FETCH:OBW?".

All SCPI commands and their responses sent via this request will be copied to all other clients joined to the scpi-log room.

This and scpi-quiet are the only way to change settings. It is not the recommended way to query trace data or setting values.

Input

<A string with exactly one SCPI command>

For example:

"SENS:FREQ:STAR 3ghz"
"*idn?"

Output

{
    errors: [
        {
            num: <Number>,
            description: <Human readable string>
        }
    ],
    command: <Literal SCPI string as entered by the user>
    quiet: false
}

scpi-quiet

Same as scpi except it does not broadcast to the scpi-log room. Instead, it replies only to the sender.

fw-update-sources

Used to get a list of the available firmware updates. Does not initiate a firmware update.

Input

None

Output

[
    {
        name: <String, identifies the packge, e.g. "1.0.0">
        recommended: <Boolean, should be only one recommended entry>
    },
]

app-version

Input

None

Output

<String, description of the running firmware version>

trace-data

The preferred way to read trace data for interactive use. Will send empty responses if nothing has changed since this client last issued the command, e.g. during single sweep mode the same trace data will not be sent repeatedly.

Always contains all points. Does not distinguish between currently sweeping trace, and the points from the previous trace.

Input

None

Output

{
    data: <String, described below>,
    start: <Number, start index of trace data. Always 0>,
    count: <Number of points in the sweep>
    stale: <String, described below>,
    status: <String, described below>,
    sweep_id: <Number, increments when a sweep completes>
}

trace-data Format

Starting each trace point is the sign (plus or minus). 8 hex digits follow. No endian conversion is required.

Using the example:

"-00000001+000000a0"

Two trace points are described: -1 and +0xa0. The length of the encoded string is nine (sign plus eight hex digits) times the count.

Each point has units milli-dBm. To convert +0xa0 to dBm, divide by one thousand:

+0xa0 / 1000.0 == 0.16 dBm

trace-data Stale Format

The stale member is a string of ASCII one's and zero's (1 and 0). Each character describes whether the trace point at that index is stale (1) or fresh (0). Stale data may be styled differently (e.g. trace is partially transparent at that point). Data will be stale when e.g. reference level changes.

Using the example:

"1001"

Four points are described: the first is stale, the second is fresh, the third is fresh, and the fourth is stale.

trace-data Status Format

Eight ASCII bytes describe the status of each trace point. For example:

"0000000012345678"

Two trace points are described: 0 and 0x12345678. No endian conversion is required. There is no delimiter between points.

The trace statuses are a 32-bit bitwise mask of possible measurement problems when that point was taken:

Bit 0: ADC Overrange
Bit 1: Power Saturated
Bit 2: SLO Lock Fail
Bit 3: LO1 Lock Fail
Bit 4: LO2 Lock Fail
Bit 5: TG Lock Fail
Bits 6 - 31: Reserved

So, a status 00000003 is 0x03, which means ADC Overrange and Power Saturated.

If any point has a non-zero status, the entire sweep is considered invalid, although it may still be useful. An invalid trace should still be displayed.

spectrum-limits

This is related to the limit SCPI commands. It translates the ~6 SCPI commands into one object. The SCPI commands define arbitrary line paths (many points connected by slopes), upper, and lower limits. In contrast, the spectrum-limits object defines many disconnected, flat upper limits only.

If the request argument is the empty object {}, then the request is treated as a query. The returned object has form:

{
    segments: [
        {
            amplitude: {
                value: <Number>,
                unit: <String, e.g. dBm>
            },
            frequency: {
                start: <Number, Hz>,
                stop: <Number, Hz>,
            }
        },
    ],
    frequencyRelative: <Bool>,
    amplitudeRelative: <Bool>,
    enabled: <Bool>
}

If that object is sent as the request argument, then it will set the limits on the instrument.

The instrument is very strict about the input schema but does not indicate the specific reason for rejecting the input.


December 14, 2015