registration: self-assigned camera IDs (Option B) + tolerate clockless status
Build (Dev) / build (push) Failing after 16s
CI / quality (push) Failing after 0s
CI / quality (pull_request) Successful in 11s

Auto-registration never completed: the firmware announced on the wrong
topic, the hub never replied, and an unregistered node couldn't receive a
reply anyway. Switch to self-assigned IDs:

firmware (esp32-mqtt-bridge.cpp):
- camera_id defaults to the device id (clientID, e.g. rig-86d978)
- always subscribe to <id>/command; announce on the contract topic
  remoterig/cameras/<id>/announce (was the unmatched announce-<id> form)
- drop the bogus numeric timestamp from status (node has no clock)

hub (subscriber.go):
- handleAnnounce registers new cameras under the node's self-assigned id
  (no cam-NNN, no registered reply)
- handleStatus tolerates an empty/invalid timestamp and stamps server-side
  (previously rejected the status outright)

docs/MQTT_CONTRACT.md updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Joshua King
2026-06-05 12:14:00 -04:00
parent 9fc80a27c9
commit 7929d1d969
3 changed files with 36 additions and 38 deletions
+11 -23
View File
@@ -151,22 +151,20 @@ func (s *Subscriber) handleStatus(cameraID string, payload []byte) {
}
// Validate required fields
if sp.CameraID == "" || sp.Timestamp == "" {
log.Printf("MQTT status missing required fields (camera_id, timestamp) from %s", cameraID)
if sp.CameraID == "" {
log.Printf("MQTT status missing camera_id from %s", cameraID)
return
}
// Validate timestamp sanity (reject >5min future, >24h past)
// Nodes have no real clock, so tolerate an empty/invalid timestamp by
// stamping server-side. Still clamp obviously-bad supplied times below.
now := time.Now()
ts, err := time.Parse(time.RFC3339, sp.Timestamp)
if err != nil {
// Try ISO8601 without timezone
ts, err = time.Parse("2006-01-02T15:04:05", sp.Timestamp)
if err != nil {
log.Printf("MQTT status invalid timestamp %q from %s", sp.Timestamp, cameraID)
return
if ts, err = time.Parse("2006-01-02T15:04:05", sp.Timestamp); err != nil {
ts = now
}
}
now := time.Now()
if ts.After(now.Add(5 * time.Minute)) {
log.Printf("MQTT status timestamp too far in future (%s) from %s — using now", ts, cameraID)
ts = now
@@ -374,30 +372,20 @@ func (s *Subscriber) handleAnnounce(cameraID string, payload []byte) {
}
log.Printf("MQTT announce: camera %s (%s) re-connected", existingID, ap.FriendlyName)
} else {
// New camera — generate sequential cam-NNN ID
var maxID string
s.db.QueryRow("SELECT MAX(camera_id) FROM cameras").Scan(&maxID)
seq := 1
if maxID != "" {
fmt.Sscanf(maxID, "cam-%d", &seq)
seq++
}
newID := fmt.Sprintf("cam-%03d", seq)
// Option B: the node self-assigns its camera_id (the announce topic id).
_, err = s.db.Exec(`
INSERT INTO cameras (camera_id, friendly_name, mac_address, created_at, updated_at)
VALUES (?, ?, ?, datetime('now'), datetime('now'))
`, newID, ap.FriendlyName, ap.MacAddress)
`, cameraID, ap.FriendlyName, ap.MacAddress)
if err != nil {
log.Printf("MQTT announce insert error for %s: %v", ap.MacAddress, err)
return
}
log.Printf("MQTT announce: new camera registered as %s (%s)", newID, ap.FriendlyName)
log.Printf("MQTT announce: new camera registered as %s (%s)", cameraID, ap.FriendlyName)
// Broadcast new camera via SSE
cam, err := getCamera(s.db, newID)
cam, err := getCamera(s.db, cameraID)
if err == nil {
s.hub.Broadcast("camera_registered", cam)
}