generated from CubeCraft-Creations/Tracehound
Dev #26
+12
-4
@@ -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`
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user