generated from CubeCraft-Creations/Tracehound
registration: self-assigned camera IDs (Option B) + tolerate clockless status
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:
+12
-4
@@ -176,11 +176,19 @@ Published once on ESP32 first boot (or factory reset). Used for auto-registratio
|
|||||||
| `capabilities` | string[] | Supported features |
|
| `capabilities` | string[] | Supported features |
|
||||||
| `friendly_name` | string | Default human-readable name |
|
| `friendly_name` | string | Default human-readable name |
|
||||||
|
|
||||||
**Hub behavior on first announce:**
|
**Camera IDs (self-assigned — "Option B"):** the node uses a stable
|
||||||
|
device-derived id (`rig-<last3 MAC bytes>`, e.g. `rig-86d978`) as its
|
||||||
|
`camera_id` from first boot, and uses it for all topics
|
||||||
|
(`announce`/`status`/`heartbeat`/`command`). There is no hub-assigned
|
||||||
|
`cam-NNN` and no `registered` reply handshake.
|
||||||
|
|
||||||
|
**Hub behavior on announce:**
|
||||||
1. Check if MAC already registered → if yes, update `friendly_name` and log
|
1. Check if MAC already registered → if yes, update `friendly_name` and log
|
||||||
2. If new MAC → create camera with auto-generated `camera_id = "cam-<NNN>"` (zero-padded sequential)
|
2. If new MAC → insert the camera using the node's self-assigned `camera_id`
|
||||||
3. Respond by publishing: `remoterig/cameras/<camera_id>/command` with `command: "registered"` payload containing the assigned `camera_id`
|
3. Broadcast via SSE that a new camera appeared
|
||||||
4. Broadcast via SSE that a new camera appeared
|
|
||||||
|
> Note: nodes have no real-time clock, so `timestamp` may be absent; the hub
|
||||||
|
> stamps received-time server-side.
|
||||||
|
|
||||||
### Topic: `remoterig/hub/status`
|
### Topic: `remoterig/hub/status`
|
||||||
|
|
||||||
|
|||||||
@@ -311,23 +311,25 @@ bool connectMQTT() {
|
|||||||
|
|
||||||
Serial.println("[MQTT] Connected");
|
Serial.println("[MQTT] Connected");
|
||||||
|
|
||||||
// Subscribe to commands (if registered)
|
// Option B: self-assigned, stable camera_id derived from the device id.
|
||||||
if (cfg.camera_id.length() > 0) {
|
if (cfg.camera_id.length() == 0) {
|
||||||
mqtt.subscribe(mqttTopic("command").c_str(), 2);
|
cfg.camera_id = clientID(); // e.g. "rig-86d978"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Announce if new
|
// Subscribe to our command topic.
|
||||||
if (cfg.camera_id.length() == 0) {
|
mqtt.subscribe(mqttTopic("command").c_str(), 2);
|
||||||
|
|
||||||
|
// Announce (retained) on the contract topic so the hub registers/tracks us.
|
||||||
|
{
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["mac_address"] = WiFi.macAddress();
|
doc["mac_address"] = WiFi.macAddress();
|
||||||
doc["firmware_version"] = "0.3.0-esp32-mqtt-bridge";
|
doc["firmware_version"] = "0.4.0-esp32-mqtt-bridge";
|
||||||
doc["friendly_name"] = "Cam-" + clientID();
|
doc["friendly_name"] = "Cam-" + cfg.camera_id;
|
||||||
JsonArray caps = doc["capabilities"].to<JsonArray>();
|
JsonArray caps = doc["capabilities"].to<JsonArray>();
|
||||||
caps.add("start_stop"); caps.add("status");
|
caps.add("start_stop"); caps.add("status");
|
||||||
String payload; serializeJson(doc, payload);
|
String payload; serializeJson(doc, payload);
|
||||||
String announceTopic = "remoterig/cameras/announce-" + clientID();
|
mqtt.publish(mqttTopic("announce").c_str(), payload.c_str(), true);
|
||||||
mqtt.publish(announceTopic.c_str(), payload.c_str(), true);
|
Serial.printf("[MQTT] Announced as %s\n", cfg.camera_id.c_str());
|
||||||
Serial.println("[MQTT] Announced for registration");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -489,7 +491,7 @@ void loop() {
|
|||||||
// Build the MQTT status payload per contract
|
// Build the MQTT status payload per contract
|
||||||
JsonDocument mqttDoc;
|
JsonDocument mqttDoc;
|
||||||
mqttDoc["camera_id"] = cfg.camera_id;
|
mqttDoc["camera_id"] = cfg.camera_id;
|
||||||
mqttDoc["timestamp"] = millis();
|
// No timestamp: the node has no real clock; the hub stamps on receipt.
|
||||||
mqttDoc["battery_raw"] = dispBatteryRaw;
|
mqttDoc["battery_raw"] = dispBatteryRaw;
|
||||||
int pct = batteryPct(dispBatteryRaw);
|
int pct = batteryPct(dispBatteryRaw);
|
||||||
if (pct >= 0) mqttDoc["battery_pct"] = pct; // omit when uncalibrated
|
if (pct >= 0) mqttDoc["battery_pct"] = pct; // omit when uncalibrated
|
||||||
|
|||||||
+11
-23
@@ -151,22 +151,20 @@ func (s *Subscriber) handleStatus(cameraID string, payload []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if sp.CameraID == "" || sp.Timestamp == "" {
|
if sp.CameraID == "" {
|
||||||
log.Printf("MQTT status missing required fields (camera_id, timestamp) from %s", cameraID)
|
log.Printf("MQTT status missing camera_id from %s", cameraID)
|
||||||
return
|
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)
|
ts, err := time.Parse(time.RFC3339, sp.Timestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Try ISO8601 without timezone
|
if ts, err = time.Parse("2006-01-02T15:04:05", sp.Timestamp); err != nil {
|
||||||
ts, err = time.Parse("2006-01-02T15:04:05", sp.Timestamp)
|
ts = now
|
||||||
if err != nil {
|
|
||||||
log.Printf("MQTT status invalid timestamp %q from %s", sp.Timestamp, cameraID)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
now := time.Now()
|
|
||||||
if ts.After(now.Add(5 * time.Minute)) {
|
if ts.After(now.Add(5 * time.Minute)) {
|
||||||
log.Printf("MQTT status timestamp too far in future (%s) from %s — using now", ts, cameraID)
|
log.Printf("MQTT status timestamp too far in future (%s) from %s — using now", ts, cameraID)
|
||||||
ts = now
|
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)
|
log.Printf("MQTT announce: camera %s (%s) re-connected", existingID, ap.FriendlyName)
|
||||||
} else {
|
} else {
|
||||||
// New camera — generate sequential cam-NNN ID
|
// Option B: the node self-assigns its camera_id (the announce topic 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)
|
|
||||||
_, err = s.db.Exec(`
|
_, err = s.db.Exec(`
|
||||||
INSERT INTO cameras (camera_id, friendly_name, mac_address, created_at, updated_at)
|
INSERT INTO cameras (camera_id, friendly_name, mac_address, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, datetime('now'), datetime('now'))
|
VALUES (?, ?, ?, datetime('now'), datetime('now'))
|
||||||
`, newID, ap.FriendlyName, ap.MacAddress)
|
`, cameraID, ap.FriendlyName, ap.MacAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("MQTT announce insert error for %s: %v", ap.MacAddress, err)
|
log.Printf("MQTT announce insert error for %s: %v", ap.MacAddress, err)
|
||||||
return
|
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
|
// Broadcast new camera via SSE
|
||||||
cam, err := getCamera(s.db, newID)
|
cam, err := getCamera(s.db, cameraID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.hub.Broadcast("camera_registered", cam)
|
s.hub.Broadcast("camera_registered", cam)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user