Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
c8db970
feat: fabric session protocol and wire format
cpunt Apr 2, 2026
fae844a
feat: fabric topic remapping and config bridge
cpunt Apr 2, 2026
2a7d48a
feat: HAL fabric integration and UART tuning
cpunt Apr 2, 2026
435025f
cleanup: consistent logging, fix indentation, document design decisions
cpunt Apr 2, 2026
b43ee67
feat: emit unretain on export when retained state is cleared
cpunt Apr 2, 2026
2e630b3
fix: retained replay overflow and double-unsubscribe panic
cpunt Apr 2, 2026
b170de8
fix: bus queue overflow on retained replay, remove dead import route
cpunt Apr 2, 2026
da1f50b
refactor: use message type constants instead of string literals
cpunt Apr 2, 2026
0680961
refactor: rename remoteNode to peerNode, align with Lua naming
cpunt Apr 2, 2026
a46446f
style: gofmt
cpunt Apr 2, 2026
c17e7d1
refactor: centralise dispatch unmarshal, remove helloSeen, remove log…
cpunt Apr 2, 2026
ba2d3e8
refactor: centralise link-up guard in dispatch
cpunt Apr 2, 2026
85a9260
refactor: centralise validation in validateInbound, add status consts
cpunt Apr 2, 2026
adc4ac3
refactor: enable exports on link-up, remove deferred arming
cpunt Apr 2, 2026
48b8512
refactor: make handlers void, always reset stale timer on receive
cpunt Apr 2, 2026
a9dc3e7
refactor: rename t to localTopic, add status consts
cpunt Apr 2, 2026
f9234d4
refactor: inline drainOutbound, extract tick const, optimise checkBus…
cpunt Apr 2, 2026
ac72ff3
refactor: rename wire.go to protocol.go, wire* structs to proto*
cpunt Apr 2, 2026
da21179
fix: restore production logging by setting handshakeOnlyOutput to false
cpunt Apr 2, 2026
1f349bc
fix: remove handshakeOnlyOutput, clean up main.go naming
cpunt Apr 2, 2026
2939b8b
refactor: remove bridge.go, inline config and dump handling in session
cpunt Apr 2, 2026
36e071f
refactor: move types and vars to top of session.go, drain HAL state o…
cpunt Apr 2, 2026
ec54325
refactor: remove halStateSub, read HAL state on demand in dump handler
cpunt Apr 2, 2026
31762fd
feat: add firmware transfer receive over fabric protocol
cpunt Apr 8, 2026
362983b
chore: bump go toolchain to 1.25.1
cpunt Apr 13, 2026
680ec7e
fix: raise pico2 stack to 8KB for fabric transfer workload
cpunt Apr 13, 2026
fa7ed75
fix: enlarge fabric UART RX/TX buffers to 4KB
cpunt Apr 13, 2026
d6d8ce7
fix: enlarge fabric session line queue
cpunt Apr 13, 2026
4baaf8c
diag: trace fabric transfer path and stamp firmware version
cpunt Apr 13, 2026
3e22ad8
diag: log active partition at boot and include in hal/dump reply
cpunt Apr 13, 2026
3c9eef3
fix: enlarge tinygo-uartx RX ring and surface drop counters
cpunt Apr 13, 2026
4f7b5d9
fix: eliminate firmware-transfer byte drops with safe-window GC
cpunt Apr 14, 2026
9906b65
cleanup: drop per-line CRC32 debug logs from serial_raw and fabric-rx
cpunt Apr 14, 2026
44a6e59
Merge branch 'main' into fabric-protocol
cpunt Apr 14, 2026
5cd7b3e
Merge branch 'fabric-protocol' into fabric-update
cpunt Apr 14, 2026
89e3688
fix: extend post-xfer_done settle to 250ms before reboot
cpunt Apr 16, 2026
a5ab390
rename: update ab-bringup -> pico2-a-b module references
cpunt Apr 16, 2026
b115173
cleanup: remove activePartitionForLogs helper
cpunt Apr 16, 2026
842fb8c
refactor: replace transferFactory interface with plain function
cpunt Apr 16, 2026
ffdd1c4
revert fabric.go spacing to match fabric-protocol
cpunt Apr 16, 2026
a2bdedf
refactor: replace protoMsg union with typed two-pass dispatch
cpunt Apr 16, 2026
b620107
refactor: split dispatch into pre- and post-handshake switches
cpunt Apr 16, 2026
5de3f1d
refactor: drop partition field from hal/dump reply
cpunt Apr 16, 2026
ea394c1
refactor: drop traceTailPreview and data_tail log fields
cpunt Apr 16, 2026
39b9aad
refactor: drop firmwareVersion const and ActivePartition boot log
cpunt Apr 16, 2026
cf9e201
refactor: restore log.Println in emitMemSnapshot
cpunt Apr 16, 2026
5df60c8
tune: trim fabric TX shmring to 2048 (one max-line frame)
cpunt Apr 16, 2026
0c6eca4
revert: restore fabric TX shmring to 4096
cpunt Apr 16, 2026
0aad351
refactor: drop rxBytesTotal heartbeat from serial_raw
cpunt Apr 16, 2026
3a2cc00
refactor: rename logRxCountersIfDue -> logRingFullChange
cpunt Apr 16, 2026
a1d1df2
revert: drop explicit fabric shmring sizing, use serial_raw default
cpunt Apr 16, 2026
6e39349
refactor: trim transfer.go + drop rp2350 prefix
cpunt Apr 16, 2026
5aa43e7
fabric: align wire schema with devicecode-lua@2c88090
cpunt Apr 28, 2026
0b20197
fabric: W3 link config + idle-chunk watchdog
cpunt Apr 28, 2026
ea93ffa
fabric: W6 active ping, session_reset, bounded inbound helpers
cpunt Apr 28, 2026
77c966d
fabric: W5 3-lane writer (control / weighted RR rpc-bulk)
cpunt Apr 28, 2026
fc3b62d
fabric: W7 swap UART roles, drop legacy CM5 telemetry JSON path
cpunt Apr 28, 2026
95eaf55
fabric: close W6/W7 gaps from review
cpunt Apr 28, 2026
d0e719f
fabric: don't untrack imported retains on transient non-retained pubs
cpunt Apr 28, 2026
3200e3a
reactor: fix CM5 peer node id "cm5-local" -> "cm5"
cpunt Apr 28, 2026
e445862
fabric: scan protoType manually to dodge TinyGo json reflect quirk
cpunt Apr 28, 2026
7523093
fabric: depth-aware protoType + fix cmd/fabric-test build
cpunt Apr 28, 2026
f8d02ba
fabric: instrument protoType bail paths + add build-tag banner
cpunt Apr 28, 2026
03e730f
reactor: keep fabric on uart1 (match proto_1 hardware wiring)
cpunt Apr 28, 2026
55a5bf7
fabric: drop hardware-bring-up debug instrumentation
cpunt Apr 28, 2026
d751b03
fabric: tighten protocol changes
cpunt May 18, 2026
c0dc2c3
fabric: remove unused protocol helpers
cpunt May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
build/
.vscode/settings.json
.vscode/settings.json
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"args": [
"build",
"-o", "${workspaceFolder}/build/devicecode.elf",
"-stack-size=3KB",
"-stack-size=8KB",
"-serial=none",
"-target=pico2",
"-tags", "pico_bb_proto_1",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
tinygo flash -stack-size=3KB -monitor -scheduler tasks -target=pico -tags "pico_bb_proto_1" main.go

## Flashing ISOC Power Board via USB port on Pico2
tinygo flash -stack-size=3KB -monitor -scheduler tasks -target=pico2 -tags "pico_bb_proto_1" main.go
tinygo flash -stack-size=8KB -monitor -scheduler tasks -target=pico2 -tags "pico_bb_proto_1" main.go

-------------------

Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
module devicecode-go

go 1.25.0
go 1.25.1

require (
pico2-a-b v0.0.0
github.com/jangala-dev/tinygo-uartx v0.0.0-20251028085354-58b6258234b3
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9
tinygo.org/x/drivers v0.33.0
)

require github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect

replace pico2-a-b => ../pico2-a-b
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/jangala-dev/tinygo-uartx v0.0.0-20251008020047-bc80b114e3cc h1:HU2VI0lw5wlu1rUgjzSuVH7IWQMNdZEbpDaoxCTVMmY=
github.com/jangala-dev/tinygo-uartx v0.0.0-20251008020047-bc80b114e3cc/go.mod h1:e3HxjGzBZBIsn/oYvWr707ug3IbkglEyivyYVxHRph4=
github.com/jangala-dev/tinygo-uartx v0.0.0-20251028085354-58b6258234b3 h1:b6mCDQEeeICoGpsbKyh/kfIRnr2DMK/wACLLi0t8uoU=
github.com/jangala-dev/tinygo-uartx v0.0.0-20251028085354-58b6258234b3/go.mod h1:e3HxjGzBZBIsn/oYvWr707ug3IbkglEyivyYVxHRph4=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
Expand Down
9 changes: 7 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ func main() {
ctx := context.Background()

log.Println("[main] bootstrapping bus …")
b := bus.NewBus(3, "+", "#")
// Queue length must cover the retained replay burst when fabric
// subscribes to wildcard export patterns (hal/cap/env/#,
// hal/cap/power/#). Each capability publishes retained info +
// status + value; pico_bb_proto_1 has ~26 retained topics across
// env and power domains. 32 provides margin for growth.
b := bus.NewBus(32, "+", "#")
halConn := b.NewConnection("hal")
uiConn := b.NewConnection("ui")

Expand All @@ -43,7 +48,7 @@ func main() {
}

// Reactor
r := reactor.NewReactor(uiConn)
r := reactor.NewReactor(b, uiConn)
r.Run(ctx)
}

Expand Down
124 changes: 124 additions & 0 deletions services/fabric/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package fabric

import (
"encoding/json"

"devicecode-go/types"
)

// decodeHALConfig extracts a HALConfig from an arbitrary payload,
// normalizing Lua empty-table encoding ({} → []) for known slice fields.
func decodeHALConfig(payload any) (types.HALConfig, string) {
switch v := payload.(type) {
case types.HALConfig:
return v, ""
case *types.HALConfig:
if v == nil {
return types.HALConfig{}, "nil_hal_config"
}
return *v, ""
case json.RawMessage:
return decodeHALConfigBytes(v)
case []byte:
return decodeHALConfigBytes(v)
default:
b, err := json.Marshal(v)
if err != nil {
return types.HALConfig{}, "payload_marshal_failed: " + err.Error()
}
return decodeHALConfigBytes(b)
}
}

func decodeHALConfigBytes(b []byte) (types.HALConfig, string) {
var probe map[string]json.RawMessage
if err := json.Unmarshal(b, &probe); err != nil {
return types.HALConfig{}, "json_unmarshal_failed: " + err.Error() + "; raw=" + truncateRawJSON(b)
}
if _, ok := probe["devices"]; !ok {
return types.HALConfig{}, "missing_devices_field; raw=" + truncateRawJSON(b)
}

// Lua encodes empty tables as {} (object) not [] (array).
// Normalize known slice fields so Go unmarshal accepts them.
for _, key := range []string{"devices", "pollers"} {
if raw, ok := probe[key]; ok && len(raw) == 2 && raw[0] == '{' && raw[1] == '}' {
probe[key] = json.RawMessage("[]")
}
}
fixed, err := json.Marshal(probe)
if err != nil {
return types.HALConfig{}, "normalize_failed: " + err.Error()
}

var out types.HALConfig
if err := json.Unmarshal(fixed, &out); err != nil {
return types.HALConfig{}, "hal_config_unmarshal_failed: " + err.Error() + "; raw=" + truncateRawJSON(fixed)
}
return out, ""
}

func decodeHALState(payload any) (types.HALState, bool) {
switch v := payload.(type) {
case types.HALState:
return v, true
case *types.HALState:
if v == nil {
return types.HALState{}, false
}
return *v, true
case json.RawMessage:
var out types.HALState
return out, json.Unmarshal(v, &out) == nil
case []byte:
var out types.HALState
return out, json.Unmarshal(v, &out) == nil
default:
b, err := json.Marshal(v)
if err != nil {
return types.HALState{}, false
}
var out types.HALState
return out, json.Unmarshal(b, &out) == nil
}
}

func decodePayload(payload any) any {
switch v := payload.(type) {
case nil:
return nil
case json.RawMessage:
if len(v) == 0 {
return nil
}
var out any
if err := json.Unmarshal(v, &out); err == nil {
return out
}
return []byte(v)
case []byte:
if len(v) == 0 {
return nil
}
var out any
if err := json.Unmarshal(v, &out); err == nil {
return out
}
cp := make([]byte, len(v))
copy(cp, v)
return cp
default:
return v
}
}

func truncateRawJSON(b []byte) string {
if len(b) == 0 {
return ""
}
const max = 160
if len(b) <= max {
return string(b)
}
return string(b[:max]) + "..."
}
119 changes: 119 additions & 0 deletions services/fabric/fabric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package fabric

import (
"context"
"sync/atomic"
"time"

"devicecode-go/bus"
"devicecode-go/x/strconvx"
)

// Transport abstracts the byte stream as newline-delimited JSON lines.
type Transport interface {
ReadLine() ([]byte, error)
WriteLine(data []byte) error
Close() error
}

const protoVersion = 1
const defaultLinkID = "mcu0"

// LinkConfig carries the fabric link parameters that the CM5 publishes
// alongside its own session/transfer-mgr instances. Mirrors the relevant
// keys in `bigbox-v1-cm-2.json` `service.fabric.links.<id>` for the
// MCU-facing link. Missing fields fall back to release defaults via
// applyDefaults so callers can pass `LinkConfig{}` to mean "release".
type LinkConfig struct {
// ChunkSize is the expected raw-byte payload per xfer_chunk. The MCU
// is receive-only for transfers, so this is informational/validation
// only on the Go side. Release: 2048 bytes.
ChunkSize uint32
// PhaseTimeout is the idle-chunk watchdog: an active inbound transfer
// is aborted with reason="timeout" if no xfer_chunk arrives within
// this window. Mirrors transfer_mgr.lua's `phase_timeout`.
// Release: 15s.
PhaseTimeout time.Duration
// PingInterval drives the unconditional outbound ping cadence after
// the link is established (`session_ctl.lua` resets next_ping_at =
// now + ping_interval after every send; not TX-activity-based).
// Release: 10s.
PingInterval time.Duration
// LivenessTimeout tears the link down if no frame arrives within
// this window once established. Mirrors session_ctl.lua's
// liveness_timeout_s. Release: 30s.
LivenessTimeout time.Duration
// MaxInboundHelpers caps the number of in-flight inbound RPC calls.
// Excess inbound calls reply `{ok=false, err="busy"}` per
// rpc_bridge.lua's `spawn_local_call_helper`. Lua default is 64
// (falls back to max_pending_calls); we keep that for parity.
MaxInboundHelpers int
// RPCQuantum and BulkQuantum control the writer's weighted
// round-robin between the rpc and bulk lanes after the control
// lane drains. Mirrors writer.lua's lane scheduler. Release: 4 and 1.
RPCQuantum int
BulkQuantum int
}

func DefaultLinkConfig() LinkConfig {
return LinkConfig{
ChunkSize: 2048,
PhaseTimeout: 15 * time.Second,
PingInterval: 10 * time.Second,
LivenessTimeout: 30 * time.Second,
MaxInboundHelpers: 64,
RPCQuantum: 4,
BulkQuantum: 1,
}
}

func (c *LinkConfig) applyDefaults() {
d := DefaultLinkConfig()
if c.ChunkSize == 0 {
c.ChunkSize = d.ChunkSize
}
if c.PhaseTimeout == 0 {
c.PhaseTimeout = d.PhaseTimeout
}
if c.PingInterval == 0 {
c.PingInterval = d.PingInterval
}
if c.LivenessTimeout == 0 {
c.LivenessTimeout = d.LivenessTimeout
}
if c.MaxInboundHelpers == 0 {
c.MaxInboundHelpers = d.MaxInboundHelpers
}
if c.RPCQuantum == 0 {
c.RPCQuantum = d.RPCQuantum
}
if c.BulkQuantum == 0 {
c.BulkQuantum = d.BulkQuantum
}
}

var nextSessionID atomic.Uint64

func newLocalSID() string {
return "mcu-sid-" + strconvx.Utoa64(nextSessionID.Add(1))
}

// Run starts the fabric session. Blocks until ctx is cancelled or the
// transport returns an unrecoverable error. The MCU is a hello
// responder (CM5 always initiates hello/hello_ack), but otherwise
// runs the symmetric session_ctl semantics: once established, it
// sends pings every PingInterval and tears the link down if no frame
// arrives within LivenessTimeout. Mirrors session_ctl.lua at
// devicecode-lua@2c88090.
func Run(ctx context.Context, tr Transport, conn *bus.Connection, nodeID, peerID string, cfg LinkConfig) {
s := session{
linkID: defaultLinkID,
nodeID: nodeID,
peerID: peerID,
localSID: newLocalSID(),
tr: tr,
conn: conn,
cfg: cfg,
}
s.run(ctx)
}
Loading