Appearance
Reference
Everything the package exposes, in tables.
Commands
| Command | Records | Replay |
|---|---|---|
/usr/bin/muxen-can-datalogger | every frame, BUSMASTER text format, one file at a time | yes |
/usr/bin/muxen-csv-datalogger | decoded fields, CSV, one file per device and frame kind | no |
muxen-can-datalogger
muxen-can-datalogger [OPTION]| Short | Long | Argument | Default | Meaning |
|---|---|---|---|---|
-h | --help | — | — | print the usage block on stdout and exit 0 |
-v | --verbose | — | 0 | repeatable; only a second -v has any effect |
-i | --interface | canX | can0 | CAN interface to record or replay onto |
-r | --replay | path | none | replay this capture instead of recording |
-l | --logFolder | path | /var/lib/muxen/datalogger | where captures are written |
| — | --maxRecordTime | seconds | 10800 | age at which a file rotates |
| — | --maxFileSizeBeforeCompression | bytes | 10485760 | size at which a file rotates |
| — | --gzip | value, ignored | — | parsed, never consulted |
| — | --no-gzip | value, ignored | — | parsed, never consulted |
muxen-csv-datalogger
muxen-csv-datalogger [OPTION]| Short | Long | Argument | Default | Meaning |
|---|---|---|---|---|
-h | --help | — | — | print the usage block on stdout and exit 0 |
-v | --verbose | — | 0 | repeatable; only a second -v has any effect |
-i | --interface | canX | can0 | CAN interface to record |
-l | --logFolder | path | /var/lib/muxen/datalogger | where captures are written |
| — | --maxRecordTime | seconds | 10800 | age at which a file rotates |
| — | --maxFileSizeBeforeCompression | bytes | 10485760 | size at which a file rotates |
| — | --gzip | value, ignored | — | parsed, never consulted |
| — | --no-gzip | value, ignored | — | parsed, never consulted |
There is no --replay here, and no --version on either command.
Option parsing, exactly
These are the behaviours that surprise people. All of them are properties of how the options are declared.
| Behaviour | Detail |
|---|---|
--gzip / --no-gzip need a value | both are declared as taking a required argument. Bare, they make the parser fail: usage block, exit 0 |
--gzip / --no-gzip do nothing | the flag they set is never read. Compression is unconditional |
-r and -s are not short forms of the rotation limits | muxen-csv-datalogger declares them for --maxRecordTime and --maxFileSizeBeforeCompression, but neither letter is in its short-option string. Use the long forms |
-r on the raw recorder is --replay | different meaning from the letter declared in the decoded recorder |
--replay matches by abbreviation | the long option is declared with trailing spaces in its name, so --replay matches as a unique prefix rather than exactly. -r avoids the question |
| the rotation limits use a plain integer conversion | a non-numeric value becomes 0, without an error |
--maxRecordTime is clamped | to at most 86400 seconds |
--maxFileSizeBeforeCompression is clamped | to at least 1048576 bytes — on the raw recorder only. The decoded recorder applies no floor |
--interface is truncated | to the platform interface-name length |
an empty --interface prints the usage block | and exits 0 |
| any unrecognised option prints the usage block | and exits 0 |
Verbosity
| Level | Effect |
|---|---|
| none | the configuration summary and the next file index |
-v | the summary reports verbose = 1; nothing else changes |
-vv | enables the CAN library's own frame-level output |
Startup output
Both recorders print their whole configuration before doing anything. Under systemd this lands in the journal.
config: verbose = 0
config: interface = can0
config: replay = (null)
config: logFolder = /var/lib/muxen/datalogger
config: maxRecordTime = 10800
config: maxFileSizeBeforeCompression = 10485760
busmaster: next log file index is 1The config: replay = line exists only on the raw recorder. The decoded recorder prints csv: next log file index is … once per device and frame kind, as each file is created.
Messages
| Message | Where from | What follows |
|---|---|---|
config: Ensure logFolder exists: <path> | startup | exit 0 |
config: Check logFolder permissions: <path> | startup | exit 0 |
config: replay file <path> not readable ! | startup | never printed — the condition guarding it cannot be true |
busmaster: failed to init the file counter, check log folder permissions | startup | busmaster: failed to init file counter, then startup fails |
busmaster: next log file index is <n> | startup | normal |
busmaster: failed to create file: <path> | startup or rotation | exit 0 |
csv: failed to init the file counter, check log folder permissions | first frame of a series | csv: failed to init file counter |
csv: next log file index is <n> | first frame of a series | normal |
csv: failed to create file: <path> | first frame, or rotation | exit 0 |
Exit status
Every exit written in this application is 0, including all of the failures above. There is no non-zero exit anywhere in either recorder.
The practical consequence, covered in Troubleshooting, is that under Restart=always a broken recorder loops silently and is never marked failed.
Termination on SIGINT or SIGTERM leaves the main loop, closes the capture, writes the closing lines where the format has them, and starts the final compression. That path lives in the shared MUXEN library that supplies main(), not in this application.
Files
| Path | Content |
|---|---|
/usr/bin/muxen-can-datalogger | the raw recorder and replay tool |
/usr/bin/muxen-csv-datalogger | the decoding recorder |
/usr/lib/systemd/system/muxen-can-datalogger@.service | raw recorder template unit |
/usr/lib/systemd/system/muxen-csv-datalogger@.service | decoding recorder template unit |
/usr/share/bash-completion/completions/muxen-can-datalogger | completion file — see the note below |
/usr/share/bash-completion/completions/muxen-csv-datalogger | completion file — see the note below |
/var/lib/muxen/datalogger/ | the log folder, created by the package |
/usr/bin/gzip | executed after every rotation. Not shipped here; a package dependency |
Both completion files are copies of the muxen-boat completion: they test for muxen-boat and register a completion for muxen-boat. Neither completes either datalogger command.
The recorders create no socket, no runtime directory, no PID file and no state outside the log folder. They read no configuration file.
Output file names
| Recorder | Pattern |
|---|---|
| raw | <logFolder>/<interface>.<index>.log |
| decoded | <logFolder>/<interface>_device_<function>_<instance>_<broadcastId>.<index>.log |
<index> is a 16-bit counter, chosen at startup as one more than the highest index already present for that pattern — compressed files included. A closed file gains a .gz suffix. Full details in Log files.
systemd
Both units are templates. The instance — the text between @ and .service — is the name of the CAN interface, and nothing else.
Common to both units:
| Directive | Value |
|---|---|
PartOf | multi-user.target |
After | network.target |
WantedBy | multi-user.target |
ExecStart | /usr/bin/muxen-can-datalogger --interface %i |
Restart | always, RestartSec=30 |
ConditionPathIsDirectory | /sys/class/net/%i |
ExecStart is identical in both units: muxen-csv-datalogger@.service starts /usr/bin/muxen-can-datalogger as well. That is not a typo in this table.
Neither unit sets User=, Group=, Environment= or EnvironmentFile=, and neither is attached to muxen.target or muxen-deploy.target. Neither passes any option other than --interface, so every other setting takes its built-in default; a drop-in that clears and replaces ExecStart is the way to change one.
Both units apply the same identity, state directory and hardening:
| Directive | Value |
|---|---|
User / Group | muxen — neither recorder runs as root |
StateDirectory | muxen/datalogger — systemd creates /var/lib/muxen/datalogger, owns it as muxen:muxen, and makes it writable despite ProtectSystem= |
AmbientCapabilities | CAP_NET_RAW — a SocketCAN raw socket needs it, and a non-root user does not have it |
PrivateTmp | true |
PrivateDevices | yes |
ProtectSystem | strict |
ProtectKernelModules | yes |
ProtectKernelTunables | yes |
ProtectControlGroups | yes |
NoNewPrivileges | true |
CapabilityBoundingSet | drops CAP_SYS_ADMIN, CAP_SYS_TIME, CAP_NET_ADMIN |
SystemCallFilter | denies @clock, @debug, @module, @mount, @raw-io, @reboot, @swap, @privileged, @resources |
SystemCallErrorNumber | EPERM |
The muxen user and group come from muxen-systemd, which this package depends on for that reason.
--logFolder defaults to /var/lib/muxen/datalogger, which is exactly what StateDirectory= provides. Point it somewhere else and ProtectSystem=strict will make that path read-only unless you add a ReadWritePaths= drop-in for it.
Enabling
sh
sudo systemctl enable --now muxen-can-datalogger@can0.service
sudo systemctl enable --now muxen-can-datalogger@can1.service
sudo systemctl enable --now muxen-csv-datalogger@can0.service
sudo systemctl disable --now muxen-can-datalogger@can0.serviceOverriding an option, per instance:
sh
sudo mkdir -p /etc/systemd/system/muxen-can-datalogger@can0.service.d
sudo tee /etc/systemd/system/muxen-can-datalogger@can0.service.d/override.conf >/dev/null <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/muxen-can-datalogger --interface %i --maxRecordTime 1800
EOF
sudo systemctl daemon-reload
sudo systemctl restart muxen-can-datalogger@can0.serviceThe empty ExecStart= is required; without it systemd appends a second command rather than replacing the first.
Rotation and compression
| Property | Raw recorder | Decoded recorder |
|---|---|---|
| Age trigger | frame time > file open time + --maxRecordTime | same |
| Size trigger | file size + 79 > --maxFileSizeBeforeCompression | file size > --maxFileSizeBeforeCompression |
| Evaluated | on each arriving frame, before writing it | same, per file |
| On rotation | closing lines, close, gzip, open next index | close, gzip, open next index, write header row |
| Compression | fork() + execve("/usr/bin/gzip", {"--best", <path>}) | same |
| Compression optional | no | no |
| Files deleted | never | never |
The compression child is not checked for success and does not exit if the exec fails.
Packaging
| Field | Value |
|---|---|
| Source and binary package | muxen-datalogger-tools |
| Section | misc |
| Architecture | any, Multi-Arch: foreign |
| Depends | ${misc:Depends}, ${shlibs:Depends}, gzip (>= 1.9) |
| Build-Depends | debhelper-compat (= 13), meson, cmake, libmosquitto-dev, libglib2.0-dev |
| Standards-Version | 4.7.2 |
| Homepage | https://www.muxen.fr |
| Source format | 3.0 (native) |
| Directories created | /var/lib/muxen/datalogger |
The package depends on no other muxen-* package. It declares no trigger and ships no maintainer script, so installing it starts nothing and upgrading it does not restart anything by itself.
CI builds it for bookworm and trixie, on amd64 and arm64.
Building from source
sh
meson setup build
meson compile -C buildThe makefile wraps the common cases: make deb builds the Debian package, make dev does a clean rebuild and runs meson test, make lint and make lint-check run clang-format.
Dependencies: GLib, and the MUXEN libcanmqtt and libstdmuxen, which are resolved as installed packages if present and as Meson subprojects otherwise. The build enables link-time optimisation, full RELRO and immediate binding.
There is no test suite, and meson test has nothing to run.
