diff --git a/cue-ios/CueIOS/AppState.swift b/cue-ios/CueIOS/AppState.swift index 074d94f..fa6aec9 100644 --- a/cue-ios/CueIOS/AppState.swift +++ b/cue-ios/CueIOS/AppState.swift @@ -323,6 +323,9 @@ final class AppState { phSlope = Double(slope) phOffset = Double(offset) status = String(format: "pH cal: slope=%.4f offset=%.4f", slope, offset) + + case .keepalive: + break } } diff --git a/cue-ios/CueIOS/Models/Protocol.swift b/cue-ios/CueIOS/Models/Protocol.swift index ab4581d..34c3664 100644 --- a/cue-ios/CueIOS/Models/Protocol.swift +++ b/cue-ios/CueIOS/Models/Protocol.swift @@ -31,6 +31,7 @@ let RSP_REFS_DONE: UInt8 = 0x22 let RSP_REF_STATUS: UInt8 = 0x23 let RSP_CL_FACTOR: UInt8 = 0x24 let RSP_PH_CAL: UInt8 = 0x25 +let RSP_KEEPALIVE: UInt8 = 0x50 // Cue -> ESP32 let CMD_SET_SWEEP: UInt8 = 0x10 @@ -131,6 +132,7 @@ enum EisMessage { case cellK(Float) case clFactor(Float) case phCal(slope: Float, offset: Float) + case keepalive } // MARK: - Response parser @@ -271,6 +273,9 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? { offset: decodeFloat(p, at: 5) ) + case RSP_KEEPALIVE: + return .keepalive + default: return nil } diff --git a/cue/src/app.rs b/cue/src/app.rs index 2ba5db2..f1a9a0d 100644 --- a/cue/src/app.rs +++ b/cue/src/app.rs @@ -866,6 +866,7 @@ impl App { self.ph_offset = Some(offset); self.status = format!("pH cal: slope={:.4} offset={:.4}", slope, offset); } + EisMessage::Keepalive => {} }, Message::TabSelected(t) => { if t == Tab::Browse { diff --git a/cue/src/protocol.rs b/cue/src/protocol.rs index 1aa589e..a0c2548 100644 --- a/cue/src/protocol.rs +++ b/cue/src/protocol.rs @@ -28,6 +28,7 @@ pub const RSP_CELL_K: u8 = 0x11; pub const RSP_REF_STATUS: u8 = 0x23; pub const RSP_CL_FACTOR: u8 = 0x24; pub const RSP_PH_CAL: u8 = 0x25; +pub const RSP_KEEPALIVE: u8 = 0x50; /* Cue → ESP32 */ pub const CMD_SET_SWEEP: u8 = 0x10; @@ -266,6 +267,7 @@ pub enum EisMessage { CellK(f32), ClFactor(f32), PhCal { slope: f32, offset: f32 }, + Keepalive, } fn decode_u16(data: &[u8]) -> u16 { @@ -432,6 +434,7 @@ pub fn parse_sysex(data: &[u8]) -> Option { offset: decode_float(&p[5..10]), }) } + RSP_KEEPALIVE => Some(EisMessage::Keepalive), _ => None, } } 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 diff --git a/main/protocol.c b/main/protocol.c index bf81691..8e53997 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 fa57d3c..77dece0 100644 --- a/main/protocol.h +++ b/main/protocol.h @@ -62,6 +62,7 @@ #define RSP_REF_STATUS 0x23 #define RSP_CL_FACTOR 0x24 #define RSP_PH_CAL 0x25 +#define RSP_KEEPALIVE 0x50 /* Session sync responses (0x4x) */ #define RSP_SESSION_CREATED 0x40 @@ -152,6 +153,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); diff --git a/main/wifi_transport.c b/main/wifi_transport.c index 277e995..2df1eb0 100644 --- a/main/wifi_transport.c +++ b/main/wifi_transport.c @@ -18,7 +18,9 @@ #define UDP_PORT 5941 #define UDP_BUF_SIZE 128 -#define MAX_UDP_CLIENTS 10 +#define UDP_CLIENTS_MAX 16 +#define CLIENT_TIMEOUT_MS 30000 + static int udp_sock = -1; static esp_netif_t *ap_netif; @@ -26,7 +28,7 @@ static struct { struct sockaddr_in addr; uint8_t mac[6]; bool active; -} clients[MAX_UDP_CLIENTS]; +} clients[UDP_CLIENTS_MAX]; static int client_count; @@ -38,7 +40,7 @@ static void client_touch(const struct sockaddr_in *addr) return; } - if (client_count < MAX_UDP_CLIENTS) { + if (client_count < UDP_CLIENTS_MAX) { clients[client_count].addr = *addr; clients[client_count].active = true; memset(clients[client_count].mac, 0, 6); @@ -56,7 +58,7 @@ static void client_touch(const struct sockaddr_in *addr) } client_count++; - printf("UDP: client added (%d/%d)\n", client_count, MAX_UDP_CLIENTS); + printf("UDP: client added (%d)\n", client_count); } } @@ -65,7 +67,7 @@ static void client_remove_by_mac(const uint8_t *mac) for (int i = 0; i < client_count; ) { if (memcmp(clients[i].mac, mac, 6) == 0) { clients[i] = clients[--client_count]; - printf("UDP: client removed by MAC (%d/%d)\n", client_count, MAX_UDP_CLIENTS); + printf("UDP: client removed by MAC (%d)\n", client_count); } else { i++; }