diff --git a/CMakeLists.txt b/CMakeLists.txt index e6f3422a..a376093c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,8 +115,8 @@ set(LIB_SOURCE_FILES src/mavlink.c src/wfbcli.hpp src/wfbcli.cpp - src/WiFiRSSIMonitor.hpp - src/WiFiRSSIMonitor.cpp + src/WiFiMonitor.hpp + src/WiFiMonitor.cpp src/scheduling_helper.hpp src/gstrtpreceiver.cpp src/gstrtpreceiver.h) diff --git a/README.md b/README.md index 341d05d7..e35ebeec 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Typical OSD config looks like: "x": 10, "y": 60, "icon_path": "device_thermostat.png", - "template": "CPU: %.0f⁰C, GPU: %.0f⁰C", + "template": "CPU: %.0f°C, GPU: %.0f°C", "facts": [ {"name": "os_mon.temperature", "tags": {"name": "soc-thermal"}, @@ -222,7 +222,27 @@ lot of facts to which widgets can subscribe to: | `video.decoder_feed_time_ms` | uint | Time to feed the video packet to hardware decoder | | `gstreamer.received_bytes` | uint | Number of bytes received from gstreamer (published for each packet) | | `osd.custom_message` | str | The custom message passed via `--osd-custom-message` feature | -| `os_mon.wifi.rssi` | uint | rssi as reported from /proc/net/rtl88x2eu//trx_info_debug | +| `os_mon.wifi.rssi` | int | rssi as reported from /proc/net/``/``/trx_info_debug | +| `os_mon.wifi.temperature` | int | RF chip temperature, degrees C, from /proc/net/``/``/thermal_state | + +The `os_mon.wifi.*` facts are only published in APFPV mode (in WFB mode the same data comes from +wfb-ng as `wfbcli.*`). They are read from the proc interface of the Realtek drivers - every +`/proc/net/rtl*` directory is scanned, so all card families (rtl88x2eu, rtl88x2cu, rtl88x2bu, ...) +are covered; cards whose driver has no `thermal_state` simply publish no temperature. +Both are tagged with `interface` (eg `wlx0013eff604e2`) and `adapter`, plus `type` +(`rssi_a`/`rssi_b`/`rssi_percent`/`connected`) for the rssi and `rf_path` (`0`/`1`) for the +temperature. `adapter` is the index of the card among all adapters found, counted from 0 in +interface-name order - bind widgets to it (`{"adapter": "0", "type": "rssi_a"}`) rather than to the +MAC-derived interface name, which differs per ground station. Note that the index of a card changes +when other adapters are plugged in or removed. Temperature is sampled every 10 seconds, because +reading it makes the driver trigger an ADC conversion on the RF chip. + +An adapter that is not associated (no drone yet, drone powered off) publishes `connected` = 0 and +`-1` for every rssi value, so a bar bound to it disappears instead of freezing at the last value it +saw. `-1` is outside the 0..100 percentage range that `IconSelectorWidget` configs cover, so it +hides by default; add a range for it (`{"range": [-1, 0], "icon_path": "no_signal.png"}`) to show an +explicit "no signal" icon instead. If an adapter is unplugged entirely, its facts are dropped back +to undefined, which hides the widget as well. There are many facts based on Mavlink telemetry, see `mavlink.c`. All of them have tags "sysid" and "compid", but some have extra tags. @@ -333,6 +353,10 @@ to display any fact (as long as datatype matches): * `{"type": "DebugWidget"}` - displays debug information (name, type, tags, value) about fact(s) * `{"type": "IconSelectorWidget"}` - display a icon based on a fact's value +Text is rendered with LVGL's built-in Montserrat font, which only covers ASCII plus the degree sign +`°` (U+00B0) and a few symbols. Any other character comes out as an empty box - watch out for +look-alikes such as `⁰` (U+2070, superscript zero). + Specific widgets expect quite concrete facts as input: * `DvrStatusWidget` - shows up when DVR is recording and is hidden when not. diff --git a/config_osd.json b/config_osd.json index 01f44c1c..8e556ee1 100644 --- a/config_osd.json +++ b/config_osd.json @@ -131,6 +131,123 @@ {"range": [-130, -91], "icon_path": "signal8.png"} ] }, + { + "type": "IconSelectorWidget", + "name": "APFPV RSSI of adapter 1 antenna 1", + "x": -250, + "y": 0, + "facts": [ + { + "__comment": "APFPV counterpart of the six widgets above: same slots, but fed by the WiFi driver instead of wfb-ng. Only one of the two sets is ever published, the other stays hidden. RSSI is a percentage here, not dBm. `adapter` counts the cards from 0 in interface-name order, `type` picks the RF path", + "name": "os_mon.wifi.rssi", "tags": {"adapter": "0", "type": "rssi_a"} + } + ], + "ranges_and_icons": [ + {"range": [70, 100], "icon_path": "signal1.png"}, + {"range": [60, 69], "icon_path": "signal2.png"}, + {"range": [50, 59], "icon_path": "signal3.png"}, + {"range": [40, 49], "icon_path": "signal4.png"}, + {"range": [30, 39], "icon_path": "signal5.png"}, + {"range": [20, 29], "icon_path": "signal6.png"}, + {"range": [10, 19], "icon_path": "signal7.png"}, + {"range": [1, 9], "icon_path": "signal8.png"} + ] + }, + { + "type": "IconSelectorWidget", + "name": "APFPV RSSI of adapter 1 antenna 2", + "x": -205, + "y": 0, + "facts": [ + {"name": "os_mon.wifi.rssi", "tags": {"adapter": "0", "type": "rssi_b"}} + ], + "ranges_and_icons": [ + {"range": [70, 100], "icon_path": "signal1.png"}, + {"range": [60, 69], "icon_path": "signal2.png"}, + {"range": [50, 59], "icon_path": "signal3.png"}, + {"range": [40, 49], "icon_path": "signal4.png"}, + {"range": [30, 39], "icon_path": "signal5.png"}, + {"range": [20, 29], "icon_path": "signal6.png"}, + {"range": [10, 19], "icon_path": "signal7.png"}, + {"range": [1, 9], "icon_path": "signal8.png"} + ] + }, + { + "type": "IconSelectorWidget", + "name": "APFPV RSSI of adapter 2 antenna 1", + "x": -160, + "y": 0, + "facts": [ + {"name": "os_mon.wifi.rssi", "tags": {"adapter": "1", "type": "rssi_a"}} + ], + "ranges_and_icons": [ + {"range": [70, 100], "icon_path": "signal1.png"}, + {"range": [60, 69], "icon_path": "signal2.png"}, + {"range": [50, 59], "icon_path": "signal3.png"}, + {"range": [40, 49], "icon_path": "signal4.png"}, + {"range": [30, 39], "icon_path": "signal5.png"}, + {"range": [20, 29], "icon_path": "signal6.png"}, + {"range": [10, 19], "icon_path": "signal7.png"}, + {"range": [1, 9], "icon_path": "signal8.png"} + ] + }, + { + "type": "IconSelectorWidget", + "name": "APFPV RSSI of adapter 2 antenna 2", + "x": -115, + "y": 0, + "facts": [ + {"name": "os_mon.wifi.rssi", "tags": {"adapter": "1", "type": "rssi_b"}} + ], + "ranges_and_icons": [ + {"range": [70, 100], "icon_path": "signal1.png"}, + {"range": [60, 69], "icon_path": "signal2.png"}, + {"range": [50, 59], "icon_path": "signal3.png"}, + {"range": [40, 49], "icon_path": "signal4.png"}, + {"range": [30, 39], "icon_path": "signal5.png"}, + {"range": [20, 29], "icon_path": "signal6.png"}, + {"range": [10, 19], "icon_path": "signal7.png"}, + {"range": [1, 9], "icon_path": "signal8.png"} + ] + }, + { + "type": "IconSelectorWidget", + "name": "APFPV RSSI of adapter 3 antenna 1", + "x": -70, + "y": 0, + "facts": [ + {"name": "os_mon.wifi.rssi", "tags": {"adapter": "2", "type": "rssi_a"}} + ], + "ranges_and_icons": [ + {"range": [70, 100], "icon_path": "signal1.png"}, + {"range": [60, 69], "icon_path": "signal2.png"}, + {"range": [50, 59], "icon_path": "signal3.png"}, + {"range": [40, 49], "icon_path": "signal4.png"}, + {"range": [30, 39], "icon_path": "signal5.png"}, + {"range": [20, 29], "icon_path": "signal6.png"}, + {"range": [10, 19], "icon_path": "signal7.png"}, + {"range": [1, 9], "icon_path": "signal8.png"} + ] + }, + { + "type": "IconSelectorWidget", + "name": "APFPV RSSI of adapter 3 antenna 2", + "x": -25, + "y": 0, + "facts": [ + {"name": "os_mon.wifi.rssi", "tags": {"adapter": "2", "type": "rssi_b"}} + ], + "ranges_and_icons": [ + {"range": [70, 100], "icon_path": "signal1.png"}, + {"range": [60, 69], "icon_path": "signal2.png"}, + {"range": [50, 59], "icon_path": "signal3.png"}, + {"range": [40, 49], "icon_path": "signal4.png"}, + {"range": [30, 39], "icon_path": "signal5.png"}, + {"range": [20, 29], "icon_path": "signal6.png"}, + {"range": [10, 19], "icon_path": "signal7.png"}, + {"range": [1, 9], "icon_path": "signal8.png"} + ] + }, { "name": "Video FPS and resolution", "type": "VideoWidget", @@ -212,6 +329,30 @@ "critical": 2, "facts": [ { "name": "gstreamer.received_bytes" } ] }, + { + "name": "APFPV WiFi RF temperature adapter 1", + "type": "---IconTplTextWidget", + "x": -250, + "y": 155, + "icon_path": "device_thermostat.png", + "template": "RF %i°C / %i°C", + "facts": [ + {"name": "os_mon.wifi.temperature", "tags": {"adapter": "0", "rf_path": "0"}}, + {"name": "os_mon.wifi.temperature", "tags": {"adapter": "0", "rf_path": "1"}} + ] + }, + { + "name": "APFPV WiFi RF temperature adapter 2", + "type": "---IconTplTextWidget", + "x": -250, + "y": 185, + "icon_path": "device_thermostat.png", + "template": "RF %i°C / %i°C", + "facts": [ + {"name": "os_mon.wifi.temperature", "tags": {"adapter": "1", "rf_path": "0"}}, + {"name": "os_mon.wifi.temperature", "tags": {"adapter": "1", "rf_path": "1"}} + ] + }, { "name": "MspDisplayPort", "type": "---MspDisplayPortWidget", diff --git a/src/WiFiMonitor.cpp b/src/WiFiMonitor.cpp new file mode 100644 index 00000000..0922c986 --- /dev/null +++ b/src/WiFiMonitor.cpp @@ -0,0 +1,284 @@ +#include "WiFiMonitor.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include "spdlog/spdlog.h" + +extern "C" { +#include "osd.h" +} + +namespace fs = std::filesystem; + +namespace { + +/* Realtek drivers register their proc directory under the driver name: + * rtl88x2eu, rtl88x2cu, rtl88x2bu, rtl8812au, ... */ +constexpr const char* DRIVER_DIR_PREFIX = "rtl"; + +void set_tag(osd_tag* tag, const char* key, const std::string& val) { + strncpy(tag->key, key, TAG_MAX_LEN - 1); + strncpy(tag->val, val.c_str(), TAG_MAX_LEN - 1); + tag->key[TAG_MAX_LEN - 1] = '\0'; + tag->val[TAG_MAX_LEN - 1] = '\0'; +} + +std::string trim(const std::string& s) { + const auto begin = s.find_first_not_of(" \t\r\n"); + if (begin == std::string::npos) { + return ""; + } + return s.substr(begin, s.find_last_not_of(" \t\r\n") - begin + 1); +} + +} // namespace + +// Constructor implementation +WiFiMonitor::WiFiMonitor() + : base_path_("/proc/net"), last_thermal_read_(), warned_no_driver_(false) {} + +// WiFiStats constructor implementation +WiFiMonitor::WiFiStats::WiFiStats() : rssi_a(0), rssi_b(0), rssi_min(0), rssi_percent(0), is_linked(false) {} + +std::vector WiFiMonitor::find_interfaces() { + std::vector interfaces; + std::error_code ec; + + // directory_iterator(ec) yields end() instead of throwing when /proc/net or a + // driver directory disappears (USB adapter unplugged) while we walk it. + for (const auto& driver : fs::directory_iterator(base_path_, ec)) { + if (!driver.is_directory(ec)) continue; + if (driver.path().filename().string().rfind(DRIVER_DIR_PREFIX, 0) != 0) continue; + + for (const auto& interface : fs::directory_iterator(driver.path(), ec)) { + if (interface.is_directory(ec)) { + interfaces.push_back(interface.path()); + } + } + } + + // The position in this list is published as the `adapter` tag, so sort by + // interface name: readdir order is not guaranteed, and cards may sit below + // different drivers. + std::sort(interfaces.begin(), interfaces.end(), + [](const fs::path& a, const fs::path& b) { return a.filename() < b.filename(); }); + + return interfaces; +} + +/* Drop the whole os_mon.wifi.* namespace when the set of adapters changes. + * + * A card that disappears (unplugged, driver unloaded) simply stops being + * published for, and nothing else retracts its facts, so its widgets would keep + * rendering the last value it ever reported. Worse, the `adapter` tag is the + * position in the list, so adding or removing a card renumbers the ones after + * it and their widgets would show another card's readings until every one of + * them happens to publish again. + * + * The adapters that are still here re-publish in the same run() below, and the + * fact processor applies pending flushes before the facts of the same batch, so + * they don't get cleared by our own flush. */ +void WiFiMonitor::flush_if_interfaces_changed(const std::vector& interfaces) { + std::vector names; + names.reserve(interfaces.size()); + for (const auto& interface : interfaces) { + names.push_back(interface.filename()); + } + + if (names == known_interfaces_) { + return; + } + known_interfaces_ = std::move(names); + + static const char* const prefix = "os_mon.wifi."; + osd_flush_facts(&prefix, 1); +} + +void WiFiMonitor::run() { + + std::vector interfaces = find_interfaces(); + flush_if_interfaces_changed(interfaces); + + if (interfaces.empty()) { + if (!warned_no_driver_) { + spdlog::error("No Realtek WiFi driver found below {}, no wifi stats will be published", base_path_); + warned_no_driver_ = true; + } + return; + } + warned_no_driver_ = false; + + // Temperature is sampled on its own, slower schedule (see THERMAL_INTERVAL) + const auto now = std::chrono::steady_clock::now(); + const bool read_thermal = (now - last_thermal_read_) >= THERMAL_INTERVAL; + if (read_thermal) { + last_thermal_read_ = now; + } + + // Initialize batch - estimate 6 facts per interface + void* batch = osd_batch_init(24); + + // Collect the stats of every interface of every driver + for (size_t adapter = 0; adapter < interfaces.size(); adapter++) { + const fs::path& interface = interfaces[adapter]; + + osd_tag base_tags[BASE_TAGS]; + make_base_tags(base_tags, interface.filename(), (int)adapter); + + std::string debug_file = interface / "trx_info_debug"; + if (fs::exists(debug_file)) { + WiFiStats stats = parse_interface_stats(debug_file); + add_interface_stats_to_batch(batch, base_tags, stats); + } + + std::string thermal_file = interface / "thermal_state"; + if (read_thermal && fs::exists(thermal_file)) { + for (const auto& thermal : parse_thermal_state(thermal_file)) { + add_temperature_fact_to_batch(batch, base_tags, thermal.rf_path, thermal.temperature); + } + } + } + + // Publish all collected facts + osd_publish_batch(batch); +} + +WiFiMonitor::WiFiStats WiFiMonitor::parse_interface_stats(const std::string& file_path) { + + WiFiStats stats; + std::ifstream file(file_path); + + if (!file.is_open()) { + return stats; + } + + std::string line; + while (std::getline(file, line)) { + // Parse RSSI A and B + if (line.find("rssi_a =") != std::string::npos) { + std::regex rssi_ab_regex(R"(rssi_a\s*=\s*(\d+)\(%\),\s*rssi_b\s*=\s*(\d+)\(%\))"); + std::smatch match; + if (std::regex_search(line, match, rssi_ab_regex) && match.size() == 3) { + stats.rssi_a = std::stoi(match[1]); + stats.rssi_b = std::stoi(match[2]); + } + } + // Parse RSSI percentage + else if (line.find("rssi :") != std::string::npos) { + std::regex rssi_regex(R"(rssi\s*:\s*(\d+)\s*\(\%\))"); + std::smatch match; + if (std::regex_search(line, match, rssi_regex) && match.size() > 1) { + stats.rssi_percent = std::stoi(match[1]); + } + } + else if (line.find("is_linked =") != std::string::npos) { + std::regex linked_regex(R"(is_linked\s*=\s*(\d+))"); + std::smatch match; + if (std::regex_search(line, match, linked_regex) && match.size() > 1) { + stats.is_linked = (std::stoi(match[1]) == 1); + } + } + } + + file.close(); + return stats; +} + +std::vector WiFiMonitor::parse_thermal_state(const std::string& file_path) { + // One line per RF path, e.g. + // rf_path: 0, thermal_value: 53, offset: 21, temperature: 112 + // Parsed as generic comma-separated "key: value" pairs (like wfb-ng does), so + // additional or reordered fields don't break the parser. + std::vector thermals; + std::ifstream file(file_path); + + if (!file.is_open()) { + return thermals; + } + + std::string line; + while (std::getline(file, line)) { + std::unordered_map fields; + std::stringstream fields_stream(line); + std::string field; + + while (std::getline(fields_stream, field, ',')) { + const auto colon = field.find(':'); + if (colon == std::string::npos) continue; + try { + fields[trim(field.substr(0, colon))] = std::stoi(field.substr(colon + 1)); + } catch (const std::exception&) { + // Not a numeric field, ignore it + } + } + + const auto rf_path = fields.find("rf_path"); + const auto temperature = fields.find("temperature"); + if (rf_path != fields.end() && temperature != fields.end()) { + thermals.push_back({rf_path->second, temperature->second}); + } + } + + file.close(); + return thermals; +} + +void WiFiMonitor::make_base_tags(osd_tag* tags, const std::string& interface_name, int adapter) { + set_tag(&tags[0], "interface", interface_name); + set_tag(&tags[1], "adapter", std::to_string(adapter)); +} + +void WiFiMonitor::add_interface_stats_to_batch(void* batch, const osd_tag* base_tags, const WiFiStats& stats) { + /* An unlinked adapter (no drone yet, drone powered off, adapter never brought + * up) has no RSSI to report. Going silent leaves the widgets showing the last + * RSSI of a link that is gone, so publish RSSI_NONE instead: it is outside + * every icon range, so IconSelectorWidget hides the bar, and a config that + * wants a visible "no signal" instead can just add a range covering it. + * Retracting per adapter, rather than with osd_flush_facts(), because that is + * scoped by fact-name prefix - it would take the other adapters and the + * temperature down with it. */ + const bool linked = stats.is_linked; + + // Publish RSSI A + add_rssi_fact_to_batch(batch, base_tags, "rssi_a", linked ? stats.rssi_a : RSSI_NONE); + + // Publish RSSI B + add_rssi_fact_to_batch(batch, base_tags, "rssi_b", linked ? stats.rssi_b : RSSI_NONE); + + // Publish RSSI Overall Percentage + add_rssi_fact_to_batch(batch, base_tags, "rssi_percent", linked ? stats.rssi_percent : RSSI_NONE); + + // Publish connection status + add_rssi_fact_to_batch(batch, base_tags, "connected", linked ? 1 : 0); +} + +void WiFiMonitor::add_rssi_fact_to_batch(void* batch, const osd_tag* base_tags, const std::string& rssi_type, int value) { + osd_tag tags[N_TAGS]; + + // Copy interface and adapter tags + memcpy(tags, base_tags, BASE_TAGS * sizeof(osd_tag)); + + // Add type tag + set_tag(&tags[BASE_TAGS], "type", rssi_type); + + // Add fact to batch + osd_add_int_fact(batch, "os_mon.wifi.rssi", tags, N_TAGS, value); +} + +void WiFiMonitor::add_temperature_fact_to_batch(void* batch, const osd_tag* base_tags, int rf_path, int value) { + osd_tag tags[N_TAGS]; + + // Copy interface and adapter tags + memcpy(tags, base_tags, BASE_TAGS * sizeof(osd_tag)); + + // Add the RF path the temperature was measured on + set_tag(&tags[BASE_TAGS], "rf_path", std::to_string(rf_path)); + + // Add fact to batch, degrees C + osd_add_int_fact(batch, "os_mon.wifi.temperature", tags, N_TAGS, value); +} diff --git a/src/WiFiMonitor.hpp b/src/WiFiMonitor.hpp new file mode 100644 index 00000000..e55c62e5 --- /dev/null +++ b/src/WiFiMonitor.hpp @@ -0,0 +1,90 @@ +#ifndef WIFI_MONITOR_H +#define WIFI_MONITOR_H + +#include +#include +#include +#include + +extern "C" { +#include "osd.h" +} + +/* + * Link stats of the directly attached WiFi adapters, published as os_mon.wifi.* + * facts. Used in APFPV mode, where wfb-ng (and with it the wfbcli.* facts) does + * not run. + * + * The Realtek rtlwifi-family drivers expose per-interface stats under + * /proc/net///: + * trx_info_debug - RSSI per RF path and link state + * thermal_state - RF chip temperature per RF path + * is the driver name and therefore differs per card family (rtl88x2eu, + * rtl88x2cu, rtl88x2bu, rtl8812au, ...), so every rtl* directory is scanned + * instead of hardcoding one. Files that a given driver does not provide are + * skipped (thermal_state only exists on the libc0607-derived 88x2 drivers). + * + * Every fact carries an `adapter` tag, the index of the card in the (sorted, so + * stable) list of all adapters found. Without it a widget can only match on + * `interface`, whose name is derived from the card's MAC - so an OSD binding + * would have to be written per ground station, and a binding that leaves the + * interface open would be fed by all cards in turn. This is the APFPV + * counterpart of the `ant_id` tag wfb-ng puts on the wfbcli.* facts. + */ +class WiFiMonitor { +public: + WiFiMonitor(); + void run(); + +private: + struct WiFiStats { + int rssi_a; + int rssi_b; + int rssi_min; + int rssi_percent; + bool is_linked; + + WiFiStats(); + }; + + /* One RF path of a card; temperature is in degrees C, as computed by the driver. */ + struct ThermalStats { + int rf_path; + int temperature; + }; + + /* Reading thermal_state makes the driver trigger an ADC conversion on the RF + * chip, so it is sampled far slower than run() is called. Matches wfb-ng's + * temp_measurement_interval default. */ + static constexpr std::chrono::seconds THERMAL_INTERVAL{10}; + + /* Published for an adapter that is not linked. Deliberately outside the 0..100 + * percentage range, so an icon widget hides unless the config maps it. */ + static constexpr int RSSI_NONE = -1; + + std::string base_path_; + std::chrono::steady_clock::time_point last_thermal_read_; + bool warned_no_driver_; + std::vector known_interfaces_; // adapters seen on the previous run() + + /* Number of tags every fact carries: `interface` and `adapter`, plus the one + * describing the value itself (`type` / `rf_path`). Kept identical for all + * facts of the same name so a widget can filter on any of them. */ + static constexpr int BASE_TAGS = 2; + static constexpr int N_TAGS = BASE_TAGS + 1; + + /* All /proc/net// directories currently present, sorted + * by interface name so the adapter index of a card doesn't depend on readdir + * order (it does change when adapters are added or removed, though). */ + std::vector find_interfaces(); + void flush_if_interfaces_changed(const std::vector& interfaces); + WiFiStats parse_interface_stats(const std::string& file_path); + std::vector parse_thermal_state(const std::string& file_path); + void make_base_tags(osd_tag* tags, const std::string& interface_name, int adapter); + void add_interface_stats_to_batch(void* batch, const osd_tag* base_tags, const WiFiStats& stats); + void add_rssi_fact_to_batch(void* batch, const osd_tag* base_tags, const std::string& rssi_type, int value); + void add_temperature_fact_to_batch(void* batch, const osd_tag* base_tags, int rf_path, int value); + +}; + +#endif diff --git a/src/WiFiRSSIMonitor.cpp b/src/WiFiRSSIMonitor.cpp deleted file mode 100644 index eb5d3803..00000000 --- a/src/WiFiRSSIMonitor.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "WiFiRSSIMonitor.hpp" -#include -#include -#include -#include -#include -#include -#include -#include "spdlog/spdlog.h" - -extern "C" { -#include "osd.h" -} - -namespace fs = std::filesystem; - -// Constructor implementation -WiFiRSSIMonitor::WiFiRSSIMonitor() : base_path_("/proc/net/rtl88x2eu") {} - -// WiFiStats constructor implementation -WiFiRSSIMonitor::WiFiStats::WiFiStats() : rssi_a(0), rssi_b(0), rssi_min(0), rssi_percent(0), is_linked(false) {} - -void WiFiRSSIMonitor::run() { - - if (!fs::exists(base_path_)) { - spdlog::error("RTL88x2eu proc path not found: {}", base_path_); - return; - } - - // Initialize batch - estimate 5 facts per interface - void* batch = osd_batch_init(20); - - // Find all WiFi interfaces and collect their stats - for (const auto& entry : fs::directory_iterator(base_path_)) { - if (!entry.is_directory()) continue; - - std::string interface_name = entry.path().filename(); - std::string debug_file = entry.path() / "trx_info_debug"; - - if (fs::exists(debug_file)) { - WiFiStats stats = parse_interface_stats(debug_file); - add_interface_stats_to_batch(batch, interface_name, stats); - } - } - - // Publish all collected facts - osd_publish_batch(batch); -} - -WiFiRSSIMonitor::WiFiStats WiFiRSSIMonitor::parse_interface_stats(const std::string& file_path) { - - WiFiStats stats; - std::ifstream file(file_path); - - if (!file.is_open()) { - return stats; - } - - std::string line; - while (std::getline(file, line)) { - // Parse RSSI A and B - if (line.find("rssi_a =") != std::string::npos) { - std::regex rssi_ab_regex(R"(rssi_a\s*=\s*(\d+)\(%\),\s*rssi_b\s*=\s*(\d+)\(%\))"); - std::smatch match; - if (std::regex_search(line, match, rssi_ab_regex) && match.size() == 3) { - stats.rssi_a = std::stoi(match[1]); - stats.rssi_b = std::stoi(match[2]); - } - } - // Parse RSSI percentage - else if (line.find("rssi :") != std::string::npos) { - std::regex rssi_regex(R"(rssi\s*:\s*(\d+)\s*\(\%\))"); - std::smatch match; - if (std::regex_search(line, match, rssi_regex) && match.size() > 1) { - stats.rssi_percent = std::stoi(match[1]); - } - } - else if (line.find("is_linked =") != std::string::npos) { - std::regex linked_regex(R"(is_linked\s*=\s*(\d+))"); - std::smatch match; - if (std::regex_search(line, match, linked_regex) && match.size() > 1) { - stats.is_linked = (std::stoi(match[1]) == 1); - } - } - } - - file.close(); - return stats; -} - -void WiFiRSSIMonitor::add_interface_stats_to_batch(void* batch, const std::string& interface_name, const WiFiStats& stats) { - if (!stats.is_linked) { - return; // Don't publish stats for disconnected interfaces - } - - // Prepare common tags - osd_tag interface_tag; - strncpy(interface_tag.key, "interface", TAG_MAX_LEN - 1); - strncpy(interface_tag.val, interface_name.c_str(), TAG_MAX_LEN - 1); - interface_tag.key[TAG_MAX_LEN - 1] = '\0'; - interface_tag.val[TAG_MAX_LEN - 1] = '\0'; - - // Publish RSSI A - add_rssi_fact_to_batch(batch, "rssi_a", stats.rssi_a, &interface_tag); - - // Publish RSSI B - add_rssi_fact_to_batch(batch, "rssi_b", stats.rssi_b, &interface_tag); - - // Publish RSSI Overall Percentage - add_rssi_fact_to_batch(batch, "rssi_percent", stats.rssi_percent, &interface_tag); - - // Publish connection status - add_rssi_fact_to_batch(batch, "connected", 1, &interface_tag); -} - -void WiFiRSSIMonitor::add_rssi_fact_to_batch(void* batch, const std::string& rssi_type, int value, osd_tag* interface_tag) { - osd_tag tags[2]; - - // Copy interface tag - memcpy(&tags[0], interface_tag, sizeof(osd_tag)); - - // Add type tag - strncpy(tags[1].key, "type", TAG_MAX_LEN - 1); - strncpy(tags[1].val, rssi_type.c_str(), TAG_MAX_LEN - 1); - tags[1].key[TAG_MAX_LEN - 1] = '\0'; - tags[1].val[TAG_MAX_LEN - 1] = '\0'; - - // Add fact to batch - std::string fact_name = "os_mon.wifi.rssi"; - osd_add_int_fact(batch, fact_name.c_str(), tags, 2, value); -} - -void WiFiRSSIMonitor::publish_reset() { - if (!fs::exists(base_path_)) { - spdlog::warn("RTL88x2eu proc path not found for reset: {}", base_path_); - return; - } - - // Initialize batch - estimate 5 facts per interface - void* batch = osd_batch_init(20); - - // Find all WiFi interfaces and publish reset values - for (const auto& entry : fs::directory_iterator(base_path_)) { - if (!entry.is_directory()) continue; - - std::string interface_name = entry.path().filename(); - publish_interface_reset(batch, interface_name); - } - - // Publish all reset facts - osd_publish_batch(batch); - - spdlog::debug("Published WiFi RSSI reset values for all interfaces"); -} - -void WiFiRSSIMonitor::publish_interface_reset(void* batch, const std::string& interface_name) { - // Prepare common tags - osd_tag interface_tag; - strncpy(interface_tag.key, "interface", TAG_MAX_LEN - 1); - strncpy(interface_tag.val, interface_name.c_str(), TAG_MAX_LEN - 1); - interface_tag.key[TAG_MAX_LEN - 1] = '\0'; - interface_tag.val[TAG_MAX_LEN - 1] = '\0'; - - // Publish all RSSI values as -1 (reset/error value) - add_rssi_fact_to_batch(batch, "rssi_a", -1, &interface_tag); - add_rssi_fact_to_batch(batch, "rssi_b", -1, &interface_tag); - add_rssi_fact_to_batch(batch, "rssi_min", -1, &interface_tag); - add_rssi_fact_to_batch(batch, "rssi_percent", -1, &interface_tag); - add_rssi_fact_to_batch(batch, "connected", 0, &interface_tag); // 0 = disconnected -} - -// C-callable function implementations -extern "C" { - -void wifi_rssi_monitor_reset(void) { - static WiFiRSSIMonitor monitor; - monitor.publish_reset(); -} - -} // extern "C" \ No newline at end of file diff --git a/src/WiFiRSSIMonitor.h b/src/WiFiRSSIMonitor.h deleted file mode 100644 index 1f925ceb..00000000 --- a/src/WiFiRSSIMonitor.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -// C-callable functions -#ifdef __cplusplus -extern "C" { -#endif - -void wifi_rssi_monitor_reset(void); - -#ifdef __cplusplus -} -#endif \ No newline at end of file diff --git a/src/WiFiRSSIMonitor.hpp b/src/WiFiRSSIMonitor.hpp deleted file mode 100644 index 4873f285..00000000 --- a/src/WiFiRSSIMonitor.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef WIFI_RSSI_MONITOR_H -#define WIFI_RSSI_MONITOR_H - -#include - -extern "C" { -#include "osd.h" -} - -class WiFiRSSIMonitor { -public: - WiFiRSSIMonitor(); - void run(); - void publish_reset(); - -private: - struct WiFiStats { - int rssi_a; - int rssi_b; - int rssi_min; - int rssi_percent; - bool is_linked; - - WiFiStats(); - }; - - std::string base_path_; - - WiFiStats parse_interface_stats(const std::string& file_path); - void add_interface_stats_to_batch(void* batch, const std::string& interface_name, const WiFiStats& stats); - void add_rssi_fact_to_batch(void* batch, const std::string& rssi_type, int value, osd_tag* interface_tag); - void publish_interface_reset(void* batch, const std::string& interface_name); - -}; - -#endif \ No newline at end of file diff --git a/src/gsmenu/colmenu_pages.c b/src/gsmenu/colmenu_pages.c index 9bece4f4..232adc46 100644 --- a/src/gsmenu/colmenu_pages.c +++ b/src/gsmenu/colmenu_pages.c @@ -175,8 +175,7 @@ static void notify_restart(const char * v) { (void)v; show_restart_notice(); } /* Apply the receiver mode: set RXMODE and the env vars the rest of the app reads * (REMOTE_IP / AIR_FIRMWARE_TYPE). Called both at startup and on a mode change, so - * the env is always in sync with the actual mode (the old gsmenu_toggle_rxmode() - * set these from menu creation onward). */ + * the env is always in sync with the actual mode. */ static void apply_rx_mode(bool apfpv) { bool changed = ((RXMODE == APFPV) != apfpv); @@ -184,7 +183,7 @@ static void apply_rx_mode(bool apfpv) setenv("REMOTE_IP", apfpv ? "192.168.0.1" : "10.5.0.10", 1); setenv("AIR_FIRMWARE_TYPE", apfpv ? "apfpv" : "wfb", 1); /* Drop the previous mode's stale facts: wfbcli.* (the wfbcli thread only runs in - * WFB) and os_mon.wifi.* (WiFiRSSIMonitor only runs in apfpv). The active source + * WFB) and os_mon.wifi.* (WiFiMonitor only runs in apfpv). The active source * re-publishes its own. Only these two prefixes - a blanket flush would also clear * facts nobody re-publishes, e.g. video.width/height (published once per decoder * frame-info change), leaving the VideoWidget stuck on "?x?". */ @@ -211,8 +210,7 @@ static bool mode_is_apfpv(void) } /* Switching RX mode: apply the env the rest of the app expects and rebuild the - * menu so the mode-specific pages appear. (Not the old gsmenu_toggle_rxmode(), - * which pokes lv_menu objects that don't exist here.) */ + * menu so the mode-specific pages appear. */ static void on_rx_mode_change(const char * value) { bool apfpv = value && strcmp(value, "apfpv") == 0; diff --git a/src/gsmenu/helper.c b/src/gsmenu/helper.c index 9f314efa..9be04b1a 100644 --- a/src/gsmenu/helper.c +++ b/src/gsmenu/helper.c @@ -2,10 +2,8 @@ #include #include #include "../../lvgl/lvgl.h" -#include "../menu.h" /* enum RXMode, RXMODE */ #include "helper.h" #include "styles.h" -#include "../WiFiRSSIMonitor.h" extern lv_indev_t * indev_drv; @@ -60,27 +58,6 @@ lv_obj_t * find_first_focusable_obj(lv_obj_t * parent) { return NULL; } -/* Apply the environment for the current RX mode (WFB vs APFPV). */ -void gsmenu_toggle_rxmode() { - switch (RXMODE) - { - case APFPV: - setenv("REMOTE_IP" , "192.168.0.1", 1); - setenv("AIR_FIRMWARE_TYPE" , "apfpv", 1); - break; - case WFB: - setenv("REMOTE_IP" , "10.5.0.10", 1); - setenv("AIR_FIRMWARE_TYPE" , "wfb", 1); -#ifndef USE_SIMULATOR - wifi_rssi_monitor_reset(); -#endif - break; - - default: - break; - } -} - typedef struct { lv_obj_t *mbox; lv_group_t *prev_group; diff --git a/src/gsmenu/helper.h b/src/gsmenu/helper.h index 6ae23920..2c208dfb 100644 --- a/src/gsmenu/helper.h +++ b/src/gsmenu/helper.h @@ -3,7 +3,6 @@ lv_obj_t * find_first_focusable_obj(lv_obj_t * parent); const char* find_resource_file(const char* relative_path); -void gsmenu_toggle_rxmode(void); void show_restart_notice(void); /* Apply the dark gsmenu theme to a msgbox (body, header, footer, content). diff --git a/src/main.cpp b/src/main.cpp index c7029c47..5bf781c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -61,7 +61,7 @@ extern "C" { #include "os_mon.hpp" #include "pixelpilot_config.h" #include -#include "WiFiRSSIMonitor.hpp" +#include "WiFiMonitor.hpp" #include "menu.h" @@ -144,7 +144,7 @@ size_t gsactions_count; uint32_t video_plane_id_override = 0; uint32_t osd_plane_id_override = 0; -WiFiRSSIMonitor wifi_monitor; +WiFiMonitor wifi_monitor; extern enum RXMode RXMODE; bool enable_live_colortrans = false; diff --git a/src/osd.cpp b/src/osd.cpp index b87f6349..8c993082 100644 --- a/src/osd.cpp +++ b/src/osd.cpp @@ -557,7 +557,9 @@ class FactMatcher { FactTags fact_tags = fact.getTags(); for (const auto& [key, match_value] : tags) { - if (auto value = fact_tags.find(key); value != tags.end()) { + // Compare against fact_tags.end(): find() returns an iterator into + // fact_tags, and an unmatched key used to be dereferenced as if found. + if (auto value = fact_tags.find(key); value != fact_tags.end()) { if (value->second != match_value) return false; } else { return false; @@ -964,7 +966,10 @@ class TplTextWidget: public Widget { std::vector tokenize(const std::string& tpl) { std::vector tokens; - std::regex token_regex(R"(%%|%[bisu]|%(\.\d+)?f|[^%]+)"); // Match placeholders and literals + // Match placeholders and literals. `d` belongs in the class: it is handled + // below (and documented) as an alias of `i`, but without it here the regex + // skips the '%' and the 'd' comes out as a literal. + std::regex token_regex(R"(%%|%[bisud]|%(\.\d+)?f|[^%]+)"); std::sregex_iterator iter(tpl.begin(), tpl.end(), token_regex); std::sregex_iterator end;