diff --git a/README.md b/README.md
index ed30c24..b1be6c8 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ client source code.
| Path | Contents |
| --- | --- |
-| [`firmware/`](firmware/) | PlatformIO firmware: hardware drivers, print queue, protocol parser, BLE, TCP, Wi-Fi AP+STA, and embedded Web UI |
+| [`firmware/`](firmware/) | PlatformIO firmware: hardware drivers, print queue, protocol parser, BLE, TCP, STA Wi-Fi with temporary setup AP, and embedded Web UI |
| [`python/`](python/) | BLE test-page client and protocol tests |
| [`docs/`](docs/) | Hardware notes, architecture, and [print protocol](docs/protocol.md) |
| [`android/`](android/) · [`ios/`](ios/) · [`wxapp/`](wxapp/) | Client source code for other platforms |
@@ -49,9 +49,12 @@ python3 -m unittest discover -s python -p 'test_*.py'
Print data uses the project's compact `MP` frame protocol. The firmware accepts
the same protocol over BLE, raw TCP port `9100`, and HTTP `POST /api/print`.
-Wi-Fi runs as AP+STA. Connect to `Mini-Printer-XXXX` and open
-`http://192.168.4.1/` for the offline setup and print page. The page can join another Wi-Fi network while keeping
-the setup AP available, and can rasterize an image in the browser for printing.
+Wi-Fi normally runs as STA only. On first boot, after a saved-network startup
+failure, or after a triple click on the printer button, the temporary
+`Mini-Printer-XXXX` setup AP becomes available. Open `http://192.168.4.1/`
+(or use the captive portal) to select a network. The AP closes after successful
+provisioning. The embedded page can rasterize images in the browser and print
+through either the temporary setup AP or normal STA operation.
The TCP endpoint is not IPP or AirPrint. See [`docs/protocol.md`](docs/protocol.md)
for frame fields and status codes, and [`firmware/README.md`](firmware/README.md)
diff --git a/docs/hal.md b/docs/hal.md
index 564a019..83f8787 100644
--- a/docs/hal.md
+++ b/docs/hal.md
@@ -6,14 +6,13 @@ The pin map in `src/em_config.h` targets the original V4 board. Each file in
| Driver | Pins and responsibility |
| --- | --- |
| `em_adc` | GPIO 4 enables ADC, GPIO 34 reads the battery divider, GPIO 36 reads the NTC divider |
-| `em_button` | GPIO 5 input pull-up, debounce, single/double/long press events |
| `em_indicator` | GPIO 18 LED; the V4 buzzer pin is shared, so buzzer mode requires a different board pin |
| `em_motor` | GPIO 23/22/21/19 stepper coil phases |
| `em_paper` | GPIO 35 paper detector and change interrupt; low means paper present |
| `em_spi` | Print-head HSPI on GPIO 15 clock, GPIO 16 MISO, and GPIO 13 data; transfers one 48-byte raster line through the full-duplex SPI path. GPIO 12 is the latch and must not be selected as SPI MISO. |
| `em_printer` | GPIO 12 latch, six strobe pins, GPIO 17 head boost gate |
-`em_task` samples sensors and publishes status. `print_service` serializes all
+`em_task` samples sensors and publishes status. GPIO 5 is configured as an input pull-up by the external `kbutton.c` Arduino adapter; button debounce and gesture recognition live in that reusable library rather than the hardware layer. `print_service` serializes all
printing and feeding through one FreeRTOS task. Manual feed steps are issued by
that task; stopping a feed needs no cross-task timer callback. The motor driver
never changes the head boost gate.
diff --git a/firmware/README.md b/firmware/README.md
index 4ff376f..c7bd1ac 100644
--- a/firmware/README.md
+++ b/firmware/README.md
@@ -14,10 +14,9 @@ TCP :9100 ─────┼── MP protocol ── print service ── print
HTTP /api/print┘ │
FreeRTOS queue
-Wi-Fi AP+STA ── wifi_manager
- ├── mDNS
- ├── TCP :9100
- └── HTTP :80
+Wi-Fi STA ────── wifi_manager ── HTTP :80 / TCP :9100 / mDNS
+ │
+ └── temporary AP+STA setup mode
```
- `hal/` owns GPIO, SPI, motor, sensors, and print-head timing.
@@ -40,7 +39,7 @@ pio test -e native
## BLE
-The default `esp32dev` build enables BLE, Wi-Fi AP+STA, HTTP, and TCP together.
+The default `esp32dev` build enables BLE, Wi-Fi, HTTP, and TCP together. Normal operation uses STA only; AP+STA is enabled temporarily for provisioning and recovery.
It starts a BLE-only controller and releases unused Bluetooth Classic controller
memory before initializing the BLE stack.
@@ -60,10 +59,22 @@ characteristic returns/notifications status frames.
## Wi-Fi
-Wi-Fi runs in `WIFI_AP_STA` mode. The setup/recovery SoftAP remains available
-even after the printer joins another network.
+Normal operation uses `WIFI_STA` only. The setup SoftAP exists only while the
+printer is being provisioned or recovered.
-The AP SSID is:
+Startup behavior is:
+
+```text
+no saved network
+ -> setup AP
+
+saved network
+ -> STA connect for up to 30 seconds
+ -> success: online, STA only
+ -> failure: setup AP
+```
+
+The setup AP SSID is:
```text
Mini-Printer-XXXX
@@ -72,19 +83,35 @@ Mini-Printer-XXXX
where `XXXX` is derived from the ESP32 ID. Development builds use an open AP
unless `WIFI_AP_PASSWORD` is configured.
+A triple click on GPIO 5 requests setup mode at any time. If saved credentials
+exist, setup mode is a temporary five-minute recovery window. A first-boot
+device with no credentials remains in setup until configured. After a successful
+connection the HTTP response is sent first, then the AP closes after a short
+delay and the printer returns to STA-only operation.
+
+While setup mode is active:
+
+- captive DNS points hostnames at the setup AP
+- `/api/wifi/scan`, `/api/wifi/connect`, and `/api/wifi/forget` are enabled
+- the embedded Web UI can still print through `POST /api/print`
+- raw MP printing on TCP port `9100` remains available
+
+The AP is still temporary; successful provisioning or the setup timeout returns
+the printer to STA-only operation.
+
The printer stores STA credentials as one NVS record only after a successful
-connection. A failed save is reported to the browser. Saved credentials are
-restored on restart; credentials from earlier firmware releases are still read.
-Failed setup attempts are not persisted.
+connection. A failed setup attempt is not persisted. During normal operation,
+a transient STA disconnect relies on automatic reconnect and does **not** open
+the setup AP.
-The printer exposes:
+Normal network services are:
- HTTP on port `80`
- MP protocol over raw TCP on port `9100`
- mDNS as `mini-printer-xxxx.local`
-Connect to `Mini-Printer-XXXX` and open `http://192.168.4.1/` in a browser.
-The setup AP does not redirect other hostnames.
+During setup, connect to `Mini-Printer-XXXX`. Captive DNS redirects generic
+HTTP probes to the embedded page; `http://192.168.4.1/` is also available.
The TCP service is not IPP, AirPrint, JetDirect, or ESC/POS. It carries the
project's MP framing protocol.
@@ -127,5 +154,6 @@ The current pin map targets the original V4 board configuration. Print-head
timing and thermistor calibration are intentionally kept in the hardware layer
so they can be validated and tuned independently of protocol/network changes.
-The GPIO 5 button prints a built-in diagnostic page on a double click. Holding
-it for about one second feeds paper, and releasing it stops the feed.
+GPIO 5 uses the shared `kbutton.c` gesture state machine. A double click prints
+a built-in diagnostic page, a triple click enters Wi-Fi setup mode, and holding
+the button for about one second feeds paper until release.
diff --git a/firmware/platformio.ini b/firmware/platformio.ini
index 9604802..1861a8b 100644
--- a/firmware/platformio.ini
+++ b/firmware/platformio.ini
@@ -8,6 +8,8 @@ framework = arduino
monitor_speed = 115200
build_flags = -D BLE_ENABLED=1
build_src_filter = +<*>
+lib_deps =
+ https://github.com/lsongdev/kbutton.c.git#f0665ecca96674aa7ee033407734f0f76b386a40
; BLE + Wi-Fi no longer fits the 1.25 MiB default app slot.
; min_spiffs keeps OTA support while giving each app partition more room.
@@ -20,4 +22,3 @@ test_build_src = yes
build_flags = -Itest/mocks
build_src_filter =
+
- +
diff --git a/firmware/src/em_config.h b/firmware/src/em_config.h
index 26f3602..4907092 100644
--- a/firmware/src/em_config.h
+++ b/firmware/src/em_config.h
@@ -1,74 +1,77 @@
-#ifndef _EM_CONFIG_H_
-#define _EM_CONFIG_H_
-
-#include
+#ifndef _EM_CONFIG_H_
+#define _EM_CONFIG_H_
+
+#include
#ifndef BLE_ENABLED
#define BLE_ENABLED 1
#endif
-
-#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
-#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
-#define BLE_NAME "Mini-Printer"
-
-// Wi-Fi runs in AP+STA mode. The AP is always available as a setup and
-// recovery path while STA credentials are stored in NVS after a successful
-// connection.
-#define WIFI_ENABLED 1
-#define WIFI_AP_SSID "Mini-Printer"
-#define WIFI_AP_PASSWORD ""
-#define WIFI_HTTP_PORT 80
-#define WIFI_PRINT_PORT 9100
-#define WIFI_CLIENT_IDLE_TIMEOUT_MS 30000
-#define WIFI_CONNECT_TIMEOUT_MS 15000
-#define WIFI_PREFS_NAMESPACE "wifi"
-
-#define DEFAULT_PRINT_DENSITY 64
-#define PRINT_QUEUE_DEPTH 16
-#define PRINT_TRAILING_FEED_STEPS 230
-
-// 按键引脚
-// 34 35 36 39 仅可以作为输入 INPUT, 不支持 INPUT_PULLUP
-#define PIN_KEY 5
-#define PIN_PAPER 35
-#define PIN_BATTERY_ADC 34
-#define PIN_ADC_EN 4
-#define BATTERY_ADC_BIT 12
-#define PIN_TEMPERATRUE 36
-#define PIN_BEEP 18
-#define PIN_LED 18
-
-#define PIN_MOTOR_AP 23
-#define PIN_MOTOR_AM 22
-#define PIN_MOTOR_BP 21
-#define PIN_MOTOR_BM 19
-
+
+#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
+#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
+#define BLE_NAME "Mini-Printer"
+
+// Wi-Fi normally runs as STA only. The setup AP is enabled only when there
+// are no credentials, the saved network is unavailable at boot, or the user
+// explicitly enters setup mode.
+#define WIFI_ENABLED 1
+#define WIFI_AP_SSID "Mini-Printer"
+#define WIFI_AP_PASSWORD ""
+#define WIFI_HTTP_PORT 80
+#define WIFI_PRINT_PORT 9100
+#define WIFI_CLIENT_IDLE_TIMEOUT_MS 30000
+#define WIFI_CONNECT_TIMEOUT_MS 15000
+#define WIFI_STARTUP_CONNECT_TIMEOUT_MS 30000
+#define WIFI_SETUP_TIMEOUT_MS 300000
+#define WIFI_SETUP_CLOSE_DELAY_MS 1500
+#define WIFI_PREFS_NAMESPACE "wifi"
+
+#define DEFAULT_PRINT_DENSITY 64
+#define PRINT_QUEUE_DEPTH 16
+#define PRINT_TRAILING_FEED_STEPS 230
+
+// 按键引脚
+// 34 35 36 39 仅可以作为输入 INPUT, 不支持 INPUT_PULLUP
+#define PIN_KEY 5
+#define PIN_PAPER 35
+#define PIN_BATTERY_ADC 34
+#define PIN_ADC_EN 4
+#define BATTERY_ADC_BIT 12
+#define PIN_TEMPERATRUE 36
+#define PIN_BEEP 18
+#define PIN_LED 18
+
+#define PIN_MOTOR_AP 23
+#define PIN_MOTOR_AM 22
+#define PIN_MOTOR_BP 21
+#define PIN_MOTOR_BM 19
+
#define PIN_LAT 12
#define PIN_SCK 15
#define PIN_SPI_MISO 16
#define PIN_SDA 13
-
-// 原厂 V4
-#define PIN_STB1 26
-#define PIN_STB2 27
-#define PIN_STB3 14
-#define PIN_STB4 32
-#define PIN_STB5 33
-#define PIN_STB6 25
-
-// 打印头电源升压控制引脚
-#define PIN_VHEN 17
-
+
+// 原厂 V4
+#define PIN_STB1 26
+#define PIN_STB2 27
+#define PIN_STB3 14
+#define PIN_STB4 32
+#define PIN_STB5 33
+#define PIN_STB6 25
+
+// 打印头电源升压控制引脚
+#define PIN_VHEN 17
+
#define PRINT_TIME 2600
#define PRINT_END_TIME 200
#define PRINTER_BOOST_SETTLE_MS 50
#define MOTOR_WATI_TIME 4000
-#define LAT_TIME 1
-
-#define PRINTER_MAX_TEMPERATURE_C 65
-#define PRINTER_TEMPERATURE_CHECK_LINES 16
-#define PRINTER_STALL_TIMEOUT_MS 20000
-
-#define us_delay(us) delayMicroseconds(us)
-
-#endif
+#define LAT_TIME 1
+
+#define PRINTER_MAX_TEMPERATURE_C 65
+#define PRINTER_TEMPERATURE_CHECK_LINES 16
+#define PRINTER_STALL_TIMEOUT_MS 20000
+
+#define us_delay(us) delayMicroseconds(us)
+
+#endif
diff --git a/firmware/src/em_task.cpp b/firmware/src/em_task.cpp
index c053367..dde4d1c 100644
--- a/firmware/src/em_task.cpp
+++ b/firmware/src/em_task.cpp
@@ -1,213 +1,259 @@
-#include "em_task.h"
-
-#include "core/print_service.h"
-#include "core/test_page.h"
+#include "em_task.h"
+
+#include "core/print_service.h"
+#include "core/test_page.h"
#include "em_config.h"
-#include "em_device.h"
-#include "hal/em_adc.h"
-#include "hal/em_button.h"
-#include "hal/em_indicator.h"
-#include "hal/em_paper.h"
-#include "hal/em_printer.h"
-#include "network/web_server.h"
-#include "network/wifi_manager.h"
+#include "em_device.h"
+#include "hal/em_adc.h"
+#include "hal/em_indicator.h"
+#include "hal/em_paper.h"
+#include "hal/em_printer.h"
+#include "network/web_server.h"
+#include "network/wifi_manager.h"
#if BLE_ENABLED
-#include "transport/ble_transport.h"
+#include "transport/ble_transport.h"
#endif
-#include "transport/tcp_transport.h"
-
-#include
-#include
-
-Button_t Button;
-static std::atomic test_page_task_running{false};
-
-void read_paper()
-{
- set_device_paper_status(paper_present() ? PAPER_STATUS_NORMAL : PAPER_STATUS_LACK);
-}
-
-void read_temperature()
-{
- float celsius = 0.0f;
- if (!adc_read_temperature_c(&celsius))
- {
- set_device_temperature(255);
- return;
- }
- const int rounded = static_cast(lroundf(celsius));
- set_device_temperature(static_cast(rounded < 0 ? 0 : rounded > 254 ? 254 : rounded));
-}
-
-void read_all_sensors()
-{
- int adc_mv = 0;
- if (adc_read_battery_mv(&adc_mv))
- {
- const int battery_mv = adc_mv * 2;
- const int percent = (battery_mv - 3300) * 100 / 900;
- set_device_battery(static_cast(percent < 0 ? 0 : percent > 100 ? 100 : percent));
- }
- else
- set_device_battery(0);
- read_temperature();
- read_paper();
-}
-
-void test_page_task(void *)
-{
- if (!print_test_page())
- {
- Serial.println("Test page could not be printed");
- indicator_beep(INDICATOR_WARN);
- indicator_led(INDICATOR_WARN);
- }
-
- test_page_task_running.store(false);
- vTaskDelete(nullptr);
-}
-
-void Btn_Double_CallBack(void *)
-{
- if (test_page_task_running.exchange(true))
- return;
-
- if (xTaskCreate(test_page_task, "TestPage", 4096, nullptr, 1, nullptr) != pdPASS)
- {
- test_page_task_running.store(false);
- indicator_beep(INDICATOR_WARN);
- indicator_led(INDICATOR_WARN);
- }
-}
-
-void Btn_Long_CallBack(void *)
-{
- if (!print_service_feed_start(100))
- {
- indicator_beep(INDICATOR_WARN);
- indicator_led(INDICATOR_WARN);
- }
-}
-
-void Btn_Long_Release_CallBack(void *)
-{
- print_service_feed_stop(100);
-}
-
-void init_key()
-{
- Button_Create(&Button, PIN_KEY, LOW);
-
- Button_Attach(&Button, BUTTON_DOUBLE, Btn_Double_CallBack);
- Button_Attach(&Button, BUTTON_LONG, Btn_Long_CallBack);
- Button_Attach(&Button, BUTTON_LONG_RELEASE, Btn_Long_Release_CallBack);
-}
-
-void task_report(void *)
-{
- TickType_t last_sensor = xTaskGetTickCount();
+#include "transport/tcp_transport.h"
+
+#include
+#include
+#include
+
+namespace {
+KButton button(PIN_KEY);
+std::atomic test_page_task_running{false};
+
+void read_paper()
+{
+ set_device_paper_status(
+ paper_present() ? PAPER_STATUS_NORMAL : PAPER_STATUS_LACK);
+}
+
+void read_temperature()
+{
+ float celsius = 0.0f;
+ if (!adc_read_temperature_c(&celsius))
+ {
+ set_device_temperature(255);
+ return;
+ }
+
+ const int rounded = static_cast(lroundf(celsius));
+ set_device_temperature(static_cast(
+ rounded < 0 ? 0 : rounded > 254 ? 254 : rounded));
+}
+
+void read_all_sensors()
+{
+ int adc_mv = 0;
+
+ if (adc_read_battery_mv(&adc_mv))
+ {
+ const int battery_mv = adc_mv * 2;
+ const int percent = (battery_mv - 3300) * 100 / 900;
+ set_device_battery(static_cast(
+ percent < 0 ? 0 : percent > 100 ? 100 : percent));
+ }
+ else
+ {
+ set_device_battery(0);
+ }
+
+ read_temperature();
+ read_paper();
+}
+
+void test_page_task(void *)
+{
+ if (!print_test_page())
+ {
+ Serial.println("Test page could not be printed");
+ indicator_beep(INDICATOR_WARN);
+ indicator_led(INDICATOR_WARN);
+ }
+
+ test_page_task_running.store(false);
+ vTaskDelete(nullptr);
+}
+
+void start_test_page()
+{
+ if (test_page_task_running.exchange(true))
+ return;
+
+ if (xTaskCreate(
+ test_page_task,
+ "TestPage",
+ 4096,
+ nullptr,
+ 1,
+ nullptr) != pdPASS)
+ {
+ test_page_task_running.store(false);
+ indicator_beep(INDICATOR_WARN);
+ indicator_led(INDICATOR_WARN);
+ }
+}
+
+void button_click(uint8_t count)
+{
+ if (count == 2)
+ {
+ start_test_page();
+ return;
+ }
+
+ if (count == 3)
+ {
+ if (!wifi_manager_start_setup())
+ {
+ indicator_beep(INDICATOR_WARN);
+ indicator_led(INDICATOR_WARN);
+ return;
+ }
+
+ Serial.println("Wi-Fi setup requested by button");
+ indicator_beep(INDICATOR_CONNECT);
+ indicator_led(INDICATOR_CONNECT);
+ }
+}
+
+void button_long_press()
+{
+ if (!print_service_feed_start(100))
+ {
+ indicator_beep(INDICATOR_WARN);
+ indicator_led(INDICATOR_WARN);
+ }
+}
+
+void button_long_release()
+{
+ print_service_feed_stop(100);
+}
+
+void init_key()
+{
+ // Preserve the original printer button timings.
+ button.setDebounceMs(40);
+ button.setClickMs(300);
+ button.setLongPressMs(1000);
+
+ button.onClicks(button_click);
+ button.onLongPress(button_long_press);
+ button.onLongRelease(button_long_release);
+ button.begin();
+}
+
+void task_report(void *)
+{
+ TickType_t last_sensor = xTaskGetTickCount();
#if BLE_ENABLED
- TickType_t last_status = 0;
+ TickType_t last_status = 0;
#endif
-
- for (;;)
- {
- const TickType_t now = xTaskGetTickCount();
-
- if (now - last_sensor >= pdMS_TO_TICKS(10000))
- {
- const device_state_t state = get_device_state();
-
- // The printer task owns ADC temperature sampling while printing.
- if (state.printer_state != PRINTER_STATUS_PRINTING)
- read_all_sensors();
- else
- read_paper();
-
- last_sensor = now;
- }
-
- if (paper_take_change())
- {
- read_paper();
- const device_state_t state = get_device_state();
-
- if (state.paper_state == PAPER_STATUS_LACK &&
- state.printer_state == PRINTER_STATUS_FEEDING)
- {
- print_service_feed_stop(0);
- }
-
+
+ for (;;)
+ {
+ const TickType_t now = xTaskGetTickCount();
+
+ wifi_manager_process();
+
+ if (now - last_sensor >= pdMS_TO_TICKS(10000))
+ {
+ const device_state_t state = get_device_state();
+
+ // The printer task owns ADC temperature sampling while printing.
+ if (state.printer_state != PRINTER_STATUS_PRINTING)
+ read_all_sensors();
+ else
+ read_paper();
+
+ last_sensor = now;
+ }
+
+ if (paper_take_change())
+ {
+ read_paper();
+ const device_state_t state = get_device_state();
+
+ if (state.paper_state == PAPER_STATUS_LACK &&
+ state.printer_state == PRINTER_STATUS_FEEDING)
+ {
+ print_service_feed_stop(0);
+ }
+
#if BLE_ENABLED
- ble_transport_report_status();
+ ble_transport_report_status();
#endif
- }
-
+ }
+
#if BLE_ENABLED
- if (now - last_status >= pdMS_TO_TICKS(1000))
- {
- ble_transport_report_status();
- last_status = now;
- }
+ if (now - last_status >= pdMS_TO_TICKS(1000))
+ {
+ ble_transport_report_status();
+ last_status = now;
+ }
#endif
-
- vTaskDelay(pdMS_TO_TICKS(50));
- }
-}
-
-void task_button(void *)
-{
- for (;;)
- {
- Button_Process();
- vTaskDelay(pdMS_TO_TICKS(20));
- }
-}
-
-void init_task()
-{
- Serial.begin(115200);
- Serial.println("mini-printer firmware");
-
- init_device_state();
- paper_init();
- indicator_init();
- adc_init();
- read_all_sensors();
- printer_init();
-
- if (!print_service_init())
- {
- Serial.println("failed to initialize print service");
- return;
- }
-
- init_key();
+
+ vTaskDelay(pdMS_TO_TICKS(50));
+ }
+}
+
+void task_button(void *)
+{
+ for (;;)
+ {
+ button.tick();
+ vTaskDelay(pdMS_TO_TICKS(20));
+ }
+}
+} // namespace
+
+void init_task()
+{
+ Serial.begin(115200);
+ Serial.println("mini-printer firmware");
+
+ init_device_state();
+ paper_init();
+ indicator_init();
+ adc_init();
+ read_all_sensors();
+ printer_init();
+
+ if (!print_service_init())
+ {
+ Serial.println("failed to initialize print service");
+ return;
+ }
+
+ init_key();
+
#if BLE_ENABLED
- init_ble_transport();
+ init_ble_transport();
#else
Serial.println("BLE disabled at build time");
#endif
-
- if (wifi_manager_init())
- {
- init_tcp_transport();
- init_web_server();
- }
-
- xTaskCreate(
- task_report,
- "Report",
- 4096,
- nullptr,
- 1,
- nullptr);
-
- xTaskCreate(
- task_button,
- "Button",
- 4096,
- nullptr,
- 1,
- nullptr);
-}
+
+ if (wifi_manager_init())
+ {
+ init_tcp_transport();
+ init_web_server();
+ }
+
+ xTaskCreate(
+ task_report,
+ "Report",
+ 4096,
+ nullptr,
+ 1,
+ nullptr);
+
+ xTaskCreate(
+ task_button,
+ "Button",
+ 4096,
+ nullptr,
+ 1,
+ nullptr);
+}
diff --git a/firmware/src/hal/em_button.cpp b/firmware/src/hal/em_button.cpp
deleted file mode 100644
index 4661c27..0000000
--- a/firmware/src/hal/em_button.cpp
+++ /dev/null
@@ -1,171 +0,0 @@
-#include "em_button.h"
-#include "em_config.h"
-
-#include
-
-namespace {
-constexpr uint8_t DEBOUNCE_TICKS = 2; // 40 ms
-constexpr uint8_t DOUBLE_TICKS = 15; // 300 ms after the first release
-constexpr uint8_t LONG_TICKS = 50; // 1 s
-
-enum Phase : uint8_t {
- IDLE,
- FIRST_PRESS,
- WAIT_SECOND_PRESS,
- SECOND_PRESS,
- LONG_PRESS,
-};
-
-Button_t *head = nullptr;
-
-void emit(Button_t *button, Button_Event event)
-{
- button->last_event = event;
- if (button->callbacks[event] != nullptr)
- button->callbacks[event](button);
-}
-
-bool stable_level_changed(Button_t *button)
-{
- const uint8_t level = digitalRead(button->pin);
- if (level == button->stable_level)
- {
- button->debounce_ticks = 0;
- return false;
- }
-
- if (level != button->candidate_level)
- {
- button->candidate_level = level;
- button->debounce_ticks = 1;
- }
- else if (++button->debounce_ticks >= DEBOUNCE_TICKS)
- {
- button->stable_level = level;
- button->debounce_ticks = 0;
- return true;
- }
- return false;
-}
-
-void begin_long_press(Button_t *button)
-{
- // A second press that becomes a long press is not a double click.
- if (button->phase == SECOND_PRESS)
- emit(button, BUTTON_SINGLE);
- button->phase = LONG_PRESS;
- emit(button, BUTTON_LONG);
-}
-} // namespace
-
-void Button_Create(Button_t *button, uint8_t pin, uint8_t active_level)
-{
- if (button == nullptr)
- return;
-
- Button_Delete(button);
- memset(button, 0, sizeof(*button));
- button->pin = pin;
- button->active_level = active_level;
- pinMode(pin, active_level == LOW ? INPUT_PULLUP : INPUT_PULLDOWN);
- button->stable_level = digitalRead(pin);
- button->candidate_level = button->stable_level;
- button->phase = IDLE;
- button->last_event = BUTTON_EVENT_COUNT;
- button->next = head;
- head = button;
-}
-
-void Button_Attach(Button_t *button, Button_Event event, Button_CallBack callback)
-{
- if (button != nullptr && event < BUTTON_EVENT_COUNT)
- button->callbacks[event] = callback;
-}
-
-void Button_Delete(Button_t *button)
-{
- for (Button_t **current = &head; *current != nullptr; current = &(*current)->next)
- {
- if (*current == button)
- {
- *current = button->next;
- button->next = nullptr;
- return;
- }
- }
-}
-
-void Button_Cycle_Process(Button_t *button)
-{
- if (button == nullptr)
- return;
-
- const bool changed = stable_level_changed(button);
- const bool pressed = button->stable_level == button->active_level;
-
- if (changed)
- {
- if (pressed)
- {
- if (button->phase == WAIT_SECOND_PRESS)
- {
- button->phase = SECOND_PRESS;
- button->hold_ticks = 0;
- }
- else if (button->phase == IDLE)
- {
- button->phase = FIRST_PRESS;
- button->hold_ticks = 0;
- }
- }
- else if (button->phase == FIRST_PRESS)
- {
- button->phase = WAIT_SECOND_PRESS;
- button->double_ticks = 0;
- }
- else if (button->phase == SECOND_PRESS)
- {
- button->phase = IDLE;
- emit(button, BUTTON_DOUBLE);
- }
- else if (button->phase == LONG_PRESS)
- {
- button->phase = IDLE;
- emit(button, BUTTON_LONG_RELEASE);
- }
- return;
- }
-
- // Do not count time while an opposite level is being debounced.
- if (button->debounce_ticks != 0)
- return;
-
- if (pressed && (button->phase == FIRST_PRESS || button->phase == SECOND_PRESS))
- {
- if (++button->hold_ticks >= LONG_TICKS)
- begin_long_press(button);
- }
- else if (!pressed && button->phase == WAIT_SECOND_PRESS)
- {
- if (++button->double_ticks >= DOUBLE_TICKS)
- {
- button->phase = IDLE;
- emit(button, BUTTON_SINGLE);
- }
- }
-}
-
-void Button_Process()
-{
- for (Button_t *button = head; button != nullptr; )
- {
- Button_t *next = button->next;
- Button_Cycle_Process(button);
- button = next;
- }
-}
-
-Button_Event Get_Button_Event(const Button_t *button)
-{
- return button == nullptr ? BUTTON_EVENT_COUNT : button->last_event;
-}
diff --git a/firmware/src/hal/em_button.h b/firmware/src/hal/em_button.h
deleted file mode 100644
index 99c0fdd..0000000
--- a/firmware/src/hal/em_button.h
+++ /dev/null
@@ -1,38 +0,0 @@
-#ifndef EM_BUTTON_H
-#define EM_BUTTON_H
-
-#include
-
-// Button_Process is called every 20 ms by the button task.
-enum Button_Event : uint8_t {
- BUTTON_SINGLE = 0,
- BUTTON_DOUBLE,
- BUTTON_LONG,
- BUTTON_LONG_RELEASE,
- BUTTON_EVENT_COUNT,
-};
-
-typedef void (*Button_CallBack)(void *button);
-
-struct Button_t {
- uint8_t pin;
- uint8_t active_level;
- uint8_t stable_level;
- uint8_t candidate_level;
- uint8_t debounce_ticks;
- uint8_t phase;
- uint8_t hold_ticks;
- uint8_t double_ticks;
- Button_Event last_event;
- Button_CallBack callbacks[BUTTON_EVENT_COUNT];
- Button_t *next;
-};
-
-void Button_Create(Button_t *button, uint8_t pin, uint8_t active_level);
-void Button_Attach(Button_t *button, Button_Event event, Button_CallBack callback);
-void Button_Delete(Button_t *button);
-void Button_Cycle_Process(Button_t *button);
-void Button_Process();
-Button_Event Get_Button_Event(const Button_t *button);
-
-#endif
diff --git a/firmware/src/network/web_server.cpp b/firmware/src/network/web_server.cpp
index 7708925..692d1d4 100644
--- a/firmware/src/network/web_server.cpp
+++ b/firmware/src/network/web_server.cpp
@@ -82,6 +82,7 @@ small{display:block;margin-top:12px;color:#5b6570}