Decoders
A protocol decoder turns logic transitions into packets: addresses, data bytes, ACKs, frame errors. Decoders are the JavaScript files in the server's script library: the same ones ScanaStudio uses, and the same ones you can write yourself.
Reached through workspace.decoders.
Finding the available scripts​
The script library belongs to the server, not to a workspace, so it is reached through the client rather than the workspace.
The server sends its script list shortly after connect() returns, so reading
it immediately can give an empty list, and scripts.find("i2c.js") then reports
the decoder missing when it is installed.
Two reliable orders:
# 1. Create the workspace first. By then the list has arrived.
workspace = server.create("se254")
if server.scripts.find("i2c.js") is None:
...
# 2. Or wait for the list, if you must check before opening anything.
import time
deadline = time.monotonic() + 10.0
while not server.scripts.all() and time.monotonic() < deadline:
time.sleep(0.05)
decoders.add() itself is unaffected: it names the script on the server, which
knows its own library. Only the client-side listing is affected.
- Python
- NodeJS
- Rust
- C
for script in server.scripts.decoders():
script.file_name # "i2c.js" — what add() takes
script.name # "I2C"
script.version
script.author
script.description
server.scripts.find("i2c.js") # a ScriptDesc, or None
server.scripts.all() # every script, decoders and generators
server.scripts.generators() # pattern-generator scripts
server.scripts.library_dir # where they live on the server
for (const script of server.scripts.decoders()) {
script.file_name; // "i2c.js" — what add() takes
script.name; // "I2C"
script.version;
}
server.scripts.find('i2c.js'); // ScriptDesc | undefined
server.scripts.all();
server.scripts.generators();
server.scripts.library_dir;
// file_name ("i2c.js") is what add() takes; name ("I2C") is for display.
for script in server.scripts().decoders() {
println!("{} — {}", script.file_name, script.name);
}
server.scripts().find("i2c.js");
server.scripts().all();
server.scripts().generators();
server.scripts().library_dir();
/* A JSON array of every script installed. */
char *scripts = ss_scripts(server);
if (scripts) { printf("%s\n", scripts); ss_string_free(scripts); }
char *dir = ss_scripts_library_dir(server); /* on the SERVER's filesystem */
if (dir) { printf("%s\n", dir); ss_string_free(dir); }
ss_decoder_add accepts a script file name directly, so you can name i2c.js
without listing the library first.
Example output. A stock library holds 44 scripts, 42 decoders and 6 generators (a few do both):
1-wire.js — 1-Wire
ADC.js — ADC
FDX-B.js — FDX-B
IR_NEC.js — IR_NEC
LPC (Low Pin count).js — LPC (Low Pin Count)
NMEA_2000.js — NMEA_2000
...
server.scripts also syncs, installs, imports, creates, renames, duplicates
and deletes scripts. Those calls belong to the server, and are described in
Server.
options​
The options a script takes. A decoder is a script, so the SDK cannot know its settings in advance; ask before you configure.
- Python
- NodeJS
- Rust
- C
for option in workspace.decoders.options("i2c.js"):
print(option)
option.id # "ch_scl" — what add() keys on
option.caption # "SCL channel" — also accepted as a key
option.type # "ch_selector", "combo", "checkbox", "engineering_input"...
option.default
option.choices # the entries of a combo, empty otherwise
option.unit # "Hz", "%"... empty when there is none
option.tab # the tab it sits under in the dialog, empty at the top
import { describe_option } from '@ikalogic/scanastudio';
// A decoder is a script, so ask it what it takes rather than guessing.
for (const option of await workspace.decoders.options('i2c.js')) {
console.log(describe_option(option));
option.id; // "ch_scl" — what add() keys on
option.caption; // "SCL channel" — also accepted as a key
option.type; // "ch_selector", "combo", "checkbox", "engineering_input"...
option.default;
option.choices; // the entries of a combo, empty otherwise
option.unit;
option.tab; // the tab it sits under in the dialog, empty at the top
}
// A decoder is a script, so ask it what it takes rather than guessing.
for option in workspace.decoders().options("i2c.js").await? {
println!("{option}"); // ScriptOption implements Display
// option.id, option.caption, option.kind (the GUI type), option.default,
// option.choices, option.unit, option.tab
}
/* A JSON array of the script's options — ask it rather than guessing. */
char *options = ss_decoder_options(workspace, "i2c.js");
if (options) { printf("%s\n", options); ss_string_free(options); }
Example output. i2c.js on an SE254. Each line is
id [tab]: caption (type) choices unit = default:
ch_sda: SDA Channel (ch_selector) = None
ch_scl: SCL Channel (ch_selector) = None
AUTO0: Advanced options (tab) = False
address_opt [Advanced options]: Address convention (combo) one of ['7 bit address', '8 bit address (inlcuding R/W flag)'] = '7 bit address'
address_format [Advanced options]: Address display format (combo) one of ['HEX', 'Binary', 'Decimal'] = 'HEX'
data_format [Advanced options]: Data display format (combo) one of ['HEX', 'Binary', 'Decimal', 'ASCII'] = 'HEX'
en_noise_flter [Advanced options]: Ignore high-frequency noise on data and clock lines (checkbox) = False
Only ch_scl and ch_sda have no default, so they are the two you must supply.
Everything under [Advanced options] can be left alone.
The type is the kind of control the script declares in its dialog, and it decides what value an option takes:
| Type | Value you pass |
|---|---|
ch_selector | A channel index, e.g. from workspace.channel("I2C SCL"). |
combo | The entry's text (case-insensitive) or its index in choices. |
checkbox, tab | True / False. |
engineering_input | A number in the base unit: 1e6 for 1 MHz. |
baud_selector, hidden_int | A whole number. |
text_input | A string. |
In Rust the type is the kind field of ScriptOption.
add​
Attaches a decoder instance and starts it decoding.
- Python
- NodeJS
- Rust
- C
i2c = workspace.decoders.add(
"i2c.js",
{
"ch_scl": workspace.channel("I2C SCL"),
"ch_sda": workspace.channel("I2C SDA"),
},
wait=True, # block until decoding is finished
)
i2c.instance_id
i2c.name
// Channels by name, so the same code works on any device that has them.
const i2c = await workspace.decoders.add(
'i2c.js',
{
ch_scl: workspace.channel('I2C SCL'),
ch_sda: workspace.channel('I2C SDA'),
},
{ wait: true }, // block until decoding is finished
);
use scanastudio_client::values;
// Channels by name, so the same code works on any device that has them.
let scl = workspace.channel("I2C SCL")?;
let sda = workspace.channel("I2C SDA")?;
let i2c = workspace.decoders()
.add("i2c.js", &values([("ch_scl", scl.into()), ("ch_sda", sda.into())]))
.await?;
// Rust does not take a `wait` flag; wait explicitly.
workspace.decoders().wait_for_instances(&[i2c.instance_id], Duration::from_secs(300)).await?;
int scl = ss_workspace_channel(workspace, "I2C SCL");
int sda = ss_workspace_channel(workspace, "I2C SDA");
char options[64];
snprintf(options, sizeof options, "{\"ch_scl\": %d, \"ch_sda\": %d}", scl, sda);
/* Returns the instance id, or 0 on failure. */
uint32_t i2c = ss_decoder_add(workspace, "i2c.js", options);
if (i2c == 0) fprintf(stderr, "add: %s\n", ss_last_error());
ss_decoder_wait(workspace, i2c, 300.0);
Parameters:
- the script's file name, e.g.
i2c.js; - the option values, keyed by id or caption. Anything you leave out keeps the script's own default;
wait— block until decoding finishes, rather than returning immediately.
Returns: the decoder instance.
Errors:
ScriptErrorwhen the script rejects its configuration. Itslogshold the script's own log lines only when the script failed while drawing its dialog. A refusal fromaddorupdatecomes with emptylogs, and the message says what was refused.UnknownOptionfor an option name the script does not know. It lists the known ones. In Python it is a subclass ofKeyError.- Invalid values are refused before anything is sent:
Nonefor a channel raisesTypeError; a non-numeric value, an unknown combo entry or a combo index out of range raisesValueErrorin Python andRangeErrorin NodeJS. These do not derive fromScanaStudioError.
workspace.channel("I2C SCL") keeps one test script working across every device
that has those channels. Hard-coded channel numbers break when the bench
changes.
Reading a decoder's results​
- Python
- NodeJS
- Rust
- C
for packet in i2c.packets():
print(packet.start, packet.title, packet.content)
for entry in i2c.hex_bytes(): # this decoder's bytes
print(hex(entry.byte))
page = i2c.hex(offset=0, count=5000) # a page of them
page.total
page.bytes
for item in i2c.items(): # this decoder's rows of the Raw View
print(item.start, item.content)
i2c.raw_items(start=0, end=100_000) # items with every form of their text
i2c.memory(offset=0, count=5000) # its Memory View, a MemPage
i2c.vacs() # the virtual analog channels it declared
i2c.options() # its current configuration
for await (const packet of i2c.packets()) {
console.log(packet.start, packet.title, packet.content);
}
// This decoder's bytes.
for await (const entry of i2c.hex_bytes()) {
console.log(entry.byte.toString(16));
}
// Or a page of them.
const page = await i2c.hex({ offset: 0, count: 5000 });
for await (const item of i2c.items()) { // this decoder's rows of the Raw View
console.log(item.start, item.content);
}
await i2c.raw_items({ start: 0, end: 100_000 }); // every form of each item's text
await i2c.memory({ offset: 0, count: 5000 }); // its Memory View
await i2c.vacs(); // virtual analog channels
await i2c.options(); // its current configuration
// Rust reads a decoder's results through DataApi, filtered by instance.
let filter = RowFilter { sources: vec![i2c.instance_id], ..Default::default() };
let packets = workspace.data().packets(&filter).await?;
// A page of this decoder's hex view, its items and its Memory View.
let page = workspace.data().instance_hex(i2c.instance_id, 0, 5000).await?;
let items = workspace.data().instance_items(i2c.instance_id, 0, None, 0).await?;
let mem = workspace.data().instance_memory(i2c.instance_id, 0, 5000).await?;
// The virtual analog channels it declared, and its current configuration.
let vacs = workspace.decoders().vacs(i2c.instance_id).await?;
let options = workspace.decoders().instance_options(i2c.instance_id).await?;
// decoders().data() is the same DataApi as workspace.data().
/* Filter by instance to read one decoder's packets. */
char filter[64];
snprintf(filter, sizeof filter, "{\"sources\": [%u]}", i2c);
ss_packets *batch = ss_packets_read(workspace, filter);
ss_packet first;
if (batch && ss_packets_count(batch) &&
ss_packets_copy(batch, 0, &first, 1) == 1) {
/* The strings belong to the batch: read them before freeing it. */
printf("%s: %s\n", first.title, first.content);
}
ss_packets_free(batch);
/* A page of this decoder's hex view, and its items over a window. */
ss_hex *page = ss_hex_read_instance(workspace, i2c, 0, 5000);
ss_items *items = ss_items_read_instance(workspace, i2c, 0, -1, 0);
ss_mem *mem = ss_mem_read_instance(workspace, i2c, 0, 5000); /* Memory View */
ss_hex_free(page);
ss_items_free(items);
ss_mem_free(mem);
char *vacs = ss_decoder_vacs(workspace, i2c); /* JSON: virtual analog channels */
if (vacs) { printf("%s\n", vacs); ss_string_free(vacs); }
Packet, item, hex and Memory View fields are described under
Reading captured data. Both
packets() and items() accept the same filter.
| Call | Returns |
|---|---|
packets() | This decoder's Packet View rows. |
items() | This decoder's Raw View rows (content is the longest form). |
raw_items(start, end, min_length) | Its items over a window, with contents: every form the script gave, longest first. |
hex(offset, count) | A page of its hex store. hex_bytes() iterates over all of it. |
memory(offset, count) | A page of its Memory View: what the script read out of the target, at the target's addresses. |
vacs() | The virtual analog channels it declared: vac_id, label, unit, color, size, and min/max over the whole curve. |
options() | Its current configuration, in the same form as decoders.options(script), ready to edit and pass to update. |
Example output. The first packets of a decoded capture:
496 I2C CH4
433 START
1138 RE-START
1837 STOP
wait and progress​
Decoding runs on the server, asynchronously. progress is 0–100.
- Python
- NodeJS
- Rust
- C
i2c.wait(timeout=300.0) # this decoder
workspace.decoders.wait() # every attached decoder
i2c.progress # 0-100
await i2c.wait(300_000); // this decoder
await workspace.decoders.wait(); // every attached decoder
i2c.progress; // 0-100
workspace.decoders().wait_for_instances(&[i2c.instance_id], Duration::from_secs(300)).await?; // these
workspace.decoders().wait(Duration::from_secs(300)).await?; // all of them
workspace.decoders().progress(i2c.instance_id); // 0-100
ss_decoder_wait(workspace, i2c, 300.0); /* one instance */
ss_decoders_wait(workspace, 300.0); /* all of them */
uint8_t percent = ss_decoder_progress(workspace, i2c); /* 0-100 */
Managing instances​
- Python
- NodeJS
- Rust
- C
workspace.decoders.all() # every attached instance
workspace.decoders.get(instance_id) # one, or None
len(workspace.decoders)
for decoder in workspace.decoders: # same as all()
decoder.instance_id, decoder.file_name, decoder.name # 1, "i2c.js", "I2C"
workspace.decoders.remove(i2c) # by Decoder or by instance id
i2c.update({"ch_scl": 5}) # reconfigure and restart decoding
i2c.relaunch() # decode again from scratch
i2c.pause() # stop decoding; results produced so far stay
i2c.remove() # detach it
workspace.decoders.clear() # detach every decoder
workspace.decoders.all(); // every attached instance
workspace.decoders.get(instance_id); // one, or undefined
workspace.decoders.length;
for (const decoder of workspace.decoders) {
decoder.instance_id; decoder.file_name; decoder.name;
}
workspace.decoders.remove(i2c); // by Decoder or by instance id
await i2c.update({ ch_scl: 5 }); // reconfigure and restart decoding
i2c.relaunch(); // decode again from scratch
i2c.pause(); // stop decoding; results produced so far stay
i2c.remove(); // detach it
workspace.decoders.clear(); // detach every decoder
workspace.decoders().all(); // every attached instance
workspace.decoders().get(instance_id); // one, or None
// Rust drives instances through DecodersApi rather than a Decoder handle.
workspace.decoders().update(i2c.instance_id, &values([("ch_scl", 5i64.into())])).await?;
workspace.decoders().relaunch(i2c.instance_id)?; // decode again from scratch
workspace.decoders().pause(i2c.instance_id)?; // stop; results so far stay
workspace.decoders().remove(i2c.instance_id)?; // detach it
char *all = ss_decoders(workspace); /* JSON: every instance */
if (all) { printf("%s\n", all); ss_string_free(all); }
char *config = ss_decoder_config(workspace, i2c); /* JSON: one instance */
if (config) { printf("%s\n", config); ss_string_free(config); }
ss_decoder_update(workspace, i2c, "{\"ch_scl\": 5}"); /* and re-decode */
ss_decoder_relaunch(workspace, i2c); /* decode again from scratch */
ss_decoder_pause(workspace, i2c); /* stop; results so far stay */
ss_decoder_remove(workspace, i2c); /* detach it */
Running a decoder on a demo device finds structure by chance, because the signal is pseudo-random rather than protocol traffic. Assert that decoding happened, not that a particular frame came out.