Skip to content

Reference ​

Everything the two programs accept, everything they install, and every value they default to. Behaviour is in the earlier chapters; this is the lookup surface.

Packages ​

PackageArchitectureContents
muxen-alarmsanymuxen-alarmsd, muxen-dtc, muxen-alarms-test-audio, the systemd unit, the nginx snippets, the Swagger UI
muxen-alarms-databaseall/usr/share/muxen-alarms/dtc.json, /usr/share/muxen-alarms/functions.json, and the voice files with their manifest.json in /usr/share/muxen-alarms/sound/, including the speaker-test clip muxen-welcome.ogg
muxen-alarms-mdnsall/etc/avahi/services/muxen-alarms.service
RelationshipOnValue
Dependsmuxen-alarmsmuxen-systemd, muxen-alarms-database (= ${source:Version}), mosquitto-clients (for muxen-alarms-test-audio)
Recommendsmuxen-alarmsmuxen-boat (>= 5.0.0)
Replaces / Breaksmuxen-alarmsmuxen-errors
Dependsmuxen-alarms-mdnsavahi-daemon, avahi-autoipd

muxen-alarms activates two dpkg triggers on install and upgrade: muxen-restart-target, which restarts muxen.target, and nginx-reload, which reloads nginx once at the end of the transaction if it is running, so a changed snippet is served at once rather than after the next reload somebody happens to make.

muxen-alarmsd ​

The daemon. One of --mqtt or --interface is required; without either it prints its usage and exits.

OptionArgumentDefaultEffect
-h, --help——print usage and exit 0
-v, --verbose—0cumulative. -v adds HTTP request logging, lws notices and alsa-lib's own diagnostics of an audio output it cannot open; -vv also dumps every CAN frame or MQTT message
-V, --version——print the version and exit 0
-i, --interfacecanX(none)take alarm reports from this CAN interface
-m, --mqtt—offtake alarm reports from MQTT instead of CAN
-p, --portnumber12001TCP port of the HTTP/WebSocket listener
-d, --dtcpath/usr/share/muxen-alarms/dtc.jsonthe alarm code catalogue
--functionspath/usr/share/muxen-alarms/functions.jsonthe equipment names in other languages (format), since 10.3.0. Optional: a file that is missing, unreadable, not JSON, not an array or empty logs one warning, the alarms carry no functionName.<LANG> and the hello no function-name-translations; the daemon runs on
-s, --swaggerpath/usr/share/muxen-alarms/swaggerdirectory served at /
--no-audio—audio onnever speak alarms. The audio state is still published, with available: false
--audio-intervalminutes, 0–14405time between two rounds of every active alarm. 0 speaks each alarm once, when it appears
--audio-gapseconds, 0–602silence between two phrases of a round
--audio-levelerror, warning or noticeerrorlowest level spoken at startup. Changed at run time over MQTT, never stored
--audio-soundspath/usr/share/muxen-alarms/soundthe voice files and their manifest.json
--audio-pcm-alarmALSA PCM namemuxen_alarmwhere error phrases play
--audio-pcm-notificationALSA PCM namemuxen_notificationwhere warning and notice phrases play
--json-pretty—compactindent the JSON the daemon produces — app/alarm/audio, the dtcs and audio WebSocket frames, the REST responses — for reading by hand. The WebSocket hello and app/alarm/info stay compact: libstdmuxen serializes them

--interface and --mqtt are exclusive in effect: with --mqtt the CAN handler table is not installed and the interface is not opened, and without it the MQTT handler table is not installed.

The listener always binds 127.0.0.1. There is no option to bind another address.

An unreadable --dtc prints config: dtc file … not readable ! and the usage, and exits 0. --functions is not checked there: the warning it gets instead, from GLib, reads

functions: Failed to open file “/nonexistent”: No such file or directory, function names are not translated

An out-of-range --audio-interval or --audio-gap, or an --audio-level that is not one of the three names, prints what is accepted followed by the usage, and exits 0 like --help.

A PCM name that ALSA does not define falls back to default, logged once. Nothing about audio is fatal: a missing manifest.json, a PCM that will not open, or a player that stops answering each turn into available: false in the audio state, and the alarm list is served as before.

The output is checked at startup. The audio state starts with available: false. The player then writes one second of silence to the --audio-pcm-notification PCM (same fallback to default), in the format of the voice files, and drains it: nothing is heard. When that completes, available turns true, within a second on a working Brain. When it fails it is tried again every 30 s, with no limit, so an output that comes up later — PipeWire starting after the daemon, a card plugged in — turns audio on without a restart. The first failure is said once, at warning level:

audio: no audio output (PCM muxen_notification: No such device); alarms are not spoken, checking again every 30 s

and the check that finally plays logs audio: output checked, PCM …, audio is on. -v logs each failed attempt as well. A check that does not answer within 10 s is stuck in alsa-lib: the daemon then behaves as with --no-audio until it is restarted, and says so at warning level.

Everything else — MQTT, the REST API, the WebSocket, the audio state published every second — starts as it always does; only speech waits. Until a check plays no alarm is spoken and a speaker test is ignored (ignored, no audio output yet); an alarm active meanwhile is announced on the first tick after available turns true.

Once available is true, a later failure to play turns it false again but does not turn audio off: the next alarm or speaker test tries the output again, and turns it back true when it plays.

The startup summary printed on every run:

config: verbose = 0
config: wsPort = 12001
config: data source = mqtt
config: dtc = /usr/share/muxen-alarms/dtc.json
config: functions = /usr/share/muxen-alarms/functions.json
config: swaggerFolder = /usr/share/muxen-alarms/swagger
config: audio = on
config: audio interval = 5 min
config: audio gap = 2 s
config: audio level = error
config: audio sounds = /usr/share/muxen-alarms/sound
config: audio pcm = muxen_alarm / muxen_notification

The config: can interface = … line is printed only on the CAN route. With --no-audio the audio block is the single line config: audio = off.

muxen-dtc ​

A read-only viewer for a shell on the Brain. MQTT only; it has no CAN route and no API.

OptionEffect
-h, --helpprint usage and exit 0
-v, --verbosemore output
-V, --versionprint the version and exit 0
-f, --flowkeep running. Without it the program listens for one second and exits
-e, --expiredalso print reports whose metadata.expireAfterSec has already passed

It subscribes to system/time and device/+/+/error/#, prints one line per surviving report, and holds no state:

Topic                   Code    Date                            Device type
device/5/0/error/1      1       2026-08-16T09:14:02Z            (Power source battery)

Freshness is measured against the time published on system/time when one has arrived, and against the Brain's own clock otherwise.

muxen-alarms-test-audio ​

A shell script that asks the daemon for its speaker test: {} published on app/alarm/test, never retained, with mosquitto_pub. It then reads app/alarm/audio for 3 seconds and says whether the test started.

OptionEffect
--helpprint usage and exit 0
-h hostthe broker, default $MUXEN_MQTT_HOST or 127.0.0.1
-p portits port, default $MUXEN_MQTT_PORT or 1883
ExitMeaning
0the audio state showed "test": true: the clip is playing
1it did not. The message says whether the audio state reported available: false, carried no test, or was not there at all
2a bad option

What the test does, and when the daemon ignores it: the app/alarm/test topic.

Internal fixed values ​

Not configurable; listed because they are visible in the published data.

ValueWhereMeaning
15 severy device alarmhow long a report is held after its own timestamp
90 scode 65000how long an offline alarm is held
5 scode 65001how long a source-lost alarm is held
1 slist sweep, filter sweepthe main tick; a change to the list goes out on the WebSocket on the tick that makes it
5 sWebSocket dtcs framethe longest between two frames while the list does not change: what carries a live alarm's moving expireTime
60 soffline scanthe interval between muxen-uds scans
1 s → 30 sdata source retrybackoff, doubling to the ceiling
3600 sfilter durationdefault when the request omits it
1 s … 365 dfilter durationaccepted range; outside it the request is refused
64 KBHTTP request bodyabove it the request is refused with 413
16WebSocket send ringmessages buffered per vhost before the slowest client is dropped
256 bytesWebSocket client messagethe longest accepted, fragments put together; a longer one is ignored
5 sMQTT keepaliveclient id muxen-alarms (muxen-dtc for the viewer)
function 9, instance 0code 65001the device id the daemon attributes its own alarm to — 576
5 sapp/alarm/audioexpireAfterSec of the audio state, which is republished every second
100 msaudio outputPCM buffer; also the longest a stop waits for the phrase being played
32spoken roundphrases in one round. The least severe, newest ones follow in the next round
60 svoice filethe longest file the player accepts
all but MP3voice filethe player accepts any format libsndfile decodes (Ogg Opus, Ogg Vorbis, FLAC, WAV, …) except MPEG audio, which it refuses on every platform. The shipped voices are Ogg Opus
30 s + 15 s and the gap per phrasespoken roundpast this the player is reported stuck and available turns false until it answers
1 sdaemon stophow long the daemon waits for the player before exiting without it
1 s of silence, every 30 soutput checkwhat the player writes to the notification PCM before available turns true, and how often it tries again while that fails
10 soutput check at startuppast this a check that has not answered turns audio off

Files and paths ​

PathPackagePurpose
/usr/bin/muxen-alarmsdmuxen-alarmsthe daemon
/usr/bin/muxen-dtcmuxen-alarmsthe viewer
/usr/bin/muxen-alarms-test-audiomuxen-alarmsthe speaker test, from a shell
/usr/lib/systemd/system/muxen-alarms.servicemuxen-alarmsthe unit
/etc/nginx/snippets/muxen-api-alarms.confmuxen-alarmsREST proxy snippet
/etc/nginx/snippets/muxen-ws-alarms.confmuxen-alarmsWebSocket proxy snippet
/usr/share/muxen-alarms/swagger/muxen-alarmsthe Swagger UI, served at /
/usr/share/muxen-alarms/dtc.jsonmuxen-alarms-databasethe alarm code catalogue
/usr/share/muxen-alarms/functions.jsonmuxen-alarms-databasethe equipment names in other languages, since 10.3.0
/usr/share/muxen-alarms/sound/muxen-alarms-databasethe alarm voice files (Ogg Opus) and manifest.json, served by nginx at /api/alarms/sound/. muxen-welcome.ogg, the speaker-test clip, is there too and is not in the manifest
/etc/avahi/services/muxen-alarms.servicemuxen-alarms-mdnsthe mDNS announcement
/etc/muxen/deploy.json(read only)the device list offline detection compares against

The daemon writes nothing except one temporary file per offline scan, which it deletes when the scan finishes. PrivateTmp=yes puts that file in the unit's private /tmp.

systemd ​

muxen-alarms.service:

DirectiveValue
ExecStart/usr/bin/muxen-alarmsd --mqtt
Typesimple
User / Groupmuxen / muxen
Restart / RestartSecalways / 30
PartOfmuxen.target
Aftermosquitto.service
WantedBymuxen.target

Hardening: NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables, ProtectKernelModules, ProtectKernelLogs, ProtectControlGroups, ProtectProc=invisible, ProcSubset=pid, ProtectClock, ProtectHostname, RestrictRealtime, RestrictSUIDSGID, LockPersonality, MemoryDenyWriteExecute, an empty CapabilityBoundingSet and AmbientCapabilities, SystemCallFilter=@system-service with SystemCallErrorNumber=EPERM, SystemCallArchitectures=native, LimitNOFILE=1024.

DirectiveValueWhy
ReadOnlyPaths/etc/muxenthe offline scan reads the deployed device list
RestrictAddressFamiliesAF_UNIX AF_INET AF_INET6 AF_NETLINK AF_CANbroker and listener, the CAN link watch, and the muxen-uds child

The unit carries nothing for audio, on purpose. Where the audio output is a system-wide PipeWire, the daemon reaches it through its socket — over AF_UNIX, with PrivateDevices left on, since nothing opens /dev/snd — and needs two more settings, which the platform that provides the audio ships as a drop-in:

ini
# muxen-alarms.service.d/audio.conf
[Service]
SupplementaryGroups=pipewire
Environment=PIPEWIRE_RUNTIME_DIR=/run/pipewire

They stay out of the unit because SupplementaryGroups names a group that exists only where PipeWire is installed, and a unit that cannot resolve it does not start. On a Brain without an audio output that would stop alarms being reported at all, to gain a voice it cannot have.

Exit status ​

StatusMeaning
0normal exit on SIGTERM/SIGINT; also --help, --version, and every configuration error
1the HTTP/WebSocket listener could not be created — main: lws init failed
255muxen-dtc only: the MQTT client could not be initialised

A configuration error exiting 0 is worth remembering: systemctl status reports success while the daemon restarts every 30 seconds. The usage block in the journal is the tell.

Network ​

EndpointBound toServed by
12001/tcp127.0.0.1the daemon: REST, WebSocket and the Swagger UI

nginx maps two public paths onto it, and serves a third from disk:

PublicDaemonSnippet
/api/alarms/<x>/api/<x>muxen-api-alarms.conf (client_max_body_size 128k)
/ws/alarms/muxen-ws-alarms.conf (proxy_read_timeout 15d)
/api/alarms/sound/<file>(not proxied)muxen-api-alarms.conf: a static alias of /usr/share/muxen-alarms/sound/

The sound location answers GET and HEAD only (403 otherwise), lists no directory, supports Range, and sends Cache-Control: no-cache with an ETag. A regenerated voice keeps its file name, so clients revalidate and get a 304 while it is unchanged. Being part of muxen-api-alarms.conf, it exists on exactly the sites that include that snippet. A site that includes only muxen-ws-alarms.conf has no sound URLs.

The snippets must be included by a site configuration; they are inert on their own. That site configuration is shipped by the boat's UI interface package (muxen-interface-*), one nginx site file per boat, which is also where the map $http_upgrade $connection_upgrade block the /ws/alarms proxy needs is defined. The includes are unconditional there — no trailing * — so on a boat whose interface site includes them, nginx will not start unless this package is installed. Which of the two snippets a given site includes is up to that site: some include both, some only muxen-ws-alarms.conf.

mDNS ​

muxen-alarms-mdns publishes:

FieldValue
NameMuxenAlarms
Type_http._tcp, IPv4 and IPv6
Port80
TXTapi=/api/alarms/, ws=/ws/alarms

It advertises nginx's port and paths, not the daemon's 12001, which nothing off the Brain can reach. The port is a fixed 80 in the service file, while the actual listening port is chosen by the boat's interface site — usually 80, but not on every boat — so on a Brain serving on another port the announcement is wrong and a client has to be pointed at the right port by hand. avahi-autoipd is a dependency so the Brain claims a 169.254.0.0/16 link-local address on a network with no DHCP server — an ad-hoc link between a laptop and the boat.

REST and WebSocket ​

Full wire format: The HTTP and WebSocket API. Machine-readable schemas: dtc-schema.json, filter-schema.json.

MethodPublic pathDaemon path
GET/api/alarms/dtcs/api/dtcs
GET/api/alarms/filter/api/filter
PUT/api/alarms/filter/api/filter
DELETE/api/alarms/filter/api/filter
GET/api/alarms/filter/{uuid}/api/filter/{uuid}
DELETE/api/alarms/filter/{uuid}/api/filter/{uuid}
—/ws/alarms/ (WebSocket; one client message, {"type": "test-audio"})
StatusMeaning
200success
400body absent, unparseable, with no constraint, or with an out-of-range duration
404unknown path, or unknown filter identifier
405path known, method not supported — carries an Allow header
413request body over 64 KB
500internal error

Input interfaces ​

MQTT ​

Subscriptions, on 127.0.0.1:1883:

TopicUsed for
device/+/+/error/#alarm reports
system/timesubscribed by both programs; the reference clock for muxen-dtc's freshness test
app/alarm/settingsthe spoken-alarm settings, see below
app/alarm/testthe speaker test, see below

A report is accepted only when its topic matches device/<digits>/<digits>/error/<digits> exactly and its payload is a JSON object carrying a metadata object with a readable rxdate.

topic   device/<functionCode>/<instance>/error/<alarmCode>
deviceId = functionCode * 64 + instance

The daemon publishes two topics, its identity and the state of the spoken alarms:

TopicRetainedPublished
app/alarm/infoyes, QoS 1on every connection to the broker with "online": true; with "online": false on a clean stop and, as the last will, by the broker when the daemon disappears. Since 10.2.0
app/alarm/audioyesonce a second, on every change — a speaker test starting or ending included — in answer to every app/alarm/settings message, and a last time with available: false on a clean stop

app/alarm/info is the same object every MUXEN daemon publishes on its app/<daemon>/info topic, so one app/+/info subscription lists them all:

json
{
  "name": "muxen-alarmsd",
  "version": "v10.2.0",
  "hostname": "brain-3",
  "features": ["alarm-list", "test-audio", "audio", "voice-files"],
  "online": true,
  "metadata": { "rxdate": "2026-09-22T07:55:19.931Z", "rxTimestamp": 1790063719, "expireAfterSec": 3124137600 }
}

name, version, hostname and features are those of the WebSocket hello frame, features included (The HTTP and WebSocket API). online says whether the daemon is running; expireAfterSec is about 99 years, so a copy never goes stale on age: online: false is what a stopped daemon leaves behind.

json
{
  "data": {
    "available": true,
    "enabled": true,
    "minLevel": "error",
    "speaking": {
      "severity": "error",
      "alarmCode": 3,
      "deviceId": 1344,
      "href": "/api/alarms/sound/low-oil-pressure.ogg"
    }
  },
  "metadata": { "rxdate": "2026-09-10T14:02:11.412Z", "rxTimestamp": 1789048931, "expireAfterSec": 5 }
}
FieldMeaning
availablethe daemon can speak. False until the output check at startup has played (the startup check), for good when it never did, with --no-audio or without a readable manifest.json; false again when a round could not open or play its PCM (until one plays), while the player is stuck, and on a clean stop
enabledthe setting below; true at every start
minLevelerror, warning or notice: the lowest level spoken
speakingthe phrase being played, null between phrases. severity is the level it plays at, href the voice file under the nginx path of the sound location. During the speaker test it is {"severity": "error", "test": true, "href": "/api/alarms/sound/muxen-welcome.ogg"}: test is there, alarmCode and deviceId are not. test is never present on an alarm
metadatathe MUXEN envelope. A copy older than expireAfterSec is stale: the daemon stopped without its last word

app/alarm/settings takes a JSON object, both fields optional:

json
{ "enabled": false }
{ "minLevel": "warning" }
{}

enabled must be a boolean and minLevel one of the three names; a message with either one wrong is ignored whole, and logged. {} changes nothing and only makes the daemon republish the state. An empty payload is ignored.

Send it non-retained. The settings live in memory only, and any restart of the daemon — a reboot, a deployment restarting muxen.target — speaks again with the command-line defaults. A retained message is refused, because the broker would replay it on every reconnect and a mute would silently outlive the restart; the journal then says ignoring a retained app/alarm/settings and gives the command that clears it:

sh
mosquitto_pub -r -n -t app/alarm/settings

app/alarm/test asks for the speaker test: the daemon plays muxen-welcome.ogg from --audio-sounds once, through the --audio-pcm-alarm output, and shows it in speaking as above. The payload is a JSON object, {}; its fields are not read. Anything that is not a JSON object is ignored and logged, an empty payload is ignored silently. muxen-alarms-test-audio sends it, and a WebSocket client can ask for the same test (The HTTP and WebSocket API).

WhenThe test
"enabled": falseplays: it exists to check the speaker, voice on or off. The minimum level does not apply either
--no-audio, no readable manifest.json, an output check stuck in alsa-lib, or muxen-welcome.ogg missingis ignored (audio is off)
no output check has played yetis ignored (no audio output yet)
an alarm is being spoken, or a round is still runningis ignored, and never queued: a real alarm is not cut off for a test
a test is already playingis ignored
an alarm falls due while it playsis stopped, and the alarm is spoken after --audio-gap
"enabled": false is sent while it playsis stopped, as anything being spoken is
the output cannot be openedfails, and available turns false. A later test, or the next alarm, tries the output again

Each outcome is a journal line: audio: speaker test from mqtt (or from websocket), then playing …, done, stopped, failed, no audio output, or ignored, … with the reason.

Send it non-retained, for the same reason as the settings: a retained test would play again at every reconnect of the daemon. The retained copy is refused the same way, with ignoring a retained app/alarm/test and the command that clears it, mosquitto_pub -r -n -t app/alarm/test. A daemon already connected when a retained message is published receives it as a live one, and plays it once; the refusal is for the copy the broker replays.

The three app/alarm/ topics exist on the MQTT route only. With --interface the daemon's only broker connection carries app/alarm/info, and subscribes to nothing: alarms are still spoken, with the command-line settings, and nothing can change them at run time. The speaker test is then reachable over the WebSocket only.

CAN ​

Used only with --interface.

PropertyValue
FrameMUXEN common DTC broadcast
Broadcast identifier170 (0xAA)
DLC3
PayloaderrorCount (uint8), errorCode (uint16)
Device idthe low 12 bits of the CAN identifier
Timestampthe socket receive timestamp

The daemon transmits nothing.

Child processes ​

CommandIntervalPurpose
muxen-uds uid --scan-to-json <tmpfile>60 sthe device list offline detection compares against /etc/muxen/deploy.json

Spawned with G_SPAWN_SEARCH_PATH and no shell, with its stdout discarded.

Configuration read from elsewhere ​

/etc/muxen/deploy.json, per device, both compared as strings:

ParameterValueEffect
VirtualDevice"1"the device is never expected on the bus
DisableOfflineAlarm"1"the device may be absent without raising 65000

Alarm catalogue format ​

One JSON array. Each entry:

FieldTypeRequiredMeaning
alarmCodeintegeryesthe code the device reports
functionCodeintegernothe equipment family. Absent means it matches any family
alarmDescriptionstringyesEnglish text
alarmDescription.FRstringnoFrench text
severitystringyeswarning or error in the shipped file
channelintegernoBloc 8 only: which output the code refers to

Lookup returns the first entry matching the code whose functionCode is absent or equal. A code with no entry is published with alarmDescription and severity both set to "unknown".

The daemon checks at startup only that the file is readable. A readable file that does not parse, or whose root is not an array, is accepted and yields unknown for every alarm.

Function name translations format ​

functions.json, since 10.3.0. One JSON array, one entry per function code:

FieldTypeRequiredMeaning
functionCodeintegeryesthe function code, 0–63
functionName.<LANG>stringnothe function's name in that language; <LANG> is the last two letters of a locale (FR, DE, ES, GR, IT, PT, TR shipped)

For each alarm the daemon takes the first entry whose functionCode equals the alarm's, and copies every string key starting with functionName. into the alarm, next to functionName, which it never changes. Other keys and non-string values are ignored, and so is the language list: a new language is a new key in the data, not a change to the daemon.

The build (meson.build, jq) accepts data/functions.json only as a non-empty array of objects with a unique integer functionCode from 0 to 63 and non-empty functionName.<XX> strings (XX two capital letters), every entry carrying the same languages. meson test also checks that every function code libstdmuxen names has an entry, except the private keel devices 13, 14, 15 and 29, which are deliberately untranslated and must have none.

TypeScript client ​

PropertyValue
Package@muxen/alarms
Entry points., ./message-types, ./severity, ./plugin
Peer dependenciesvue ^3.3.0, pinia ^2.1.0 — both optional
Node>= 18
Store idmuxen-alarms
Default WebSocket endpoint/ws/alarms
Default REST endpoint/api/alarms
Default reconnect delay5 s

message-types is pure TypeScript with no runtime dependency, so a non-Vue client can take the type definitions alone. severity is the same for the severity ladder and its helpers (normalizeSeverity(), isSeverityAtLeast(), …).

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