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 |
| `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
2. If new MAC → create camera with auto-generated `camera_id = "cam-<NNN>"` (zero-padded sequential)
3. Respond by publishing: `remoterig/cameras/<camera_id>/command` with `command: "registered"` payload containing the assigned `camera_id`
4. Broadcast via SSE that a new camera appeared
2. If new MAC → insert the camera using the node's self-assigned `camera_id`
3. 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`
+13 -11
View File
@@ -311,23 +311,25 @@ bool connectMQTT() {
Serial.println("[MQTT] Connected");
// Subscribe to commands (if registered)
if (cfg.camera_id.length() > 0) {
mqtt.subscribe(mqttTopic("command").c_str(), 2);
// Option B: self-assigned, stable camera_id derived from the device id.
if (cfg.camera_id.length() == 0) {
cfg.camera_id = clientID(); // e.g. "rig-86d978"
}
// Announce if new
if (cfg.camera_id.length() == 0) {
// Subscribe to our command topic.
mqtt.subscribe(mqttTopic("command").c_str(), 2);
// Announce (retained) on the contract topic so the hub registers/tracks us.
{
JsonDocument doc;
doc["mac_address"] = WiFi.macAddress();
doc["firmware_version"] = "0.3.0-esp32-mqtt-bridge";
doc["friendly_name"] = "Cam-" + clientID();
doc["firmware_version"] = "0.4.0-esp32-mqtt-bridge";
doc["friendly_name"] = "Cam-" + cfg.camera_id;
JsonArray caps = doc["capabilities"].to<JsonArray>();
caps.add("start_stop"); caps.add("status");
String payload; serializeJson(doc, payload);
String announceTopic = "remoterig/cameras/announce-" + clientID();
mqtt.publish(announceTopic.c_str(), payload.c_str(), true);
Serial.println("[MQTT] Announced for registration");
mqtt.publish(mqttTopic("announce").c_str(), payload.c_str(), true);
Serial.printf("[MQTT] Announced as %s\n", cfg.camera_id.c_str());
}
return true;
@@ -489,7 +491,7 @@ void loop() {
// Build the MQTT status payload per contract
JsonDocument mqttDoc;
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;
int pct = batteryPct(dispBatteryRaw);
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
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)
}