Appearance
The project file
A project is one JSON object in one file. /etc/muxen/deploy.json is a copy of such a file, so everything below describes the deployed configuration too.
This chapter is for someone reading or writing those files directly — building a template, auditing a boat, or implementing a client. Use the API for anything routine: it maintains the timestamps, the generated parameters and the cross-references that hand editing has to get right by itself.
Shape
json
{
"version": 1,
"metadata": { … },
"settings": [ … ],
"devices": [ … ],
"muxing": [ … ],
"sensors": [ … ],
"energy": [ … ],
"media": [ … ],
"buttonMapping": [ … ],
"views": [ … ],
"commissioning": [ … ],
"synapse": { … }
}version is the integer 1 on a project created by this daemon.
Sections are created lazily. A newly created project carries metadata, settings, devices, muxing, sensors, energy, media and buttonMapping; views, commissioning and synapse appear the first time something touches them. A missing section reads as empty, so absence and [] mean the same thing.
Everything except synapse is an array of objects identified by a field, not a map. Order in the array is not meaningful, except in commissioning[].rules, where the index is the addressable identity of a rule.
Timestamps
Two keys recur throughout:
| Key | Written | Meaning |
|---|---|---|
ctm | when the object is created | creation time |
mtm | in metadata, on every save | last modification time |
Both are UTC in %Y-%m-%dT%H:%M:%SZ — 2026-08-16T09:12:44Z. Only metadata.mtm is refreshed; the ctm on a device, a setting or a switching rule is the moment that object was added and is never touched again.
metadata
| Key | Type | Written by |
|---|---|---|
id | string | create, restore-by-duplication — the short name, and the filename |
name | string | the name field of the create or duplicate body; "No name" by default |
version | string | "1.0" on creation. A free-text configuration version, unrelated to the top-level version integer |
uid | string | a UUID v4: generated at creation, and again for each duplicate; kept by a rename |
ctm | string | creation time; re-stamped on duplication, kept by a rename |
mtm | string | every save through the API |
fromTemplate | string | duplication, holding the source project's metadata.id |
duplicateFrom | string | duplication, holding the source project's metadata.uid; absent when the source has none |
withCommissioning | object | duplication with commissioning, holding the answers that were applied |
icon | string | not written by the daemon; reported in the project list when present |
The uid names a project, not a file: it follows the project through a rename (PUT /project/<id>/rename/<new-id>) and never through a duplicate. A duplicate is a new project, so it gets a new uid, and duplicateFrom records the uid it was copied from — the way fromTemplate records the short name. The shipped templates carry no uid, so a project duplicated from one has a uid of its own and no duplicateFrom. Projects duplicated before this rule share their source's uid; nothing in the daemon or the interfaces reads it yet.
metadata.id is not rewritten by PUT /project/<id>/restore. After a restore under a different short name, the filename and metadata.id disagree until something rewrites it.
settings
Boat-wide values. Each entry is the posted body plus a name and a ctm:
json
{ "name": "PrimaryLanguage", "value": "US", "ctm": "2026-08-16T09:12:44Z" }A PUT /project/<id>/settings/<name> removes any existing entry with that name and appends the new one, so a setting is unique by name and the body may carry any fields at all.
Three settings exist on a freshly created project:
| Name | Value |
|---|---|
DegradedModeName1 | "Nominal mode" |
PrimaryLanguage | "US" |
AlternativeLanguages | [] |
Two names have meaning to the daemon itself:
| Name | Meaning |
|---|---|
password | its hash field protects the project — see Projects |
FeatureCommissioning | removed from the copy when a duplication applies commissioning |
devices
One entry per MUXEN device fitted.
json
{
"ctm": "2026-08-16T09:12:44Z",
"function": 1,
"functionName": "Bloc 8",
"instance": 0,
"deviceId": 64,
"parameters": [ … ]
}| Key | Type | Meaning |
|---|---|---|
function | integer | the MUXEN function code, 0–63 |
functionName | string | the human name of that function code |
instance | integer | the instance of that function, 0–63 |
deviceId | integer | function × 64 + instance |
parameters | array | the device's configuration |
The three identity fields are redundant on purpose: deviceId is what URLs use, function and instance are what lookups compare. A device is found by matching both function and instance, so an entry whose deviceId disagrees with them is unreachable through the API.
functionName is filled from the function code at creation and is not re-derived afterwards — it is a label, not an identity.
parameters
json
{ "name": "Name", "value": "Saloon lights", "ctm": "…" }
{ "name": "CabineType", "value": "B2", "extra": true, "ctm": "…" }| Key | Type | Meaning |
|---|---|---|
name | string | parameter name, unique within the device |
value | string | always stored as a string, whatever JSON type was posted |
extra | boolean | present only when true: the parameter has no firmware counterpart and exists for the interface's benefit |
ctm | string | when it was set |
Writing a parameter that already exists removes the old entry and appends the new one, so the array order follows the order of writes.
Parameters created automatically
Adding a device through PUT /project/<id>/device/<deviceId> generates:
| Parameter | Value |
|---|---|
Name | "<functionName> #<instance + 1>", e.g. Bloc 8 #1 |
and, for an SFSP interrupter (function 23) only:
| Parameter | Value |
|---|---|
VirtualDevice | "1" |
NameLed1 | "Bottom Left" |
NameLed2 | "Top Left" |
NameLed3 | "Bottom Right" |
NameLed4 | "Top Right" |
Model | "simple" |
muxing
The switching rules: what drives what. Each entry is the posted body plus name and ctm, and the body's config array is the part the daemon acts on.
json
{
"name": "OPT-saloon-lights",
"ctm": "…",
"config": [
{ "deviceId": 64, "name": "EqnOut2", "value": "B1_1=O1_2" },
{ "deviceId": 64, "name": "ButtonMomentary2", "value": "1", "extra": true }
]
}config entries are device parameters with the target device named inline:
| Key | Type | Required |
|---|---|---|
deviceId | integer | yes |
name | string | yes |
value | string | yes |
extra | boolean | no |
Writing a muxing entry applies every config entry to its device before the rule is stored. If any of them cannot be applied — a deviceId not present in the project, a missing name or value — the whole rule is rejected with 400 and the muxing array is left untouched.
Deleting a muxing entry reverses all of that:
- every parameter named in
configis removed from its device, - every
commissioningrule of typemuxingwhosetargetis this entry's name is removed, - every commissioning group left with no rules is removed.
So muxing is the one section with side effects on two others. A hand edit that removes an entry without unwinding its config leaves orphan parameters on the devices.
sensors
Analogue sensor definitions: which device channel, which unit, and how to scale between them. Each entry is the posted body plus name and ctm.
json
{
"name": "FreshWaterPortside",
"input": { "deviceType": 1, "deviceInstance": 1,
"deviceChannel": "analogInput0", "min": 0.0, "max": 5.0 },
"output": { "unit": "obix:units/liter", "min": 0, "max": 250 },
"scaleType": "polynomial",
"polynomial": [ 0, 50 ],
"ctm": "…"
}The daemon stores the body without validating it: input, output, scaleType and the scaling coefficients are interpreted by the services that consume the deployment, not here.
energy
The electrical zones, one entry per zone, each the posted body plus name and ctm. Stored, not interpreted. What the energy daemon actually reads from a deployment is documented with that daemon.
buttonMapping
Maps a physical button to a named function. Each entry is the posted body plus name — the function identifier, which is the URL segment — and ctm.
| Key | Type | Meaning |
|---|---|---|
deviceId | integer | the MUXEN device id of the button panel |
index | integer | the button channel index on that device |
The REST path is /button-mapping; the JSON key is buttonMapping.
media
Images and other blobs, carried inside the project.
json
{
"name": "logo",
"ctm": "…",
"data": "iVBORw0KGgo…",
"metadata": { "size": 20416, "etag": "\"3241589\"", "mime": "image/png" }
}| Key | Meaning |
|---|---|
data | the bytes, base64-encoded |
metadata.size | the length of the original body in bytes |
metadata.mime | the Content-Type of the upload, or application/octet-stream if it had none |
metadata.etag | a quoted hash of the encoded data, used as the HTTP validator |
data is what makes projects large, and it is the field stripped from GET /project/<id> and GET /deploy. GET /project/<id>/media/<name> decodes and returns the bytes with the stored MIME type.
views
The screen pages. A view is mostly opaque to the daemon — it stores what the editor posts — but three of its fields are treated specially and five more have a dedicated endpoint.
| Key | Meaning |
|---|---|
name | the page name, unique, [a-zA-Z0-9-]+ because it is a URL segment |
elements | the page content: every drawable element |
conditionGroups | visibility conditions |
image | the page's own embedded image |
tags | replaced wholesale by PUT …/view/<name>/tags |
enabled | boolean, replaced by PUT …/view/<name>/enabled |
elements, conditionGroups and image are the heavy fields: they are removed from GET /project/<id>, from GET /project/<id>/view and from GET /deploy. Fetch a whole page with GET /project/<id>/view/<name>.
PUT …/view/<name>/settings accepts exactly these keys and ignores any other:
label, description, background, arrowSpacing, defaultIconRadius, defaultArrowStrokeWidth, defaultTextFontSize.
PUT …/view/<name>/texts takes an array of patches { "id": …, "content": …, "translations": … } and applies each to the element of elements[] whose type is "text" and whose id matches. A patch that matches nothing is ignored; content and translations are each replaced only if the patch carries them.
PUT …/view/<name>/name renames a page. It refuses an empty name, a name outside [a-zA-Z0-9-], and a name already used by another view (409).
commissioning
The option groups a template offers. Present on templates, and removed from a project the moment commissioning is applied to it.
json
{
"id": "watermaker",
"name": "Watermaker",
"ctm": "…",
"rules": [
{ "type": "device", "target": 1280 },
{ "type": "muxing", "target": "OPT-watermaker" },
{ "type": "device-parameter",
"target": [ { "deviceId": 64, "name": "EqnOut3", "value": "…" } ] }
]
}| Key | Type | Meaning |
|---|---|---|
id | string | required — the key the duplication body answers with a boolean |
name | string | required — display name. A group missing it aborts the apply |
rules | array | required, and must be an array |
Rule objects:
| Key | Type | Meaning |
|---|---|---|
type | string | muxing, sensor, view, device or device-parameter. Any other value aborts the apply |
target | depends on type | a name for muxing / sensor / view, an integer device id for device, an array of {deviceId, name, value, extra?} for device-parameter |
reverse | boolean | inverts the condition — see Projects |
Rules are addressed by index: DELETE /project/<id>/commissioning/<groupId>/rules/<index>. Removing the last rule of a group removes the group.
synapse
The one section that is a JSON object, not an array, keyed by automation instance id:
json
"synapse": {
"3": { … },
"7": { … }
}A PUT /project/<id>/synapse/<instanceId> replaces the value at that key outright. The daemon stores the body without interpreting it.
Writing a project file by hand
It works — the daemon parses whatever is on disk — but four rules keep it from producing a file that reads back wrong:
- Keep
deviceId,functionandinstanceconsistent. Lookups match onfunctionandinstance; URLs usedeviceId. - Keep parameter
valuea string. The API coerces; a hand-written number stays a number and consumers that expect a string will not read it. - Do not remove a
muxingentry without removing the parameters itsconfigwrote, and the commissioning rules that point at it. - Fix the ownership afterwards. A file written as
rootunder/etc/muxenstops the service from saving over it. Runsystemctl start muxen-config-perms.
A file that does not parse is not an error anywhere visible: it is skipped by the project list, answers 404 on every endpoint, and — if it is deploy.json — makes GET /deploy answer 404 while the nginx fallback happily serves the broken bytes.
