Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2718735
updater: phase 1 — service + state machine + RPC handlers + boot_id
cpunt Apr 28, 2026
d52d9a9
fabric: phase 2 — receiver invocation + buffer sink + stage/apply split
cpunt Apr 28, 2026
c678f35
updater: phase 3 — W10 post-hello_ack republish on link-ready edge
cpunt Apr 28, 2026
dfca6a8
telemetry: phase 4 — W7 retained facts + W8 charger alert FSM
cpunt Apr 28, 2026
1e1a0a4
updater: phase 5 — W11 metadata writer interface + memory-backed default
cpunt Apr 28, 2026
0926ea8
updater + telemetry: address Codex review on update-safety semantics
cpunt Apr 28, 2026
1cb2b98
updater: address Codex H1/M2 — apply contract + payload-hash separation
cpunt Apr 28, 2026
9fd173e
boot_id: drop stale stack-address-mixing comment
cpunt Apr 28, 2026
88cac8f
telemetry: phase 6 — finish W7 charger config + W8 analog alert kinds
cpunt Apr 28, 2026
0432739
fabric: phase 7 — W12 legacy remap removal
cpunt Apr 28, 2026
c0d01a2
telemetry: charger config source field reflects defaults vs effective
cpunt Apr 28, 2026
0e933a1
boot_id: skip crypto/rand on TinyGo to dodge "no rng" panic at boot
cpunt Apr 28, 2026
27139f1
updater: passthrough verifier + abupdate-backed sink/applier on tinygo
cpunt Apr 28, 2026
5d40ab5
fabric: re-request next byte on malformed frame during active transfer
cpunt Apr 28, 2026
5c9b335
fabric: per-chunk xxHash CRC catches corruption inside base64 data
cpunt Apr 28, 2026
ed1f972
fabric: refresh transfer deadline on recovery xfer_need
cpunt Apr 28, 2026
5559f12
updater: stream transfer straight into the A/B slot, drop RAM buffer
cpunt Apr 28, 2026
8f1c827
fabric: stabilize MCU firmware updates
cpunt May 12, 2026
fc19ef9
updater: tighten update branch comments
cpunt May 18, 2026
d2ec15b
updater: remove test-only production 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
build/
zz_fw_update_e2e_identity.go
.vscode/settings.json
22 changes: 21 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@ import (
"devicecode-go/bus"
"devicecode-go/services/hal"
"devicecode-go/services/reactor"
"devicecode-go/services/updater"
"devicecode-go/types"
"devicecode-go/utilities"
)

// HAL
const halTimeout = 5 * time.Second

var halReadiness = bus.T("hal", "state")

// Firmware identity is set by host build tooling before main runs. The e2e
// harness generates a same-package init file because TinyGo's -X support is
// narrower than the standard Go linker's support.
var (
FirmwareVersion = "0.0.0-dev"
FirmwareBuild = "local"
FirmwareImageID = "img-dev"
)

// -----------------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -47,6 +58,15 @@ func main() {
}
}

// boot_id (master R3 / fabric-update W6): generate AFTER HAL ready
// and BEFORE the reactor opens fabric. RAM-only — never persisted.
bootID := updater.GenerateBootID()
log.Println("[main] boot_id =", bootID)

reactor.FirmwareVersion = FirmwareVersion
reactor.FirmwareBuild = FirmwareBuild
reactor.FirmwareImageID = FirmwareImageID

// Reactor
r := reactor.NewReactor(b, uiConn)
r.Run(ctx)
Expand Down Expand Up @@ -76,4 +96,4 @@ func waitHALReady(ctx context.Context, c *bus.Connection, d time.Duration) bool
}

// Global logger instance
var log = utilities.Logger{LineStart: true}
var log = utilities.Logger{LineStart: true}
104 changes: 10 additions & 94 deletions services/fabric/config.go
Original file line number Diff line number Diff line change
@@ -1,88 +1,15 @@
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
}
}

import "encoding/json"

// decodePayload normalises whatever shape the bus delivered into a
// reasonable Go value for the reply path. The wire delivers
// json.RawMessage; in-process callers may pass already-typed values.
// Used by session.onReply when forwarding RPC replies onto the
// originating Request's reply path.
//
// This file intentionally contains only reply-payload decoding; legacy
// config/device and rpc/hal/dump glue is no longer part of the MCU contract.
func decodePayload(payload any) any {
switch v := payload.(type) {
case nil:
Expand Down Expand Up @@ -111,14 +38,3 @@ func decodePayload(payload any) any {
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]) + "..."
}
8 changes: 6 additions & 2 deletions services/fabric/fabric.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

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

Expand All @@ -16,7 +17,6 @@ type Transport interface {
Close() error
}

const protoVersion = 1
const defaultLinkID = "mcu0"

// LinkConfig carries the fabric link parameters that the CM5 publishes
Expand Down Expand Up @@ -95,7 +95,11 @@ func (c *LinkConfig) applyDefaults() {
var nextSessionID atomic.Uint64

func newLocalSID() string {
return "mcu-sid-" + strconvx.Utoa64(nextSessionID.Add(1))
bootID := updater.BootID()
if bootID == "" {
bootID = updater.GenerateBootID()
}
return "mcu-sid-" + bootID + "-" + strconvx.Utoa64(nextSessionID.Add(1))
}

// Run starts the fabric session. Blocks until ctx is cancelled or the
Expand Down
Loading