Read and change instrument settings as JSON
getSetupJSON returns an instrument's setup as a tree of the names and values you see in the setup editor. setSetupJSON takes back only the settings you change, checks them against the instrument's rules and writes them. This guide shows how to read that tree, what every field means and how to change settings safely.
- 1Read
getSetupJSONgives the tree and a fingerprint. - 2Look upFind the setting, what it accepts and when it applies.
- 3Check
setSetupJSONwithcheck: truewrites nothing. - 4WriteThe same request without
check.
Quick start
Read the setup of instrument 123456, preview one change, then write it. Every request is a POST of JSON to your SvanLINK's API address. token is the session token from the login request.
import requests
SVANLINK = "http://192.168.1.10:8000/" # your SvanLINK
TOKEN = "userToken" # from the login request
SERIAL = "123456" # the instrument's serial number
def call(request, **params):
reply = requests.post(SVANLINK, json={"Request": request, "Params": params, "token": TOKEN},
timeout=120).json()
if reply["Status"] != "ok":
raise RuntimeError(reply["StatusMessage"])
return reply["Response"]
setup = call("getSetupJSON", device=SERIAL)
print(setup["settings"]["Measurement"]["Profile 1"]["Filter"]) # A
change = {"Measurement": {"Profile 1": {"Filter": "C"}}}
preview = call("setSetupJSON", device=SERIAL, fingerprint=setup["fingerprint"],
settings=change, check=True)
print(preview["changed"]) # [{'path': 'Measurement/Profile 1/Filter', 'code': 'BI0', 'before': 'A', 'after': 'C'}]
result = call("setSetupJSON", device=SERIAL, fingerprint=setup["fingerprint"], settings=change)
print("measurement restarted:", result["restarted"])const SVANLINK = 'http://192.168.1.10:8000/'; // your SvanLINK
const TOKEN = 'userToken'; // from the login request
const SERIAL = '123456'; // the instrument's serial number
async function call(request, params) {
const reply = await fetch(SVANLINK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ Request: request, Params: params, token: TOKEN }),
}).then((r) => r.json());
if (reply.Status !== 'ok') throw new Error(reply.StatusMessage);
return reply.Response;
}
const setup = await call('getSetupJSON', { device: SERIAL });
console.log(setup.settings.Measurement['Profile 1'].Filter); // A
const change = { Measurement: { 'Profile 1': { Filter: 'C' } } };
const preview = await call('setSetupJSON', {
device: SERIAL, fingerprint: setup.fingerprint, settings: change, check: true,
});
console.log(preview.changed); // [{ path: 'Measurement/Profile 1/Filter', code: 'BI0', before: 'A', after: 'C' }]
const result = await call('setSetupJSON', { device: SERIAL, fingerprint: setup.fingerprint, settings: change });
console.log('measurement restarted:', result.restarted);# 1. Read. Keep Response.fingerprint for the write.
curl -s http://192.168.1.10:8000/ -H 'Content-Type: application/json' -d '{
"Request": "getSetupJSON",
"Params": { "device": "123456" },
"token": "userToken"
}'
# 2. Preview. check: true writes nothing. Drop it to write.
curl -s http://192.168.1.10:8000/ -H 'Content-Type: application/json' -d '{
"Request": "setSetupJSON",
"Params": {
"device": "123456",
"fingerprint": "0a5e1f293d50844f",
"check": true,
"settings": { "Measurement": { "Profile 1": { "Filter": "C" } } }
},
"token": "userToken"
}'"restarted": true. Preview with check: true first.What you get back
The answer names three things. Groups are JSON objects nested in the same order as the setup editor's menus. A setting is text, exactly as the editor shows it. The example below is shortened: the SV 303's full tree has 187 settings in six top groups.
{
"Request": "getSetupJSON",
"Status": "ok",
"Response": {
"deviceType": 303,
"fingerprint": "0a5e1f293d50844f",
"settings": {
"Function": { "Measurement Function": "1/3 Octave" },
"Measurement": {
"General Settings": {
"Start Delay": "0s",
"Integration Period": "1 h",
"Rolling Time(1)": "30m"
},
"Profile 1": { "Filter": "A", "Peak Filter": "C", "Detector": "Fast" },
"Logging": {
"Logger Setup": { "Logger": "On", "Logger Step": "1 s", "Logger Name": "L105" }
}
},
"Display": {},
"Instrument": {},
"Auxiliary Setup": {},
"Communication": {}
}
}
}
- deviceType
- The instrument type as a number, here
303.listSetupsreports the same type as text,"303". - fingerprint
- A short digest of every value an edit can change. Send it back with
setSetupJSON. If the setup changed in between, SvanLINK refuses the write instead of overwriting the newer setup. Date and time settings are not part of it. - settings
- The tree. Use every name exactly as it comes back: names depend on the instrument type and firmware. When two siblings would share a name, the name gets a suffix, such as
Rolling Time(1)orLevel (XX)with the instrument's code.
Measurement/Profile 1/Filter, and SvanLINK reports changes the same way. Some names contain a slash themselves, such as 1/3 Octave Source, so in code keep a path as a list of names.Values or full detail
By default each setting is only its value. Add "options": true to the read and each setting becomes an object that also says what it accepts and when the editor shows it. Hidden settings are included too, so the answer is much larger: 441 settings instead of 187 for this SV 303.
Use the values tree to read or change a few known settings. Use full detail when you build a form, validate input before sending it, or need to know why a setting is hidden.
"Logger Setup": {
"Logger": "On",
"Logger Step": "1 s"
}
"Logger Step": {
"code": "DB",
"type": "msec",
"value": "1 s",
"raw": "1000",
"visible": true,
"shownWhen": "Logger is On",
"baseVisible": true,
"visibility": [
{ "when": { "param": "DA", "op": "==", "value": "0" }, "show": false }
],
"description": "Specifies how often results will be saved in the logger file.",
"hint": "e.g. 500 ms, 2 s, 1 m",
"ranges": [
{ "label": "100 ms", "from": 100, "to": 100 },
{ "label": "125 ms", "from": 125, "to": 125 },
{ "label": "200 ms", "from": 200, "to": 200 },
{ "label": "500 ms", "from": 500, "to": 500 },
{ "label": "1 s … 1 m, every 1 s", "from": 1000, "to": 60000, "step": 1000 },
{ "label": "1 m … 1 h, every 1 m", "from": 60000, "to": 3600000, "step": 60000 }
]
}
The same setting, Measurement/Logging/Logger Setup/Logger Step, in both shapes. DA is the code of Logger; its raw value 0 means Off.
Setting fields
With "options": true every setting has code, type, value and visible. The other fields appear only when they apply.
| Field | What it holds | Use it to |
|---|---|---|
| code | The instrument's own code for the setting, such as BI0. | Store a reference that survives renamed groups. Codes are the same across languages. |
| type | bool, enum, flags, text, numeric or a time type (sec, msec, ptime, svantime…). | Pick the input control. See Value types. |
| value | The value as a person reads it. | Show it, edit it, send it back. It is the only field SvanLINK reads on a write. |
| raw | The instrument's stored form, such as "2" for filter A. | Evaluate conditions. Never send it as the value. |
| codes | For a time of day, the codes of its hour, minute and second, in place of raw. | Evaluate conditions on a time of day. |
| visible | Whether the setup editor shows it now. | Hide settings that do not apply. |
| baseVisible visibility | The starting visibility and the rules that change it. | Recompute visible after a local edit. See Show, hide and allow. |
| shownWhen | The same rules in words, such as Logger is On. | Explain to a user why a setting is missing. |
| choices | [{"label", "raw"}]. A choice with allowedIf is legal only while that condition holds; allowedWhen says it in words. | Fill a dropdown or a set of checkboxes. |
| ranges | [{"label", "from", "to", "step"}] in the raw unit; step defaults to 1. | Validate a number or time before sending it. |
| text | A text's limits: maxLength or maxBytes, and the allowed characters. | Limit a text field. |
| hint | A unit (Hz, or 0.01dB when the number counts in that unit) or a typing example. | Label or placeholder of the input. |
| accepts | What it takes when nothing above says, such as any number (0.01dB). | Show as help text. |
| description | The help text from the instrument's documentation. | Show as help text. |
| readOnly | true when the value cannot be set. | Show the value without an input. |
Value types
Always read and send the readable form in value. SvanLINK converts it to the instrument's form and refuses anything the instrument would not accept. Every row below is a real SV 303 setting, and every value in the last column was accepted.
| Type | Setting | As read | How to write it | Example |
|---|---|---|---|---|
| bool | Speaker check schedule/System Check | Off | One of the two choice labels. | On |
| enum | Profile 1/Filter | A | One choice label: Z, A, C, B, LF. | C |
| flags | Logger Results/Profile 1 | Lpeak, Lmax, Lmin, Leq, LR(1), LR(2) | Labels joined with a comma and a space. raw is the sum of the chosen bits: 111 = 1+2+4+8+32+64. | Lpeak, Lmax, Leq |
| text | Logger Setup/Logger Name | L105 | At most 8 characters from 0-9, A-Z, _ and space. | NIGHT_1 |
| numeric | Calibration/Level | 11400 | A whole number in the unit of hint, here 0.01 dB. | 11400 = 114.00 dB |
| sec | General Settings/Start Delay | 0s | A duration such as 30 s, 5m or 1h, inside ranges (seconds). | 10s |
| msec | Logger Setup/Logger Step | 1 s | A duration inside ranges (milliseconds). 1.5 s is refused. | 2 s |
| svantime | General Settings/Integration Period | 1 h | A duration or infinity. | 15m |
| ptime | Logger Setup/Splitting Time 1 (hh:mm) | 00:00 | A time of day, HH:MM. It has codes instead of raw. | 06:30 |
| time_shift | Speaker check schedule/Time | 00:00 | HH:MM between 00:00 and 23:59. | 05:30 |
Other instruments add types such as min and time_min. They follow the same pattern: read hint and ranges, send readable text.
Show, hide and allow
Some settings only matter while another one has a certain value. Logger Step matters only while Logger is On. With "options": true each setting carries these rules, so you can show the same form the setup editor shows and update it while the user edits.
To work out whether a setting is shown:
- Collect the raw value of every setting by its code:
raw, or for a time of day each part ofvalueunder itscodes. - Start from
baseVisible. - Go through
visibilityin order. When a rule'swhenholds, set the result to itsshow. The last rule that holds wins.
A condition is {"param", "op", "value"}, or {"and": [...]} or {"or": [...]} around more conditions, or plain true or false when SvanLINK has already worked it out. param is a code. == and != compare the raw values as text; <, >, <= and >= compare them as whole numbers.
Choices use the same conditions. On this SV 303, Measurement Function offers 1/1 Octave only while the instrument's hidden 1/3 Octave Locked setting (YE) is 1. It is 0, so SvanLINK refuses that choice and says why.
def holds(condition, raw):
if isinstance(condition, bool):
return condition
if "and" in condition:
return all(holds(c, raw) for c in condition["and"])
if "or" in condition:
return any(holds(c, raw) for c in condition["or"])
current, op, value = raw.get(condition["param"]), condition["op"], condition["value"]
if current is None:
return False
if op in ("==", "!="):
return (str(current).strip() == value) == (op == "==")
try:
left, right = int(current), int(value)
except ValueError:
return False
return {"<": left < right, ">": left > right, "<=": left <= right, ">=": left >= right}[op]
def is_visible(setting, raw):
shown = setting.get("baseVisible", setting["visible"])
for rule in setting.get("visibility", []):
if holds(rule["when"], raw):
shown = rule["show"]
return shownfunction holds(condition, raw) {
if (typeof condition === 'boolean') return condition;
if (condition.and) return condition.and.every((c) => holds(c, raw));
if (condition.or) return condition.or.some((c) => holds(c, raw));
const current = raw[condition.param];
const { op, value } = condition;
if (current == null) return false;
if (op === '==' || op === '!=') return (String(current).trim() === value) === (op === '==');
const left = Number(current), right = Number(value);
if (!Number.isInteger(left) || !Number.isInteger(right)) return false;
return { '<': left < right, '>': left > right, '<=': left <= right, '>=': left >= right }[op];
}
function isVisible(setting, raw) {
let shown = setting.baseVisible ?? setting.visible;
for (const rule of setting.visibility ?? []) if (holds(rule.when, raw)) shown = rule.show;
return shown;
}Try it
Fifteen settings from an SV 303 setup (#123456), in full detail. Pick a setting and change its value to see the object and the request that would change it. Set Logger to Off and watch the logger settings hide. Nothing is sent anywhere: the request is built on this page.
Send changes
setSetupJSON takes the setting you change in one of three forms. In each case SvanLINK compares with the setup it reads from the instrument, writes only what differs and leaves every other setting as it was.
A branch of the tree
Send the groups down to the settings you change, with the new values as text. Several settings, even in different groups, go in one request. Related settings are checked together against the setup as it will be after all of them, so switching the logger on and setting its step in one request works.
{
"device": "123456",
"fingerprint": "0a5e1f293d50844f",
"settings": {
"Measurement": {
"Profile 1": { "Filter": "C" },
"Logging": { "Logger Setup": { "Logger": "On", "Logger Step": "500 ms", "Logger Name": "NIGHT_1" } }
}
}
}Objects from a full-detail read
A setting may also be {"value": "C"}. That is the shape "options": true returns, so a form can send its edited objects back unchanged. Only value is read.
{
"device": "123456",
"fingerprint": "0a5e1f293d50844f",
"settings": { "Measurement": { "Profile 1": { "Filter": { "code": "BI0", "type": "enum", "value": "C" } } } }
}One setting by name
Send name and value instead of settings. name can be the setting's name when it is unique, its code, or its group and name.
{ "device": "123456", "name": "BI0", "value": "C" }
{ "device": "123456", "name": "Profile 1/Filter", "value": "C" }
{ "device": "123456", "name": "Logger Step", "value": "2 s" }Filter alone is refused on this instrument: four settings have that name, and the error lists them with their codes.
Check, then write
- Always send the
fingerprintof the read you based the change on. - Send the request with
"check": true. It answers withchangedand writes nothing. - Send the same request without
checkto write it.
Answers and errors
After a write
{
"Request": "setSetupJSON",
"Status": "ok",
"Response": {
"changed": [
{ "path": "Measurement/Profile 1/Filter", "code": "BI0", "before": "A", "after": "C" },
{ "path": "Measurement/Logging/Logger Setup/Logger Step", "code": "DB", "before": "1 s", "after": "2 s" }
],
"restarted": true
}
}- changed
- One entry per setting that now differs, with its path, code and readable values before and after.
- restarted
truewhen a running measurement was stopped for the write and started again.falsefor a check, for a stored setup and when nothing needed writing.- message
- Only when nothing was written:
Check only: nothing was written, orEvery value already matches the device.
Errors
On an error Status is "error" and StatusMessage says why. Several problems are joined into one message. These are real messages from the SV 303.
| StatusMessage | What to do |
|---|---|
| The setup on the device changed since it was read. Read it again with getSetupJSON and redo the changes. | Someone changed the setup after your read. Read again, apply your change to the new tree, send the new fingerprint. |
| Unknown setting: Measurement/Profile 1/Filtr | The name is not in this instrument's setup. Copy names from the tree you read. |
| Measurement/Profile 1/Filter: 'Filter' has no option 'X'. Allowed: Z; A; C; B; LF | Send one of the listed labels. |
| Measurement/Logging/Logger Setup/Logger Step: 'Logger Step': expected a time like 500 ms, 2 s or 1 m — got '1.5 s' | Send a value inside ranges. |
| Measurement/Logging/Logger Setup/Logger Name: at most 8 characters, got 11 | Keep within text.maxLength and text.characters. |
| Function/Measurement Function: can't be '1/1 Octave' unless 1/3 Octave Locked is 1 | That choice is not allowed with the instrument's current settings. Offer only the choices whose allowedIf holds. |
| Device 123456 is not connected | Check the serial number and that the instrument is connected to SvanLINK. |
| Failed to download setup from device (timeout or connection error) | The instrument did not answer in time. Try again. |
| Either "device", or "setup" and "type", are required | Send exactly one target: a connected instrument, or a stored setup. |
Stored setups
Both requests also work on a setup file stored on SvanLINK. Send setup (its name) and type (as listSetups reports it) instead of device. setSetupJSON then saves the edited file over the stored one and no instrument is touched.
{
"setup": "Night survey",
"type": "303",
"settings": { "Measurement": { "Logging": { "Logger Setup": { "Logger Step": "500 ms" } } } }
}Ready-made parsers
Two small modules that do the work of this guide. Copy one into your project. Both have no dependencies apart from requests in Python.
read()returns aSetupthat walks the tree, looks up settings by path, name or code, and works in either shape.raw_values(),is_visible()andallowed_choices()apply the rules from Show, hide and allow.patch()turns{path: value}into the nested tree.change()sends it with the fingerprint of the read.
"""Read, inspect and change instrument settings through SvanLINK's setup JSON.
from svanlink_setup import SvanLink
svanlink = SvanLink("http://192.168.1.10:8000", token="userToken")
setup = svanlink.read("123456") # getSetupJSON
print(setup.get("Measurement/Profile 1/Filter")) # A
change = {"Measurement/Profile 1/Filter": "C"}
print(svanlink.change(setup, change, check=True)) # what would change
svanlink.change(setup, change) # write it
Needs Python 3.8+ and `requests`.
"""
import requests
class SvanLinkError(Exception):
"""SvanLINK answered with Status "error". The message says why."""
def _path(path):
"""'Measurement/Profile 1/Filter' or a tuple of names -> a tuple of names."""
return tuple(path.split("/")) if isinstance(path, str) else tuple(path)
def _is_setting(node):
# In the values tree a setting is text. With "options": true it is an
# object that has a code and a type. A group is any other object.
return not isinstance(node, dict) or ("code" in node and "type" in node)
def patch(changes):
"""{"Measurement/Profile 1/Filter": "C", ...} -> the nested tree setSetupJSON takes.
A key can also be a tuple of names, which you need for names that contain
"/" (such as "1/3 Octave Source"); find() returns paths in that form."""
tree = {}
for path, value in changes.items():
*groups, name = _path(path)
node = tree
for group in groups:
node = node.setdefault(group, {})
node[name] = value
return tree
def holds(condition, raw):
"""Whether a visibility `when` or a choice's `allowedIf` holds for these raw values."""
if isinstance(condition, bool): # already worked out by SvanLINK
return condition
if "and" in condition:
return all(holds(c, raw) for c in condition["and"])
if "or" in condition:
return any(holds(c, raw) for c in condition["or"])
current = raw.get(condition["param"])
if current is None:
return False
op, value = condition["op"], condition["value"]
if op == "==":
return str(current).strip() == value
if op == "!=":
return str(current).strip() != value
try: # < > <= >= compare whole numbers
left, right = int(current), int(value)
except (TypeError, ValueError):
return False
return {"<": left < right, ">": left > right, "<=": left <= right, ">=": left >= right}[op]
class Setup:
"""One getSetupJSON answer."""
def __init__(self, response, serial=None):
self.serial = serial
self.device_type = response["deviceType"]
self.fingerprint = response["fingerprint"]
self.tree = response["settings"]
def items(self):
"""(path, setting) for every setting. A path is a tuple of names."""
def walk(node, path):
for name, child in node.items():
if _is_setting(child):
yield path + (name,), child
else:
yield from walk(child, path + (name,))
return walk(self.tree, ())
@property
def detailed(self):
"""True when read with "options": true."""
return any(isinstance(setting, dict) for _, setting in self.items())
def get(self, path):
"""The setting at a path: text, or an object when detailed."""
node = self.tree
for name in _path(path):
node = node[name]
return node
def value(self, path):
"""The readable value at a path, in either form."""
setting = self.get(path)
return setting["value"] if isinstance(setting, dict) else setting
def find(self, name):
"""Paths of every setting with this name, or with this code when detailed."""
return [path for path, setting in self.items()
if path[-1] == name or (isinstance(setting, dict) and setting.get("code") == name)]
def raw_values(self):
"""{code: raw} for every setting, the values conditions compare. Detailed only."""
raw = {}
for _, setting in self.items():
if "raw" in setting:
raw[setting["code"]] = setting["raw"]
elif "codes" in setting: # a time of day: hour, minute, second
for code, part in zip(setting["codes"], setting["value"].split(":")):
raw[code] = str(int(part))
return raw
def is_visible(self, setting, raw=None):
"""Whether the editor shows a setting, for these raw values (default: as read)."""
if "visibility" not in setting:
return setting["visible"]
raw = self.raw_values() if raw is None else raw
shown = setting["baseVisible"]
for rule in setting["visibility"]: # in order; the last rule that holds wins
if holds(rule["when"], raw):
shown = rule["show"]
return shown
def allowed_choices(self, setting, raw=None):
"""The choices that are legal now (a choice with allowedIf only while it holds)."""
raw = self.raw_values() if raw is None else raw
return [c for c in setting.get("choices", []) if holds(c.get("allowedIf", True), raw)]
class SvanLink:
"""A SvanLINK host. `url` is its API address, e.g. http://192.168.1.10:8000"""
def __init__(self, url, token=None, timeout=120):
self.url = url.rstrip("/") + "/"
self.token = token
self.timeout = timeout # reading and writing a setup takes seconds
def call(self, request, **params):
body = {"Request": request, "Params": params}
if self.token:
body["token"] = self.token
reply = requests.post(self.url, json=body, timeout=self.timeout)
reply.raise_for_status()
data = reply.json()
if data.get("Status") != "ok":
raise SvanLinkError(data.get("StatusMessage") or request + " failed")
return data["Response"]
def read(self, serial, options=False):
"""The connected instrument's current setup."""
return Setup(self.call("getSetupJSON", device=serial, options=options), serial)
def change(self, setup, changes, check=False):
"""Write {path: value} to the instrument `setup` was read from.
Refused if its setup changed since that read. check=True only reports
what would change. A running measurement is stopped for the write and
started again ("restarted": true)."""
return self.call("setSetupJSON", device=setup.serial, fingerprint=setup.fingerprint,
settings=patch(changes), check=check)// svanlink-setup.js: read, inspect and change instrument settings through SvanLINK's setup JSON.
//
// import { SvanLink } from './svanlink-setup.js';
// const svanlink = new SvanLink('http://192.168.1.10:8000', 'userToken');
// const setup = await svanlink.read('123456'); // getSetupJSON
// setup.value('Measurement/Profile 1/Filter'); // 'A'
// const change = { 'Measurement/Profile 1/Filter': 'C' };
// await svanlink.change(setup, change, { check: true }); // what would change
// await svanlink.change(setup, change); // write it
//
// Works in Node 18+ and in browsers (both have fetch).
export class SvanLinkError extends Error {}
const toPath = (path) => (typeof path === 'string' ? path.split('/') : [...path]);
// In the values tree a setting is text. With "options": true it is an object
// that has a code and a type. A group is any other object.
const isSetting = (node) =>
typeof node !== 'object' || node === null || ('code' in node && 'type' in node);
/**
* { 'Measurement/Profile 1/Filter': 'C', ... } -> the nested tree setSetupJSON takes.
* Also takes [[path, value], ...], where a path can be an array of names
* (needed for names that contain '/', such as '1/3 Octave Source').
*/
export function patch(changes) {
const tree = {};
for (const [path, value] of Array.isArray(changes) ? changes : Object.entries(changes)) {
const names = toPath(path);
const name = names.pop();
let node = tree;
for (const group of names) node = node[group] ??= {};
node[name] = value;
}
return tree;
}
/** Whether a visibility `when` or a choice's `allowedIf` holds for these raw values. */
export function holds(condition, raw) {
if (typeof condition === 'boolean') return condition; // already worked out by SvanLINK
if (condition.and) return condition.and.every((c) => holds(c, raw));
if (condition.or) return condition.or.some((c) => holds(c, raw));
const current = raw[condition.param];
if (current === undefined || current === null) return false;
const { op, value } = condition;
if (op === '==') return String(current).trim() === value;
if (op === '!=') return String(current).trim() !== value;
const left = Number(current), right = Number(value); // < > <= >= compare whole numbers
if (!Number.isInteger(left) || !Number.isInteger(right)) return false;
return { '<': left < right, '>': left > right, '<=': left <= right, '>=': left >= right }[op];
}
/** One getSetupJSON answer. */
export class Setup {
constructor(response, serial) {
this.serial = serial;
this.deviceType = response.deviceType;
this.fingerprint = response.fingerprint;
this.tree = response.settings;
}
/** [path, setting] for every setting. A path is an array of names. */
*items(node = this.tree, path = []) {
for (const [name, child] of Object.entries(node)) {
if (isSetting(child)) yield [[...path, name], child];
else yield* this.items(child, [...path, name]);
}
}
/** True when read with "options": true. */
get detailed() {
for (const [, setting] of this.items()) return typeof setting === 'object';
return false;
}
/** The setting at a path: text, or an object when detailed. */
get(path) {
return toPath(path).reduce((node, name) => node[name], this.tree);
}
/** The readable value at a path, in either form. */
value(path) {
const setting = this.get(path);
return typeof setting === 'object' ? setting.value : setting;
}
/** Paths (arrays of names) of every setting with this name, or with this code when detailed. */
find(name) {
const found = [];
for (const [path, setting] of this.items()) {
if (path.at(-1) === name || setting?.code === name) found.push(path);
}
return found;
}
/** { code: raw } for every setting, the values conditions compare. Detailed only. */
rawValues() {
const raw = {};
for (const [, setting] of this.items()) {
if ('raw' in setting) raw[setting.code] = setting.raw;
else if (setting.codes) { // a time of day: hour, minute, second
setting.value.split(':').forEach((part, i) => {
if (setting.codes[i]) raw[setting.codes[i]] = String(parseInt(part, 10));
});
}
}
return raw;
}
/** Whether the editor shows a setting, for these raw values (default: as read). */
isVisible(setting, raw = this.rawValues()) {
if (!setting.visibility) return setting.visible;
let shown = setting.baseVisible;
for (const rule of setting.visibility) { // in order; the last rule that holds wins
if (holds(rule.when, raw)) shown = rule.show;
}
return shown;
}
/** The choices that are legal now (a choice with allowedIf only while it holds). */
allowedChoices(setting, raw = this.rawValues()) {
return (setting.choices ?? []).filter((c) => holds(c.allowedIf ?? true, raw));
}
}
/** A SvanLINK host. `url` is its API address, e.g. http://192.168.1.10:8000 */
export class SvanLink {
constructor(url, token) {
this.url = url.replace(/\/+$/, '') + '/';
this.token = token;
}
async call(request, params) {
const body = { Request: request, Params: params };
if (this.token) body.token = this.token;
const reply = await fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!reply.ok) throw new SvanLinkError(`${request}: HTTP ${reply.status}`);
const data = await reply.json();
if (data.Status !== 'ok') throw new SvanLinkError(data.StatusMessage || `${request} failed`);
return data.Response;
}
/** The connected instrument's current setup. */
async read(serial, { options = false } = {}) {
return new Setup(await this.call('getSetupJSON', { device: serial, options }), serial);
}
/** Write { path: value } to the instrument `setup` was read from. See the Python version. */
change(setup, changes, { check = false } = {}) {
return this.call('setSetupJSON', {
device: setup.serial, fingerprint: setup.fingerprint, settings: patch(changes), check,
});
}
}Recipes
Each recipe uses the ready-made parser, with svanlink created as in its first lines.
Use the same filter on every profile
setup = svanlink.read("123456")
changes = {path: "A" for path in setup.find("Filter") if path[-2].startswith("Profile")}
print(svanlink.change(setup, changes, check=True)["changed"])
svanlink.change(setup, changes)const setup = await svanlink.read('123456');
const changes = setup.find('Filter')
.filter((path) => path.at(-2).startsWith('Profile'))
.map((path) => [path, 'A']);
console.log((await svanlink.change(setup, changes, { check: true })).changed);
await svanlink.change(setup, changes);Apply one change to several instruments
Read each instrument first. Each read has its own fingerprint, and names can differ between instrument types, so look the setting up in every tree.
for serial in ["123456", "168718", "45123"]:
setup = svanlink.read(serial)
paths = setup.find("Logger Step")
if len(paths) != 1:
print(serial, "skipped: Logger Step found", len(paths), "times")
continue
result = svanlink.change(setup, {paths[0]: "1 s"})
print(serial, result["changed"] or "already 1 s", "(restarted)" if result["restarted"] else "")for (const serial of ['123456', '168718', '45123']) {
const setup = await svanlink.read(serial);
const paths = setup.find('Logger Step');
if (paths.length !== 1) {
console.log(serial, `skipped: Logger Step found ${paths.length} times`);
continue;
}
const result = await svanlink.change(setup, [[paths[0], '1 s']]);
console.log(serial, result.changed.length ? result.changed : 'already 1 s', result.restarted ? '(restarted)' : '');
}List what each setting accepts
A starting point for a form: every setting the editor shows now, with the values it takes.
setup = svanlink.read("123456", options=True)
raw = setup.raw_values()
for path, s in setup.items():
if not setup.is_visible(s, raw) or s.get("readOnly"):
continue
accepts = ([c["label"] for c in setup.allowed_choices(s, raw)]
+ [r["label"] for r in s.get("ranges", [])])
print(" > ".join(path), "=", s["value"], "| accepts:",
"; ".join(accepts) or s.get("accepts") or s.get("hint", ""))const setup = await svanlink.read('123456', { options: true });
const raw = setup.rawValues();
for (const [path, s] of setup.items()) {
if (!setup.isVisible(s, raw) || s.readOnly) continue;
const accepts = [
...setup.allowedChoices(s, raw).map((c) => c.label),
...(s.ranges ?? []).map((r) => r.label),
];
console.log(path.join(' > '), '=', s.value, '| accepts:', accepts.join('; ') || s.accepts || s.hint || '');
}Function > Calibration > Speaker check schedule > System Check = Off | accepts: Off; On
Function > Calibration > Level = 11400 | accepts: any number (0.01dB)
Function > Calibration > Max. file size = 10 | accepts: No limit; 1 … 255
Function > Calibration > Post Calibration = Files after last calibration | accepts: Off; Last file; Files after last calibration
Function > Calibration > Auto Calibration = Off | accepts: Off; On
Function > Measurement Function = 1/3 Octave | accepts: Level Meter; 1/3 Octave
Measurement > General Settings > Start Delay = 0s | accepts: 0s … 1m; 1m … 1h, every 1m
Measurement > General Settings > Start Sync. = 1s | accepts: Off; 1s; 1m; 15m; 30m; 1h
Measurement > General Settings > Integration Period = 1 h | accepts: infinity … inf.
… 178 morePitfalls
- Names belong to one instrument type
- Group and setting names come from the instrument type and its firmware. Read before you write and look settings up in that tree. If you must store a reference, store the
code. - A name can contain a slash
Measurement/Measurement Trigger/1/3 Octave Sourceis three groups and one name, not five names. Keep paths as lists of names in code.- A name can repeat
- The SV 303 has
Filterin Profile 1, 2 and 3 and in Spectrum. In a tree that is never a problem. Withnameandvalue, use the code or the group and name. - Hidden settings
- The values tree leaves out settings the editor hides. Full detail includes them with
visible: false. You can still set them. - Send value, compare raw
- Send the readable
value.rawis for evaluating conditions only. - Give the requests time
- A read downloads the setup from the instrument and a write uploads it, each over the instrument's own link. Allow a minute before timing out.