Skip to content

Reference ​

Lookup surface for muxen-sensors. Everything here is what the shipped binary, unit and package do.

Command line ​

muxen-sensors [OPTION]
ShortLongArgumentDefaultMeaning
-h--help——print the usage block on stdout and exit 0
-V--version——print the version and exit 0
-v--verbose—0raise verbosity; repeatable
-i--interfacecanXnoneCAN interface to read; required unless --mqtt
-m--mqtt—offtake readings from MQTT device/… topics instead of CAN
-s--sensorspathnoneconfiguration file; always required
-p--prefixtopicapp/sensorprefix the published topic is built on, as <prefix>/<name>
—--json-pretty—offindent every published JSON payload, for debugging; compact otherwise
  • --sensors is mandatory in every mode.
  • --interface is mandatory unless --mqtt is 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.
  • --prefix is 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. variable is 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. --help prints it on stdout and exits 0.

Verbosity levels:

-v countEffect
0configuration summary, the variable list, the dispatcher table, state changes
1adds the per-variable tree (channel, limits, curve, coefficients, points) and CAN monitor detail
2adds 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.

SettingValue
ExecStart/usr/bin/muxen-sensors --interface can0 --sensors ${MUXEN_DEPLOY} --prefix ${MUXEN_TOPIC_PREFIX}
EnvironmentMUXEN_DEPLOY=/etc/muxen/deploy.json, MUXEN_TOPIC_PREFIX=app/sensor
User / Groupmuxen / muxen
PartOfmuxen-deploy.target
Aftermosquitto.service
WantedBymuxen-deploy.target
Restartalways, RestartSec=30
ConfigurationDirectorymuxen, mode 0755

The unit never passes --mqtt. Running from MQTT requires a drop-in that replaces ExecStart:

sh
sudo systemctl edit muxen-sensors
ini
[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.

KeyTypeDefaultNotes
namestring—required; the topic suffix; may not contain /, #, + or a space
inputobject—required
input.deviceTypeinteger0must be 1, 3 or 17; 0 is refused
input.deviceInstanceinteger00–63
input.deviceChannelstringchannel 0must be valid for the device type
input.minnumber−∞reading clamped up to this
input.maxnumber+∞reading clamped down to this
outputobject—required
output.unitstringabsentcopied verbatim; omitted from the payload when unset
output.minnumber−∞result clamped up to this
output.maxnumber+∞result clamped down to this
scaleTypestringidentityidentity, polynomial, linear, step
polynomialarray of numbers—required with scaleType: polynomial; at most 5, lowest order first
lineararray of [number, number]—required with scaleType: linear; at most 32, strictly increasing by reading
steparray of [number, number]—required with scaleType: step; same limits
liquidobjectabsentrepublished verbatim when non-empty
liquid.filterPeriodintegerabsentenables 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:

deviceTypedeviceChannelinput.channel
1 — Bloc 8digitalInput0 … digitalInput50 … 5
1 — Bloc 8analogInput0 … analogInput26 … 8
3 — Interconnectionsource0 … source30 … 3
17 — Generic I/Orelay0, relay10, 1
17 — Generic I/OdigitalInput0, digitalInput12, 3
17 — Generic I/OanalogInput0, analogInput14, 5

Scaling ​

scaleTypeResult
identitythe clamped reading
polynomialc0 + c1·x + c2·x² + c3·x³ + c4·x⁴, missing coefficients zero
linearlinear interpolation between the bracketing points; flat outside the table
stepthe 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.

TopicDirectionRetainedRate
app/sensor/calibration/requestinnoon demand
app/sensor/calibration/responseoutnoup 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 }
}
FieldAlwaysMeaning
nameyesthe variable name
data.valueyesthe finished value; filtered when a filter is configured
data.rawonly when filteredthe same value before filtering — not the sensor reading
data.unitwhen output.unit is setverbatim from the configuration
data.min / data.maxwhen output.min / output.max are setthe declared result limits
input.functionCodeyesthe source device's function code
input.instanceyesthe source device's instance
input.channelyesthe numeric channel within the device's frame
input.valueyesthe reading as received, before clamping and curve
input.min / input.maxwhen setthe declared reading limits
metadata.rxdateyesUTC time of the last reading received, millisecond resolution
metadata.rxTimestampyesthe same instant in epoch seconds
metadata.expireAfterSecyesalways 30
liquidwhen liquid is non-emptythe 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.

FeatureMeaning
sensor-values<prefix>/<name> is published
calibrationcalibration sessions on app/sensor/calibration/… are served
source-canreadings come from the CAN interface (--interface)
source-mqttreadings come from the device/… MQTT topics (--mqtt)

@muxen/sensors reads it with useSensorsInfo().

Subscribed ​

Only in --mqtt mode, matched by anchored regular expression:

deviceTypeTopicRegex
1device/1/<instance>/io^device/1/<instance>/io$
3device/3/<instance>/current^device/3/<instance>/current$
17device/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:

FunctionFrameBroadcast IDIdentifier, instance 0DLC
1 — Bloc 8I/O20x0020407
3 — Interconnectioncurrent40x0040C08
17 — Generic I/Olife10x0014405

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.

EventLogged asAction
interface healthycan: <if> is UP (state = …, attempt = N)open the socket if needed, re-issue the Bloc 8 requests
interface returnedcan: <if> is RETRY (…)rebuild the socket
interface down or absentcan: <if> is DOWN (…)close the socket, keep retrying
controller off the buscan: <if> is BUS_OFF (…)close the socket, keep retrying

Retries are unbounded: the daemon never gives up on the interface.

Files ​

PathContent
/usr/bin/muxen-sensorsthe daemon
/usr/lib/systemd/system/muxen-sensors.servicethe unit
/etc/muxen/deploy.jsonthe 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 ​

CodeCause
0--help or --version; also a clean shutdown on SIGINT / SIGTERM
1invalid invocation — missing --sensors, missing --interface without --mqtt, or an unknown option. The usage block goes to stderr
255initialisation 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 ​

MessageMeaning
config: verbose = Nverbosity level in effect
config: sensors = <path>the configuration file being read
config: data source = can | mqttwhich input mode
config: can interface = <if>CAN mode only
sensors: N variablesentries that survived parsing
can: dispatcher table size: N + tableresolved CAN identifier, mask, length and name per variable
mqtt: dispatcher table size: N + tableresolved topic regex and name per variable, --mqtt only
main: mqtt connectedthe broker accepted the connection
main: mqtt connection failed (rc = N)it did not

Configuration errors ​

MessageMeaning
config: --sensors is requiredno configuration file given (stderr)
config: --interface is required unless --mqtt is usedno 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 fileshort read
sensor: failed to parse the JSONnot valid JSON
sensor: '<path>' has no 'sensors' keythe array is missing
sensor: output name missingentry without name
sensor: output name '<n>' contains invalid MQTT characters/, #, + or a space in the name
sensor: input missing / sensor: output missinga required object is absent
sensor: input.deviceType (N) is not supportednot 1, 3 or 17
sensor: input.deviceType must be an integerwrong JSON type
sensor: input.deviceInstance must be an integerwrong JSON type
sensor: input.deviceChannel is unknown (<name>)not a channel of that device
sensor: input.deviceChannel must be an stringwrong JSON type
sensor: input.min must be an integer or a doublewrong JSON type; likewise input.max, output.min, output.max
sensor: scaleType must be a stringwrong JSON type
sensor: scaleType '<s>' is unknownnot one of the four
sensor: polynomial key is missingscaleType: polynomial without coefficients; likewise step and linear
polynomial: must be an arraywrong JSON type
polynomial: order too high (N), max = 5too many coefficients
polynomial: terms must be integer or doublea non-numeric coefficient
points: must be an arraywrong JSON type
points: array is emptyno points
points: count are too high (N), max = 32too many points
points: point (N) must be an array of two doublea 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 loadeda partial load
sensor: no usable sensor out of Nnothing survived — fatal

Runtime ​

MessageMeaning
can: <if> is <REASON> (state = <STATE>, attempt = N)interface state change
mqtt: <name> value is not finite (raw = …, value = …), not publishingthe sample was dropped; printed once per transition
timer: <name>, failed to publish into mqttthe publish call failed
dispatcher: <name> has an unsupported deviceType (N), skippinga device type reached the dispatcher; unreachable from a configuration file
init: Failed to load the sensors configuration filefatal
init: Failed to create mqtt dispatcher / init: Failed to create can dispatcherfatal
init: failed to start the CAN monitorfatal

Integration of multiplexed solutions
MUXEN and the MUXEN logo are trademarks of MUXEN SAS.