Dev #26

Open
overseer wants to merge 65 commits from dev into main
3 changed files with 36 additions and 38 deletions
Showing only changes of commit 7929d1d969 - Show all commits
+12 -4
View File
@@ -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`
+13 -11
View File
@@ -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
View File
@@ -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)
} }