diff --git a/docs/known-issues.md b/docs/known-issues.md index 649b710..d09026c 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -107,6 +107,13 @@ scripts for this are worth keeping; note that wrapping them in `timeout` SIGTERM ## Possible follow-ups (not done) +- **A retained `Env` payload may still sit on the broker.** `GGS/FWT//Env` was + published retained until it was removed, so a broker that saw the old build still holds + the last ambient reading and hands it to every new subscriber — a frozen temperature and + humidity with an old timestamp, indistinguishable from a sensor that stopped reporting. + Nothing in the code does this any more, so nothing here would otherwise explain it. + Clear it broker-side by publishing a zero-length retained message to that topic. + - **NWU vs ENU is decided by a constant, not by the device.** `kMtiReference` ([MtiProtocol.h](../include/fgc/MtiProtocol.h)) says the MTi reports against NWU, and the host re-references it to NED. The two candidates give *identical* roll and pitch and diff --git a/docs/modules-reference.md b/docs/modules-reference.md index 934f2ac..d927060 100644 --- a/docs/modules-reference.md +++ b/docs/modules-reference.md @@ -24,7 +24,7 @@ Per-file reference for the refactored tree, plus the shared data structures. | [include/fgc/sensors/Sht41Protocol.h](../include/fgc/sensors/Sht41Protocol.h), [src/sensors/Sht41Protocol.cpp](../src/sensors/Sht41Protocol.cpp) | Sensirion SHT4x wire protocol, pure: command bytes, `sht41Crc8` (poly 0x31/init 0xFF), `decodeSht41Measurement` (6-byte response → `Sht41Reading`, CRC-checked, raw→°C/%RH with RH clamped 0..100) | | [include/fgc/CameraProfile.h](../include/fgc/CameraProfile.h), [src/core/CameraProfile.cpp](../src/core/CameraProfile.cpp) | `loadCameraProfile` / `loadCameraProfiles`: one VimbaX settings XML per profile. Hashes the XML, regenerates the `.txt` record when it changes, and reports which settings moved. A missing XML throws — the record is never a fallback | | [include/fgc/CameraSettingsXml.h](../include/fgc/CameraSettingsXml.h), [src/core/CameraSettingsXml.cpp](../src/core/CameraSettingsXml.cpp) | Parses the Viewer export's `` features (ordered, selector-aware), renders the human-readable `.txt` record, reads one back, and diffs two settings lists. Used for display and change detection only — never to configure a camera | -| [include/fgc/ChannelTopics.h](../include/fgc/ChannelTopics.h), [src/core/ChannelTopics.cpp](../src/core/ChannelTopics.cpp) | The MQTT wire format in one place: topic builders for the `GGS/FWT//…` tree and the `CamEvent`/`EnvEvent`/status payload builders. Pure, so the documented format is unit-tested rather than assembled inline at each publish site | +| [include/fgc/ChannelTopics.h](../include/fgc/ChannelTopics.h), [src/core/ChannelTopics.cpp](../src/core/ChannelTopics.cpp) | The MQTT wire format in one place: topic builders for the `GGS/FWT//…` tree and the `CamEvent`/status payload builders. Pure, so the documented format is unit-tested rather than assembled inline at each publish site | | [include/fgc/ChannelHistory.h](../include/fgc/ChannelHistory.h), [src/core/ChannelHistory.cpp](../src/core/ChannelHistory.cpp) | Ring buffer of recently published messages (time, topic, payload, outcome) plus the delivery counters — sent, failed, connects, losses, last status. Feeds the expanded Connectivity view so a publish can be confirmed without a broker-side subscriber | | [include/fgc/Hash.h](../include/fgc/Hash.h), [src/core/Hash.cpp](../src/core/Hash.cpp) | FNV-1a 64 over a string or a file, with an algorithm-prefixed label. Change detection for the profile records, deliberately not a crypto hash | | [include/fgc/CaptureScheduler.h](../include/fgc/CaptureScheduler.h), [src/core/CaptureScheduler.cpp](../src/core/CaptureScheduler.cpp) | Capture state machine over the interfaces; injectable clock; `setGeometry` adopts a recalibration | @@ -38,7 +38,7 @@ Per-file reference for the refactored tree, plus the shared data structures. | File | Interface | Shared structs | |------|-----------|----------------| | [include/fgc/IMotorController.h](../include/fgc/IMotorController.h) | `IMotorController` | `MotorTelemetry` | -| [include/fgc/IControlChannel.h](../include/fgc/IControlChannel.h) | `IControlChannel` | `ControlCommand`, `CamEvent`, `EnvEvent` | +| [include/fgc/IControlChannel.h](../include/fgc/IControlChannel.h) | `IControlChannel` | `ControlCommand`, `CamEvent` | | [include/fgc/ICameraSource.h](../include/fgc/ICameraSource.h) | `ICameraSource` | `Frame` | | [include/fgc/IImuSource.h](../include/fgc/IImuSource.h) | `IImuSource` (incl. `config()`, `configProblems()`, `refreshConfig()`, `noRotation()`, `headingReset()`, `setFilterProfile()`) | `ImuSample` (from `MtiProtocol.h`) | | [include/fgc/IEnvSensor.h](../include/fgc/IEnvSensor.h) | `IEnvSensor` (`connected()`, `sample()`, `name()`) | `EnvSample` | @@ -98,10 +98,6 @@ homing or `gimbal reset`, so a lit badge means "position untrustworthy since the `tower`, `camera` (RGB/ACR/NIR), `heading_decideg` (yaw×10), `pitch_decideg` (pitch×10) and `timestamp_ms` (which matches the image filename). Serialized to the CamEvent JSON payload (see [mqtt-api.md](mqtt-api.md)). -### `EnvEvent` ([IControlChannel.h](../include/fgc/IControlChannel.h)) -`tower`, `temp_c`, `humidity_pct`, `timestamp_ms`. Published once per **fresh** sample (deduplicated -on `timestamp_ms`, not per control-loop tick) to the `Env` topic — see [mqtt-api.md](mqtt-api.md). - ### `EnvSample` ([IEnvSensor.h](../include/fgc/IEnvSensor.h)) One ambient reading: `valid`, `temp_c` (°C), `humidity_pct` (0..100), `timestamp_ms` (epoch ms). `IEnvSensor::sample()` returns `nullopt` rather than a stale sample once nothing valid has arrived diff --git a/docs/mqtt-api.md b/docs/mqtt-api.md index f68b584..53755c3 100644 --- a/docs/mqtt-api.md +++ b/docs/mqtt-api.md @@ -56,7 +56,6 @@ When a ControlCode message arrives, the program echoes the current code back on |-------|------|---------|--------------| | `GGS/FWT//StatusCode` | At startup (`"0"`); whenever a ControlCode message is received (echoes the code) | integer as string | 1 / retained | | `GGS/FWT//CamEvent` | After each image is saved | JSON object (below) | 1 / retained | -| `GGS/FWT//Env` | Once per fresh ambient sensor sample (see `[Env] period_ms`) | JSON object (below) | 1 / retained | ### CamEvent payload @@ -101,22 +100,6 @@ Both `hdg` and `pit` are sampled **when the frame arrives**, not when its JPEG X finishes, so a slow encode or a backed-up queue cannot attribute a later pose to earlier pixels. -### Env payload - -Built by `MqttControlChannel::publishEnv` from an `EnvEvent`, sourced from whichever `IEnvSensor` backend is -active (SHT41 on the LattePanda's own I2C bus by default; see [configuration.md](configuration.md)): - -```json -{ "fwt":"ExampleTower", "temp_c":22.4, "humid_pct":46.1, "time":1719312345678 } -``` - -| Field | Type | Meaning | -|-------|------|---------| -| `fwt` | string | Tower name (`config.ini` `tower_name`) | -| `temp_c` | float | Ambient temperature, °C | -| `humid_pct` | float | Ambient relative humidity, % | -| `time` | int | Sample timestamp, Unix epoch **milliseconds** | - ## Topic summary ``` @@ -125,7 +108,6 @@ subscribe: GGS/FWT//target_HDG (int heading) publish: GGS/FWT//StatusCode (echoed control code) GGS/FWT//CamEvent (JSON: fwt, cam, hdg×10, pit×10, time-ms) - GGS/FWT//Env (JSON: fwt, temp_c, humid_pct, time-ms) ``` ## Local testing diff --git a/include/fgc/ChannelTopics.h b/include/fgc/ChannelTopics.h index e430635..71fa987 100644 --- a/include/fgc/ChannelTopics.h +++ b/include/fgc/ChannelTopics.h @@ -18,12 +18,10 @@ inline constexpr const char* kTopicTargetHdg = "target_HDG"; inline constexpr const char* kTopicControlCode = "ControlCode"; inline constexpr const char* kTopicStatusCode = "StatusCode"; inline constexpr const char* kTopicCamEvent = "CamEvent"; -inline constexpr const char* kTopicEnv = "Env"; std::string towerTopic(const std::string& tower, const std::string& leaf); // Payload builders. One definition each, used by both channels. std::string camEventJson(const CamEvent& e); -std::string envJson(const EnvEvent& e); } // namespace fgc diff --git a/include/fgc/IControlChannel.h b/include/fgc/IControlChannel.h index 4144f2a..3c22ecd 100644 --- a/include/fgc/IControlChannel.h +++ b/include/fgc/IControlChannel.h @@ -23,14 +23,6 @@ struct CamEvent { long long timestamp_ms = 0; // Unix epoch ms; matches the image filename }; -// Ambient temperature/humidity reading published once per fresh sensor sample. -struct EnvEvent { - std::string tower; - float temp_c = 0.0f; - float humidity_pct = 0.0f; - long long timestamp_ms = 0; -}; - // What became of an outbound message. Dropped and Failed are separated because // they mean different things operationally: Dropped is "the link was down and we // discarded it silently", Failed is "the broker or library rejected it". @@ -71,7 +63,6 @@ public: virtual void publishStatus(int code) = 0; virtual void publishCamEvent(const CamEvent& event) = 0; - virtual void publishEnv(const EnvEvent& event) = 0; // Latest control input; clears the *_available flags so each update is // acted on once. diff --git a/include/fgc/MqttControlChannel.h b/include/fgc/MqttControlChannel.h index bec08d3..1984942 100644 --- a/include/fgc/MqttControlChannel.h +++ b/include/fgc/MqttControlChannel.h @@ -35,7 +35,6 @@ public: void publishStatus(int code) override; void publishCamEvent(const CamEvent& event) override; - void publishEnv(const EnvEvent& event) override; ControlCommand poll() override; std::vector recentMessages() const override { return history_.recent(); } @@ -59,7 +58,6 @@ private: std::string topic_control_code_; std::string topic_status_; std::string topic_cam_event_; - std::string topic_env_; mqtt::async_client client_; mqtt::connect_options conn_opts_; diff --git a/include/fgc/mock/NullControlChannel.h b/include/fgc/mock/NullControlChannel.h index a830202..f2fc764 100644 --- a/include/fgc/mock/NullControlChannel.h +++ b/include/fgc/mock/NullControlChannel.h @@ -38,13 +38,6 @@ public: << " (null channel)"; drop(kTopicCamEvent, camEventJson(e)); } - void publishEnv(const EnvEvent& e) override { - LOG_TRACE_CAT(LogCat::Mqtt) << "PUB Env temp_c=" << e.temp_c - << " humid_pct=" << e.humidity_pct << " t=" << e.timestamp_ms - << " (null channel)"; - drop(kTopicEnv, envJson(e)); - } - ControlCommand poll() override { return {}; } std::vector recentMessages() const override { return history_.recent(); } diff --git a/src/core/Application.cpp b/src/core/Application.cpp index 96d5b71..b0b1053 100644 --- a/src/core/Application.cpp +++ b/src/core/Application.cpp @@ -230,7 +230,6 @@ struct Application::Impl { bool test_drift_gate_ = false; mutable ImuConfigView imu_config_view_; // formatted MTi config (read once) mutable bool imu_config_done_ = false; - mutable long long last_env_published_ts_ = 0; // gate MQTT publish to one per fresh sample // Latches the "no IMU sample, publishing encoder pitch" warning so an outage // costs one line, not one per frame. Written from the camera delivery // thread(s) via the orientation supplier, hence atomic. @@ -746,16 +745,6 @@ struct Application::Impl { setField(g.fields[0], fmt1(m->temp_c)); setField(g.fields[1], fmt1(m->humidity_pct)); } - // Publish once per fresh sample, not every ~10ms control-loop tick. - if (channel && m->timestamp_ms != last_env_published_ts_) { - last_env_published_ts_ = m->timestamp_ms; - EnvEvent ev; - ev.tower = cfg.general.tower_name; - ev.temp_c = m->temp_c; - ev.humidity_pct = m->humidity_pct; - ev.timestamp_ms = m->timestamp_ms; - channel->publishEnv(ev); - } } else { s.sensors.env.status = "no fix"; } diff --git a/src/core/ChannelTopics.cpp b/src/core/ChannelTopics.cpp index fc5e178..280aa91 100644 --- a/src/core/ChannelTopics.cpp +++ b/src/core/ChannelTopics.cpp @@ -13,10 +13,4 @@ std::string camEventJson(const CamEvent& e) { ", \"time\":" + std::to_string(e.timestamp_ms) + " }"; } -std::string envJson(const EnvEvent& e) { - return "{ \"fwt\":\"" + e.tower + "\", \"temp_c\":" + std::to_string(e.temp_c) + - ", \"humid_pct\":" + std::to_string(e.humidity_pct) + - ", \"time\":" + std::to_string(e.timestamp_ms) + " }"; -} - } // namespace fgc diff --git a/src/mqtt/MqttControlChannel.cpp b/src/mqtt/MqttControlChannel.cpp index d76833d..a34c5d7 100644 --- a/src/mqtt/MqttControlChannel.cpp +++ b/src/mqtt/MqttControlChannel.cpp @@ -22,7 +22,6 @@ MqttControlChannel::MqttControlChannel(const std::string& broker_uri, const std: topic_control_code_(towerTopic(tower, kTopicControlCode)), topic_status_(towerTopic(tower, kTopicStatusCode)), topic_cam_event_(towerTopic(tower, kTopicCamEvent)), - topic_env_(towerTopic(tower, kTopicEnv)), client_(broker_uri, tower) { conn_opts_.set_keep_alive_interval(20); conn_opts_.set_clean_session(true); @@ -136,8 +135,4 @@ void MqttControlChannel::publishCamEvent(const CamEvent& e) { publish(topic_cam_event_, camEventJson(e)); } -void MqttControlChannel::publishEnv(const EnvEvent& e) { - publish(topic_env_, envJson(e)); -} - } // namespace fgc diff --git a/src/ui/TuiUi.cpp b/src/ui/TuiUi.cpp index 85733d7..cad52f6 100644 --- a/src/ui/TuiUi.cpp +++ b/src/ui/TuiUi.cpp @@ -544,7 +544,7 @@ Element connDetailPanel(const ConnView& v, ScrollState& st) { struct TopicRow { const char* dir; const char* leaf; }; static const TopicRow kTopics[] = { {"sub", kTopicTargetHdg}, {"sub", kTopicControlCode}, {"pub", kTopicStatusCode}, - {"pub", kTopicCamEvent}, {"pub", kTopicEnv}, + {"pub", kTopicCamEvent}, }; for (const auto& t : kTopics) { const ChannelMessage* last = nullptr; diff --git a/tests/test_channelhistory.cpp b/tests/test_channelhistory.cpp index a29759f..1d4e40d 100644 --- a/tests/test_channelhistory.cpp +++ b/tests/test_channelhistory.cpp @@ -82,11 +82,4 @@ TEST_CASE("topic and payload builders match the documented wire format") { CHECK(j.find("\"time\":1719312345678") != std::string::npos); // The quality gate is gone; nothing should still be publishing a verdict. CHECK(j.find("degraded") == std::string::npos); - - EnvEvent e; - e.tower = "Tower"; e.temp_c = 21.5f; e.humidity_pct = 48.0f; e.timestamp_ms = 42; - const std::string ej = envJson(e); - CHECK(ej.find("\"fwt\":\"Tower\"") != std::string::npos); - CHECK(ej.find("\"temp_c\":21.5") != std::string::npos); - CHECK(ej.find("\"time\":42") != std::string::npos); } diff --git a/tests/test_scheduler.cpp b/tests/test_scheduler.cpp index 8c29f16..74ee66c 100644 --- a/tests/test_scheduler.cpp +++ b/tests/test_scheduler.cpp @@ -44,7 +44,6 @@ struct FakeChannel : IControlChannel { bool connected() const override { return true; } void publishStatus(int c) override { last_status = c; } void publishCamEvent(const CamEvent&) override {} - void publishEnv(const EnvEvent&) override {} ControlCommand poll() override { ControlCommand c = next; next = {};