RFC-0003: Mode Buses #158
Closed
kn4oqw-clint
announced in
RFCs
Replies: 1 comment
|
This has already been implemented. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
9f15099(internal/config/render.goRetiredBridgeUnits, the dormantysf2dmr/dmr2ysf/ysf2nxdn/dmr2nxdn/nxdn2dmrstore sections)Summary
A bus is a named, operator-created object (e.g. "Local Bus A") to which modes are attached (DMR → Bus A, YSF → Bus A). Voice entering the bus from one attached mode is converted and emitted to every other attached mode, with IDs, callsigns, and talkgroups translated to each destination's addressing. One bus replaces what used to be a hand-wired pile of pairwise
YSF2DMR/DMR2YSF/… daemons: the operator names the endpoints and the plumbing is generated.This RFC fixes the transport, the transcoding envelope (what may be attached together), the addressing/translation model, the store shape and its migration from the dormant bridge sections, the loop-prevention rules, and the test contract. It contains no implementation — the daemon internals are a follow-up.
Motivation
The incumbent stacks (Pi-Star, WPSD) expose cross-mode transcoding as a grid of independent binaries from
juribeparada/MMDVM_CM— one process, INI, and enable toggle per ordered pair. Waypoint mirrored that in its first cut (five*2*store sections, five renderers, five systemd units) and then retired it in9f15099, because the per-pair model has three defects the operator pays for:internal/config/render.go:434-452: YSF3200/4200, P2532010/42020, NXDN14021/14020, D-Star20011/20010, M1717011/17010; DMR62031/62032). A bridge that terminates that pair is the sole consumer of it. Two bridges touching the same mode cannot both own its loopback — so the pairwise model does not compose into a hub even in principle.YSF2DMRandNXDN2DMReach carried their own DMR-masterAddress+Password(internal/config/model.go:72-79,114-122), duplicating credentials Waypoint already models once asNetworks[]behind the local DMRGateway.The bus is the missing object. It gives the operator the topology directly, owns each attached mode's loopback exactly once, and sources credentials from the existing model instead of re-entering them.
Design
1. Transport & topology
Decision: a single Waypoint-owned bus daemon per bus (
waypoint-bus@<id>.service, a templated unit) that terminates each attached mode's loopback endpoint and hubs frames between them. It sits where the CM bridges sat — on the g4klx loopback — but as a hub, not a leg.The three candidates and why this one:
(a) Waypoint bus daemon on the loopback (chosen). The daemon binds each attached mode's fixed loopback pair (the ports in
render.go:434-452) and, per frame, reframes/transcodes and re-emits to the other attachments. Only this shape can hub N modes: the bus is the single consumer of each mode's one loopback endpoint, which is exactly the ownership the pairwise bridges fought over. It reuses upstream engine code rather than reimplementing DSP — the AMBE+2 reframe path is packet surgery (lift 49-bit AMBE+2 frames from the source superframe, repack into the destination's), and the cross-codec path links the same software vocoders MMDVM_CM uses (md380_vocoder,imbe_vocoder,mbelib; see §2). This honours the architecture rule (docs/architecture.md): add the layer upstream doesn't provide, don't fork the protocol implementations.(b) MQTT-native router — rejected as infeasible for voice. The pinned stack is MQTT-era, so this was worth checking against the code, not assumed. It fails: MMDVM-Host's MQTT plane carries only telemetry. The only
publish()call sites arelog,json, anddisplay-out(MMDVMHost/Log.cpp:86,104;MMDVMHost/MMDVM-Host.cpp:1227,3108—writeJSONMode/writeJSONMessage). The voice data plane leaves over UDP:CDMRNetwork::write→m_socket.write(...)on aCUDPSocket(MMDVMHost/DMRNetwork.cpp:356-364),CYSFNetworklikewise (MMDVMHost/YSFNetwork.cpp:104). There is no AMBE/voice payload on any MQTT topic, so an MQTT router has nothing to route. MQTT remains the bus's status plane (§6, dashboard), never its media plane.(c) Orchestrate existing MMDVM_CM binaries as pairwise legs — rejected as the architecture, adopted as the engine. Running the stock binaries would mean an N-mode bus is N(N-1)/2 processes, each wanting exclusive ownership of a mode's single loopback pair — so three legs on a {DMR,YSF,NXDN} bus collide on the YSF and DMR loopbacks and cannot coexist (defect 2 above). The UX would also have to paper over process fan-out. We therefore reject the pairwise topology but keep faith with its code: the bus daemon is the MMDVM_CM reframing/vocoder logic re-hosted as a hub. A 2-attachment bus is the degenerate case — one source, one destination, exactly one MMDVM_CM-equivalent leg — which is why migration from a saved bridge is lossless (§4).
Accepted cost, stated plainly: the bus daemon is new code on the media path, where the CM bridges were upstream code. We contain the risk by (i) reusing the upstream frame/vocoder routines verbatim, (ii) shipping only the reframe envelope first (§2), and (iii) a pure-render + simulated-traffic test contract (§6).
2. Transcoding reality — what may be attached together
Cross-checked against
juribeparada/MMDVM_CM(the directory listing and the per-tool INIs/READMEs), not folklore. Two findings correct the received wisdom:[AMBE]/vocoder section in their INI — confirmed inDMR2YSF/DMR2YSF.ini,NXDN2DMR/NXDN2DMR.ini, andYSF2DMR/YSF2DMR.ini. Conversion is packet reframing; it costs no DSP and loses no audio.DMR2P25/README.md: "performs software transcoding between IMBE 4400x2800(P25) and AMBE+2 2450x1150(DMR)" viaimbe_vocoder+md380_vocoder.M172DMR/README.md: "performs software transcoding between ICodec2(M17) and AMBE+2 2450x1150(DMR)" and notesmd380_vocoder"uses md380 firmware for vocoding, so this software needs to be run on an ARM based platform i.e. raspberri pi." The repo README describes the fork as using "md380 firmware to encode/decode AMBE+2 2450x1150 used by DMR/YSF/NXDN." So a Pi can transcode DMR↔P25↔M17↔D-Star in software — at CPU cost, with double-transcode audio degradation, and using extracted proprietary firmware whose redistribution is a licensing question Waypoint must not paper over.This yields two attachment tiers, and the UI validates at attach time, never at runtime:
Hard constraint — Waypoint distributes no extracted firmware. The software-transcode path in MMDVM_CM works by loading a vocoder blob extracted from TYT MD-380 radio firmware (
md380_vocoder), which is copyrighted and cannot be redistributed regardless of the underlying AMBE+2/IMBE patent status. Waypoint will not embed, bundle, build, or ship that blob. This is not an open question or a UI-consent checkbox — it is a line the project does not cross. Consequently the transcode tier, if and when it ships, may only obtain a vocoder by (a) a DVSI-licensed hardware device the operator owns (a ThumbDV/DVstick/AMBEserver — licensed silicon, no copyright question), or (b) a blob the operator supplies themselves, which Waypoint never carries. The whole transcode tier — modes, addressing depth, and the vocoder-sourcing decision above — is deferred to a later dedicated effort (planned alongside the encryption work), and is a non-goal for this RFC beyond reserving the model shape (§4) for it.Rules the attach-time validator enforces (table-driven, §6):
DMR2M17,DMR2NXDN,DMR2P25,DMR2YSF,DSTAR2YSF,M172DMR,M172YSF,NXDN2DMR,P252DMR,USRP2*,YSF2DMR,YSF2NXDN,YSF2P25) bounds what is ever expressible: D-Star, notably, has onlyDSTAR2YSF, so D-Star could share a bus only with YSF, never directly with DMR/NXDN/P25/M17. The validator refuses any combination outside the committed reframe tier until the deferred effort lands; it does not fail at runtime.The UI presents attach as a picker that greys out any mode which would make the bus invalid given what is already attached, with the reason shown ("no converter for D-Star↔DMR"; "transcode tier disabled"). Invalid buses cannot be saved, so they cannot render an unstartable unit.
3. Addressing & translation
Translation is per attachment, because each attachment is one edge of the hub and needs its own view of the addressing.
62031/62032(asDMR2YSF/DMR2YSF.inidoes:[DMR Network] RptPort=62032 LocalPort=62031), not its own upstream master. Translation params:slot(1|2), adefault_tg/startup target ([DMR Network] DefaultDstTG/StartupDstIdin the CM INIs), and an optional TG map (source-mode target → DMR TG). DMR ID ↔ callsign resolution reuses the station'sDMRIds.datlookup already wired for every gateway (render.goDMR Id Lookup/Id Lookupsections) — the bus adds no second lookup file.[YSF Network]keys the CM tools expose. Callsign is carried natively (YSF is callsign-addressed); the DMR side resolves the callsign→ID via the shared lookup.[NXDN Network] Id/TG/DefaultID(asNXDN2DMR.inishows). NXDN shares the DMR ID space, so ID resolution reusesDMRIds.dat(matchingrender.go's NXDNId Lookuppointing atDMRIds.dat).Credentials: reuse the existing network secrets; a bus holds none of its own. This is the decisive departure from the fat-bridge model. Because a DMR attachment rides the local DMRGateway, upstream authentication is already handled once by
Networks[](BrandMeister/TGIF/…Password,internal/config/model.goNetwork) and multiplexed by DMRGateway's generated routing (render.goRenderDMRGateway). The bus reaches a specific upstream by targeting the talkgroup/network the existingDMRRoute/Networkmachinery already routes — it never opens its own master and never stores a password. The dormantYSF2DMR.Password/NXDN2DMR.Passwordsecrets therefore migrate by pointing the bus's DMR attachment at the existing DMR network of that master (§4), retiring the duplicate-secret smell (defect 3). If no matching network exists, migration flags it for the operator rather than silently minting a credential.4. Model shape
Two new store sections (RFC-0001 rows; disabling a bus flips a bool, deletes nothing):
buses[]—{ id, name, enabled }.attachments[]—{ bus_id, mode, translation params (per §3), credentials_ref }, wherecredentials_refnames an existingNetworks[]entry (or gateway) rather than embedding a secret. A mode may appear in at most one attachment across all buses (enforced at write; see §5).RenderTargets(internal/config/render.go:85) grows one target per enabled bus —waypoint-bus@<id>.service+ its generated config — not one per leg. This keeps the hub as one unit (one supervised process, one restart on change) and keeps the render registry's "one mode/object contributes one entry" shape (issue #21 gateway-plugin seam). A bus with N attachments is still one target; the N endpoints are rows inside that target's rendered config. The renderer stays a pure function of the model (RFC-0001 property 1), so the same buses/attachments render byte-identically.Migration from the dormant bridge sections. The five retired sections carry a complete, previously-working attachment pair each. A saved
YSF2DMR(internal/config/model.go:72-79) seeds: oneBus(enabled = YSF2DMR.Enable), a YSF attachment (target TG → the DMR side'sStartupDstId), and a DMR attachment whosecredentials_refis theNetworks[]entry matchingYSF2DMR.Master(or a flag if none).DMR2YSF,YSF2NXDN,DMR2NXDN,NXDN2DMRmigrate the same way; the two that shared a DMR master (YSF2DMR,NXDN2DMR) may fold into the same bus if the operator wants DMR/YSF/NXDN to interoperate. Migration is one-way (sections stay dormant, not deleted, per RFC-0001's disable-preserves-data rule) and is covered by a render-equivalence property (§6.5).5. Loop prevention & arbitration
A hub that re-emits every inbound frame to every other attachment is a feedback loop by construction. Four rules make it safe; all are pure functions of frame origin + bus state and are exercised by the simulated-traffic test (§6.4):
SetSection's unknown-field rejection,model.go:533).6. Test contract
CI enforces these as release-blocking properties, in the RFC-0001 style (pure render + property-based + table-driven):
RenderTargetsproduces byte-identical output across repeated renders and is unchanged by unrelated store edits (RFC-0001 properties 1–2 extended to the two new sections). Each enabled bus contributes exactly one target/unit; a disabled bus contributes none and deletes no rows.{set of attached modes} → {valid | invalid, reason}asserts: reframe-tier subsets of {DMR,YSF,NXDN} are valid; any pair lacking an MMDVM_CM converter is invalid with the right reason (D-Star with anything but YSF; transcode-tier combos when that tier is disabled); a mode already attached elsewhere is invalid. The validator refuses to save invalid buses — so this is tested at the model boundary, never as a runtime failure.Alternatives considered
md380_vocoder(extracted MD-380 firmware). Rejected outright, and not as a cost trade-off: it would mean Waypoint redistributing a copyrighted firmware blob (§2 hard constraint). No CPU or UX argument reaches that question — the project does not ship extracted firmware. A future transcode tier sources its vocoder from licensed hardware or an operator-supplied blob only.Networks[]and re-solves upstream routing that DMRGateway already generates. Riding the local gateway loopback reuses both (§3).Open questions
Migrated from
docs/rfcs/0003-mode-buses.md; the drafting history is in the git log.All reactions