Skip to content

muxen-detect — remote port audit and identity extraction ​

muxen-detect walks a serial or CAN port behind a MUXEN gateway through a portal mirror and extracts a device identity dossier — manufacturer, model, serial number, firmware and hardware revision — plus the state of the line itself.

It audits, it does not operate. Production firmware reads only the registers it needs to run a device and ignores identity metadata: the Blue Airco driver polls temperature, mode and fan and never asks the unit who it is. This tool does the opposite, and extracts exactly the provenance metadata operational firmware skips. Identifying the protocol is a means; the identity record is the deliverable.

Two properties shape everything below.

  • It never transmits on a bus it does not own. Every byte of evidence is read: wire counters and the gap histogram from the gateway, then a bounded capture of the mirror. Nothing goes out on a segment that reaches customer equipment, so there is no master to collide with and no third-party device to disturb.

    The one exception is licensed by the gateway, not by the operator: a board whose remote interface is a component of the gateway itself — today only muxen_can-enocean and its on-board EnOcean module — has no other party on the wire, so four fixed read-only queries ask the module who it is. That is the only write this tool performs, its bytes are compile-time constants, and the record says so in its note. --no-probe disables it; --replay and --no-portal disable it implicitly.

  • Every field carries its provenance. A record says how each value was obtained and whether a checksum validated the frame it came from. An inferred value is not an identification, and "serial not disclosed by this device" is a valid audit result rather than a failure.

What it can identify ​

BusFamilyIdentity it yields
serialVE.Direct (text blocks)manufacturer (Victron Energy), model from PID, serial from SER#, firmware from FW
serialNMEA 0183talker IDs, a device category (inferred, never a model), manufacturer mnemonic from proprietary $P sentences
serialModbus RTUslave address(es) seen on the wire; identity strings only when a Read Device Identification (0x2B/0x0E) response is overheard
serialEnOcean ESP3passively: the transceiver variant inferred from the protocol dialect, the Source IDs it hears, and its own RSSI. Probed: the module's Chip ID as a serial, its app version, the name it calls itself, radio channel and frequency
CANNMEA 2000 / J1939manufacturer code and name, device class, unique number and source address from Address Claim (PGN 60928); model, serial, firmware and product string from Product Info (PGN 126996)

LIN is not supported. LIN identity is a different problem: no address claim, no product-info equivalent, and what exists — the 2.x diagnostic pair 0x3C/0x3D — is request/response, so nothing is obtainable passively. LIN gateways are skipped by the walk with that reason recorded.

Before you run a walk ​

A walk is a maintenance act, not a monitoring tool. Read this once. All of it applies to --target as well, one gateway at a time instead of every one.

  • It opens a portal session per gateway, so that gateway's products go offline while it is audited. During a session the gateway runs portal firmware, not its product firmware: it is not on the MUXEN bus, and whatever it drives is not being driven.
  • Strictly one gateway at a time. The portal runs one session per Brain, and the walk waits for each gateway's verified revert before opening the next. Budget roughly 40 s of fixed overhead per gateway — the firmware upload alone is ~16 s — on top of the measuring.
  • RS-485 segments normally go quiet during an audit. On a Muxen-native Modbus channel the polling master is the gateway firmware the session just replaced, so the traffic a passive audit would have used is gone precisely because you are auditing. quiet line is the expected result there, and the report says so rather than claiming the port is dead.
  • Bus extenders are audited last, because opening a portal on one takes the whole segment behind it offline; anything still queued behind it would fail to open.
  • There is no confirmation prompt. Running muxen-detect --walk is already a deliberate act. What you get instead is a pre-flight summary of what is about to go offline, a live ETA, and two cheap ways to stop: P pauses after the current gateway, never leaving a session open, and q closes and reverts before exiting.

--target does all of the above for the single gateway it names, pre-flight summary included. --attach and --replay do none of it: they own no session and take nothing offline.

Running it ​

muxen-detect --walk [options]              # audit every gateway on the bus
muxen-detect --target <id> [options]       # audit ONE gateway, no bus scan
muxen-detect --attach <mirror> [options]   # audit a session someone else opened
muxen-detect --replay <file> [options]     # re-analyse a capture, no hardware

One of --walk, --target, --attach or --replay is required. --walk and --target are mutually exclusive.

OptionDefaultMeaning
-W, --walk—scan the MUXEN bus and audit every portal-capable gateway in turn, opening and closing a session for each
--target <id>—audit exactly one gateway, named by UID or deviceId, without scanning the bus first
-a, --attach <mirror>—audit one mirror of a session that is already open: a PTY path (/dev/tty-portal0-ch2) or a CAN interface name (portal0)
-R, --replay <file>—re-analyse a --dump-raw capture through the same pipeline; implies no portal and no hardware
-P, --no-portalofftreat the mirror as an ordinary physical port; loses the wire counters, line errors and gap histogram — and the identity probe with them, since without the ctl there is no way to learn whether one is licensed
--no-probeoffnever transmit, even on a gateway whose remote interface is a component of the gateway itself
-c, --portal-ctl <path>/run/muxen-portal/ctl.sockctl socket of the daemon
-w, --window-ms <n>5000capture window per channel
-t, --timing-window-ms <n>5000wire-timing collector window; 0 disables it
-T, --text <file>—text report; - means stdout
-J, --json <file>muxen-detected.json under --walk, muxen-detected-<id>.json under --target, else —JSON report; - means stdout
-D, --dump-raw <file>—write the raw capture plus its manifest (attach mode)
-B, --batchautono TUI, line-oriented progress; implied when stdout or stdin is not a TTY
-f, --forceoffoverwrite an existing report path
-v, --verboseoffper-stage detail on stderr (wire, timing, capture, family)
-h, --help, -V, --version—the MUXEN standard pass

If both --json and --text are given, the JSON path wins.

--walk always produces a report. Naming no path writes muxen-detected.json in the working directory, overwritten on each write — W is meant to be pressable repeatedly during a walk, and the file should hold the audit's latest state rather than accumulate copies.

A file rather than stdout, deliberately: curses owns the screen for the duration, so a report aimed at stdout can only be written by dropping out of curses and back, which makes the audit something you watch scroll past instead of something you keep. --text - still works and W still handles it that way; it is just not the default. --attach is unaffected — it prints its record to stderr already.

Auditing one gateway — --target ​

--target is --walk with the enumeration removed: same session, same capture, same report, one gateway. It takes a UID or a deviceId:

muxen-detect --target 4500310003503159      # by UID — prefer this
muxen-detect --target 1344                  # by deviceId

Prefer the UID. It is the stable identity, and at the parking instance a deviceId is shared by every parked gateway on the bus and addresses none of them (Supported gateways).

Nothing is checked before the open. The tool does not know whether the target exists, is a supported board, or is a gateway at all — the daemon answers all three when it refuses the open, and its refusal becomes the row's reason. Board and type are likewise not guessed here: they are adopted from the session once it is up, from the daemon that owns the board table.

What you give up against a walk is the bus context. A walk tells you what else is out there and orders the work; --target tells you only about the gateway you named. What you get back is the scan — a full UID scan plus a readconfig chain over every device on the bus, which on a thirty-device boat is most of the wall clock of auditing one gateway.

The report defaults to muxen-detected-<id>.json rather than the walk's muxen-detected.json, so auditing one gateway cannot quietly replace the report of a full walk with a file that looks like one and holds a single row.

Examples ​

sh
# Audit the whole bus from a terminal, JSON to a dated file.
muxen-detect --walk --json /var/tmp/audit-$(date +%F).json

# Re-check one gateway you already know, without walking the bus.
muxen-detect --target 4500310003503159 --batch --json -

# The same from a script or over a flaky SSH link: no ncurses, progress
# on stderr, report on stdout.
muxen-detect --walk --batch --json - > audit.json

# One channel of a session a colleague already opened, with detail.
muxen-portal open-channel 2
muxen-detect --attach /dev/tty-portal0-ch2 --verbose --text -

# The remote CAN segment of an open session, 20 s window.
muxen-detect --attach portal0 --window-ms 20000 --json can.json

# Keep the evidence: raw bytes plus the wire-side manifest beside them.
muxen-detect --attach /dev/tty-portal0-ch1 --dump-raw ch1.bin

# Re-analyse that capture later, on any machine, with no gateway.
muxen-detect --replay ch1.bin --verbose --text -

Exit codes ​

CodeMeaning
0identified — at least one device yielded an identity field
2partial — a record was produced but no identity, or a gateway failed during the walk
3nothing found, or no portal session behind the mirror
5refused — the mirror is already being read, or the session is recovering
1error (bad arguments, unreachable daemon, report not written)

The automatic walk ​

--walk runs this sequence:

  1. Scan the bus through the daemon: a UID scan plus a readconfig per device. Slow by nature; it is done once.
  2. Classify. LIN gateways and devices whose type the firmware is too old to report are marked skipped with that reason — never silently dropped, because a missing row reads as a bug in the scan.
  3. Order the queue: serial gateways first (their audits are the long ones, so an interrupted walk has completed the expensive work), then CAN, then bus extenders last.
  4. Pre-flight summary on stderr: how many gateways will be audited, how many were skipped and why, and the time estimate. Displayed, not asked.
  5. For each gateway, one at a time: open the session, survey every channel at once and then identify the ones with traffic, close, and wait for the verified revert. A gateway that fails to open is recorded with the daemon's own reason and the walk continues.
  6. Report to --text / --json.

The ETA is measured, not assumed: a rolling mean of the gateways already completed, so it becomes meaningful after the first one.

In the TUI the walk does not start by itself — press S. In batch mode it starts immediately and prints each action as it changes.

Two passes on a serial gateway ​

Pass 1 — survey, all channels at once. Wire counters, then one timing window covering every open channel. Neither half needs a PTY: the counters are the daemon's STATUS deltas, and the collector runs in the gateway's UART interrupt, per channel, with a per-channel deadline. So the windows are armed together and measured in the same five seconds. The result is published before anything is identified — the TUI shows every channel with its rate and burst shape while the first capture is still running, which is what tells an operator which channels are worth waiting for.

Pass 2 — identify, where the survey found something. A channel the gateway counted zero bytes on for a full window has nothing for a capture to find, so it keeps its survey verdict and the note says plainly that no capture was run. That is the difference between an idle four-channel gateway costing 5 s and costing 40 s.

The skip is only ever taken on evidence. If the collector was unavailable — an older image refuses it — the walk captures anyway, because a missing measurement is not a measurement of nothing. The same applies where a probe is licensed: silence is exactly the case that most needs asking, so a probe-capable channel is always identified even if the gateway counted nothing.

Promotion. Opening a session gives a PTY to one channel, so the others come back as monitors — counted by the gateway but not shipped, and therefore unreadable. Pass 2 promotes the ones it intends to read, issuing all requests first and then a single shared wait, so four promotions cost one five-second budget rather than four. Promotion transmits nothing of its own, and the per-gateway close reverts the unit wholesale, so nothing is left changed. It does cancel that channel's collector, which is why it happens strictly after the survey. A refused promotion leaves the channel with its survey-only verdict and says so.

CAN channels are unchanged: a single capture window, then Address Claim, Product Info and a PGN scan.

The TUI ​

Default when stdout and stdin are both TTYs and --batch was not given.

┌─────────────────────────────────────────────────────────────────────────────┐
│ muxen-detect  running 4/12                                        ETA 12:40 │
├─────────────────────────────────────────────────────────────────────────────┤
│ DEVICES                        │ muxen_can-serial_v2                        │
│                                │ 3100410003504257  deviceId 5  auditing     │
│ + 3100…4257 serial_v2   serial │                                            │
│ + 2000…9007 cancan      can    │ ch1  in use     9600 bd  modbus_rtu        │
│ > 3100…1182 serial_v2   serial │   shape framed        6 bursts             │
│   3100…7741 cancan      can    │   manufacturer  Blue Airco       verified  │
│ - 2000…3310 can-lin     lin    │ ch2  in use    19200 bd  vedirect          │
│     LIN is out of scope        │   shape block-stream  4 bursts             │
│ x 1900…0042 (unknown)          │   model         SmartSolar 75/15 verified  │
│     gateway type not reported  │   serial        HQ2148ABCDE      verified  │
│                                │ ch3  monitor   19200 bd  —                 │
│                                │   monitor channel: promote it with …       │
├─────────────────────────────────────────────────────────────────────────────┤
│ 3100…1182 ch2: capturing (5000 ms)                     [████████░░░░]  62%  │
├─────────────────────────────────────────────────────────────────────────────┤
│ [S] start/pause [A] one [X] skip [P] pause [W] write [R] rescan [?] [Q] quit │
└─────────────────────────────────────────────────────────────────────────────┘

The left pane is the device list from the pre-walk scan — a snapshot, not a live view, because mid-walk the audited gateway leaves the bus as its product identity and reappears as a portal device. The right pane follows the selection and shows that gateway's channels. The status line is the one to watch on a long walk: it says what is happening right now and never goes more than a second without an update, so a 45-second capture cannot be mistaken for a hang.

State is glyph-first, so it survives a terminal with no colour:

GlyphState
(blank)queued
>auditing now
+done, something was identified
~done, partial — records exist but no identity field
xfailed — open refused, or the session never became active
-skipped, with the reason on the following line
KeyAction
↑ ↓ PgUp PgDn Home Endmove the device cursor, or scroll the channel pane when focused right
Tab ← →switch pane focus
j kscroll the channel pane
Sstart the walk, or pause it after the current gateway
Aaudit only the highlighted gateway
Ppause after the current gateway completes
Xskip the selected device (queued only)
Wwrite the report now
Rrescan the bus — refused while a walk is running, rather than silently closing a session
?key help
q Ctrl-Cquit; any open session is closed and reverted first

Below 80×12 the panes are unusable, so the TUI says so and the walk carries on regardless — the audit does not depend on the window size.

Reading the output ​

Outcomes ​

Every channel produces a record, including the ones that found nothing. Four different kinds of "nothing" are four different follow-ups, and collapsing them into a blank row is the fastest way to make an audit tool untrustworthy.

OutcomeWhat it meansWhat to do
oka validated frame was decoded and a family was resolvedread the identity fields
quiet linethe channel is open, the line is clean, and nothing is talking: a slave awaiting a masterexpected on a Muxen RS-485 segment — the poller was the firmware this session replaced
quiet line, note says no capture was runthe same verdict, reached from the survey alone: the gateway counted no bytes for a whole timing window, so pass 2 skipped the channelnothing to do — a capture would have spent another window confirming the same silence
wrong baudthe gateway is receiving bytes at a high error ratethe configured rate is wrong; re-open the channel at another rate
no signalno bytes locally and the gateway counted nonenothing is connected to that port
insufficient datatraffic seen, but no complete validated frame in the windowlengthen --window-ms, or the traffic is a family the tool does not decode
no portal sessionthe mirror does not exist or nothing backs itopen a session first
busyanother process holds the PTYtwo readers split a byte stream, so the tool refuses rather than corrupt both captures

A monitor channel is called out ahead of any of these: the gateway counts it but ships no stream, so nothing can be captured until it is promoted with muxen-portal open-channel N. A walk promotes monitor channels itself, so seeing this in a walk report means the promotion was refused, and the verdict there rests on the survey alone. In --attach mode the promotion is yours to make.

The timing shape is reported alongside — quiet, framed (short bursts with a turnaround gap: a request/response exchange), block-stream (long unprompted runs, e.g. VE.Direct), streaming (no clean boundary) or unknown (no valid report). It comes from the gateway's own UART interrupt, because the tunnel carries byte order and contiguity but not silences.

Provenance — the reason to trust a field ​

Each identity field is printed as value [provenance, verified], and carries the same two attributes in JSON.

ProvenanceMeaning
passive-streamthe device emitted it unprompted — the strongest thing a listening audit can have
overheardseen in another master's traffic (Modbus identity is only ever this)
portal-statsgateway context, not the device: line settings and the like. It never populates an identity field
inferredderived, not an identification — an NMEA 0183 talker ID gives a device category, and a category is not a model
probedobtained by asking — the EnOcean identity probe, the only writes this tool performs, and only on a gateway that declares an on-board peer
can-inventoryreserved for evidence already known upstream via the MUXEN CAN bus; not currently produced

verified means a checksum or CRC validated the frame the value came from and the tunnel lost nothing during that window. Without it, the value is what was decoded, not what was proven.

A record also carries evidenceComplete. When it is false — tunnel loss, or the session changed under the capture — every field parsed from that window is suspect, and the text and TUI output both say so on the record rather than only in the file.

Records are keyed by gateway UID, channel and address: two identical MPPTs behind two gateways are two records.

The EnOcean probe, as an example of provenance ​

On a can-enocean gateway the module is soldered to the board, so the tool is licensed to ask. The difference between the two runs is exactly what the provenance model predicts:

Probed:

manufacturer   EnOcean                                      [probed, verified]
model          EnOcean 2.4 GHz 802.15.4 transceiver gateway (TCM515Z)
                                                            [probed, verified]
serial         8B864D46                                     [probed, verified]
firmware       1.2.0.0                                      [probed, verified]
radioChannel   11                                           [passive-stream]
frequency      2.4 GHz 802.15.4                             [passive-stream]
variantFrom    frequency-info

Telegrams only (a paired switch pressed during the window):

model          … (TCM 515Z)                                 [inferred]
senderId       0x2A515001                        [passive-stream, verified]
senders        1                                 [passive-stream, verified]
rssi           -61 dBm                           [passive-stream, verified]
variantFrom    esp3-packet-type
line           57600 8N1                                    [portal-stats]
serial         (absent)

variantFrom is the field to read first: esp3-packet-type means the variant was derived from the dialect, frequency-info means the module was asked and answered.

The serial number is the point of the exception. From telegrams alone there is no serial: the Source IDs in them belong to the switches the module hears, and reporting one as the port's serial number would be the most misleading thing this parser could do. Asking the module yields its own Chip ID, and that is recorded probed, never inferred.

A refusal is also an answer: a command the module reports as unsupported is recorded as absent, never as a value of zero.

The report ​

--json writes one object:

json
{
  "schemaVersion": 1,
  "tool": "muxen-detect",
  "toolVersion": "1.1.0",
  "startedUtc": "2026-07-27T09:14:02Z",
  "gateways": [
    { "uid": "3100410003504257", "board": "muxen_can-serial_v2",
      "state": "done" },
    { "uid": "2000...3310", "board": "muxen_can-lin", "state": "skipped",
      "reason": "LIN identity cannot be obtained passively: ..." }
  ],
  "devices": [
    {
      "bus": "serial", "protocol": "vedirect",
      "mirror": "/dev/tty-portal0-ch2", "channel": 2, "address": 0,
      "gatewayUid": "3100410003504257",
      "gatewayBoard": "muxen_can-serial_v2", "gatewayDeviceId": 5,
      "manufacturer": { "value": "Victron Energy",
                        "provenance": "passive-stream", "integrityOk": true },
      "model":        { "value": "SmartSolar MPPT 75/15",
                        "provenance": "passive-stream", "integrityOk": true },
      "serialNumber": { "value": "HQ2148ABCDE",
                        "provenance": "passive-stream", "integrityOk": true },
      "extra": { "line": { "value": "19200 8N1",
                           "provenance": "portal-stats",
                           "integrityOk": false } },
      "outcome": "ok", "evidenceComplete": true,
      "capturedUtc": "2026-07-27T09:15:30Z"
    }
  ]
}

The report states the walk, not only the devices: every gateway appears in gateways[] with the state it reached and, where relevant, the reason. A skipped or failed gateway is part of the audit result.

--text writes the same content with a plain header and one line per gateway, followed by the device records. Both come from the same in-memory records, so they cannot disagree. During a run each record is also printed to stderr in the human form as it lands.

An existing report path is never overwritten silently: --force is required. In a walk the report is rewritten in place each time it is written, so --force is not needed there.

Captures and replay ​

--dump-raw FILE (attach mode) writes the capture exactly as it arrived, before any interpretation, plus FILE.manifest.json beside it. Serial captures are raw bytes; CAN captures are candump log format, so they read in every can-util and replay with canplayer.

The manifest is what makes a dump worth keeping. The bytes alone lose the baud they were decoded at, the gateway's line-error counters and the burst shape, and none of that is recoverable from the file. The manifest records the mirror and channel, the window, the gateway UID, board and device id, the full wire counters, the timing histogram, and the applied baud and framing.

It also records "rawTiming": "local-arrival", and that caveat matters: inter-frame spacing is not preserved in a raw dump. Read timing from the manifest's gap histogram, never from the file.

--replay FILE pushes a dump back through the identical pipeline — the same detectors, the same parsers, the same record assembly — with no portal, no hardware and no session. The format is auto-detected (candump versus raw bytes). With the manifest present the replay is as faithful as the live run, including the Modbus timing half; without it, it degrades to content-only evidence and says so. If the manifest flags the capture as wrong-baud, the replay warns that the bytes are unreliable.

What it consumes from the portal ​

Everything the tool knows about the wire comes from the daemon, over /run/muxen-portal/ctl.sock. The mirror alone cannot answer any of it.

Factctl commandWhy the mirror cannot answer it
which gateways exist, and what kindlist-targetsit is a bus scan, not a port property
is this port alive; is the baud wrongstatsa PTY has no UART — TIOCGICOUNT returns ENOTTY, so local line-error counts are always zero
true byte rate on the wirestatsthe local arrival rate is the tunnel's batching rate
burst shape and inter-frame gapstimingthe tunnel preserves byte order and contiguity, not silences
did the tunnel lose datastats (seqGapLost)a gap in a byte stream is invisible
are we mid-recoverystatusthe mirror survives a session loss and simply goes quiet

stats resolution is 1 s, so a capture window shorter than ~3 s yields counters too coarse to score. A session in recovering is refused outright rather than audited through the gap: during it the mirror is alive but silent, which would read as "the device went quiet".

--no-portal skips all of it and treats the mirror as an ordinary physical port. That is how fixtures are built without a gateway; it costs the wire prior, the line errors and the shape.

Packaging and runtime posture ​

muxen-detect is a second binary of the same source package, installed to /usr/bin/muxen-detect. It links json-c, ncursesw and pthreads, and carries no data files and no signature database — the detectors and parsers are in the binary. Shipping inside the muxen-portal package is what guarantees that the tool and the daemon whose ctl JSON and timing opcode it consumes are the same build.

No service, no unit, no capabilities, no privileges: it is a one-shot CLI run by an operator or a script. The ctl socket is mode 0666, the PTY slaves are 0666, and portal0 is an ordinary SocketCAN interface. Writing a report or a capture bundle only needs a writable path.

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