removed Env MQTT topic

This commit is contained in:
pgdalmeida 2026-09-09 13:43:29 +02:00
parent 8eda0b4a36
commit cd8491dcc0
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
13 changed files with 10 additions and 75 deletions

View File

@ -107,6 +107,13 @@ scripts for this are worth keeping; note that wrapping them in `timeout` SIGTERM
## Possible follow-ups (not done) ## Possible follow-ups (not done)
- **A retained `Env` payload may still sit on the broker.** `GGS/FWT/<tower>/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` - **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 ([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 host re-references it to NED. The two candidates give *identical* roll and pitch and

View File

@ -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/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/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 `<RemoteDevice>` 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/CameraSettingsXml.h](../include/fgc/CameraSettingsXml.h), [src/core/CameraSettingsXml.cpp](../src/core/CameraSettingsXml.cpp) | Parses the Viewer export's `<RemoteDevice>` 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/<tower>/…` 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/<tower>/…` 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/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/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 | | [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 | | File | Interface | Shared structs |
|------|-----------|----------------| |------|-----------|----------------|
| [include/fgc/IMotorController.h](../include/fgc/IMotorController.h) | `IMotorController` | `MotorTelemetry` | | [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/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/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` | | [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 `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)). `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)) ### `EnvSample` ([IEnvSensor.h](../include/fgc/IEnvSensor.h))
One ambient reading: `valid`, `temp_c` (°C), `humidity_pct` (0..100), `timestamp_ms` (epoch ms). 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 `IEnvSensor::sample()` returns `nullopt` rather than a stale sample once nothing valid has arrived

View File

@ -56,7 +56,6 @@ When a ControlCode message arrives, the program echoes the current code back on
|-------|------|---------|--------------| |-------|------|---------|--------------|
| `GGS/FWT/<tower>/StatusCode` | At startup (`"0"`); whenever a ControlCode message is received (echoes the code) | integer as string | 1 / retained | | `GGS/FWT/<tower>/StatusCode` | At startup (`"0"`); whenever a ControlCode message is received (echoes the code) | integer as string | 1 / retained |
| `GGS/FWT/<tower>/CamEvent` | After each image is saved | JSON object (below) | 1 / retained | | `GGS/FWT/<tower>/CamEvent` | After each image is saved | JSON object (below) | 1 / retained |
| `GGS/FWT/<tower>/Env` | Once per fresh ambient sensor sample (see `[Env] period_ms`) | JSON object (below) | 1 / retained |
### CamEvent payload ### 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 finishes, so a slow encode or a backed-up queue cannot attribute a later pose to earlier
pixels. 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 ## Topic summary
``` ```
@ -125,7 +108,6 @@ subscribe: GGS/FWT/<tower>/target_HDG (int heading)
publish: GGS/FWT/<tower>/StatusCode (echoed control code) publish: GGS/FWT/<tower>/StatusCode (echoed control code)
GGS/FWT/<tower>/CamEvent (JSON: fwt, cam, hdg×10, pit×10, time-ms) GGS/FWT/<tower>/CamEvent (JSON: fwt, cam, hdg×10, pit×10, time-ms)
GGS/FWT/<tower>/Env (JSON: fwt, temp_c, humid_pct, time-ms)
``` ```
## Local testing ## Local testing

View File

@ -18,12 +18,10 @@ inline constexpr const char* kTopicTargetHdg = "target_HDG";
inline constexpr const char* kTopicControlCode = "ControlCode"; inline constexpr const char* kTopicControlCode = "ControlCode";
inline constexpr const char* kTopicStatusCode = "StatusCode"; inline constexpr const char* kTopicStatusCode = "StatusCode";
inline constexpr const char* kTopicCamEvent = "CamEvent"; inline constexpr const char* kTopicCamEvent = "CamEvent";
inline constexpr const char* kTopicEnv = "Env";
std::string towerTopic(const std::string& tower, const std::string& leaf); std::string towerTopic(const std::string& tower, const std::string& leaf);
// Payload builders. One definition each, used by both channels. // Payload builders. One definition each, used by both channels.
std::string camEventJson(const CamEvent& e); std::string camEventJson(const CamEvent& e);
std::string envJson(const EnvEvent& e);
} // namespace fgc } // namespace fgc

View File

@ -23,14 +23,6 @@ struct CamEvent {
long long timestamp_ms = 0; // Unix epoch ms; matches the image filename 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 // 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 // they mean different things operationally: Dropped is "the link was down and we
// discarded it silently", Failed is "the broker or library rejected it". // discarded it silently", Failed is "the broker or library rejected it".
@ -71,7 +63,6 @@ public:
virtual void publishStatus(int code) = 0; virtual void publishStatus(int code) = 0;
virtual void publishCamEvent(const CamEvent& event) = 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 // Latest control input; clears the *_available flags so each update is
// acted on once. // acted on once.

View File

@ -35,7 +35,6 @@ public:
void publishStatus(int code) override; void publishStatus(int code) override;
void publishCamEvent(const CamEvent& event) override; void publishCamEvent(const CamEvent& event) override;
void publishEnv(const EnvEvent& event) override;
ControlCommand poll() override; ControlCommand poll() override;
std::vector<ChannelMessage> recentMessages() const override { return history_.recent(); } std::vector<ChannelMessage> recentMessages() const override { return history_.recent(); }
@ -59,7 +58,6 @@ private:
std::string topic_control_code_; std::string topic_control_code_;
std::string topic_status_; std::string topic_status_;
std::string topic_cam_event_; std::string topic_cam_event_;
std::string topic_env_;
mqtt::async_client client_; mqtt::async_client client_;
mqtt::connect_options conn_opts_; mqtt::connect_options conn_opts_;

View File

@ -38,13 +38,6 @@ public:
<< " (null channel)"; << " (null channel)";
drop(kTopicCamEvent, camEventJson(e)); 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 {}; } ControlCommand poll() override { return {}; }
std::vector<ChannelMessage> recentMessages() const override { return history_.recent(); } std::vector<ChannelMessage> recentMessages() const override { return history_.recent(); }

View File

@ -230,7 +230,6 @@ struct Application::Impl {
bool test_drift_gate_ = false; bool test_drift_gate_ = false;
mutable ImuConfigView imu_config_view_; // formatted MTi config (read once) mutable ImuConfigView imu_config_view_; // formatted MTi config (read once)
mutable bool imu_config_done_ = false; 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 // Latches the "no IMU sample, publishing encoder pitch" warning so an outage
// costs one line, not one per frame. Written from the camera delivery // costs one line, not one per frame. Written from the camera delivery
// thread(s) via the orientation supplier, hence atomic. // 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[0], fmt1(m->temp_c));
setField(g.fields[1], fmt1(m->humidity_pct)); 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 { } else {
s.sensors.env.status = "no fix"; s.sensors.env.status = "no fix";
} }

View File

@ -13,10 +13,4 @@ std::string camEventJson(const CamEvent& e) {
", \"time\":" + std::to_string(e.timestamp_ms) + " }"; ", \"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 } // namespace fgc

View File

@ -22,7 +22,6 @@ MqttControlChannel::MqttControlChannel(const std::string& broker_uri, const std:
topic_control_code_(towerTopic(tower, kTopicControlCode)), topic_control_code_(towerTopic(tower, kTopicControlCode)),
topic_status_(towerTopic(tower, kTopicStatusCode)), topic_status_(towerTopic(tower, kTopicStatusCode)),
topic_cam_event_(towerTopic(tower, kTopicCamEvent)), topic_cam_event_(towerTopic(tower, kTopicCamEvent)),
topic_env_(towerTopic(tower, kTopicEnv)),
client_(broker_uri, tower) { client_(broker_uri, tower) {
conn_opts_.set_keep_alive_interval(20); conn_opts_.set_keep_alive_interval(20);
conn_opts_.set_clean_session(true); conn_opts_.set_clean_session(true);
@ -136,8 +135,4 @@ void MqttControlChannel::publishCamEvent(const CamEvent& e) {
publish(topic_cam_event_, camEventJson(e)); publish(topic_cam_event_, camEventJson(e));
} }
void MqttControlChannel::publishEnv(const EnvEvent& e) {
publish(topic_env_, envJson(e));
}
} // namespace fgc } // namespace fgc

View File

@ -544,7 +544,7 @@ Element connDetailPanel(const ConnView& v, ScrollState& st) {
struct TopicRow { const char* dir; const char* leaf; }; struct TopicRow { const char* dir; const char* leaf; };
static const TopicRow kTopics[] = { static const TopicRow kTopics[] = {
{"sub", kTopicTargetHdg}, {"sub", kTopicControlCode}, {"pub", kTopicStatusCode}, {"sub", kTopicTargetHdg}, {"sub", kTopicControlCode}, {"pub", kTopicStatusCode},
{"pub", kTopicCamEvent}, {"pub", kTopicEnv}, {"pub", kTopicCamEvent},
}; };
for (const auto& t : kTopics) { for (const auto& t : kTopics) {
const ChannelMessage* last = nullptr; const ChannelMessage* last = nullptr;

View File

@ -82,11 +82,4 @@ TEST_CASE("topic and payload builders match the documented wire format") {
CHECK(j.find("\"time\":1719312345678") != std::string::npos); CHECK(j.find("\"time\":1719312345678") != std::string::npos);
// The quality gate is gone; nothing should still be publishing a verdict. // The quality gate is gone; nothing should still be publishing a verdict.
CHECK(j.find("degraded") == std::string::npos); 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);
} }

View File

@ -44,7 +44,6 @@ struct FakeChannel : IControlChannel {
bool connected() const override { return true; } bool connected() const override { return true; }
void publishStatus(int c) override { last_status = c; } void publishStatus(int c) override { last_status = c; }
void publishCamEvent(const CamEvent&) override {} void publishCamEvent(const CamEvent&) override {}
void publishEnv(const EnvEvent&) override {}
ControlCommand poll() override { ControlCommand poll() override {
ControlCommand c = next; ControlCommand c = next;
next = {}; next = {};