Skip to content

Reference ​

Everything the package installs, exposes and answers. Tables, not prose.

muxen-configd ​

muxen-configd [OPTION]
FlagArgumentDefaultEffect
-h, --help——print the option list on stdout and exit 0
-v, --verbose—offadd libwebsockets NOTICE logging, and print one line per request and per commissioning decision
-V, --version——print the version (git describe --tags --always --dirty) and exit 0
-p, --portport number12000TCP port of the HTTP server
-c, --configdirectory/etc/muxenconfiguration folder — must exist and be writable
-s, --swaggerdirectory/usr/share/muxen-config/swaggerfolder served at / — must exist

Notes on parsing:

  • The option string is +hvVp:c:s:, so option parsing stops at the first non-option argument. The daemon takes no positional argument; the built-in help text mentions a command, and there is none.
  • --port is parsed with base 0, so 0x2EE0 is accepted, and stored in a 16-bit field — a value above 65535 wraps rather than being refused.
  • An unrecognised option prints the help text and exits 0.

Startup ​

In order: seed the PRNG, parse options, check the swagger folder is a directory, check the configuration folder is a directory and writable, print the configuration summary, create the glib main loop, install SIGINT/SIGTERM handlers, start libwebsockets bound to 127.0.0.1, start the MQTT info client (see MQTT).

The summary printed on every successful start:

config: verbose = 0
config: wsPort = 12000
config: configFolder = /etc/muxen
config: swaggerFolder = /usr/share/muxen-config/swagger

Exit codes ​

CodeWhen
0--help, --version, or a clean shutdown on SIGINT / SIGTERM
1swagger folder missing; configuration folder missing or not writable; libwebsockets context creation failed (lws init failed, typically a port already bound); a route regex failed to compile

muxen-config-backup ​

muxen-config-backup

No options. Copies /etc/muxen/deploy.json into /etc/muxen/backup/ when it differs from the previous copy.

ConstantValue
Source/etc/muxen/deploy.json
Destination directory/etc/muxen/backup (created if absent)
Copy namedeploy.json-%Y-%m-%d_%H-%M-%S (local time)
Change detectioncmp against /etc/muxen/backup/last_backup
Pointer to the newest copylast_backup, a symlink
Copies retained30, newest first by modification time

Does nothing if the source file does not exist. Prints Backup created: <path> when it copies, nothing when it does not.

systemd units ​

UnitTypeRole
muxen-config.servicelong-runningthe daemon. PartOf=muxen.target, WantedBy=muxen.target, After=muxen-config-perms.service, Restart=always, RestartSec=30
muxen-config-perms.serviceoneshotfixes ownership and modes under the configuration folder. PartOf=muxen.target, WantedBy=muxen.target
muxen-config-backup.timertimerOnCalendar=daily, Persistent=true, WantedBy=timers.target
muxen-config-backup.serviceoneshotruns /usr/bin/muxen-config-backup

muxen-config.service runs as muxen:muxen with UMask=0022, ConfigurationDirectory=muxen (mode 0755) and ExecStart=/usr/bin/muxen-configd -c ${MUXEN_CONFIG_FOLDER}.

Its hardening, in one place: ProtectSystem=strict, ProtectHome, ProtectProc=invisible, ProtectClock, ProtectHostname, ProtectKernelLogs, ProtectKernelModules, ProtectKernelTunables, ProtectControlGroups, PrivateDevices, PrivateTmp, RestrictNamespaces, RestrictRealtime, RestrictSUIDSGID, RestrictNetworkInterfaces=lo, RemoveIPC, LockPersonality, NoNewPrivileges, SystemCallArchitectures=native, SystemCallErrorNumber=EPERM, a SystemCallFilter denying @clock, @cpu-emulation, @debug, @module, @mount, @obsolete, @privileged, @raw-io, @reboot, @resources, @swap, and a CapabilityBoundingSet dropping every capability the daemon does not need.

RestrictNetworkInterfaces=lo is worth noting on its own: the service cannot use any interface but loopback, whatever --port is set to.

muxen-config-perms.service runs, ignoring individual failures:

chown -R muxen:muxen ${MUXEN_CONFIG_FOLDER}
chmod 755 ${MUXEN_CONFIG_FOLDER}
chmod 755 ${MUXEN_CONFIG_FOLDER}/configuration
chmod 644 ${MUXEN_CONFIG_FOLDER}/deploy.json
chmod 644 ${MUXEN_CONFIG_FOLDER}/configuration/*

Environment ​

VariableDefaultSet byUsed for
MUXEN_CONFIG_FOLDER/etc/muxenmuxen-config.service, muxen-config-perms.servicethe -c argument, and the path the perms unit fixes

An override goes in a drop-in (systemctl edit muxen-config).

Files and directories ​

PathOwnerContent
/usr/bin/muxen-configdpackagethe daemon
/usr/bin/muxen-config-backuppackagethe backup script
/etc/muxen/package (empty)the configuration folder
/etc/muxen/configuration/<id>.jsondaemonone project per file
/etc/muxen/deploy.jsondaemonthe deployed configuration — what the rest of the boat reads
/etc/muxen/deploy.json.tmpdaemontransient, during a deploy only
/etc/muxen/apps/<id>daemonopaque per-application blobs, any content type
/etc/muxen/backup/deploy.json-<timestamp>backup scriptdated copies of the deployed configuration
/etc/muxen/backup/last_backupbackup scriptsymlink to the newest copy
/usr/share/muxen-config/templates/<id>.jsonpackage (empty)read-only templates
/usr/share/muxen-config/swagger/packagethe bundled Swagger UI, served at /
/etc/nginx/snippets/muxen-api-config.confpackageexposes the API at /api/config/
/etc/nginx/snippets/muxen-deploy.confpackageexposes the deployed configuration at /api/deploy.json
/usr/share/bash-completion/completions/muxen-configdpackagebash completion

There is no socket, no PID file and no lock file.

Network ​

PropertyValue
Bind address127.0.0.1 only, hard-coded
Port12000, --port to change
ProtocolHTTP/1.1
Root mountthe swagger folder, index.html by default, cache_max_age 300
Request body limitnone in the daemon; nginx sets client_max_body_size 50M on /api/config/

nginx mapping:

Public pathDaemon path
/api/config/<path>/<path>
/api/deploy.json/deploy, falling back to the file /etc/muxen/deploy.json on connect error, 1 s connect timeout, 502, 503 or 504

Both snippets set proxy_set_header Connection "close".

MQTT ​

The daemon's only MQTT use is announcing itself; it subscribes to nothing.

ParameterValue
Broker127.0.0.1:1883, hard-coded
Client IDmuxen-config
Keepalive30 s
Subscriptionsnone
Broker down at startupnot an error: the API is served anyway and the connection is retried every 10 s; after the first connection libcanmqtt reconnects on its own

Published:

TopicQoSRetainWhen
app/config/info1yeson each connection with online: true; online: false as the last will and on a clean exit

The payload is the daemon identity every MUXEN daemon publishes on app/<daemon>/info:

json
{
  "name": "muxen-config",
  "version": "v10.15.0",
  "hostname": "brain-3",
  "features": ["deploy", "backup", "transaction", "duplicate", "rename", "settings", "device", "device-legend", "sensor", "synapse", "energy", "muxing", "button-mapping", "media", "view", "commissioning", "apps"],
  "online": true,
  "metadata": { "rxdate": "2026-09-22T08:01:58.406Z", "rxTimestamp": 1790064118, "expireAfterSec": 3124137600 }
}

version is the git describe of the build and is for display only. expireAfterSec is about 99 years: the info never goes stale, online carries liveness. GET /info answers the same identity without online and metadata.

features has one entry per REST resource family, named after its URL segment; a family added later gets its own entry. Clients test for the one they need and ignore the ones they do not know:

FeatureRoutes
deployGET /deploy
backupGET /project/<id>/backup, PUT /project/<id>/restore
transactionPOST /project/<id>/transaction
duplicatePUT /project/<id>/duplicate/<newId>
renamePUT /project/<id>/rename/<newId>
settings/project/<id>/settings…
device/project/<id>/device…, parameters included
device-legend/project/<id>/device/<n>/legend…
sensor/project/<id>/sensor…
synapse/project/<id>/synapse…
energy/project/<id>/energy…
muxing/project/<id>/muxing…
button-mapping/project/<id>/button-mapping…
media/project/<id>/media…
view/project/<id>/view…, partial updates included
commissioning/project/<id>/commissioning…
apps/apps…

The server block that includes them is not in this package: it is shipped by the boat's UI interface package (muxen-interface-*), one site file per boat, which is also where the listening port is decided and where the map $http_upgrade $connection_upgrade block used by the WebSocket snippets of other MUXEN packages is defined. The two includes are unconditional there — no trailing * — so on a boat whose interface site includes them, nginx will not start unless this package is installed.

HTTP conventions ​

AspectBehaviour
Response content typeapplication/json unless a handler sets another
Response bodypretty-printed JSON
Content-Lengthnever sent — responses are terminated by closing the connection
Connectionalways close
Request bodyread when Content-Length is greater than zero; parsed as JSON except for media and app uploads, which are stored raw
Unknown path or method404

Error bodies produced by the daemon:

json
{"error":"save project","errno":30,"code":"EROFS","message":"Read-only file system"}

errno, code and message are present only when the failure came from a syscall. code is the symbolic name for the common cases (EACCES, EBUSY, EDQUOT, EEXIST, EFBIG, EINVAL, EIO, EISDIR, ELOOP, ENAMETOOLONG, ENOENT, ENOMEM, ENOSPC, ENOTDIR, EPERM, EROFS) and absent otherwise.

Status codes ​

CodeMeaning in this API
200read succeeded, or a delete/update took effect
201a create or replace took effect
304If-None-Match matched the current ETag (/deploy, media)
400malformed short name, a numeric path segment malformed or out of range (see Path segment rules), missing or unparseable body, wrong JSON type
401the project has a password setting and the Authorization header did not match
404no such project, section entry, media, app or route
409a view rename collided with an existing view, or a duplicate or a rename targeted a project that already exists
500the project could not be saved, memory could not be allocated, or an internal invariant failed
503/health only — the routing table is not compiled yet

Authentication ​

ItemValue
Where the secret livesthe project's settings[] entry named password, field hash
HashSHA-256 of the password, lowercase hex
HeaderAuthorization: Bearer <hash>
Comparisonthe header is split on the first space and the second field is compared to hash; the scheme word is not checked
Scopeevery endpoint that opens the project, plus PUT /project/<id>, PUT /project/<id>/restore and DELETE /project/<id>, which are gated on the password of the file already on disk
No password settingall requests allowed

Endpoints ​

Blank means the method is not routed and answers 404.

Service ​

PathGETPUTPOSTDELETE
/health200 ready / 503 starting
/info200 {name, version, hostname, features}, see MQTT
/deploy200 / 304 / 404
/ and belowthe bundled Swagger UI

Projects ​

PathGETPUTPOSTDELETE
/project200 list of projects and templates
/project/<id>200 stripped project201 create or replace (blank project; body may set name)200 / 404
/project/<id>/backup200 whole project
/project/<id>/restore201 write the body as the project
/project/<id>/deploy201 copy to deploy.json
/project/<id>/duplicate/<newId>201 copy with a new uid and duplicateFrom, optionally applying commissioning; 409 if <newId> exists
/project/<id>/rename/<newId>201 move an /etc project, keeping its uid; 409 if <newId> exists
/project/<id>/transaction200 run a step list

Sections ​

PathGETPUTDELETE
/project/<id>/settings200 array
/project/<id>/settings/<name>201 upsert200
/project/<id>/device200 array
/project/<id>/device/<deviceId>200 one device201 create or reset200
/project/<id>/device/<deviceId>/parameters200 array201 upsert a list200 remove a list of names
/project/<id>/device/<deviceId>/parameter/<name>201 upsert one200
/project/<id>/sensor200 array
/project/<id>/sensor/<name>200201 upsert200
/project/<id>/synapse200 object
/project/<id>/synapse/<instanceId>200201 upsert200
/project/<id>/energy200 array
/project/<id>/energy/<name>200201 upsert200
/project/<id>/muxing200 array
/project/<id>/muxing/<name>200201 upsert, applying config to devices; a replace keeps the commissioning rules naming it200, reversing config and cascading into commissioning
/project/<id>/button-mapping200 array
/project/<id>/button-mapping/<name>200201 upsert200
/project/<id>/media200 array, metadata only
/project/<id>/media/<name>200 the decoded bytes201 store the raw body200
/project/<id>/view200 array, bodies stripped
/project/<id>/view/<name>200 whole view201 upsert200
/project/<id>/view/<name>/tags200 replace tags
/project/<id>/view/<name>/enabled200 replace enabled
/project/<id>/view/<name>/texts200 patch text elements
/project/<id>/view/<name>/settings200 patch allowed view settings
/project/<id>/view/<name>/name200 rename, 409 on collision
/project/<id>/commissioning200 array
/project/<id>/commissioning/<groupId>200201 upsert200
/project/<id>/commissioning/<groupId>/rules201 append one rule
/project/<id>/commissioning/<groupId>/rules/<index>201 remove rule at index

Applications ​

PathGETPUTDELETE
/apps200 array of file names
/apps/<id>200 raw bytes, application/octet-stream201 store the raw body200 / 404

/apps is a plain keyed blob store under /etc/muxen/apps/. It is not part of any project, is not deployed, and is not covered by the daily backup.

Path segment rules ​

SegmentPatternNotes
project <id>[a-zA-Z0-9-]+also re-validated by the handler; 64 characters or more is rejected
<deviceId>decimal, 0–4095function × 64 + instance, function and instance 0–63 each
legend <zone>decimal, 16–35, then an optional .<REGION>the BPU 8S e-paper slots; the bulk legends body takes the same range
setting and parameter <name>[a-zA-Z0-9.-]+dot allowed
every other <name> / <id>[a-zA-Z0-9-]+
<index>decimal, 0 to the rule count − 1position in the group's rules array

The numeric segments are parsed strictly: decimal digits only, no sign, no leading or trailing characters, no overflow, and in range. Anything else — 4096, -1, 57abc, +57, a 20-digit number — is answered 400 before the project is touched, so the file stays byte-identical. They used to go through atoi(): PUT …/device/12345 was stored as 12345 and read back, masked, as device 57.

Caching ​

EndpointValidatorcache-control
GET /deployETag: SHA-256 of the bytes of deploy.json, quotedmax-age=0, no-cache
GET /project/<id>/media/<name>ETag from the media's stored metadata.etag, sent only when presentmax-age=60, no-cache

Both honour If-None-Match and answer 304 with an empty body.

Transactions ​

POST /project/<id>/transaction takes a JSON array of steps:

KeyTypeRequiredMeaning
urlstringyesthe path the step would have been sent to
methodstringyesGET, PUT, POST or DELETE, case-insensitive
dataany JSONnothe step's request body

Behaviour:

  • Steps run in order against one in-memory copy of the project. The file is written once, after the last step.
  • A step missing url or method is skipped without an error.
  • A step whose path matches no route gets 404.
  • The first step whose code is outside 200–299 stops the run; the request answers 500 and nothing is saved.
  • Each step contributes three response headers, numbered from 1: X-Step-URL-<n>, X-Step-Method-<n>, X-Step-Code-<n>.
  • On success the request answers 200.

One sharp edge: the project id inside each step's url is ignored. The project is opened once from the transaction's own URL, and every step operates on it whatever id its path names.

The bundled Swagger UI ​

The daemon serves /usr/share/muxen-config/swagger/ at its root, so http://127.0.0.1:12000/ — or /api/config/ through nginx — is a browsable API console.

Its swagger.json describes the project, settings, device, device parameter, sensor, media, muxing and button-mapping routes, and /info. It does not describe /health, /deploy, /apps, the transaction, view, energy, synapse or commissioning routes; this chapter is the complete list.

TypeScript packages ​

MUXEN also publishes three npm packages to the @muxen scope on the MUXEN GitLab registry. They share the daemon's version.

PackageWhat it is
@muxen/projecttyped project model plus two Pinia stores: useConfigAPIStore (default endpoint /api/config) and useConfigDeployStore (default endpoint /api/deploy.json)
@muxen/unitsthe unit database and conversions
@muxen/muxing-parametersturns a muxing form into the list of device parameters the switching rule needs

useConfigAPIStore implements the authentication described above: setProjectPassword() hashes with SHA-256 and stores Bearer <hash>, keeping it in localStorage under raken-last-open-authorization so a reload can restore it with restoreAuthFromStorage(); clearAuth() removes it.

useConfigDeployStore reads the stripped deployment and fetches media and view bodies individually as they are needed.

@muxen/project also exports the daemon identity types: ConfigInfo (the GET /info body), DaemonInfo (the app/config/info payload), isDaemonInfo() and CONFIG_INFO_TOPIC. The package talks to configd over HTTP only and does not subscribe to the topic itself.

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