From a2a48f47a3941ded2c0ea8bb8c6409d8d01de89f Mon Sep 17 00:00:00 2001 From: jess Date: Fri, 3 Apr 2026 02:25:26 -0700 Subject: [PATCH 1/5] remove arbitrary MAX_UDP_CLIENTS cap from wifi transport --- main/wifi_transport.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/main/wifi_transport.c b/main/wifi_transport.c index b5b6caa..ede3efa 100644 --- a/main/wifi_transport.c +++ b/main/wifi_transport.c @@ -17,7 +17,7 @@ #define UDP_PORT 5941 #define UDP_BUF_SIZE 128 -#define MAX_UDP_CLIENTS 4 +#define UDP_CLIENTS_MAX 16 #define CLIENT_TIMEOUT_MS 30000 static int udp_sock = -1; @@ -26,7 +26,7 @@ static struct { struct sockaddr_in addr; TickType_t last_seen; bool active; -} clients[MAX_UDP_CLIENTS]; +} clients[UDP_CLIENTS_MAX]; static int client_count; @@ -42,12 +42,12 @@ static void client_touch(const struct sockaddr_in *addr) } } - if (client_count < MAX_UDP_CLIENTS) { + if (client_count < UDP_CLIENTS_MAX) { clients[client_count].addr = *addr; clients[client_count].last_seen = now; clients[client_count].active = true; client_count++; - printf("UDP: client added (%d/%d)\n", client_count, MAX_UDP_CLIENTS); + printf("UDP: client added (%d)\n", client_count); } } @@ -59,7 +59,7 @@ static void clients_expire(void) for (int i = 0; i < client_count; ) { if ((now - clients[i].last_seen) > timeout) { clients[i] = clients[--client_count]; - printf("UDP: client expired (%d/%d)\n", client_count, MAX_UDP_CLIENTS); + printf("UDP: client expired (%d)\n", client_count); } else { i++; } From 292a1a2e871fec00edf322e1d0d684cf55c7a4b0 Mon Sep 17 00:00:00 2001 From: jess Date: Fri, 3 Apr 2026 02:25:49 -0700 Subject: [PATCH 2/5] add RSP_KEEPALIVE protocol message type --- main/protocol.c | 8 ++++++++ main/protocol.h | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/main/protocol.c b/main/protocol.c index 08a9c47..c81d8b1 100644 --- a/main/protocol.c +++ b/main/protocol.c @@ -144,6 +144,14 @@ static int send_sysex(const uint8_t *sysex, uint16_t len) return wifi_send_sysex(sysex, len); } +/* ---- outbound: keepalive ---- */ + +int send_keepalive(void) +{ + uint8_t sx[] = { 0xF0, 0x7D, RSP_KEEPALIVE, 0xF7 }; + return send_sysex(sx, sizeof(sx)); +} + /* ---- outbound: EIS ---- */ int send_sweep_start(uint32_t num_points, float freq_start, float freq_stop) diff --git a/main/protocol.h b/main/protocol.h index 54b756b..dfab429 100644 --- a/main/protocol.h +++ b/main/protocol.h @@ -56,6 +56,7 @@ #define RSP_REF_LP_RANGE 0x21 #define RSP_REFS_DONE 0x22 #define RSP_REF_STATUS 0x23 +#define RSP_KEEPALIVE 0x50 /* Session sync responses (0x4x) */ #define RSP_SESSION_CREATED 0x40 @@ -138,6 +139,9 @@ int send_ref_lp_range(uint8_t mode, uint8_t low_idx, uint8_t high_idx); int send_refs_done(void); int send_ref_status(uint8_t has_refs); +/* keepalive (sent during long blocking ops) */ +int send_keepalive(void); + /* session management */ const Session *session_get_all(uint8_t *count); uint8_t session_get_current(void); From c1721dfd1fdbf52d411c882cbae9b69ae2ecfd42 Mon Sep 17 00:00:00 2001 From: jess Date: Fri, 3 Apr 2026 02:26:35 -0700 Subject: [PATCH 3/5] send keepalives during blocking waits (clean, pH, chlorine settling) --- main/echem.c | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/main/echem.c b/main/echem.c index cb20334..964029f 100644 --- a/main/echem.c +++ b/main/echem.c @@ -327,7 +327,14 @@ int echem_clean(float v_mv, float duration_s) AD5940_LPDAC0WriteS(code, VZERO_CODE); printf("Clean: %.0f mV for %.0f s\n", v_mv, duration_s); - vTaskDelay(pdMS_TO_TICKS((uint32_t)(duration_s * 1000.0f))); + + uint32_t remain_ms = (uint32_t)(duration_s * 1000.0f); + while (remain_ms > 0) { + uint32_t chunk = remain_ms > 3000 ? 3000 : remain_ms; + vTaskDelay(pdMS_TO_TICKS(chunk)); + remain_ms -= chunk; + if (remain_ms > 0) send_keepalive(); + } echem_shutdown_lp(); AD5940_AFECtrlS(AFECTRL_ALL, bFALSE); @@ -497,7 +504,15 @@ static uint32_t sample_phase(float v_mv, float t_dep_ms, float t_meas_ms, AD5940_LPDAC0WriteS(mv_to_vbias_code(v_mv), VZERO_CODE); /* settling — no samples recorded */ - vTaskDelay(pdMS_TO_TICKS((uint32_t)t_dep_ms)); + { + uint32_t remain_ms = (uint32_t)t_dep_ms; + while (remain_ms > 0) { + uint32_t chunk = remain_ms > 3000 ? 3000 : remain_ms; + vTaskDelay(pdMS_TO_TICKS(chunk)); + remain_ms -= chunk; + if (remain_ms > 0) send_keepalive(); + } + } /* measurement — sample at ~50ms intervals */ uint32_t n_samples = (uint32_t)(t_meas_ms / 50.0f + 0.5f); @@ -541,7 +556,15 @@ int echem_chlorine(const ClConfig *cfg, ClPoint *out, uint32_t max_points, printf("Cl: conditioning at %.0f mV for %.0f ms\n", cfg->v_cond, cfg->t_cond_ms); AD5940_LPDAC0WriteS(mv_to_vbias_code(cfg->v_cond), VZERO_CODE); - vTaskDelay(pdMS_TO_TICKS((uint32_t)cfg->t_cond_ms)); + { + uint32_t remain_ms = (uint32_t)cfg->t_cond_ms; + while (remain_ms > 0) { + uint32_t chunk = remain_ms > 3000 ? 3000 : remain_ms; + vTaskDelay(pdMS_TO_TICKS(chunk)); + remain_ms -= chunk; + if (remain_ms > 0) send_keepalive(); + } + } printf("Cl: free chlorine at %.0f mV\n", cfg->v_free); idx = sample_phase(cfg->v_free, cfg->t_dep_ms, cfg->t_meas_ms, @@ -579,7 +602,14 @@ int echem_ph_ocp(const PhConfig *cfg, PhResult *result) AD5940_ADCBaseCfgS(&adc); printf("pH: stabilizing %0.f s\n", cfg->stabilize_s); - vTaskDelay(pdMS_TO_TICKS((uint32_t)(cfg->stabilize_s * 1000.0f))); + + uint32_t remain_ms = (uint32_t)(cfg->stabilize_s * 1000.0f); + while (remain_ms > 0) { + uint32_t chunk = remain_ms > 3000 ? 3000 : remain_ms; + vTaskDelay(pdMS_TO_TICKS(chunk)); + remain_ms -= chunk; + if (remain_ms > 0) send_keepalive(); + } /* average N readings of V(SE0) and V(RE0) */ #define PH_AVG_N 10 From d409f3569ea6c10041d6668cd45f78759116fb54 Mon Sep 17 00:00:00 2001 From: jess Date: Fri, 3 Apr 2026 02:27:09 -0700 Subject: [PATCH 4/5] desktop: parse and discard RSP_KEEPALIVE messages --- cue/src/app.rs | 1 + cue/src/protocol.rs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/cue/src/app.rs b/cue/src/app.rs index 4a5ffba..c58e6bc 100644 --- a/cue/src/app.rs +++ b/cue/src/app.rs @@ -734,6 +734,7 @@ impl App { self.cal_cell_constant = Some(k); self.status = format!("Device cell constant: {:.4} cm-1", k); } + EisMessage::Keepalive => {} }, Message::TabSelected(t) => { if t == Tab::Browse { diff --git a/cue/src/protocol.rs b/cue/src/protocol.rs index 9ab45d6..c819b9a 100644 --- a/cue/src/protocol.rs +++ b/cue/src/protocol.rs @@ -26,6 +26,7 @@ pub const RSP_REF_LP_RANGE: u8 = 0x21; pub const RSP_REFS_DONE: u8 = 0x22; pub const RSP_CELL_K: u8 = 0x11; pub const RSP_REF_STATUS: u8 = 0x23; +pub const RSP_KEEPALIVE: u8 = 0x50; /* Cue → ESP32 */ pub const CMD_SET_SWEEP: u8 = 0x10; @@ -258,6 +259,7 @@ pub enum EisMessage { RefsDone, RefStatus { has_refs: bool }, CellK(f32), + Keepalive, } fn decode_u16(data: &[u8]) -> u16 { @@ -413,6 +415,7 @@ pub fn parse_sysex(data: &[u8]) -> Option { let p = &data[2..]; Some(EisMessage::CellK(decode_float(&p[0..5]))) } + RSP_KEEPALIVE => Some(EisMessage::Keepalive), _ => None, } } From 618e9ed4c8c73d68cf322bbd35c9c945a76974c5 Mon Sep 17 00:00:00 2001 From: jess Date: Fri, 3 Apr 2026 02:27:43 -0700 Subject: [PATCH 5/5] iOS: parse and discard RSP_KEEPALIVE messages --- cue-ios/CueIOS/AppState.swift | 3 +++ cue-ios/CueIOS/Models/Protocol.swift | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/cue-ios/CueIOS/AppState.swift b/cue-ios/CueIOS/AppState.swift index e3243e2..f431e73 100644 --- a/cue-ios/CueIOS/AppState.swift +++ b/cue-ios/CueIOS/AppState.swift @@ -251,6 +251,9 @@ final class AppState { case .cellK(let k): calCellConstant = Double(k) status = String(format: "Device cell constant: %.4f cm\u{207B}\u{00B9}", k) + + case .keepalive: + break } } diff --git a/cue-ios/CueIOS/Models/Protocol.swift b/cue-ios/CueIOS/Models/Protocol.swift index a0702f3..47bb394 100644 --- a/cue-ios/CueIOS/Models/Protocol.swift +++ b/cue-ios/CueIOS/Models/Protocol.swift @@ -29,6 +29,7 @@ let RSP_REF_FRAME: UInt8 = 0x20 let RSP_REF_LP_RANGE: UInt8 = 0x21 let RSP_REFS_DONE: UInt8 = 0x22 let RSP_REF_STATUS: UInt8 = 0x23 +let RSP_KEEPALIVE: UInt8 = 0x50 // Cue -> ESP32 let CMD_SET_SWEEP: UInt8 = 0x10 @@ -123,6 +124,7 @@ enum EisMessage { case refsDone case refStatus(hasRefs: Bool) case cellK(Float) + case keepalive } // MARK: - Response parser @@ -254,6 +256,9 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? { case RSP_CELL_K where p.count >= 5: return .cellK(decodeFloat(p, at: 0)) + case RSP_KEEPALIVE: + return .keepalive + default: return nil }