Skip to content

Commit 0b63b6e

Browse files
author
Ravi Singh
committed
feat: robust pairing flow + motion v3 (Kalman) + debounced UI saves
Pairing flow (PR-A): - mesh.c broadcasts MSG_PAIR every 1s while pairing window open - Asymmetric pairing: receiving MSG_PAIR auto-opens own window — clicking Pair on either device pulls the other in (no need to tap both) - mesh_identify(mac) unicasts MSG_IDENTIFY; recipient blinks status LED at 10 Hz for 5 s so users can physically locate which board is which - Coordinator election now has 5 s hysteresis — prevents brief flap when a single heartbeat is dropped - mesh_event_cb_t: peer-joined / pairing-opened/closed / identify-requested events surface to main.c which drives the status LED reactions - POST /api/mesh/identify {mac} endpoint - GET /api/mesh now returns pairing/pairing_ms_left/my_mac for live UI countdown and self-vs-peer card highlighting Status LED: - New STATUS_LED_PAIRING (5 Hz) and STATUS_LED_IDENTIFY (10 Hz) patterns - status_led_oneshot(pattern, ms) — temporary pattern with auto-revert to last stable pattern; identify becomes one line in main.c Button (new component): - Polling-based long-press detector (50 Hz, 40 ms debounce) - 3 s long-press → mesh_open_pairing(); 10 s reserved for v6.1 factory reset Motion v3 (PR-B): - New motion_kalman.{c,h}: 1-D Kalman over [position, velocity], energy- aware observation noise (R *= 4 when energy < 30), ±200 cm/s velocity clamp, 3-sample direction hysteresis - motion.c integrates: motion.mode NVS string ("kalman" default | "pi"), motion.resp 0..100 (Calm⇄Snappy), motion.la_ms 0..500, motion.outl 0/1/2 (Off / median-3 / median-7); legacy ps/vs/pf/pg/ig kept as advanced - motion_reload() called by /api/settings POST so changes apply live Frontend: - ScreenMesh: SVG circular pairing countdown, per-peer Identify button with "Blinking…" 5 s cooldown, self-card highlight - ScreenMotion: Algorithm picker (Kalman/Legacy PI), Response slider with context-aware hint text, Look-ahead-ms slider, Outlier segmented control, collapsible Advanced (5 PI knobs) - New atoms icons: link, plus, search Debounced saves (fixes ERR_CONNECTION_RESET): - useDebouncedSave hook: 300 ms tail; coalesces multiple slider changes into one POST /api/settings batch. Fixes the C3-single-core httpd saturation when sliders fired ~30 POST/s during drag (verified with 50× rapid POST stress test — all 200 OK) Verified: idf.py build green; ambisense.bin 1.17 MB / 1.4 MB partition (18% free); both C3s flashed; /api/mesh, /api/settings round-trip works.
1 parent d88bd66 commit 0b63b6e

20 files changed

Lines changed: 1056 additions & 153 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
idf_component_register(
2+
SRCS "button.c"
3+
INCLUDE_DIRS "include"
4+
REQUIRES driver log freertos esp_timer
5+
)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#include "button.h"
2+
3+
#include "driver/gpio.h"
4+
#include "esp_log.h"
5+
#include "esp_timer.h"
6+
#include "freertos/FreeRTOS.h"
7+
#include "freertos/task.h"
8+
9+
static const char *TAG = "button";
10+
11+
#define POLL_MS 20
12+
#define DEBOUNCE_MS 40
13+
#define LONG_MS 3000
14+
#define VERYLONG_MS 10000
15+
16+
static struct {
17+
uint8_t gpio;
18+
bool active_low;
19+
button_event_cb_t cb;
20+
} s_b;
21+
22+
static inline bool read_pressed(void) {
23+
int level = gpio_get_level(s_b.gpio);
24+
return s_b.active_low ? (level == 0) : (level != 0);
25+
}
26+
27+
static void button_task(void *arg) {
28+
(void)arg;
29+
bool pressed = false;
30+
uint64_t press_started_us = 0;
31+
int debounce_ticks = 0;
32+
bool fired_long = false;
33+
bool fired_verylong = false;
34+
35+
while (1) {
36+
bool now_pressed = read_pressed();
37+
38+
if (now_pressed != pressed) {
39+
/* Edge detected — but don't trust it until DEBOUNCE_MS of
40+
* matching reads have stacked up. Counter approach is simpler
41+
* (and has identical effect to) a 40-ms blocking sleep. */
42+
debounce_ticks += POLL_MS;
43+
if (debounce_ticks >= DEBOUNCE_MS) {
44+
pressed = now_pressed;
45+
debounce_ticks = 0;
46+
if (pressed) {
47+
press_started_us = (uint64_t)esp_timer_get_time();
48+
fired_long = false;
49+
fired_verylong = false;
50+
ESP_LOGD(TAG, "press start");
51+
} else {
52+
/* Released — fire short if no long fired. */
53+
uint64_t held_ms = ((uint64_t)esp_timer_get_time() - press_started_us) / 1000ULL;
54+
ESP_LOGI(TAG, "press end after %llu ms", (unsigned long long)held_ms);
55+
if (!fired_long && !fired_verylong && held_ms < LONG_MS && held_ms >= 50) {
56+
if (s_b.cb) s_b.cb(BUTTON_PRESS_SHORT);
57+
}
58+
}
59+
}
60+
} else {
61+
debounce_ticks = 0;
62+
/* While held, fire long/verylong on threshold crossing. */
63+
if (pressed && !fired_long) {
64+
uint64_t held_ms = ((uint64_t)esp_timer_get_time() - press_started_us) / 1000ULL;
65+
if (held_ms >= LONG_MS) {
66+
fired_long = true;
67+
ESP_LOGI(TAG, "long press @ %llu ms", (unsigned long long)held_ms);
68+
if (s_b.cb) s_b.cb(BUTTON_PRESS_LONG);
69+
}
70+
}
71+
if (pressed && fired_long && !fired_verylong) {
72+
uint64_t held_ms = ((uint64_t)esp_timer_get_time() - press_started_us) / 1000ULL;
73+
if (held_ms >= VERYLONG_MS) {
74+
fired_verylong = true;
75+
ESP_LOGI(TAG, "very-long press @ %llu ms", (unsigned long long)held_ms);
76+
if (s_b.cb) s_b.cb(BUTTON_PRESS_VERYLONG);
77+
}
78+
}
79+
}
80+
81+
vTaskDelay(pdMS_TO_TICKS(POLL_MS));
82+
}
83+
}
84+
85+
esp_err_t button_init(uint8_t gpio_num, bool active_low, button_event_cb_t cb) {
86+
s_b.gpio = gpio_num;
87+
s_b.active_low = active_low;
88+
s_b.cb = cb;
89+
90+
gpio_config_t cfg = {
91+
.pin_bit_mask = 1ULL << gpio_num,
92+
.mode = GPIO_MODE_INPUT,
93+
.pull_up_en = active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE,
94+
.pull_down_en = active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE,
95+
.intr_type = GPIO_INTR_DISABLE,
96+
};
97+
esp_err_t err = gpio_config(&cfg);
98+
if (err != ESP_OK) {
99+
ESP_LOGE(TAG, "gpio_config(%u) failed: 0x%x", gpio_num, err);
100+
return err;
101+
}
102+
103+
BaseType_t ok = xTaskCreate(button_task, "button", 2048, NULL, 4, NULL);
104+
if (ok != pdPASS) return ESP_ERR_NO_MEM;
105+
106+
ESP_LOGI(TAG, "button on GPIO %u (active_%s)", gpio_num, active_low ? "low" : "high");
107+
return ESP_OK;
108+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#pragma once
2+
3+
/*
4+
* AmbiSense v6 — physical button driver.
5+
*
6+
* Single-button polling driver designed for the C3 SuperMini's BOOT
7+
* button (GPIO 9, active-low, internal pull-up). Drives three callbacks:
8+
*
9+
* short — press < 1 s. Could be used for "toggle LEDs" later; unused
10+
* in v6.0.
11+
* long — press >= 3 s. Wired to mesh_open_pairing(): the standard
12+
* "physically pair this device" gesture.
13+
* verylong — press >= 10 s. Wired to factory reset (TODO; not in v6.0).
14+
*
15+
* Polled at 50 Hz from a tiny dedicated task. Polling beats GPIO ISRs for
16+
* mechanical buttons because the state machine inherently debounces — a
17+
* spurious 1 ms blip simply doesn't survive across two 20 ms samples.
18+
*/
19+
20+
#include <stdbool.h>
21+
#include <stdint.h>
22+
#include "esp_err.h"
23+
24+
#ifdef __cplusplus
25+
extern "C" {
26+
#endif
27+
28+
typedef enum {
29+
BUTTON_PRESS_SHORT = 1,
30+
BUTTON_PRESS_LONG = 2, /* >= 3 s */
31+
BUTTON_PRESS_VERYLONG = 3, /* >= 10 s */
32+
} button_event_t;
33+
34+
typedef void (*button_event_cb_t)(button_event_t evt);
35+
36+
/* Initialize and start the polling task. `gpio_num` is the button pin;
37+
* `active_low` true means the button reads 0 when pressed (default for
38+
* BOOT-style pull-up wiring). */
39+
esp_err_t button_init(uint8_t gpio_num, bool active_low, button_event_cb_t cb);
40+
41+
#ifdef __cplusplus
42+
}
43+
#endif

firmware/components/mesh/include/mesh.h

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,30 @@ typedef struct {
5252
esp_err_t mesh_init(void);
5353

5454
/* Open a 30 s pairing window during which new peers' broadcasts are
55-
* accepted. */
55+
* accepted. While the window is open this device also broadcasts a
56+
* MSG_PAIR beacon every second; any peer that receives it auto-opens
57+
* its own window (asymmetric pairing). */
5658
esp_err_t mesh_open_pairing(void);
5759

60+
/* Window introspection — used by the UI to render a live countdown. */
61+
bool mesh_in_pairing(void);
62+
uint32_t mesh_pairing_remaining_ms(void);
63+
64+
/* Send a unicast MSG_IDENTIFY to `mac`. The recipient blinks its status
65+
* LED at 10 Hz for ~5 s so the user can physically locate it. */
66+
esp_err_t mesh_identify(const uint8_t mac[6]);
67+
68+
/* Events the mesh layer emits to its consumer (main.c wires this so the
69+
* status LED can react). Currently: peer joined, identify-me received. */
70+
typedef enum {
71+
MESH_EVT_PEER_JOINED = 1,
72+
MESH_EVT_IDENTIFY_REQUESTED = 2,
73+
MESH_EVT_PAIRING_OPENED = 3,
74+
MESH_EVT_PAIRING_CLOSED = 4,
75+
} mesh_event_t;
76+
typedef void (*mesh_event_cb_t)(mesh_event_t evt, const uint8_t mac[6]);
77+
void mesh_set_event_cb(mesh_event_cb_t cb);
78+
5879
/* Snapshot of currently-known peers (live + stale). Returns count. */
5980
size_t mesh_peers_snapshot(mesh_peer_t *out, size_t max);
6081

0 commit comments

Comments
 (0)