diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cdae774..a3672d9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -975,7 +975,9 @@ Color TFT panels (T114, T096, Wireless Tracker) are wrapped as 1bpp displays for ### 8.2 Pages (Button UI) -**Companion** (up to 12 pages): Messages, Recent, Radio, Bluetooth, Advert, GPS, Buzzer (if buzzer present), LEDs, Sensors, Offgrid, DFU, Shutdown +**Companion** (up to 14 pages): Messages, Recent, Radio, Traffic, Bluetooth, WiFi (WiFi companion builds only), Advert, GPS, Buzzer (if buzzer present), LEDs, Sensors, Offgrid, DFU, Shutdown + +Connection state on these pages: "an app is connected" (`UI_EVENT_APP_CONNECTED` / `_DISCONNECTED`, any transport) gates the buzzer, the message LED flash and the "Connected" line; which transport it is comes from `ui_set_link_transports()` (`UI_LINK_BLE` / `_USB` / `_TCP`, pushed by `process_link_change()` on every link edge). The Messages badge names the transport; the Bluetooth page says "Connected" only for a BLE client. The WiFi page shows the station state, SSID, IPv4 address, TCP port and whether an app is on the socket, pushed every housekeeping tick by `companion_wifi_ui_refresh()` through `ui_set_wifi()` (redraws only on a change, e-paper included). The joystick UI shows the same WiFi details as System > Info > WiFi (`WifiScreen`) on WiFi companion builds (ThinkNode M9); elsewhere its two hooks are no-ops. **Repeater** (3 pages): Status, Radio, Shutdown @@ -1007,8 +1009,8 @@ Pages with a **shared** flow that only tints per-row (Recent, GPS, Sensors, Status) stay as single functions with inline `if (mc_display_has_color())` — that already is the "one layout, colored" ideal, and the color branch dead-code-eliminates on mono via the constant-false `mc_display_has_color()`. -Split pages: Messages, Radio, Traffic, Bluetooth, Advert, LEDs, Offgrid, DFU, -Shutdown. +Split pages: Messages, Radio, Traffic, Bluetooth, WiFi, Advert, LEDs, Offgrid, +DFU, Shutdown. ### 8.3 Multi-Tap Input diff --git a/releasenotes/RELEASE_NOTES_1.18.0-zephcore.md b/releasenotes/RELEASE_NOTES_1.18.0-zephcore.md index 21aa601..61c4152 100644 --- a/releasenotes/RELEASE_NOTES_1.18.0-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.18.0-zephcore.md @@ -1,7 +1,7 @@ # ZephCore 1.18.0-zephcore -Safer off-grid mode, working buttons on four boards with a display, a GPS clock fix, a new KISS -modem role, and the changes from upstream MeshCore 1.18.0. +Safer off-grid mode, working buttons on four boards with a display, a WiFi page on the display of +WiFi companions, a GPS clock fix, a new KISS modem role, and the changes from upstream MeshCore 1.18.0. > [!NOTE] > A normal upgrade keeps your identity, settings, contacts and phone pairing. @@ -31,6 +31,20 @@ More in [OFFGRID.md](../docs/OFFGRID.md). Thanks to **N8HOP** for the report. confirmed again. - On all four boards the confirmation is a second **hold** within 3 seconds. +## Display: WiFi page, and the right connection shown + +- **New WiFi page** on WiFi companions with a one-button display (Heltec V3, V4, V4.3, Wireless + Tracker, T3S3, Station G2 and others), next to the Bluetooth page. It shows whether WiFi is off, + joining or connected, and once connected the network, IP address and port to enter in the app, + and whether the app is connected over WiFi. On the ThinkNode M9 the same details are under + **System > Info > WiFi**. +- **The home page names the connection:** "WiFi OK", "USB OK" or "BLE OK" instead of always + "BLE OK", and "BLE OFF" when Bluetooth is turned off and no app is connected. +- **The Bluetooth page says "Connected" only for a Bluetooth connection.** An app connected over + WiFi or USB used to show there too. + +Thanks to **bisbille** for the report. + ## GPS no longer sets the clock to an old time Just after waking, a GPS receiver can report its last position and its own, possibly stale, clock diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp index 3c2972b..d5f211f 100644 --- a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp @@ -503,6 +503,11 @@ bool zephcore_usb_companion_is_text_session(void) return usb_session_is_text; } +bool zephcore_usb_companion_link_up(void) +{ + return usb_link_up; +} + void zephcore_usb_companion_set_cli_line_cb(void (*cb)(const char *line)) { s_cli_line_cb = cb; diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.h b/zephcore/adapters/usb/ZephyrCompanionUSB.h index 8cd2562..22b6465 100644 --- a/zephcore/adapters/usb/ZephyrCompanionUSB.h +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.h @@ -74,6 +74,12 @@ void zephcore_usb_companion_write_text(const char *text, size_t len); */ bool zephcore_usb_companion_is_text_session(void); +/** + * True while a session of either kind (binary app or text CLI) is open and + * the bus is not suspended: the state behind the link callbacks' edges. + */ +bool zephcore_usb_companion_link_up(void); + #ifdef __cplusplus } #endif diff --git a/zephcore/app/CompanionWifi.cpp b/zephcore/app/CompanionWifi.cpp index e2b1831..8fabae8 100644 --- a/zephcore/app/CompanionWifi.cpp +++ b/zephcore/app/CompanionWifi.cpp @@ -14,6 +14,7 @@ #include #include #include +#include "ui_task.h" LOG_MODULE_REGISTER(zephcore_wifi_companion, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); @@ -49,6 +50,19 @@ void companion_wifi_start(const NodePrefs &prefs) } } +/* Off when start() did not join (s_ssid stays empty); up once there is a + * lease, which is what the app needs to connect. */ +void companion_wifi_ui_refresh(void) +{ + char ip[16] = ""; + uint8_t state = UI_WIFI_OFF; + + if (s_ssid[0] != '\0') { + state = zc_wifi_station_ip(ip, sizeof(ip)) ? UI_WIFI_UP : UI_WIFI_JOINING; + } + ui_set_wifi(state, s_ssid, ip, CONFIG_ZEPHCORE_TCP_PORT); +} + bool companion_wifi_cli(const char *command, NodePrefs &prefs, void (*save)(void), char *reply) { if (memcmp(command, "set wifi.ssid ", 14) == 0) { diff --git a/zephcore/app/CompanionWifi.h b/zephcore/app/CompanionWifi.h index 0de65e9..fb0043b 100644 --- a/zephcore/app/CompanionWifi.h +++ b/zephcore/app/CompanionWifi.h @@ -13,6 +13,10 @@ * wifi.* change applies on the next reboot. */ void companion_wifi_start(const NodePrefs &prefs); +/* Push the station's state, SSID and IP to the display (ui_set_wifi). From + * the main loop's housekeeping tick. */ +void companion_wifi_ui_refresh(void); + /* Upstream's companion wifi.* commands. True when `command` was one of them * (reply filled); save persists the prefs. */ bool companion_wifi_cli(const char *command, NodePrefs &prefs, void (*save)(void), char *reply); diff --git a/zephcore/helpers/ui-button/ui_pages.c b/zephcore/helpers/ui-button/ui_pages.c index 3ab6f93..30ded3f 100644 --- a/zephcore/helpers/ui-button/ui_pages.c +++ b/zephcore/helpers/ui-button/ui_pages.c @@ -170,6 +170,9 @@ static const enum ui_page active_pages[] = { UI_PAGE_RADIO, UI_PAGE_TRAFFIC, UI_PAGE_BLUETOOTH, +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + UI_PAGE_WIFI, +#endif UI_PAGE_ADVERT, UI_PAGE_GPS, #ifdef CONFIG_ZEPHCORE_UI_BUZZER @@ -454,6 +457,40 @@ static const char *radio_state_label(void) state.lora_radio_ready ? "RDY" : "WAIT"; } +/* The transport the app is connected on (BLE first when several are), for the + * home page. Meaningful only while state.app_connected. */ +static const char *link_label(void) +{ + return (state.link_transports & UI_LINK_BLE) ? "BLE" : + (state.link_transports & UI_LINK_TCP) ? "WiFi" : + (state.link_transports & UI_LINK_USB) ? "USB" : "App"; +} + +#if MC_DISPLAY_COLOR_PANEL +/* The home page's link badge: " OK" while an app is connected, + * else whether BLE is advertising or off. */ +static const char *link_badge(char *buf, size_t len) +{ + if (state.app_connected) { + snprintf(buf, len, "%s OK", link_label()); + return buf; + } + return state.ble_enabled ? "BLE ADV" : "BLE OFF"; +} + +static uint16_t link_badge_color(void) +{ + return state.app_connected ? UI_COLOR_OK : + state.ble_enabled ? UI_COLOR_WARN : UI_COLOR_DISABLED; +} +#endif + +/* True when BLE itself has a client (not just some transport). */ +static bool ble_link_up(void) +{ + return (state.link_transports & UI_LINK_BLE) != 0; +} + #if MC_DISPLAY_COLOR_PANEL static uint8_t clamp_activity_delta(uint32_t delta) { @@ -565,6 +602,7 @@ static const char *tiny_page_title(enum ui_page p) case UI_PAGE_RADIO: return "RADIO"; case UI_PAGE_TRAFFIC: return "TRAFFIC"; case UI_PAGE_BLUETOOTH: return "BLUETOOTH"; + case UI_PAGE_WIFI: return "WIFI"; case UI_PAGE_ADVERT: return "ADVERT"; case UI_PAGE_GPS: return "GPS"; case UI_PAGE_BUZZER: return "BUZZER"; @@ -601,14 +639,19 @@ static void render_tiny_title(void) */ static void render_messages_mono(void) { - /* 3 centered rows: msg count, BLE status, offgrid status */ + /* 3 centered rows: msg count, link status, offgrid status */ char buf[24]; if (ui_tiny()) { snprintf(buf, sizeof(buf), "MSG:%u", state.msg_count); draw_centered(centered_row(0, 3), buf); - draw_centered(centered_row(1, 3), - state.ble_connected ? "BLE:conn" : "BLE:adv"); + if (state.app_connected) { + snprintf(buf, sizeof(buf), "%s:conn", link_label()); + } else { + snprintf(buf, sizeof(buf), "BLE:%s", + state.ble_enabled ? "adv" : "off"); + } + draw_centered(centered_row(1, 3), buf); uint32_t up_s = (uint32_t)(k_uptime_get() / 1000); @@ -621,8 +664,9 @@ static void render_messages_mono(void) snprintf(buf, sizeof(buf), "MSG: %u", state.msg_count); draw_centered(centered_row(0, 4), buf); - if (state.ble_connected) { - draw_centered(centered_row(1, 4), "< Connected >"); + if (state.app_connected) { + snprintf(buf, sizeof(buf), "< Connected: %s >", link_label()); + draw_centered(centered_row(1, 4), buf); } else { draw_centered(centered_row(1, 4), "Waiting for app..."); } @@ -644,15 +688,15 @@ static void render_messages_color(void) if (use_compact_color_home()) { int y = CONTENT_Y; - uint16_t ble_color = state.ble_connected ? UI_COLOR_OK : UI_COLOR_WARN; - const char *ble = state.ble_connected ? "BLE OK" : "BLE ADV"; + char badge[12]; + const char *link = link_badge(badge, sizeof(badge)); const char *radio = radio_state_label(); uint16_t radio_color = state.lora_tx_active ? UI_COLOR_TX : state.lora_in_rx ? UI_COLOR_RX : state.lora_radio_ready ? UI_COLOR_OK : UI_COLOR_WARN; - draw_badge(0, y, ble, ble_color); + draw_badge(0, y, link, link_badge_color()); draw_badge(DISP_W - color_text_width(radio) - 4, y, radio, radio_color); y += LINE_H + 2; @@ -663,7 +707,7 @@ static void render_messages_color(void) state.msg_count ? UI_COLOR_WARN : UI_COLOR_VALUE); y += LINE_H; - if (state.ble_connected) { + if (state.app_connected) { draw_centered_color(y, "Connected", UI_COLOR_OK); } else { mc_display_color_text(30, y, "Waiting for app", UI_COLOR_WARN); @@ -685,10 +729,9 @@ static void render_messages_color(void) } int y = CONTENT_Y; - uint16_t ble_color = state.ble_connected ? UI_COLOR_OK : UI_COLOR_WARN; - const char *ble = state.ble_connected ? "BLE OK" : "BLE ADV"; + char badge[12]; - draw_badge(0, y, ble, ble_color); + draw_badge(0, y, link_badge(badge, sizeof(badge)), link_badge_color()); draw_badge(DISP_W - color_text_width(state.offgrid_enabled ? "GRID" : "LOCAL") - 4, y, state.offgrid_enabled ? "GRID" : "LOCAL", state.offgrid_enabled ? UI_COLOR_ACTIVE : UI_COLOR_DISABLED); @@ -700,7 +743,7 @@ static void render_messages_color(void) state.msg_count ? UI_COLOR_WARN : UI_COLOR_VALUE); y += LINE_H; - if (state.ble_connected) { + if (state.app_connected) { draw_centered_color(y, "Connected", UI_COLOR_OK); } else { draw_centered_color(y, "Waiting for app", UI_COLOR_WARN); @@ -1008,7 +1051,7 @@ static void render_bluetooth_mono(void) return; } - if (state.ble_connected) { + if (ble_link_up()) { draw_centered(centered_row(0, 2), "BLE: Connected"); draw_centered(centered_row(1, 2), "Press to Disable"); } else { @@ -1022,12 +1065,12 @@ static void render_bluetooth_color(void) { int y = CONTENT_Y; uint16_t color = !state.ble_enabled ? UI_COLOR_DISABLED : - state.ble_connected ? UI_COLOR_OK : UI_COLOR_WARN; + ble_link_up() ? UI_COLOR_OK : UI_COLOR_WARN; draw_badge(0, y, "BLE", color); mc_display_color_text(32, y, !state.ble_enabled ? "disabled" : - state.ble_connected ? "connected" : "advertising", + ble_link_up() ? "connected" : "advertising", color); y += LINE_H + (FONT_H / 4); draw_centered_color(y, @@ -1048,6 +1091,81 @@ static void render_bluetooth(void) render_bluetooth_mono(); } +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI +/* WiFi page: state, network, and the address the app connects to. */ +static void render_wifi_mono(void) +{ + char buf[24]; + + if (state.wifi_state == UI_WIFI_OFF) { + draw_centered(centered_row(0, 2), "WiFi: OFF"); + draw_centered(centered_row(1, 2), "Set up via CLI"); + return; + } + if (state.wifi_state == UI_WIFI_JOINING) { + draw_centered(centered_row(0, 2), "WiFi: Joining"); + draw_centered(centered_row(1, 2), state.wifi_ssid); + return; + } + + draw_centered(centered_row(0, 4), state.wifi_ssid); + draw_centered(centered_row(1, 4), state.wifi_ip); + snprintf(buf, sizeof(buf), "Port %u", state.wifi_port); + draw_centered(centered_row(2, 4), buf); + draw_centered(centered_row(3, 4), + (state.link_transports & UI_LINK_TCP) ? "App connected" + : "No app"); +} + +#if MC_DISPLAY_COLOR_PANEL +static void render_wifi_color(void) +{ + int y = CONTENT_Y; + char buf[8]; + uint16_t color = state.wifi_state == UI_WIFI_UP ? UI_COLOR_OK : + state.wifi_state == UI_WIFI_JOINING ? UI_COLOR_WARN : + UI_COLOR_DISABLED; + + draw_badge(0, y, "WiFi", color); + mc_display_color_text(color_text_width("WiFi") + 8, y, + state.wifi_state == UI_WIFI_UP ? "connected" : + state.wifi_state == UI_WIFI_JOINING ? "joining" : "off", + color); + y += LINE_H + (FONT_H / 4); + + if (state.wifi_state == UI_WIFI_OFF) { + draw_centered_color(y, "Set up via CLI", UI_COLOR_VALUE); + return; + } + draw_color_segments(y, "SSID ", state.wifi_ssid, UI_COLOR_VALUE); + y += LINE_H; + if (state.wifi_state == UI_WIFI_JOINING) { + return; + } + draw_color_segments(y, "IP ", state.wifi_ip, UI_COLOR_VALUE); + y += LINE_H; + snprintf(buf, sizeof(buf), "%u", state.wifi_port); + draw_color_segments(y, "Port ", buf, UI_COLOR_VALUE); + y += LINE_H; + draw_color_segments(y, "App ", + (state.link_transports & UI_LINK_TCP) ? "connected" : "none", + (state.link_transports & UI_LINK_TCP) ? UI_COLOR_OK + : UI_COLOR_DISABLED); +} +#endif /* MC_DISPLAY_COLOR_PANEL */ + +static void render_wifi(void) +{ +#if MC_DISPLAY_COLOR_PANEL + if (mc_display_has_color()) { + render_wifi_color(); + return; + } +#endif + render_wifi_mono(); +} +#endif /* CONFIG_ZEPHCORE_COMPANION_WIFI */ + static void render_advert_mono(void) { /* Check for recent "Sent!" feedback (show for 2 seconds) */ @@ -1643,6 +1761,9 @@ static const page_render_fn renderers[] = { [UI_PAGE_RADIO] = render_radio, [UI_PAGE_TRAFFIC] = render_traffic, [UI_PAGE_BLUETOOTH] = render_bluetooth, +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + [UI_PAGE_WIFI] = render_wifi, +#endif [UI_PAGE_ADVERT] = render_advert, [UI_PAGE_GPS] = render_gps, [UI_PAGE_BUZZER] = render_buzzer, diff --git a/zephcore/helpers/ui-button/ui_pages.h b/zephcore/helpers/ui-button/ui_pages.h index 638e44d..4cf06d5 100644 --- a/zephcore/helpers/ui-button/ui_pages.h +++ b/zephcore/helpers/ui-button/ui_pages.h @@ -25,6 +25,7 @@ enum ui_page { UI_PAGE_RADIO, /* LoRa params (freq, SF, BW, CR, power, noise) */ UI_PAGE_TRAFFIC, /* TX/RX packet activity graph */ UI_PAGE_BLUETOOTH, /* BLE toggle */ + UI_PAGE_WIFI, /* WiFi companion state and IP (WiFi builds) */ UI_PAGE_ADVERT, /* Send broadcast advert */ UI_PAGE_GPS, /* GPS status / position */ UI_PAGE_BUZZER, /* Buzzer mute toggle */ @@ -49,7 +50,8 @@ struct ui_state { /* Messages page */ uint16_t msg_count; - bool ble_connected; + bool app_connected; /* an app is connected, on any transport */ + uint8_t link_transports; /* UI_LINK_* bits: which transports have one */ /* Recently heard */ struct { @@ -79,6 +81,14 @@ struct ui_state { /* Bluetooth page */ bool ble_enabled; /* true = BLE active, false = serial mode */ +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + /* WiFi page */ + uint8_t wifi_state; /* enum ui_wifi_state */ + uint16_t wifi_port; /* TCP port of the app socket */ + char wifi_ssid[33]; + char wifi_ip[16]; /* dotted IPv4, "" without a lease */ +#endif + /* GPS page */ bool gps_available; /* true = GNSS hardware detected at boot */ bool gps_enabled; /* GPS hardware on/off */ diff --git a/zephcore/helpers/ui-button/ui_task.c b/zephcore/helpers/ui-button/ui_task.c index 242d06b..b80ba63 100644 --- a/zephcore/helpers/ui-button/ui_task.c +++ b/zephcore/helpers/ui-button/ui_task.c @@ -53,6 +53,7 @@ #include #include #include +#include #include #include @@ -195,7 +196,7 @@ static void splash_work_handler(struct k_work *work) static void schedule_render(void); static void schedule_render_auto(void); -static void set_ble_connected(bool connected); +static void set_app_connected(bool connected); static void advert_defer_handler(struct k_work *work) { @@ -819,7 +820,7 @@ void ui_notify(enum ui_event event) is_msg_event = true; #ifdef CONFIG_ZEPHCORE_UI_BUZZER /* Only buzz if no phone is connected to receive the message */ - if (!get_state()->ble_connected) { + if (!get_state()->app_connected) { buzzer_play(MELODY_MSG_CONTACT); } #endif @@ -828,7 +829,7 @@ void ui_notify(enum ui_event event) case UI_EVENT_CHANNEL_MSG: is_msg_event = true; #ifdef CONFIG_ZEPHCORE_UI_BUZZER - if (!get_state()->ble_connected) { + if (!get_state()->app_connected) { buzzer_play(MELODY_MSG_CHANNEL); } #endif @@ -837,7 +838,7 @@ void ui_notify(enum ui_event event) case UI_EVENT_ROOM_MSG: is_msg_event = true; #ifdef CONFIG_ZEPHCORE_UI_BUZZER - if (!get_state()->ble_connected) { + if (!get_state()->app_connected) { buzzer_play(MELODY_MSG_CHANNEL); } #endif @@ -849,23 +850,23 @@ void ui_notify(enum ui_event event) #endif break; - case UI_EVENT_BLE_CONNECTED: - set_ble_connected(true); + case UI_EVENT_APP_CONNECTED: + set_app_connected(true); break; - case UI_EVENT_BLE_DISCONNECTED: - set_ble_connected(false); + case UI_EVENT_APP_DISCONNECTED: + set_app_connected(false); break; default: break; } - if (is_msg_event && !get_state()->ble_connected) { + if (is_msg_event && !get_state()->app_connected) { ui_led_flash_msg(); } - /* Wake display on non-message notifications (BLE connect/disconnect etc). + /* Wake display on non-message notifications (app connect/disconnect etc). * Message notifications use buzzer + LED flash instead of waking the display. */ #ifdef CONFIG_ZEPHCORE_UI_DISPLAY #ifndef ZEPHCORE_REPEATER @@ -910,11 +911,11 @@ void ui_set_msg_count(uint16_t count) #endif } -static void set_ble_connected(bool connected) +static void set_app_connected(bool connected) { struct ui_state *s = get_state(); - s->ble_connected = connected; + s->app_connected = connected; if (ui_initialized) { schedule_render(); @@ -1125,6 +1126,53 @@ void ui_set_ble_enabled(bool enabled) s->ble_enabled = enabled; } +void ui_set_link_transports(uint8_t mask) +{ + struct ui_state *s = get_state(); + + if (s->link_transports == mask) { + return; + } + s->link_transports = mask; + if (ui_initialized) { + schedule_render(); + } +} + +void ui_set_wifi(uint8_t state, const char *ssid, const char *ip, uint16_t port) +{ +#if IS_ENABLED(CONFIG_ZEPHCORE_COMPANION_WIFI) + struct ui_state *s = get_state(); + char new_ssid[sizeof(s->wifi_ssid)]; + char new_ip[sizeof(s->wifi_ip)]; + + snprintf(new_ssid, sizeof(new_ssid), "%s", ssid ? ssid : ""); + snprintf(new_ip, sizeof(new_ip), "%s", ip ? ip : ""); + + if (s->wifi_state == state && s->wifi_port == port && + strcmp(s->wifi_ssid, new_ssid) == 0 && strcmp(s->wifi_ip, new_ip) == 0) { + return; + } + s->wifi_state = state; + s->wifi_port = port; + memcpy(s->wifi_ssid, new_ssid, sizeof(new_ssid)); + memcpy(s->wifi_ip, new_ip, sizeof(new_ip)); + + /* Repaint only a page that shows it; e-paper included, since this + * changes rarely. */ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + if (ui_initialized && ui_pages_current() == UI_PAGE_WIFI) { + schedule_render(); + } +#endif +#else + ARG_UNUSED(state); + ARG_UNUSED(ssid); + ARG_UNUSED(ip); + ARG_UNUSED(port); +#endif +} + void ui_set_buzzer_mode(uint8_t mode) { struct ui_state *s = get_state(); diff --git a/zephcore/helpers/ui-joystick/joystick_screens.h b/zephcore/helpers/ui-joystick/joystick_screens.h index a3a7e74..c940b39 100644 --- a/zephcore/helpers/ui-joystick/joystick_screens.h +++ b/zephcore/helpers/ui-joystick/joystick_screens.h @@ -287,6 +287,17 @@ public: bool handleInput(char c) override; }; +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI +/* ===== WifiScreen: WiFi companion state and the address the app uses ===== */ +class WifiScreen : public UIScreen { + JoystickUITask *_task; +public: + WifiScreen(JoystickUITask *task); + int render(JoystickDisplay &display) override; + bool handleInput(char c) override; +}; +#endif + /* ===== StopwatchScreen ===== */ class StopwatchScreen : public UIScreen { JoystickUITask *_task; diff --git a/zephcore/helpers/ui-joystick/joystick_ui_hooks.cpp b/zephcore/helpers/ui-joystick/joystick_ui_hooks.cpp index 762e57f..831ba01 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_hooks.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_hooks.cpp @@ -117,13 +117,13 @@ extern "C" void ui_notify(enum ui_event event) } switch (event) { - case UI_EVENT_BLE_CONNECTED: - s_task->setBLEConnected(true); + case UI_EVENT_APP_CONNECTED: + s_task->setAppConnected(true); s_task->notify(); break; - case UI_EVENT_BLE_DISCONNECTED: - s_task->setBLEConnected(false); + case UI_EVENT_APP_DISCONNECTED: + s_task->setAppConnected(false); s_task->notify(); break; @@ -269,4 +269,24 @@ extern "C" void ui_set_gps_enabled(bool) {} extern "C" void ui_set_gps_state(uint8_t, uint32_t, uint32_t) {} extern "C" void ui_set_buzzer_mode(uint8_t) {} extern "C" void ui_set_offgrid_mode(bool) {} +/* Kept for the WiFi screen (System > Info > WiFi); WiFi companion builds only. + * Both arrive on the main thread, where the UI runs. */ +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI +extern "C" void ui_set_link_transports(uint8_t mask) +{ + if (s_task) { + s_task->setLinkTransports(mask); + } +} + +extern "C" void ui_set_wifi(uint8_t state, const char *ssid, const char *ip, uint16_t port) +{ + if (s_task) { + s_task->setWifi(state, ssid, ip, port); + } +} +#else +extern "C" void ui_set_link_transports(uint8_t) {} +extern "C" void ui_set_wifi(uint8_t, const char *, const char *, uint16_t) {} +#endif extern "C" void ui_set_msg_count(uint16_t count) {} diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index 5b9cde4..b167b20 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -512,7 +512,7 @@ JoystickUITask::JoystickUITask() _next_refresh(0), _last_blt_ms(0), _pending_render(false), _render_immediate(false), _screen_off_ms(AUTO_OFF_MILLIS), _was_display_on(false), _cached_batt_mv(0), _battery_display_mode(0), - _brightness(100), _wake_on_msg(true), _ble_connected(false), + _brightness(100), _wake_on_msg(true), _app_connected(false), _ble_enabled(true), _noise_floor(-120), _pkt_recv(0), _pkt_sent(0), _pkt_errors(0), _alert_expiry(0), _locked(false), _lock_step(0), @@ -596,6 +596,9 @@ void JoystickUITask::begin(BaseChatMesh *mesh, mesh::ZephyrRTCClock *rtc, NodePr _rename_node = new RenameNodeScreen(this); _ble_code = new BLECodeScreen(this); _stats = new StatsScreen(this); +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + _wifi = new WifiScreen(this); +#endif _stopwatch = new StopwatchScreen(this); _countdown = new CountdownScreen(this); _snake = new SnakeScreen(this); @@ -646,6 +649,17 @@ void JoystickUITask::gotoT9InputScreenWithPrefix(const char *prefix) void JoystickUITask::gotoRenameNodeScreen() { setCurrScreen(_rename_node); } void JoystickUITask::gotoBLECodeScreen() { setCurrScreen(_ble_code); } void JoystickUITask::gotoStatsScreen() { setCurrScreen(_stats); } +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI +void JoystickUITask::gotoWifiScreen() { setCurrScreen(_wifi); } + +void JoystickUITask::setWifi(uint8_t state, const char *ssid, const char *ip, uint16_t port) +{ + _wifi_state = state; + _wifi_port = port; + snprintf(_wifi_ssid, sizeof(_wifi_ssid), "%s", ssid ? ssid : ""); + snprintf(_wifi_ip, sizeof(_wifi_ip), "%s", ip ? ip : ""); +} +#endif void JoystickUITask::gotoStopwatchScreen() { setCurrScreen(_stopwatch); } void JoystickUITask::gotoCountdownScreen() { setCurrScreen(_countdown); } void JoystickUITask::gotoSnakeScreen() { setCurrScreen(_snake); } @@ -1165,19 +1179,19 @@ void JoystickUITask::newMsg(uint8_t path_len, const char *from_name, const char { (void)msgcount; /* tracked by CompanionMesh */ if (_unread) { - /* When BLE phone is connected it pulls offline queue and marks read; - * keep the message in our local history but don't count as unread. */ + /* A connected app (any transport) pulls the offline queue and marks it + * read; keep the message in our local history but don't count as unread. */ _unread->addPreview(path_len, from_name, text, - /*initially_read=*/_ble_connected); + /*initially_read=*/_app_connected); } - /* Wake display if wake on msg is enabled and no BLE peer is connected. + /* Wake display if wake on msg is enabled and no app is connected. * mc_display_on() already reschedules display.c's auto-off timer. */ - if (_wake_on_msg && !_ble_connected && !_display.isOn()) { + if (_wake_on_msg && !_app_connected && !_display.isOn()) { _display.turnOn(); } _next_refresh = 0; #ifdef CONFIG_ZEPHCORE_UI_BUZZER - if (!_ble_connected) { + if (!_app_connected) { buzzer_play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); } #endif @@ -1201,16 +1215,16 @@ void JoystickUITask::newChannelMsg(const char *channel_name, const char *text, if (_unread) { char ch_from[48]; snprintf(ch_from, sizeof(ch_from), "#%s", channel_name ? channel_name : "?"); - /* BLE-connected → phone reads via offline queue, don't count as unread. */ + /* App connected → it reads via the offline queue, don't count as unread. */ _unread->addPreview(path_len, ch_from, text, - /*initially_read=*/_ble_connected); + /*initially_read=*/_app_connected); } - if (_wake_on_msg && !_ble_connected && !_display.isOn()) { + if (_wake_on_msg && !_app_connected && !_display.isOn()) { _display.turnOn(); /* display.c reschedules auto-off internally */ } _next_refresh = 0; #ifdef CONFIG_ZEPHCORE_UI_BUZZER - if (!_ble_connected) { + if (!_app_connected) { buzzer_play("kerplop:d=16,o=6,b=120:32g#,32c#"); } #endif diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.h b/zephcore/helpers/ui-joystick/joystick_ui_task.h index f3ba78f..11be9a5 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.h +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.h @@ -74,6 +74,9 @@ public: void gotoRenameNodeScreen(); void gotoBLECodeScreen(); void gotoStatsScreen(); +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + void gotoWifiScreen(); +#endif void gotoStopwatchScreen(); void gotoCountdownScreen(); void gotoSnakeScreen(); @@ -180,11 +183,23 @@ public: uint32_t getPktSent() const { return _pkt_sent; } uint32_t getPktErrors() const { return _pkt_errors; } - /* BLE status (fed from main companion loop) */ - void setBLEConnected(bool connected) { _ble_connected = connected; } - bool isBLEConnected() const { return _ble_connected; } + /* An app is connected on some transport (fed from main companion loop) */ + void setAppConnected(bool connected) { _app_connected = connected; } + bool isAppConnected() const { return _app_connected; } void setBLEEnabled(bool enabled) { _ble_enabled = enabled; } +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + /* WiFi companion state, pushed by ui_set_wifi() / ui_set_link_transports() + * on the main thread, read by WifiScreen. */ + void setWifi(uint8_t state, const char *ssid, const char *ip, uint16_t port); + void setLinkTransports(uint8_t mask) { _link_transports = mask; } + uint8_t getWifiState() const { return _wifi_state; } + const char *getWifiSsid() const { return _wifi_ssid; } + const char *getWifiIp() const { return _wifi_ip; } + uint16_t getWifiPort() const { return _wifi_port; } + uint8_t getLinkTransports() const { return _link_transports; } +#endif + /* RepeaterAdmin callbacks */ void onRepeaterAdminLoginResult(const uint8_t *pub_key_prefix, bool success, uint8_t permissions, uint32_t server_time); @@ -233,6 +248,9 @@ private: RenameNodeScreen *_rename_node; BLECodeScreen *_ble_code; StatsScreen *_stats; +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + WifiScreen *_wifi = nullptr; +#endif StopwatchScreen *_stopwatch; CountdownScreen *_countdown; SnakeScreen *_snake; @@ -264,8 +282,15 @@ private: int _battery_display_mode; uint8_t _brightness; bool _wake_on_msg; - bool _ble_connected; + bool _app_connected; bool _ble_enabled; +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + uint8_t _wifi_state = 0; /* enum ui_wifi_state */ + uint8_t _link_transports = 0; /* UI_LINK_* bits */ + uint16_t _wifi_port = 0; + char _wifi_ssid[33] = {}; + char _wifi_ip[16] = {}; +#endif int16_t _noise_floor; uint32_t _pkt_recv, _pkt_sent, _pkt_errors; diff --git a/zephcore/helpers/ui-joystick/screens/system.cpp b/zephcore/helpers/ui-joystick/screens/system.cpp index 4fa909e..2de07ca 100644 --- a/zephcore/helpers/ui-joystick/screens/system.cpp +++ b/zephcore/helpers/ui-joystick/screens/system.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -40,7 +41,11 @@ enum SysDspItem { SYSDSP_BRIGHT=0, SYSDSP_SCROFF, SYSDSP_BATT, SYSDSP_WAKE, #endif SYSDSP_INPUT_ROTATE, SYSDSP_COUNT }; /* Info submenu items */ -enum SysInfoItem { SYSINFO_TIME=0, SYSINFO_STATS, SYSINFO_RADIO, SYSINFO_COUNT }; +enum SysInfoItem { SYSINFO_TIME=0, SYSINFO_STATS, SYSINFO_RADIO, +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + SYSINFO_WIFI, +#endif + SYSINFO_COUNT }; /* Power submenu items */ enum SysPwrItem { SYSPWR_REBOOT=0, SYSPWR_SHUTDOWN, SYSPWR_COUNT }; @@ -112,7 +117,11 @@ int SystemScreen::render(JoystickDisplay &display) return 500; } if (_mode == SYSMODE_INFO) { - static const char * const kItems[SYSINFO_COUNT] = { "Time", "System Stats", "Radio Stats" }; + static const char * const kItems[SYSINFO_COUNT] = { "Time", "System Stats", "Radio Stats", +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + "WiFi", +#endif + }; renderSubMenu(display, "Info", _selected, kItems, SYSINFO_COUNT); return 500; } @@ -258,6 +267,9 @@ bool SystemScreen::handleInput(char c) case SYSINFO_TIME: _task->gotoSystemTimeScreen(); return true; case SYSINFO_STATS: _task->gotoStatsScreen(); return true; case SYSINFO_RADIO: _task->gotoRadioStatsScreen(); return true; +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI + case SYSINFO_WIFI: _task->gotoWifiScreen(); return true; +#endif default: return false; } } @@ -970,6 +982,60 @@ bool StatsScreen::handleInput(char c) return false; } +#ifdef CONFIG_ZEPHCORE_COMPANION_WIFI +WifiScreen::WifiScreen(JoystickUITask *task) + : _task(task) +{ +} + +/* Same states and wording as the button UI's WiFi page; redrawn every second. */ +int WifiScreen::render(JoystickDisplay &display) +{ + renderScreenHeader(display, "WiFi", 0, 0); + display.setTextSize(1); + + char line[48]; + int y = kContentY + 2; + uint8_t state = _task->getWifiState(); + + if (state == UI_WIFI_OFF) { + display.setColor(JoystickDisplay::LIGHT); + display.drawTextLeftAlign(0, y, "Status: off"); + y += kMenuLineH; + display.drawTextLeftAlign(0, y, "Set up via CLI"); + return 1000; + } + + display.setColor(state == UI_WIFI_UP ? JoystickDisplay::GREEN : JoystickDisplay::YELLOW); + display.drawTextLeftAlign(0, y, state == UI_WIFI_UP ? "Status: connected" : "Status: joining"); + y += kMenuLineH; + + display.setColor(JoystickDisplay::LIGHT); + snprintf(line, sizeof(line), "SSID: %s", _task->getWifiSsid()); + display.drawTextLeftAlign(0, y, line); + y += kMenuLineH; + if (state != UI_WIFI_UP) { + return 1000; + } + + snprintf(line, sizeof(line), "IP: %s", _task->getWifiIp()); + display.drawTextLeftAlign(0, y, line); + y += kMenuLineH; + snprintf(line, sizeof(line), "Port: %u", (unsigned)_task->getWifiPort()); + display.drawTextLeftAlign(0, y, line); + y += kMenuLineH; + display.drawTextLeftAlign(0, y, (_task->getLinkTransports() & UI_LINK_TCP) + ? "App: connected" : "App: none"); + return 1000; +} + +bool WifiScreen::handleInput(char c) +{ + if (c == KEY_CANCEL || c == KEY_HOME) { _task->gotoSystemScreen(); return true; } + return false; +} +#endif + TelemetryScreen::TelemetryScreen(JoystickUITask *task, mesh::RTCClock *rtc) : _task(task), _rtc(rtc), _selected(0) { diff --git a/zephcore/helpers/ui/ui_headless_stubs.c b/zephcore/helpers/ui/ui_headless_stubs.c index c87156e..525fd62 100644 --- a/zephcore/helpers/ui/ui_headless_stubs.c +++ b/zephcore/helpers/ui/ui_headless_stubs.c @@ -115,6 +115,17 @@ WEAK void ui_set_ble_enabled(bool enabled) ARG_UNUSED(enabled); } +WEAK void ui_set_link_transports(uint8_t mask) +{ + ARG_UNUSED(mask); +} + +WEAK void ui_set_wifi(uint8_t state, const char *ssid, const char *ip, + uint16_t port) +{ + ARG_UNUSED(state); ARG_UNUSED(ssid); ARG_UNUSED(ip); ARG_UNUSED(port); +} + WEAK void ui_set_buzzer_mode(uint8_t mode) { ARG_UNUSED(mode); diff --git a/zephcore/helpers/ui/ui_task.h b/zephcore/helpers/ui/ui_task.h index f84a315..fbb5d7b 100644 --- a/zephcore/helpers/ui/ui_task.h +++ b/zephcore/helpers/ui/ui_task.h @@ -26,8 +26,20 @@ enum ui_event { UI_EVENT_CHANNEL_MSG, /* New channel message received */ UI_EVENT_ROOM_MSG, /* New room message received */ UI_EVENT_ACK, /* ACK received */ - UI_EVENT_BLE_CONNECTED, /* BLE peer connected */ - UI_EVENT_BLE_DISCONNECTED, /* BLE peer disconnected */ + UI_EVENT_APP_CONNECTED, /* an app connected, on any transport */ + UI_EVENT_APP_DISCONNECTED, /* the last app session ended */ +}; + +/* Companion transports with a client connected (ui_set_link_transports). */ +#define UI_LINK_BLE (1u << 0) +#define UI_LINK_USB (1u << 1) /* wired serial: a binary app or a text-CLI session */ +#define UI_LINK_TCP (1u << 2) /* the WiFi companion's app socket */ + +/* WiFi companion station state (ui_set_wifi). */ +enum ui_wifi_state { + UI_WIFI_OFF = 0, /* wifi.enabled 0, or no network set */ + UI_WIFI_JOINING, /* joining, or waiting for a DHCP lease */ + UI_WIFI_UP, /* associated with an IPv4 address */ }; /** @@ -163,6 +175,22 @@ void ui_set_gps_state(uint8_t state, uint32_t last_fix_age_s, uint32_t next_sear */ void ui_set_ble_enabled(bool enabled); +/** + * Which companion transports have a client connected (UI_LINK_* bits). + * Pushed on every link edge; the app-connected state itself comes from + * UI_EVENT_APP_CONNECTED / _DISCONNECTED. + */ +void ui_set_link_transports(uint8_t mask); + +/** + * WiFi companion state for the WiFi page. ssid and ip are copied; either may + * be NULL or empty. Repaints only when something changed. + * + * @param state enum ui_wifi_state + * @param port TCP port the app connects to + */ +void ui_set_wifi(uint8_t state, const char *ssid, const char *ip, uint16_t port); + /** * Set notification mode (for display page). */ diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index ebddc6c..fd16373 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -284,16 +284,41 @@ static void companion_session_cleanup(void) #endif } +/* Which transports have a client right now, for the display (UI_LINK_*). */ +static uint8_t link_transports(void) +{ + uint8_t mask = 0; + +#if IS_ENABLED(CONFIG_BT) + if (zephcore_ble_is_connected()) { + mask |= UI_LINK_BLE; + } +#endif +#if ZEPHCORE_USB_STACK + if (zephcore_usb_companion_link_up()) { + mask |= UI_LINK_USB; + } +#endif +#if IS_ENABLED(CONFIG_ZEPHCORE_TRANSPORT_TCP) + if (tcp_companion_is_connected()) { + mask |= UI_LINK_TCP; + } +#endif + return mask; +} + /* MESH_EVENT_LINK: a transport (USB and TCP sessions included) connected or - * disconnected. */ + * disconnected. The app-connected events stay transport-agnostic (they gate + * buzzer, LED and unread handling); the mask says which transport it is. */ static void process_link_change(void) { + ui_set_link_transports(link_transports()); if (atomic_set(&link_ups, 0) > 0) { - ui_notify(UI_EVENT_BLE_CONNECTED); + ui_notify(UI_EVENT_APP_CONNECTED); } if (atomic_set(&link_downs, 0) > 0 && !companion_serial.isConnected()) { companion_session_cleanup(); - ui_notify(UI_EVENT_BLE_DISCONNECTED); + ui_notify(UI_EVENT_APP_DISCONNECTED); } } @@ -461,6 +486,9 @@ static void mesh_event_loop(void) #endif mesh_housekeeping_ui_refresh(); +#if IS_ENABLED(CONFIG_ZEPHCORE_COMPANION_WIFI) && defined(ZEPHCORE_LORA) + companion_wifi_ui_refresh(); +#endif if (companion_mesh_ptr) { companion_mesh_ptr->timeSyncTick();