From 6cc346ac2ecc7db6cd3fad6f3e02c72b2464fcca Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Mon, 27 Jul 2026 17:11:32 +0200 Subject: [PATCH 1/2] Put the clock and build stamp where they can be found, and Wi-Fi Country where it belongs Three things from using the device rather than reading it. Wi-Fi Country sat on the setup portal's parameter page, among the webhook URL and the button labels. That portal calls setParamsPage(true), so its parameters render away from the network list, which is the one place the country setting matters: it decides which channels the scan shows. It is now only in the Wi-Fi portal, where parameters share a page with the scan results, and where you already are when a network is missing from them. The status screen, one button hold from idle, now shows the local time and the build stamp. "clock not set" there is the visible symptom of a receiver that is not returning a clock, which is otherwise only findable over serial and stops the scheduled reset without any other sign. The build stamp was already on the device info screen, but that is three steps into the settings menu and nobody found it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SFniCJ94a3sciMP9MtbPfs --- .../src/display/display.cpp | 43 +++++++++++++++++++ Firmware/HomeButtonsArduino/src/setup.cpp | 8 +++- README.md | 10 ++--- docs/counter-guide.md | 16 +++++-- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/Firmware/HomeButtonsArduino/src/display/display.cpp b/Firmware/HomeButtonsArduino/src/display/display.cpp index 196bd57..7aaa584 100644 --- a/Firmware/HomeButtonsArduino/src/display/display.cpp +++ b/Firmware/HomeButtonsArduino/src/display/display.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "bitmaps.h" #include "config.h" @@ -557,6 +558,48 @@ void Display::draw_info() { u8g2.print(status); } + // Clock and build stamp. This screen is one button hold away from idle, + // where the device info screen is three steps into the settings menu, so + // the two things worth checking on a device that is behaving oddly live + // here: whether it knows the time, and what is actually flashed on it. + u8g2.setFont(u8g2_font_profont12_tr); + + if (device_state_.clock_valid()) { + const time_t local = + time(nullptr) + static_cast(device_state_.tz_offset()); + struct tm tm_local = {}; + gmtime_r(&local, &tm_local); + text = UIState::MessageType("%04d-%02d-%02d %02d:%02d", + tm_local.tm_year + 1900, tm_local.tm_mon + 1, + tm_local.tm_mday, tm_local.tm_hour, + tm_local.tm_min); + } else { + // No RTC and no NTP: the clock arrives with the first webhook response. + text = "clock not set"; + } + w = u8g2.getUTF8Width(text.c_str()); + u8g2.setCursor(WIDTH / 2 - w / 2, 272); + u8g2.print(text.c_str()); + + // One line while the two images agree, which is the normal case. A + // mismatch means a forgotten uploadfs and is worth the second line. + if (spiffs_build_ == BUILD_ID) { + text = UIState::MessageType("build %s", BUILD_ID); + w = u8g2.getUTF8Width(text.c_str()); + u8g2.setCursor(WIDTH / 2 - w / 2, 286); + u8g2.print(text.c_str()); + } else { + text = UIState::MessageType("fw %s", BUILD_ID); + w = u8g2.getUTF8Width(text.c_str()); + u8g2.setCursor(WIDTH / 2 - w / 2, 283); + u8g2.print(text.c_str()); + text = UIState::MessageType( + "fs %s", spiffs_build_.empty() ? "missing" : spiffs_build_.c_str()); + w = u8g2.getUTF8Width(text.c_str()); + u8g2.setCursor(WIDTH / 2 - w / 2, 294); + u8g2.print(text.c_str()); + } + disp->display(); } diff --git a/Firmware/HomeButtonsArduino/src/setup.cpp b/Firmware/HomeButtonsArduino/src/setup.cpp index 4722fa4..d008e2d 100644 --- a/Firmware/HomeButtonsArduino/src/setup.cpp +++ b/Firmware/HomeButtonsArduino/src/setup.cpp @@ -324,7 +324,13 @@ void HBSetup::start_setup() { wifi_manager.addParameter(&auth_token_param); wifi_manager.addParameter(&awake_mode_param); wifi_manager.addParameter(&reset_spec_param); - wifi_manager.addParameter(&wifi_country_param); + // Wi-Fi Country is deliberately not here. This portal calls + // setParamsPage(true), which puts every parameter on a page of its own, + // away from the network list and the password field - so a Wi-Fi setting + // would sit among the webhook URL and the button labels. It belongs in + // the Wi-Fi portal (settings, button 2), where parameters render on the + // same page as the scan results, which is also where you are standing + // when you notice a network missing from that list. wifi_manager.addParameter(&static_ip_param); wifi_manager.addParameter(&gateway_param); wifi_manager.addParameter(&subnet_param); diff --git a/README.md b/README.md index 7473027..d125128 100644 --- a/README.md +++ b/README.md @@ -120,12 +120,12 @@ The short version: hold any two buttons for 5 s → settings → button 1. The device starts an access point `HB-` with password `HB-`, both unique per device and taken from the burnt eFuse. Portal fields are device name, **webhook URL**, **auth token**, **counter -reset** schedule, **Wi-Fi country**, awake mode, static IP settings, and the -six button labels. +reset** schedule, awake mode, static IP settings, and the six button labels. -Set the Wi-Fi country if your router sits on channel 12 or 13. The ESP-IDF -default defers to whatever the access point advertises and reverts on -disconnect, so those channels can be missing from the scan list entirely. +**Wi-Fi country** lives in the Wi-Fi portal instead (settings, then button 2), +next to the network list. Set it if your router sits on channel 12 or 13: the +ESP-IDF default defers to whatever the access point advertises and reverts on +disconnect, so those channels can be missing from the scan entirely. > The remaining pages under `docs/` are inherited from upstream and describe > the MQTT firmware. They do not apply to this fork. diff --git a/docs/counter-guide.md b/docs/counter-guide.md index d16e442..c4ebc27 100644 --- a/docs/counter-guide.md +++ b/docs/counter-guide.md @@ -122,10 +122,10 @@ fields are empty — the device boots, counts locally, and silently logs |---|---| | Hold **any two buttons, 5 s** | Settings menu | | Then press **button 1** | Setup portal (Wi-Fi + all settings) | -| Then press **button 2** | Wi-Fi only — also carries the **Wi-Fi Country** field | +| Then press **button 2** | Wi-Fi only, and the only place with the **Wi-Fi Country** field | | Then press **button 3** | Restart | | Then press **button 4** | Cancel | -| In settings, hold **button 1, 2 s** | Device Info screen | +| In settings, hold **button 1, 2 s** | Device info screen: name, versions, both build stamps, IP, battery | | In settings, hold **button 3, 10 s** | Factory reset | The settings menu times out after 30 s. @@ -154,7 +154,6 @@ The portal opens at `http://192.168.4.1` and closes after 10 minutes. | **Device Name** | Cosmetic; shown on the settings screen | | **Webhook URL** | Full HTTPS URL of the n8n **production** webhook. Max 128 chars | | **Auth Token** | Sent as `Authorization: Bearer `. Max 128 chars. Masked in the page | -| **Wi-Fi Country** | ISO code, e.g. `PL`, `DE`, `GB`, `US`. Blank uses the ESP-IDF default. **Set this if your router uses channel 12 or 13** — see below | | **Awake Mode** | `1` keeps the device from deep sleeping, which is what you want while watching a serial log. `0` for normal use. Drains the battery fast | | **Counter Reset** | When the counters clear themselves. `off`, `daily 03:00`, `weekly mon 03:00`, `monthly 1 03:00`. See below | | Static IP / Gateway / Subnet / DNS / DNS 2 | Optional — leave blank for DHCP. All three of IP, gateway and subnet must be set for static to apply | @@ -164,6 +163,13 @@ The portal opens at `http://192.168.4.1` and closes after 10 minutes. verifies the chain; a plain `http://` URL or an untrusted certificate will fail the POST. +**Wi-Fi Country is not on this page.** It is in the Wi-Fi portal, settings +then button 2, on the same page as the network list. That is a deliberate +split: this portal puts its parameters on a page of their own, away from +the scan results, and the country is the setting that decides which +channels appear in those results. Setting it next to the list you are +staring at is the point. + ### Counter reset schedule One free-text field, parsed into four modes: @@ -386,6 +392,10 @@ curl -s -X POST https://your-n8n/webhook/ \ 4. Press three more times in quick succession. Only the first is slow. Presses 2-4 reuse the open TLS session and land in roughly 130 ms. 5. Press **button 5** to decrement, and confirm the count goes back down. + Hold any single button for 2 s to see the status screen: battery, the + local time the device believes it is, and the build stamp of what is + flashed. `clock not set` there means the receiver is not returning a + clock, and the scheduled reset will not run. 6. Press **button 1**. It is a title button, so it blinks twice and does nothing else. No POST, no change to the count. 7. After ~30 s of no input the device goes back to deep sleep. From f21be09766d2d5725a63a73416a8795ec717ede4 Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Tue, 28 Jul 2026 12:10:25 +0200 Subject: [PATCH 2/2] Show Wi-Fi Country on the Wi-Fi page as well, not instead Removing it from the setup portal fixed the placement by taking the field away, which is worse than having it in the wrong spot. setParamsPage() bundles two unrelated things: it sets _paramsInWifi, which gates whether parameters render and save on the Wi-Fi page, and it swaps the menu. Calling it with true confined the parameters to Setup. What the flag does not gate is the Setup page itself, because handleParam() renders unconditionally, so turning the flag on and putting "param" back in the menu by hand gives both pages, and a save from either commits everything. Wi-Fi Country is registered first so that on the Wi-Fi page it lands under the network fields rather than below six button labels. One behaviour change worth knowing: saving from the Wi-Fi page now runs save_params_callback, which sets web_portal_saved_ and ends the portal. That did not happen before, and it is the right outcome after changing networks. Parameters are seeded from the current state, so a save that touched nothing writes back what was already there. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SFniCJ94a3sciMP9MtbPfs --- Firmware/HomeButtonsArduino/src/setup.cpp | 25 +++++++++++++++-------- README.md | 15 ++++++++------ docs/counter-guide.md | 13 ++++++------ 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Firmware/HomeButtonsArduino/src/setup.cpp b/Firmware/HomeButtonsArduino/src/setup.cpp index d008e2d..6e6f56d 100644 --- a/Firmware/HomeButtonsArduino/src/setup.cpp +++ b/Firmware/HomeButtonsArduino/src/setup.cpp @@ -279,7 +279,20 @@ void HBSetup::start_setup() { wifi_manager.setSaveParamsCallback( std::bind(&HBSetup::save_params_callback, this)); wifi_manager.setBreakAfterConfig(true); - wifi_manager.setParamsPage(true); + // Parameters render on the Wi-Fi page and on the Setup page both. + // + // setParamsPage(true) confines them to Setup, which is where Wi-Fi + // Country used to sit: the field that decides which channels a scan + // returns, on a different page from the scan results. WiFiManager gates + // rendering and saving on the same _paramsInWifi flag, so there is no way + // to place one parameter on the Wi-Fi page and leave the rest on Setup. + // + // What the flag does not gate is the Setup page itself: handleParam() + // renders unconditionally. So turning the flag on and putting "param" + // back in the menu by hand gives both pages, and either one saves. + wifi_manager.setParamsPage(false); + std::vector menu = {"wifi", "param", "info", "exit"}; + wifi_manager.setMenu(menu); wifi_manager.setDarkMode(true); wifi_manager.setShowInfoUpdate(true); @@ -319,18 +332,14 @@ void HBSetup::start_setup() { set_btn_label_params_from_device_state(app_.device_state_); #endif + // First, so that on the Wi-Fi page it lands directly under the network + // fields rather than below the button labels. + wifi_manager.addParameter(&wifi_country_param); wifi_manager.addParameter(&device_name_param); wifi_manager.addParameter(&endpoint_url_param); wifi_manager.addParameter(&auth_token_param); wifi_manager.addParameter(&awake_mode_param); wifi_manager.addParameter(&reset_spec_param); - // Wi-Fi Country is deliberately not here. This portal calls - // setParamsPage(true), which puts every parameter on a page of its own, - // away from the network list and the password field - so a Wi-Fi setting - // would sit among the webhook URL and the button labels. It belongs in - // the Wi-Fi portal (settings, button 2), where parameters render on the - // same page as the scan results, which is also where you are standing - // when you notice a network missing from that list. wifi_manager.addParameter(&static_ip_param); wifi_manager.addParameter(&gateway_param); wifi_manager.addParameter(&subnet_param); diff --git a/README.md b/README.md index d125128..c1da1a6 100644 --- a/README.md +++ b/README.md @@ -120,12 +120,15 @@ The short version: hold any two buttons for 5 s → settings → button 1. The device starts an access point `HB-` with password `HB-`, both unique per device and taken from the burnt eFuse. Portal fields are device name, **webhook URL**, **auth token**, **counter -reset** schedule, awake mode, static IP settings, and the six button labels. - -**Wi-Fi country** lives in the Wi-Fi portal instead (settings, then button 2), -next to the network list. Set it if your router sits on channel 12 or 13: the -ESP-IDF default defers to whatever the access point advertises and reverts on -disconnect, so those channels can be missing from the scan entirely. +reset** schedule, **Wi-Fi country**, awake mode, static IP settings, and the +six button labels. They appear on the Wi-Fi page as well as on their own +Setup page, so either one saves them. + +Set the Wi-Fi country if your router sits on channel 12 or 13. The ESP-IDF +default defers to whatever the access point advertises and reverts on +disconnect, so those channels can be missing from the scan entirely. It is +listed first, directly under the network fields, because it decides what the +scan returns. > The remaining pages under `docs/` are inherited from upstream and describe > the MQTT firmware. They do not apply to this fork. diff --git a/docs/counter-guide.md b/docs/counter-guide.md index c4ebc27..4fe5a11 100644 --- a/docs/counter-guide.md +++ b/docs/counter-guide.md @@ -122,7 +122,7 @@ fields are empty — the device boots, counts locally, and silently logs |---|---| | Hold **any two buttons, 5 s** | Settings menu | | Then press **button 1** | Setup portal (Wi-Fi + all settings) | -| Then press **button 2** | Wi-Fi only, and the only place with the **Wi-Fi Country** field | +| Then press **button 2** | Wi-Fi only, carrying the **Wi-Fi Country** field | | Then press **button 3** | Restart | | Then press **button 4** | Cancel | | In settings, hold **button 1, 2 s** | Device info screen: name, versions, both build stamps, IP, battery | @@ -154,6 +154,7 @@ The portal opens at `http://192.168.4.1` and closes after 10 minutes. | **Device Name** | Cosmetic; shown on the settings screen | | **Webhook URL** | Full HTTPS URL of the n8n **production** webhook. Max 128 chars | | **Auth Token** | Sent as `Authorization: Bearer `. Max 128 chars. Masked in the page | +| **Wi-Fi Country** | ISO code, e.g. `PL`, `DE`, `GB`, `US`. Blank uses the ESP-IDF default. **Set this if your router uses channel 12 or 13.** Shown on the Wi-Fi page too, under the network fields | | **Awake Mode** | `1` keeps the device from deep sleeping, which is what you want while watching a serial log. `0` for normal use. Drains the battery fast | | **Counter Reset** | When the counters clear themselves. `off`, `daily 03:00`, `weekly mon 03:00`, `monthly 1 03:00`. See below | | Static IP / Gateway / Subnet / DNS / DNS 2 | Optional — leave blank for DHCP. All three of IP, gateway and subnet must be set for static to apply | @@ -163,12 +164,10 @@ The portal opens at `http://192.168.4.1` and closes after 10 minutes. verifies the chain; a plain `http://` URL or an untrusted certificate will fail the POST. -**Wi-Fi Country is not on this page.** It is in the Wi-Fi portal, settings -then button 2, on the same page as the network list. That is a deliberate -split: this portal puts its parameters on a page of their own, away from -the scan results, and the country is the setting that decides which -channels appear in those results. Setting it next to the list you are -staring at is the point. +**Wi-Fi Country** is on this page and on the Wi-Fi page, because it decides +which channels a scan returns and that is where you need it. Every field +below appears in both places and saving from either commits all of them, +so use whichever page you are already on. ### Counter reset schedule