Skip to content

Sensor Configuration Reference

WeSpeakEnglish edited this page Sep 25, 2026 · 1 revision

Sensor Configuration Reference

Every sensor is described by a JSON object. The default list lives in sensors.json; you can also load your own file through Custom JSON Sensor Configuration in the UI.

How a frame is processed

  1. Connect → serial port opened with the port settings.
  2. command (or the commands sequence) is sent.
  3. Incoming bytes are buffered.
  4. A frame is detected using startByte, endByte and length (after un-stuffing, if stuffing is set).
  5. checksum.eval is computed and compared with checksum.compare.
  6. If they match, every data expression is evaluated, charts are updated, the row is stored for CSV, and the packet is logged with ✅. Otherwise it is logged with ❌.

Top-level structure

{
  "sensors": [
    { "...": "sensor object" }
  ]
}

Sensor object

Field Required Type Description
name yes string Unique name shown in the dropdown
inherits_from no string Name of another sensor to copy settings from — see Inheritance
command no string Hex string sent on connection, e.g. "7E 00 03 00 FC 7E", or "none"
send_cmd_period no number 0 = send command once; > 0 = resend every N seconds
start_command no string Hex string sent after connect. Classic mode only — ignored if commands is present
stop_command no string Hex string sent on disconnect. Works in both classic and multi-command modes
commands no array Command sequence — see Multi-Command Sensors
port yes object Serial settings
frame yes object Frame detection
checksum yes* object Checksum validation
data yes* object Signal definitions

* In multi-command mode, frame, checksum and data can be defined per command instead.

port

Field Type Example
baudRate integer 9600, 19200, 115200
dataBits integer 8
stopBits integer 1
parity string "none", "even", "odd"

frame

Field Required Description
startByte yes One or more start bytes: [66, 77], ["0x42", "0x4D"], 170, "0xAA", or "none"
endByte yes Terminator, same formats as startByte
length yes Total frame length including start/end bytes. With byte-stuffing, this is the length after un-stuffing
stuffing no Pairs of sequence to find → replacement, e.g. [["7D 5E", "0x7E"], ["7D 5D", "0x7D"]]

checksum

Both fields are JavaScript expressions where data[i] is the i-th byte of the frame (a normal JS array, so slice, reduce etc. work).

Field Description Example
eval Computes the checksum "data.slice(0, 30).reduce((a, b) => (a + b) & 0xFFFF, 0)"
compare The value it must equal "(data[30] << 8) + data[31]"

Common patterns:

Algorithm eval
16-bit sum data.slice(0, N).reduce((a, b) => (a + b) & 0xFFFF, 0)
8-bit sum data.slice(1, N).reduce((a, b) => (a + b) & 0xFF, 0)
XOR data.slice(1, N).reduce((a, b) => a ^ b, 0)
Two's complement (0x100 - (data.slice(1, N).reduce((a, b) => a + b, 0) & 0xFF)) & 0xFF
Sensirion SHDLC (SPS30, SEN55) 0xFF-(data.slice(1, N).reduce((a,b)=>a+b,0)&0xFF) — see SEN5x UART Protocol

data

Each key is a signal name (shown in the UI and used as the CSV column).

"data": {
  "PM2.5": {
    "value": "(data[6] << 8) + data[7]",
    "unit": "μg/m³"
  }
}
Field Required Description
value yes JavaScript expression evaluated against the frame bytes
unit yes Unit label, e.g. "μg/m³", "ppm", "%", "°C"

Useful expression patterns:

Need Expression
Unsigned 16-bit big-endian (data[i] << 8) + data[i+1]
Unsigned 16-bit little-endian data[i] + (data[i+1] << 8)
Unsigned 32-bit (force unsigned) ((data[i] << 24) + (data[i+1] << 16) + (data[i+2] << 8) + data[i+3]) >>> 0
Signed 16-bit ((data[i] << 8) | data[i+1]) << 16 >> 16
Scaled value ((data[i] << 8) + data[i+1]) / 10
Calibration factor ((data[6] << 8) + data[7]) * 0.4
IEEE-754 float (big-endian) new DataView(new Uint8Array(data.slice(i, i+4)).buffer).getFloat32(0)

Reading multi-byte values with DataView

For frames with many 16- or 32-bit values, the Sensirion configs create one DataView in the first signal and reuse it in the others:

"data": {
  "PM1.0": { "value": "(dv=new DataView(new Uint8Array(data).buffer),dv.getUint16(5,false)/10)", "unit": "µg/m³" },
  "PM2.5": { "value": "dv.getUint16(7,false)/10", "unit": "µg/m³" },
  "RH":    { "value": "dv.getInt16(13,false)/100", "unit": "%RH" }
}
  • getUint16 / getInt16 / getFloat32(offset, false) read big-endian values; pass true for little-endian.
  • The signal that defines dv must come first in data, because signals are evaluated in order.
  • Offsets are positions in the full un-stuffed frame, including the start byte.

Number formats

  • Use decimal (66) or hex strings ("0x42").
  • Raw hex literals like 0x42 are not valid JSON and will fail to load.

Full example

{
  "sensors": [
    {
      "name": "Honeywell HPMA115S0-XXX",
      "command": "none",
      "port": { "baudRate": 9600, "dataBits": 8, "stopBits": 1, "parity": "none" },
      "frame": { "length": 32, "startByte": [66, 77], "endByte": "none" },
      "data": {
        "PM2.5": { "value": "(data[6] << 8) + data[7]", "unit": "μg/m³" },
        "PM10":  { "value": "(data[8] << 8) + data[9]", "unit": "μg/m³" }
      },
      "checksum": {
        "eval": "data.slice(0, 30).reduce((a, b) => (a + b) & 0xFFFF, 0)",
        "compare": "(data[30] << 8) + data[31]"
      }
    }
  ]
}

A second template is in the repo: custom_example.json.

🔒 Security note: value, eval and compare are executed with JavaScript eval(). Only load JSON files you trust.

Clone this wiki locally