Appearance
Reference
Lookup surface for muxen-sensors. Everything here is what the shipped binary, unit and package do.
Command line
muxen-sensors [OPTION]| Short | Long | Argument | Default | Meaning |
|---|---|---|---|---|
-h | --help | — | — | print the usage block on stdout and exit 0 |
-V | --version | — | — | print the version and exit 0 |
-v | --verbose | — | 0 | raise verbosity; repeatable |
-i | --interface | canX | none | CAN interface to read; required unless --mqtt |
-m | --mqtt | — | off | take readings from MQTT device/… topics instead of CAN |
-s | --sensors | path | none | configuration file; always required |
-p | --prefix | topic | app/sensor | prefix the published topic is built on, as <prefix>/<name> |
| — | --json-pretty | — | off | indent every published JSON payload, for debugging; compact otherwise |
--sensorsis mandatory in every mode.--interfaceis mandatory unless--mqttis given, and the two are mutually exclusive in effect: with--mqtt, no CAN socket is opened and no CAN traffic is sent or received.- The interface name is truncated to 16 characters.
--prefixis rejected — usage block, non-zero exit — when it is empty, starts or ends with/, contains an empty level (//), or contains+or#. Each of those builds a topic that is legal MQTT and that no sensible subscription matches, so the daemon refuses to start rather than publish somewhere nobody is listening.variableis the prefix used before 5.0.0; nothing runs on it now.- An invalid invocation prints the usage block on stderr and exits non-zero.
--helpprints it on stdout and exits 0.
Verbosity levels:
-v count | Effect |
|---|---|
| 0 | configuration summary, the variable list, the dispatcher table, state changes |
| 1 | adds the per-variable tree (channel, limits, curve, coefficients, points) and CAN monitor detail |
| 2 | adds a trace of every CAN frame and every MQTT message |
Bash completion for these flags is installed with the package at /usr/share/bash-completion/completions/muxen-sensors.
The systemd unit
muxen-sensors.service.
| Setting | Value |
|---|---|
ExecStart | /usr/bin/muxen-sensors --interface can0 --sensors ${MUXEN_DEPLOY} --prefix ${MUXEN_TOPIC_PREFIX} |
Environment | MUXEN_DEPLOY=/etc/muxen/deploy.json, MUXEN_TOPIC_PREFIX=app/sensor |
User / Group | muxen / muxen |
PartOf | muxen-deploy.target |
After | mosquitto.service |
WantedBy | muxen-deploy.target |
Restart | always, RestartSec=30 |
ConfigurationDirectory | muxen, mode 0755 |
The unit never passes --mqtt. Running from MQTT requires a drop-in that replaces ExecStart:
sh
sudo systemctl edit muxen-sensorsini
[Service]
ExecStart=
ExecStart=/usr/bin/muxen-sensors --mqtt --sensors ${MUXEN_DEPLOY}The unit is hardened: NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateDevices, PrivateTmp, ProtectClock, ProtectKernelModules, ProtectKernelTunables, ProtectKernelLogs, ProtectProc=invisible, ProtectControlGroups, ProtectHostname, LockPersonality, RemoveIPC, RestrictNamespaces, RestrictNetworkInterfaces=lo, RestrictRealtime, RestrictSUIDSGID, SystemCallArchitectures=native, a SystemCallFilter denying @clock @cpu-emulation @debug @module @mount @obsolete @privileged @raw-io @reboot @resources @swap, and a CapabilityBoundingSet that drops 21 capabilities. SystemCallErrorNumber=EPERM.
If the daemon suddenly cannot open its socket after a system upgrade, the hardening block is the first thing to compare.
Configuration
A JSON file whose root object carries a sensors array. Every other key in the file, and every unrecognised key inside an entry, is ignored.
| Key | Type | Default | Notes |
|---|---|---|---|
name | string | — | required; the topic suffix; may not contain /, #, + or a space |
input | object | — | required |
input.deviceType | integer | 0 | must be 1, 3 or 17; 0 is refused |
input.deviceInstance | integer | 0 | 0–63 |
input.deviceChannel | string | channel 0 | must be valid for the device type |
input.min | number | −∞ | reading clamped up to this |
input.max | number | +∞ | reading clamped down to this |
output | object | — | required |
output.unit | string | absent | copied verbatim; omitted from the payload when unset |
output.min | number | −∞ | result clamped up to this |
output.max | number | +∞ | result clamped down to this |
scaleType | string | identity | identity, polynomial, linear, step |
polynomial | array of numbers | — | required with scaleType: polynomial; at most 5, lowest order first |
linear | array of [number, number] | — | required with scaleType: linear; at most 32, strictly increasing by reading |
step | array of [number, number] | — | required with scaleType: step; same limits |
liquid | object | absent | republished verbatim when non-empty |
liquid.filterPeriod | integer | absent | enables the filter; clamped to 12…900 |
min and max accept both JSON integers and JSON reals. deviceType, deviceInstance and filterPeriod must be integers.
Channels
The input.channel field of the payload carries the numeric channel, which is what these names resolve to:
deviceType | deviceChannel | input.channel |
|---|---|---|
| 1 — Bloc 8 | digitalInput0 … digitalInput5 | 0 … 5 |
| 1 — Bloc 8 | analogInput0 … analogInput2 | 6 … 8 |
| 3 — Interconnection | source0 … source3 | 0 … 3 |
| 17 — Generic I/O | relay0, relay1 | 0, 1 |
| 17 — Generic I/O | digitalInput0, digitalInput1 | 2, 3 |
| 17 — Generic I/O | analogInput0, analogInput1 | 4, 5 |
Scaling
scaleType | Result |
|---|---|
identity | the clamped reading |
polynomial | c0 + c1·x + c2·x² + c3·x³ + c4·x⁴, missing coefficients zero |
linear | linear interpolation between the bracketing points; flat outside the table |
step | the result of the highest point whose reading has been reached; the first point's result below the table |
Order of operations: clamp to input.min/input.max, apply the curve, clamp to output.min/output.max, then filter if one is configured.
Filter
Present only when liquid.filterPeriod is set. The window holds filterPeriod samples, one per second, fed by the publishing timer whether or not new data arrived. Each publication sorts the window, drops the lowest quarter and the highest half, and averages the rest. The window is pre-filled with the first sample, so there is no settling ramp after a restart.
MQTT
Broker 127.0.0.1:1883, client identifier muxen-sensors, keepalive 30 seconds. The daemon reconnects on its own after a disconnection; it does not wait for the broker at startup — a broker that is down when the daemon starts is a startup failure.
Calibration
Two topics, neither retained, carry a live calibration session. See chapter 3 for the payloads and the session rules.
| Topic | Direction | Retained | Rate |
|---|---|---|---|
app/sensor/calibration/request | in | no | on demand |
app/sensor/calibration/response | out | no | up to 4 Hz per session, 1 Hz while a session has no data |
Subscribed whatever --mqtt is set to: a session reads the device/<function>/<instance>/{io,current,life} topics muxen-boat publishes, not this daemon's own input, and subscribes them for the duration of the session. A session on a BLOC8 also publishes device/<function>/<instance>/rtr/2 while the channel is quiet, which is how a channel no sensor is configured for is made to report.
At most 4 sessions at once; ttl clamped to 10…600 seconds, 120 by default. Sessions live in memory only and never reach the configuration file.
These two topics are fixed and --prefix does not move them: @muxen/sensors and raken hold them as constants, and an install that pins the legacy prefix for its published values still calibrates on app/sensor/calibration/…. One consequence of the default prefix is that app/sensor/# now matches the calibration traffic as well as the values.
Published
<prefix>/<name> — app/sensor/<name> unless --prefix says otherwise — one message per variable per second, compact JSON on one line (--json-pretty indents it). Publication starts with the first reading received; a variable that has never received anything publishes nothing.
json
{
"name": "FreshWaterPortside",
"data": {
"value": 142.8,
"raw": 143.5,
"unit": "obix:units/liter",
"min": 0.0,
"max": 250.0
},
"input": {
"functionCode": 1,
"instance": 1,
"channel": 6,
"value": 2.87,
"min": 0.0,
"max": 5.0
},
"metadata": {
"rxdate": "2026-08-16T09:14:22.310Z",
"rxTimestamp": 1786000462,
"expireAfterSec": 30
},
"liquid": { "filterPeriod": 60 }
}| Field | Always | Meaning |
|---|---|---|
name | yes | the variable name |
data.value | yes | the finished value; filtered when a filter is configured |
data.raw | only when filtered | the same value before filtering — not the sensor reading |
data.unit | when output.unit is set | verbatim from the configuration |
data.min / data.max | when output.min / output.max are set | the declared result limits |
input.functionCode | yes | the source device's function code |
input.instance | yes | the source device's instance |
input.channel | yes | the numeric channel within the device's frame |
input.value | yes | the reading as received, before clamping and curve |
input.min / input.max | when set | the declared reading limits |
metadata.rxdate | yes | UTC time of the last reading received, millisecond resolution |
metadata.rxTimestamp | yes | the same instant in epoch seconds |
metadata.expireAfterSec | yes | always 30 |
liquid | when liquid is non-empty | the configuration block, verbatim |
metadata.rxdate does not advance while a sensor is silent, even though the message keeps being published every second.
Daemon info
app/sensor/info, retained, QoS 1: the daemon identity, the same shape every MUXEN daemon publishes on app/<daemon>/info. Published on every (re)connection with online: true; the same payload with online: false is the last will and is also published on a clean exit, so a subscriber always gets the daemon's last known state. The topic is fixed: --prefix does not move it, since it names the daemon and not a sensor.
The sensor name info is therefore reserved: a sensor of that name in the configuration is skipped at load, with a warning (sensor: warning: sensors[N] is named 'info', reserved for the daemon info (app/sensor/info), ignoring...), and the other sensors load as usual.
json
{
"name": "muxen-sensors",
"version": "v5.1.0",
"hostname": "brain-3",
"features": ["sensor-values", "calibration", "source-can"],
"online": true,
"metadata": { "rxdate": "2026-09-22T07:57:40.696Z", "rxTimestamp": 1790063860, "expireAfterSec": 3124137600 }
}version is the git describe of the build and is for display only; clients test features and ignore the ones they do not know. expireAfterSec is about 99 years: the info never goes stale, online carries liveness.
| Feature | Meaning |
|---|---|
sensor-values | <prefix>/<name> is published |
calibration | calibration sessions on app/sensor/calibration/… are served |
source-can | readings come from the CAN interface (--interface) |
source-mqtt | readings come from the device/… MQTT topics (--mqtt) |
@muxen/sensors reads it with useSensorsInfo().
Subscribed
Only in --mqtt mode, matched by anchored regular expression:
deviceType | Topic | Regex |
|---|---|---|
| 1 | device/1/<instance>/io | ^device/1/<instance>/io$ |
| 3 | device/3/<instance>/current | ^device/3/<instance>/current$ |
| 17 | device/17/<instance>/life | ^device/17/<instance>/life$ |
The reading is taken from the payload's channel field and time-stamped on arrival with the local clock. In CAN mode the daemon subscribes to nothing.
CAN
Used unless --mqtt is given. A raw AF_CAN socket on the named interface, with kernel receive timestamps — metadata.rxdate is the frame's arrival time, not the publication time.
Received
Extended frames. The identifier is broadcastId << 12 | function << 6 | instance, matched under mask 0xFFFFFF, and the data length must match exactly:
| Function | Frame | Broadcast ID | Identifier, instance 0 | DLC |
|---|---|---|---|---|
| 1 — Bloc 8 | I/O | 2 | 0x002040 | 7 |
| 3 — Interconnection | current | 4 | 0x0040C0 | 8 |
| 17 — Generic I/O | life | 1 | 0x001440 | 5 |
Add the instance to the identifier: instance 1 of a Bloc 8 is 0x002041. The daemon prints the resolved identifier, mask and length per variable at startup.
Every handler matching a frame is invoked, so several variables reading different channels of the same device all update from one frame.
Transmitted
One frame type only: an RTR request for the Bloc 8 I/O frame — extended, remote, zero data length, the same identifier as the I/O frame above. It is sent for every configured Bloc 8 variable each time the CAN interface comes up, including the initial start, because a Bloc 8 reports its inputs in answer to a request rather than spontaneously. Several variables on one Bloc 8 produce one request each.
Nothing else is ever transmitted. No command frame is sent to any device.
Interface supervision
The interface is watched over netlink rather than required at startup.
| Event | Logged as | Action |
|---|---|---|
| interface healthy | can: <if> is UP (state = …, attempt = N) | open the socket if needed, re-issue the Bloc 8 requests |
| interface returned | can: <if> is RETRY (…) | rebuild the socket |
| interface down or absent | can: <if> is DOWN (…) | close the socket, keep retrying |
| controller off the bus | can: <if> is BUS_OFF (…) | close the socket, keep retrying |
Retries are unbounded: the daemon never gives up on the interface.
Files
| Path | Content |
|---|---|
/usr/bin/muxen-sensors | the daemon |
/usr/lib/systemd/system/muxen-sensors.service | the unit |
/etc/muxen/deploy.json | the configuration, as passed by the unit |
Package muxen-sensors. Depends: muxen-systemd (>= 1.2.0), Recommends: muxen-boat (>= 5.0.0). Installing or upgrading it activates the muxen-restart-target dpkg trigger, so dependent services restart with it.
It Breaks: muxen-synapse (<< 2.0.0), muxen-energy (<< 6.0.0), muxen-database (<< 2.0.0) and muxen-diagnostic-tools (<< 2.0.0): the versions of those readers that still subscribe to variable/<name>. Beside this daemon, which publishes on app/sensor/<name> only, they would see no sensor value at all, with no error. Each of them already Depends: muxen-sensors (>= 5.0.0); the Breaks covers the other direction, so apt upgrades them with this package or refuses it.
Exit codes
| Code | Cause |
|---|---|
| 0 | --help or --version; also a clean shutdown on SIGINT / SIGTERM |
| 1 | invalid invocation — missing --sensors, missing --interface without --mqtt, or an unknown option. The usage block goes to stderr |
| 255 | initialisation failed — configuration file unusable, no usable sensor, MQTT broker unreachable, dispatcher or CAN monitor could not be built |
Under systemd, code 255 means a restart 30 seconds later, indefinitely.
Journal messages
All output goes to stdout, and therefore to the journal.
Startup
| Message | Meaning |
|---|---|
config: verbose = N | verbosity level in effect |
config: sensors = <path> | the configuration file being read |
config: data source = can | mqtt | which input mode |
config: can interface = <if> | CAN mode only |
sensors: N variables | entries that survived parsing |
can: dispatcher table size: N + table | resolved CAN identifier, mask, length and name per variable |
mqtt: dispatcher table size: N + table | resolved topic regex and name per variable, --mqtt only |
main: mqtt connected | the broker accepted the connection |
main: mqtt connection failed (rc = N) | it did not |
Configuration errors
| Message | Meaning |
|---|---|
config: --sensors is required | no configuration file given (stderr) |
config: --interface is required unless --mqtt is used | no input source given (stderr) |
sensor: failed to open '<path>' | missing or unreadable |
sensor: failed to get file size for '<path>' | unseekable file |
sensor: failed to allocate N bytes for '<path>' | out of memory |
sensor: failed to read all the file | short read |
sensor: failed to parse the JSON | not valid JSON |
sensor: '<path>' has no 'sensors' key | the array is missing |
sensor: output name missing | entry without name |
sensor: output name '<n>' contains invalid MQTT characters | /, #, + or a space in the name |
sensor: input missing / sensor: output missing | a required object is absent |
sensor: input.deviceType (N) is not supported | not 1, 3 or 17 |
sensor: input.deviceType must be an integer | wrong JSON type |
sensor: input.deviceInstance must be an integer | wrong JSON type |
sensor: input.deviceChannel is unknown (<name>) | not a channel of that device |
sensor: input.deviceChannel must be an string | wrong JSON type |
sensor: input.min must be an integer or a double | wrong JSON type; likewise input.max, output.min, output.max |
sensor: scaleType must be a string | wrong JSON type |
sensor: scaleType '<s>' is unknown | not one of the four |
sensor: polynomial key is missing | scaleType: polynomial without coefficients; likewise step and linear |
polynomial: must be an array | wrong JSON type |
polynomial: order too high (N), max = 5 | too many coefficients |
polynomial: terms must be integer or double | a non-numeric coefficient |
points: must be an array | wrong JSON type |
points: array is empty | no points |
points: count are too high (N), max = 32 | too many points |
points: point (N) must be an array of two double | a malformed point |
points: point (N) input (x) must be less than point (M) input (y) | points out of order |
sensor: failed to load sensors[N], ignoring... | that entry was skipped |
sensor: X of Y sensors loaded | a partial load |
sensor: no usable sensor out of N | nothing survived — fatal |
Runtime
| Message | Meaning |
|---|---|
can: <if> is <REASON> (state = <STATE>, attempt = N) | interface state change |
mqtt: <name> value is not finite (raw = …, value = …), not publishing | the sample was dropped; printed once per transition |
timer: <name>, failed to publish into mqtt | the publish call failed |
dispatcher: <name> has an unsupported deviceType (N), skipping | a device type reached the dispatcher; unreachable from a configuration file |
init: Failed to load the sensors configuration file | fatal |
init: Failed to create mqtt dispatcher / init: Failed to create can dispatcher | fatal |
init: failed to start the CAN monitor | fatal |
