replace BLE provisioning with captive portal, add 9-point pH calibration with temperature correction

This commit is contained in:
jess 2026-04-09 14:56:48 -07:00
parent ff51b9ef23
commit 80319bb259
14 changed files with 767 additions and 460 deletions

View File

@ -1,4 +1,4 @@
idf_component_register(SRCS "eis4.c" "eis.c" "echem.c" "protocol.c" "wifi_transport.c" "temp.c" "refs.c" idf_component_register(SRCS "eis4.c" "eis.c" "echem.c" "protocol.c" "wifi_transport.c" "temp.c" "refs.c"
"wifi_cfg.c" "ble_prov.c" "wifi_cfg.c" "captive_portal.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES ad5941 ad5941_port nvs_flash esp_wifi esp_netif esp_event esp_timer bt) REQUIRES ad5941 ad5941_port nvs_flash esp_wifi esp_netif esp_event esp_timer esp_http_server)

View File

@ -1,391 +0,0 @@
#include "ble_prov.h"
#include "wifi_cfg.h"
#include "protocol.h"
#include <string.h>
#include <stdio.h>
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/ble_uuid.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "esp_mac.h"
/* wifi_transport provides these */
extern esp_err_t wifi_transport_reconnect_sta(const char *ssid, const char *pass);
extern void wifi_transport_get_sta_state(uint8_t *state, uint32_t *ip);
#define DEVICE_NAME "EIS4"
#define FW_VERSION "4.0"
#define RSP_BUF_MAX 128
/* EIS4 provisioning service UUID: 4e455334-a001-4801-b947-001122334455 */
static const ble_uuid128_t svc_uuid =
BLE_UUID128_INIT(0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x47, 0xb9,
0x01, 0x48, 0x01, 0xa0, 0x34, 0x53, 0x45, 0x4e);
/* EIS4 provisioning characteristic UUID: 4e455334-a002-4801-b947-001122334455 */
static const ble_uuid128_t chr_uuid =
BLE_UUID128_INIT(0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x47, 0xb9,
0x01, 0x48, 0x02, 0xa0, 0x34, 0x53, 0x45, 0x4e);
static bool s_ble_active;
static uint16_t s_chr_handle;
static uint8_t s_rsp_buf[RSP_BUF_MAX];
static uint16_t s_rsp_len;
static uint8_t s_own_addr_type;
static void start_advertise(void);
/* -- response builders -- */
static void build_wifi_cfg_rsp(void)
{
char ssid[WIFI_CFG_SSID_MAX], pass[WIFI_CFG_PASS_MAX];
uint8_t *p = s_rsp_buf;
*p++ = 0xF0;
*p++ = 0x7D;
*p++ = RSP_WIFI_CFG;
/* STA SSID */
if (wifi_cfg_get_sta(ssid, sizeof(ssid), pass, sizeof(pass)) == ESP_OK) {
uint8_t slen = (uint8_t)strlen(ssid);
*p++ = slen;
memcpy(p, ssid, slen); p += slen;
/* STA status */
uint8_t state; uint32_t ip;
wifi_transport_get_sta_state(&state, &ip);
*p++ = state;
} else {
*p++ = 0;
*p++ = WIFI_STATE_DISCONNECTED;
}
/* AP SSID */
wifi_cfg_get_ap(ssid, sizeof(ssid), pass, sizeof(pass));
{
uint8_t slen = (uint8_t)strlen(ssid);
*p++ = slen;
memcpy(p, ssid, slen); p += slen;
}
*p++ = 0xF7;
s_rsp_len = (uint16_t)(p - s_rsp_buf);
}
static void build_set_result(uint8_t ok)
{
uint8_t *p = s_rsp_buf;
*p++ = 0xF0;
*p++ = 0x7D;
*p++ = RSP_SET_WIFI_RESULT;
*p++ = ok ? 0 : 1;
*p++ = 0xF7;
s_rsp_len = (uint16_t)(p - s_rsp_buf);
}
static void build_device_info_rsp(void)
{
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
uint8_t *p = s_rsp_buf;
*p++ = 0xF0;
*p++ = 0x7D;
*p++ = RSP_DEVICE_INFO;
/* version */
uint8_t vlen = (uint8_t)strlen(FW_VERSION);
*p++ = vlen;
memcpy(p, FW_VERSION, vlen); p += vlen;
/* name */
uint8_t nlen = (uint8_t)strlen(DEVICE_NAME);
*p++ = nlen;
memcpy(p, DEVICE_NAME, nlen); p += nlen;
/* MAC (6 bytes, each masked to 7-bit halves) */
for (int i = 0; i < 6; i++) {
*p++ = mac[i] & 0x7F;
*p++ = (mac[i] >> 7) & 0x01;
}
*p++ = 0xF7;
s_rsp_len = (uint16_t)(p - s_rsp_buf);
}
static void build_wifi_status_rsp(void)
{
uint8_t state;
uint32_t ip;
wifi_transport_get_sta_state(&state, &ip);
uint8_t *p = s_rsp_buf;
*p++ = 0xF0;
*p++ = 0x7D;
*p++ = RSP_WIFI_STATUS;
*p++ = state;
if (state == WIFI_STATE_CONNECTED) {
*p++ = (ip >> 0) & 0x7F;
*p++ = (ip >> 7) & 0x7F;
*p++ = (ip >> 14) & 0x7F;
*p++ = (ip >> 21) & 0x7F;
*p++ = (ip >> 28) & 0x0F;
}
*p++ = 0xF7;
s_rsp_len = (uint16_t)(p - s_rsp_buf);
}
/* -- command processing -- */
static void process_command(const uint8_t *data, uint16_t len)
{
if (len < 4 || data[0] != 0xF0 || data[1] != 0x7D)
return;
uint16_t end = 0;
for (uint16_t i = 2; i < len; i++) {
if (data[i] == 0xF7) { end = i; break; }
}
if (!end) return;
uint8_t cmd = data[2];
switch (cmd) {
case CMD_GET_WIFI_CFG:
build_wifi_cfg_rsp();
break;
case CMD_SET_WIFI_STA: {
if (end < 6) { build_set_result(0); break; }
uint8_t ssid_len = data[3];
if (3 + 1 + ssid_len + 1 >= end) { build_set_result(0); break; }
char ssid[WIFI_CFG_SSID_MAX] = {0};
memcpy(ssid, &data[4], ssid_len);
uint8_t pass_len = data[4 + ssid_len];
char pass[WIFI_CFG_PASS_MAX] = {0};
if (pass_len > 0 && (4 + ssid_len + 1 + pass_len) <= end)
memcpy(pass, &data[5 + ssid_len], pass_len);
esp_err_t err = wifi_cfg_set_sta(ssid, pass);
if (err == ESP_OK)
err = wifi_transport_reconnect_sta(ssid, pass);
build_set_result(err == ESP_OK);
break;
}
case CMD_SET_WIFI_AP: {
if (end < 6) { build_set_result(0); break; }
uint8_t ssid_len = data[3];
if (3 + 1 + ssid_len + 1 >= end) { build_set_result(0); break; }
char ssid[WIFI_CFG_SSID_MAX] = {0};
memcpy(ssid, &data[4], ssid_len);
uint8_t pass_len = data[4 + ssid_len];
char pass[WIFI_CFG_PASS_MAX] = {0};
if (pass_len > 0 && (4 + ssid_len + 1 + pass_len) <= end)
memcpy(pass, &data[5 + ssid_len], pass_len);
esp_err_t err = wifi_cfg_set_ap(ssid, pass);
build_set_result(err == ESP_OK);
break;
}
case CMD_GET_DEVICE_INFO:
build_device_info_rsp();
break;
case CMD_WIFI_STATUS:
build_wifi_status_rsp();
break;
default:
return;
}
}
/* -- GATT -- */
static int chr_access_cb(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
(void)conn_handle;
(void)attr_handle;
(void)arg;
switch (ctxt->op) {
case BLE_GATT_ACCESS_OP_READ_CHR:
if (s_rsp_len > 0)
os_mbuf_append(ctxt->om, s_rsp_buf, s_rsp_len);
return 0;
case BLE_GATT_ACCESS_OP_WRITE_CHR: {
uint8_t buf[RSP_BUF_MAX];
uint16_t out_len = 0;
uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om);
if (om_len > sizeof(buf)) om_len = sizeof(buf);
ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &out_len);
process_command(buf, out_len);
return 0;
}
default:
return BLE_ATT_ERR_UNLIKELY;
}
}
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = &svc_uuid.u,
.characteristics = (struct ble_gatt_chr_def[]) {
{
.uuid = &chr_uuid.u,
.access_cb = chr_access_cb,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE,
.val_handle = &s_chr_handle,
},
{ 0 },
},
},
{ 0 },
};
static int gatt_svr_init(void)
{
ble_svc_gap_init();
ble_svc_gatt_init();
int rc = ble_gatts_count_cfg(gatt_svr_svcs);
if (rc != 0) return rc;
return ble_gatts_add_svcs(gatt_svr_svcs);
}
/* -- GAP -- */
static int gap_event_cb(struct ble_gap_event *event, void *arg)
{
(void)arg;
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status != 0)
start_advertise();
return 0;
case BLE_GAP_EVENT_DISCONNECT:
if (s_ble_active)
start_advertise();
return 0;
case BLE_GAP_EVENT_ADV_COMPLETE:
if (s_ble_active)
start_advertise();
return 0;
default:
return 0;
}
}
static void start_advertise(void)
{
struct ble_hs_adv_fields fields = {0};
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.tx_pwr_lvl_is_present = 1;
fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
const char *name = ble_svc_gap_device_name();
fields.name = (uint8_t *)name;
fields.name_len = strlen(name);
fields.name_is_complete = 1;
ble_gap_adv_set_fields(&fields);
struct ble_gap_adv_params params = {0};
params.conn_mode = BLE_GAP_CONN_MODE_UND;
params.disc_mode = BLE_GAP_DISC_MODE_GEN;
ble_gap_adv_start(s_own_addr_type, NULL, BLE_HS_FOREVER,
&params, gap_event_cb, NULL);
}
static void on_sync(void)
{
ble_hs_util_ensure_addr(0);
ble_hs_id_infer_auto(0, &s_own_addr_type);
start_advertise();
printf("BLE: advertising\n");
}
static void on_reset(int reason)
{
printf("BLE: host reset, reason=%d\n", reason);
}
static void ble_host_task(void *param)
{
(void)param;
nimble_port_run();
nimble_port_freertos_deinit();
}
/* -- public API -- */
esp_err_t ble_prov_start(void)
{
if (s_ble_active)
return ESP_OK;
esp_err_t err = nimble_port_init();
if (err != ESP_OK) {
printf("BLE: nimble_port_init failed: %d\n", (int)err);
return err;
}
ble_hs_cfg.reset_cb = on_reset;
ble_hs_cfg.sync_cb = on_sync;
ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_NO_IO;
int rc = gatt_svr_init();
if (rc != 0) {
printf("BLE: GATT init failed: %d\n", rc);
nimble_port_deinit();
return ESP_FAIL;
}
ble_svc_gap_device_name_set(DEVICE_NAME);
nimble_port_freertos_init(ble_host_task);
s_ble_active = true;
s_rsp_len = 0;
printf("BLE: provisioning started\n");
return ESP_OK;
}
esp_err_t ble_prov_stop(void)
{
if (!s_ble_active)
return ESP_OK;
s_ble_active = false;
if (ble_gap_adv_active())
ble_gap_adv_stop();
nimble_port_stop();
nimble_port_deinit();
printf("BLE: provisioning stopped\n");
return ESP_OK;
}
bool ble_prov_is_active(void)
{
return s_ble_active;
}

View File

@ -1,11 +0,0 @@
#ifndef BLE_PROV_H
#define BLE_PROV_H
#include "esp_err.h"
#include <stdbool.h>
esp_err_t ble_prov_start(void);
esp_err_t ble_prov_stop(void);
bool ble_prov_is_active(void);
#endif

427
main/captive_portal.c Normal file
View File

@ -0,0 +1,427 @@
#include "captive_portal.h"
#include "wifi_cfg.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_http_server.h"
#include "esp_wifi.h"
#include "esp_system.h"
#include "lwip/sockets.h"
#define DNS_PORT 53
#define HTTP_PORT 80
#define MAX_SCAN_AP 20
static httpd_handle_t s_httpd;
static TaskHandle_t s_dns_task;
static int s_dns_sock = -1;
static bool s_active;
static const uint8_t AP_IP[4] = {192, 168, 4, 1};
/* ------------------------------------------------------------------ */
/* Config page HTML */
/* ------------------------------------------------------------------ */
static const char CONFIG_PAGE[] =
"<!DOCTYPE html>"
"<html><head>"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
"<title>EIS4 Setup</title>"
"<style>"
"*{box-sizing:border-box;margin:0;padding:0}"
"body{font-family:system-ui,-apple-system,sans-serif;background:#111;"
"color:#e0e0e0;padding:20px;max-width:400px;margin:0 auto}"
"h1{font-size:1.4em;margin-bottom:20px;color:#fff}"
"label{display:block;margin:12px 0 4px;font-size:.9em;color:#aaa}"
"input{width:100%;padding:10px;border:1px solid #333;border-radius:6px;"
"background:#1a1a1a;color:#fff;font-size:1em}"
"input:focus{outline:none;border-color:#4a9eff}"
"button{width:100%;padding:12px;border:none;border-radius:6px;"
"background:#4a9eff;color:#fff;font-size:1em;cursor:pointer;margin-top:12px}"
"button:active{background:#3580d4}"
".btn-s{background:#333;margin-top:8px;padding:8px;font-size:.9em}"
".nets{list-style:none;max-height:200px;overflow-y:auto;"
"border:1px solid #333;border-radius:6px;margin-top:4px}"
".nets li{padding:10px;cursor:pointer;border-bottom:1px solid #222;"
"display:flex;justify-content:space-between}"
".nets li:hover,.nets li.sel{background:#1a2a3a}"
".rs{color:#666;font-size:.85em}"
".hint{font-size:.8em;color:#666;margin-top:4px}"
"#st{margin-top:16px;padding:10px;border-radius:6px;display:none}"
".ok{background:#1a3a1a;color:#4caf50;display:block!important}"
".err{background:#3a1a1a;color:#f44;display:block!important}"
"</style></head><body>"
"<h1>EIS4 Setup</h1>"
"<form id=\"f\" method=\"POST\" action=\"/save\">"
"<label>WiFi Network</label>"
"<ul class=\"nets\" id=\"nl\"><li>Scanning...</li></ul>"
"<button type=\"button\" class=\"btn-s\" onclick=\"scan()\">Rescan</button>"
"<label>Network Name</label>"
"<input type=\"text\" name=\"ssid\" id=\"ssid\" "
"placeholder=\"Select above or type for hidden network\">"
"<label>WiFi Password</label>"
"<input type=\"password\" name=\"pass\" id=\"pass\">"
"<label>EIS4 Network Password</label>"
"<input type=\"text\" name=\"ap_pass\" id=\"ap_pass\" "
"placeholder=\"Leave blank for open\">"
"<p class=\"hint\">Minimum 8 characters, or leave blank for open network</p>"
"<button type=\"submit\">Save &amp; Restart</button>"
"</form>"
"<div id=\"st\"></div>"
"<script>"
"function scan(){"
"var ul=document.getElementById('nl');"
"ul.innerHTML='<li>Scanning...</li>';"
"fetch('/scan').then(function(r){return r.json()}).then(function(d){"
"ul.innerHTML='';"
"d.forEach(function(n){"
"var li=document.createElement('li');"
"var nm=document.createElement('span');nm.textContent=n.s;"
"var rs=document.createElement('span');rs.className='rs';"
"rs.textContent=n.r+'dBm';"
"li.appendChild(nm);li.appendChild(rs);"
"li.onclick=function(){"
"document.querySelectorAll('.nets li').forEach(function(l){"
"l.classList.remove('sel')});"
"li.classList.add('sel');"
"document.getElementById('ssid').value=n.s;};"
"ul.appendChild(li);});"
"if(!d.length)ul.innerHTML='<li>No networks found</li>';"
"}).catch(function(){ul.innerHTML='<li>Scan failed</li>';});}"
"scan();"
"document.getElementById('f').onsubmit=function(e){"
"e.preventDefault();"
"var s=document.getElementById('st');"
"var ssid=document.getElementById('ssid').value;"
"if(!ssid){s.className='err';s.textContent='Enter a network name';return;}"
"var ap=document.getElementById('ap_pass').value;"
"if(ap&&ap.length<8){"
"s.className='err';s.textContent='AP password must be at least 8 characters';"
"return;}"
"var b='ssid='+encodeURIComponent(ssid)"
"+'&pass='+encodeURIComponent(document.getElementById('pass').value)"
"+'&ap_pass='+encodeURIComponent(ap);"
"s.className='';s.style.display='none';"
"fetch('/save',{method:'POST',"
"headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b})"
".then(function(r){"
"if(r.ok){s.className='ok';s.textContent='Saved. Restarting device...';}"
"else{s.className='err';s.textContent='Invalid configuration';}})"
".catch(function(){s.className='ok';"
"s.textContent='Device is restarting...';});};"
"</script></body></html>";
/* ------------------------------------------------------------------ */
/* URL parameter helpers */
/* ------------------------------------------------------------------ */
static void url_decode(char *dst, const char *src, size_t dst_sz)
{
size_t di = 0;
while (*src && di < dst_sz - 1) {
if (*src == '%' && src[1] && src[2]) {
char hex[3] = {src[1], src[2], 0};
dst[di++] = (char)strtol(hex, NULL, 16);
src += 3;
} else if (*src == '+') {
dst[di++] = ' ';
src++;
} else {
dst[di++] = *src++;
}
}
dst[di] = '\0';
}
static void get_param(const char *body, const char *key,
char *val, size_t val_sz)
{
val[0] = '\0';
size_t klen = strlen(key);
const char *p = body;
while ((p = strstr(p, key)) != NULL) {
if ((p == body || *(p - 1) == '&') && p[klen] == '=') {
p += klen + 1;
const char *end = strchr(p, '&');
size_t len = end ? (size_t)(end - p) : strlen(p);
char raw[128];
if (len >= sizeof(raw)) len = sizeof(raw) - 1;
memcpy(raw, p, len);
raw[len] = '\0';
url_decode(val, raw, val_sz);
return;
}
p++;
}
}
/* ------------------------------------------------------------------ */
/* JSON escape for SSIDs */
/* ------------------------------------------------------------------ */
static int json_escape(char *dst, size_t dst_sz, const char *src)
{
size_t di = 0;
while (*src && di < dst_sz - 2) {
if (*src == '"' || *src == '\\') {
if (di + 2 >= dst_sz) break;
dst[di++] = '\\';
}
dst[di++] = *src++;
}
dst[di] = '\0';
return (int)di;
}
/* ------------------------------------------------------------------ */
/* HTTP handlers */
/* ------------------------------------------------------------------ */
static esp_err_t handle_root(httpd_req_t *req)
{
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, CONFIG_PAGE, HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
static esp_err_t handle_scan(httpd_req_t *req)
{
wifi_scan_config_t scan_cfg = {0};
esp_wifi_scan_start(&scan_cfg, true);
uint16_t count = 0;
esp_wifi_scan_get_ap_num(&count);
if (count > MAX_SCAN_AP) count = MAX_SCAN_AP;
wifi_ap_record_t *aps = calloc(count ? count : 1, sizeof(wifi_ap_record_t));
if (!aps) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL);
return ESP_FAIL;
}
esp_wifi_scan_get_ap_records(&count, aps);
char json[2048];
int off = 0;
off += snprintf(json + off, sizeof(json) - off, "[");
for (int i = 0; i < count && off < (int)sizeof(json) - 80; i++) {
if (!aps[i].ssid[0]) continue;
/* skip duplicates (scan is sorted by rssi, first is strongest) */
bool dup = false;
for (int j = 0; j < i; j++) {
if (strcmp((char *)aps[i].ssid, (char *)aps[j].ssid) == 0) {
dup = true;
break;
}
}
if (dup) continue;
char escaped[66];
json_escape(escaped, sizeof(escaped), (char *)aps[i].ssid);
if (off > 1) off += snprintf(json + off, sizeof(json) - off, ",");
off += snprintf(json + off, sizeof(json) - off,
"{\"s\":\"%s\",\"r\":%d}", escaped, aps[i].rssi);
}
off += snprintf(json + off, sizeof(json) - off, "]");
free(aps);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, json, off);
return ESP_OK;
}
static esp_err_t handle_save(httpd_req_t *req)
{
char buf[256];
int n = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (n <= 0) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, NULL);
return ESP_FAIL;
}
buf[n] = '\0';
char ssid[WIFI_CFG_SSID_MAX] = {0};
char pass[WIFI_CFG_PASS_MAX] = {0};
char ap_pass[WIFI_CFG_PASS_MAX] = {0};
get_param(buf, "ssid", ssid, sizeof(ssid));
get_param(buf, "pass", pass, sizeof(pass));
get_param(buf, "ap_pass", ap_pass, sizeof(ap_pass));
if (!ssid[0]) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing SSID");
return ESP_FAIL;
}
if (ap_pass[0] && strlen(ap_pass) < 8) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"AP password must be 8+ characters or empty");
return ESP_FAIL;
}
printf("Portal: saving STA \"%s\", AP pass %s\n",
ssid, ap_pass[0] ? "set" : "open");
wifi_cfg_set_sta(ssid, pass);
wifi_cfg_set_ap("EIS4", ap_pass);
httpd_resp_sendstr(req, "OK");
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return ESP_OK;
}
static esp_err_t handle_catchall(httpd_req_t *req)
{
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
/* ------------------------------------------------------------------ */
/* DNS redirect server */
/* ------------------------------------------------------------------ */
static void dns_task(void *arg)
{
(void)arg;
s_dns_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (s_dns_sock < 0) {
printf("DNS: socket failed\n");
vTaskDelete(NULL);
return;
}
struct sockaddr_in bind_addr = {
.sin_family = AF_INET,
.sin_port = htons(DNS_PORT),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
if (bind(s_dns_sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
printf("DNS: bind failed\n");
close(s_dns_sock);
s_dns_sock = -1;
vTaskDelete(NULL);
return;
}
printf("DNS: redirecting all queries to " "%d.%d.%d.%d\n",
AP_IP[0], AP_IP[1], AP_IP[2], AP_IP[3]);
uint8_t buf[256];
struct sockaddr_in src;
socklen_t slen;
for (;;) {
slen = sizeof(src);
int n = recvfrom(s_dns_sock, buf, sizeof(buf), 0,
(struct sockaddr *)&src, &slen);
if (n < 12) {
if (n < 0) break;
continue;
}
/* walk question section to find its end */
int qend = 12;
bool valid = true;
while (qend < n && buf[qend] != 0) {
uint8_t ll = buf[qend];
if (ll > 63 || qend + ll + 1 > n) { valid = false; break; }
qend += ll + 1;
}
if (!valid) continue;
qend += 5; /* null terminator + QTYPE(2) + QCLASS(2) */
if (qend > n) continue;
/* build response: header + question (copied) + one A answer */
uint8_t rsp[280];
if (qend + 16 > (int)sizeof(rsp)) continue;
memcpy(rsp, buf, qend);
rsp[2] = 0x81; rsp[3] = 0x80; /* flags: standard response */
rsp[6] = 0x00; rsp[7] = 0x01; /* 1 answer RR */
int off = qend;
rsp[off++] = 0xC0; rsp[off++] = 0x0C; /* name pointer */
rsp[off++] = 0x00; rsp[off++] = 0x01; /* type A */
rsp[off++] = 0x00; rsp[off++] = 0x01; /* class IN */
rsp[off++] = 0x00; rsp[off++] = 0x00;
rsp[off++] = 0x00; rsp[off++] = 0x01; /* TTL 1s */
rsp[off++] = 0x00; rsp[off++] = 0x04; /* rdlength */
rsp[off++] = AP_IP[0]; rsp[off++] = AP_IP[1];
rsp[off++] = AP_IP[2]; rsp[off++] = AP_IP[3];
sendto(s_dns_sock, rsp, off, 0, (struct sockaddr *)&src, slen);
}
vTaskDelete(NULL);
}
/* ------------------------------------------------------------------ */
/* Public API */
/* ------------------------------------------------------------------ */
esp_err_t captive_portal_start(void)
{
if (s_active) return ESP_OK;
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
cfg.server_port = HTTP_PORT;
cfg.stack_size = 6144;
cfg.uri_match_fn = httpd_uri_match_wildcard;
cfg.max_uri_handlers = 8;
esp_err_t err = httpd_start(&s_httpd, &cfg);
if (err != ESP_OK) {
printf("Portal: HTTP start failed: %d\n", err);
return err;
}
const httpd_uri_t uri_scan = {"/scan", HTTP_GET, handle_scan, NULL};
const httpd_uri_t uri_save = {"/save", HTTP_POST, handle_save, NULL};
const httpd_uri_t uri_root = {"/", HTTP_GET, handle_root, NULL};
const httpd_uri_t uri_catch = {"/*", HTTP_GET, handle_catchall, NULL};
httpd_register_uri_handler(s_httpd, &uri_scan);
httpd_register_uri_handler(s_httpd, &uri_save);
httpd_register_uri_handler(s_httpd, &uri_root);
httpd_register_uri_handler(s_httpd, &uri_catch);
xTaskCreate(dns_task, "dns", 3072, NULL, 5, &s_dns_task);
s_active = true;
printf("Portal: active on port %d\n", HTTP_PORT);
return ESP_OK;
}
esp_err_t captive_portal_stop(void)
{
if (!s_active) return ESP_OK;
if (s_httpd) {
httpd_stop(s_httpd);
s_httpd = NULL;
}
if (s_dns_sock >= 0) {
close(s_dns_sock);
s_dns_sock = -1;
}
s_active = false;
printf("Portal: stopped\n");
return ESP_OK;
}
bool captive_portal_is_active(void)
{
return s_active;
}

11
main/captive_portal.h Normal file
View File

@ -0,0 +1,11 @@
#ifndef CAPTIVE_PORTAL_H
#define CAPTIVE_PORTAL_H
#include "esp_err.h"
#include <stdbool.h>
esp_err_t captive_portal_start(void);
esp_err_t captive_portal_stop(void);
bool captive_portal_is_active(void);
#endif

View File

@ -1,4 +1,5 @@
#include "echem.h" #include "echem.h"
#include "eis.h"
#include "ad5940.h" #include "ad5940.h"
#include "protocol.h" #include "protocol.h"
#include <math.h> #include <math.h>
@ -623,11 +624,18 @@ int echem_ph_ocp(const PhConfig *cfg, PhResult *result)
float v_re0 = sum_re0 / PH_AVG_N; float v_re0 = sum_re0 / PH_AVG_N;
float ocp = v_se0 - v_re0; float ocp = v_se0 - v_re0;
float t_k = cfg->temp_c + 273.15f; float ocp_corrected = ocp;
float slope = 0.1984f * t_k; /* mV/pH at temperature */ float tc_cold = eis_get_ph_temp_slope_cold();
float tc_hot = eis_get_ph_temp_slope_hot();
if (tc_cold != 0.0f || tc_hot != 0.0f) {
float dt = cfg->temp_c - 25.0f;
float alpha = (dt < 0.0f) ? tc_cold : tc_hot;
if (alpha != 0.0f)
ocp_corrected = ocp - alpha * dt;
}
result->v_ocp_mv = ocp; result->v_ocp_mv = ocp;
result->ph = 7.0f - ocp / slope; result->ph = eis_get_ph_slope() * ocp_corrected + eis_get_ph_offset();
result->temp_c = cfg->temp_c; result->temp_c = cfg->temp_c;
printf("pH: SE0=%.1f mV, RE0=%.1f mV, OCP=%.1f mV, pH=%.2f\n", printf("pH: SE0=%.1f mV, RE0=%.1f mV, OCP=%.1f mV, pH=%.2f\n",

View File

@ -651,30 +651,168 @@ void eis_load_cl_factor(void)
nvs_close(h); nvs_close(h);
} }
void eis_set_ph_cal(float slope, float offset) float eis_get_ph_slope(void) { return ph_slope_cached; }
float eis_get_ph_offset(void) { return ph_offset_cached; }
/* ---- 3-buffer × 3-temperature pH calibration ---- */
static const float PH_BUFFERS[PH_CAL_BUFFERS] = {4.0f, 6.86f, 9.0f};
#define NVS_PH_CAL_PTS_KEY "ph_cal9"
typedef struct {
float ocp_mv;
float temp_c;
} PhCalSample;
static struct {
PhCalSample s[PH_CAL_BUFFERS][PH_CAL_TEMPS];
uint16_t valid;
} ph_cal;
static float ph_temp_slope_cold;
static float ph_temp_slope_hot;
static void ph_cal_recalculate(void)
{
/* baseline slope/offset from the 3 baseline (tslot=1) points */
int n = 0;
float sx = 0, sy = 0, sxx = 0, sxy = 0;
for (int i = 0; i < PH_CAL_BUFFERS; i++) {
int bit = i * PH_CAL_TEMPS + PH_TEMP_BASE;
if (!(ph_cal.valid & (1 << bit))) continue;
float x = ph_cal.s[i][PH_TEMP_BASE].ocp_mv;
float y = PH_BUFFERS[i];
sx += x; sy += y; sxx += x * x; sxy += x * y;
n++;
}
if (n < 2) {
ph_slope_cached = 0;
ph_offset_cached = 0;
} else {
float d = (float)n * sxx - sx * sx;
if (fabsf(d) < 1e-10f) {
ph_slope_cached = 0;
ph_offset_cached = 0;
} else {
ph_slope_cached = ((float)n * sxy - sx * sy) / d;
ph_offset_cached = (sy - ph_slope_cached * sx) / (float)n;
}
}
printf("pH cal: baseline slope=%.6f offset=%.4f (%d pts)\n",
ph_slope_cached, ph_offset_cached, n);
/* temperature drift from off-temperature points */
ph_temp_slope_cold = 0;
ph_temp_slope_hot = 0;
int nc = 0, nh = 0;
for (int i = 0; i < PH_CAL_BUFFERS; i++) {
int base_bit = i * PH_CAL_TEMPS + PH_TEMP_BASE;
if (!(ph_cal.valid & (1 << base_bit))) continue;
float ocp_base = ph_cal.s[i][PH_TEMP_BASE].ocp_mv;
int cold_bit = i * PH_CAL_TEMPS + PH_TEMP_BELOW;
if (ph_cal.valid & (1 << cold_bit)) {
float dt = ph_cal.s[i][PH_TEMP_BELOW].temp_c - 25.0f;
if (fabsf(dt) > 0.5f) {
ph_temp_slope_cold += (ph_cal.s[i][PH_TEMP_BELOW].ocp_mv - ocp_base) / dt;
nc++;
}
}
int hot_bit = i * PH_CAL_TEMPS + PH_TEMP_ABOVE;
if (ph_cal.valid & (1 << hot_bit)) {
float dt = ph_cal.s[i][PH_TEMP_ABOVE].temp_c - 25.0f;
if (fabsf(dt) > 0.5f) {
ph_temp_slope_hot += (ph_cal.s[i][PH_TEMP_ABOVE].ocp_mv - ocp_base) / dt;
nh++;
}
}
}
if (nc > 0) ph_temp_slope_cold /= nc;
if (nh > 0) ph_temp_slope_hot /= nh;
if (nc > 0 || nh > 0)
printf("pH cal: temp drift cold=%.4f hot=%.4f mV/C\n",
ph_temp_slope_cold, ph_temp_slope_hot);
}
static void ph_cal_save(void)
{ {
ph_slope_cached = slope;
ph_offset_cached = offset;
nvs_handle_t h; nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READWRITE, &h) != ESP_OK) return; if (nvs_open(NVS_OCAL_NS, NVS_READWRITE, &h) != ESP_OK) return;
nvs_set_blob(h, NVS_PH_SLOPE_KEY, &slope, sizeof(slope)); nvs_set_blob(h, NVS_PH_CAL_PTS_KEY, &ph_cal, sizeof(ph_cal));
nvs_set_blob(h, NVS_PH_OFFSET_KEY, &offset, sizeof(offset));
nvs_commit(h); nvs_commit(h);
nvs_close(h); nvs_close(h);
} }
float eis_get_ph_slope(void) { return ph_slope_cached; } float eis_get_ph_temp_slope_cold(void) { return ph_temp_slope_cold; }
float eis_get_ph_offset(void) { return ph_offset_cached; } float eis_get_ph_temp_slope_hot(void) { return ph_temp_slope_hot; }
void eis_load_ph_cal(void) void eis_load_ph_cal(void)
{ {
nvs_handle_t h; nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READONLY, &h) != ESP_OK) return; if (nvs_open(NVS_OCAL_NS, NVS_READONLY, &h) != ESP_OK) return;
size_t len = sizeof(ph_slope_cached); size_t len = sizeof(ph_cal);
if (nvs_get_blob(h, NVS_PH_SLOPE_KEY, &ph_slope_cached, &len) != ESP_OK || len != sizeof(ph_slope_cached)) if (nvs_get_blob(h, NVS_PH_CAL_PTS_KEY, &ph_cal, &len) != ESP_OK
ph_slope_cached = 0.0f; || len != sizeof(ph_cal)) {
len = sizeof(ph_offset_cached); memset(&ph_cal, 0, sizeof(ph_cal));
if (nvs_get_blob(h, NVS_PH_OFFSET_KEY, &ph_offset_cached, &len) != ESP_OK || len != sizeof(ph_offset_cached)) }
ph_offset_cached = 0.0f; nvs_close(h);
nvs_close(h); ph_cal_recalculate();
}
int eis_ph_cal_set_point(uint8_t buf, uint8_t tslot, float ocp_mv, float temp_c)
{
if (buf >= PH_CAL_BUFFERS || tslot >= PH_CAL_TEMPS) return -1;
ph_cal.s[buf][tslot].ocp_mv = ocp_mv;
ph_cal.s[buf][tslot].temp_c = temp_c;
ph_cal.valid |= (1 << (buf * PH_CAL_TEMPS + tslot));
ph_cal_recalculate();
ph_cal_save();
return 0;
}
int eis_ph_cal_clear_point(uint8_t buf, uint8_t tslot)
{
if (buf >= PH_CAL_BUFFERS || tslot >= PH_CAL_TEMPS) return -1;
int bit = buf * PH_CAL_TEMPS + tslot;
ph_cal.valid &= ~(1 << bit);
memset(&ph_cal.s[buf][tslot], 0, sizeof(PhCalSample));
ph_cal_recalculate();
ph_cal_save();
return 0;
}
void eis_ph_cal_clear_all(void)
{
memset(&ph_cal, 0, sizeof(ph_cal));
ph_slope_cached = 0;
ph_offset_cached = 0;
ph_temp_slope_cold = 0;
ph_temp_slope_hot = 0;
ph_cal_save();
}
bool eis_ph_cal_get_point(uint8_t buf, uint8_t tslot, float *ocp_mv, float *temp_c)
{
if (buf >= PH_CAL_BUFFERS || tslot >= PH_CAL_TEMPS) return false;
if (!(ph_cal.valid & (1 << (buf * PH_CAL_TEMPS + tslot)))) return false;
if (ocp_mv) *ocp_mv = ph_cal.s[buf][tslot].ocp_mv;
if (temp_c) *temp_c = ph_cal.s[buf][tslot].temp_c;
return true;
}
int eis_ph_cal_count(void)
{
int n = 0;
for (int i = 0; i < PH_CAL_BUFFERS * PH_CAL_TEMPS; i++)
if (ph_cal.valid & (1 << i)) n++;
return n;
}
float eis_ph_cal_buffer_ph(uint8_t buf)
{
if (buf >= PH_CAL_BUFFERS) return 0;
return PH_BUFFERS[buf];
} }

View File

@ -2,6 +2,7 @@
#define EIS_H #define EIS_H
#include "ad5940.h" #include "ad5940.h"
#include <stdbool.h>
#define EIS_MAX_POINTS 100 #define EIS_MAX_POINTS 100
@ -71,9 +72,23 @@ void eis_set_cl_factor(float f);
float eis_get_cl_factor(void); float eis_get_cl_factor(void);
void eis_load_cl_factor(void); void eis_load_cl_factor(void);
void eis_set_ph_cal(float slope, float offset);
float eis_get_ph_slope(void); float eis_get_ph_slope(void);
float eis_get_ph_offset(void); float eis_get_ph_offset(void);
float eis_get_ph_temp_slope_cold(void);
float eis_get_ph_temp_slope_hot(void);
void eis_load_ph_cal(void); void eis_load_ph_cal(void);
#define PH_CAL_BUFFERS 3
#define PH_CAL_TEMPS 3
#define PH_TEMP_BELOW 0
#define PH_TEMP_BASE 1
#define PH_TEMP_ABOVE 2
int eis_ph_cal_set_point(uint8_t buf, uint8_t tslot, float ocp_mv, float temp_c);
int eis_ph_cal_clear_point(uint8_t buf, uint8_t tslot);
void eis_ph_cal_clear_all(void);
bool eis_ph_cal_get_point(uint8_t buf, uint8_t tslot, float *ocp_mv, float *temp_c);
int eis_ph_cal_count(void);
float eis_ph_cal_buffer_ph(uint8_t buf);
#endif #endif

View File

@ -250,10 +250,50 @@ void app_main(void)
send_cl_factor(eis_get_cl_factor()); send_cl_factor(eis_get_cl_factor());
break; break;
case CMD_SET_PH_CAL: case CMD_PH_CAL_POINT: {
eis_set_ph_cal(cmd.ph_cal.slope, cmd.ph_cal.offset); uint8_t bid = cmd.ph_cal_point.buffer_id;
send_ph_cal(cmd.ph_cal.slope, cmd.ph_cal.offset); uint8_t tsl = cmd.ph_cal_point.temp_slot;
printf("pH cal set: slope=%.4f offset=%.4f\n", cmd.ph_cal.slope, cmd.ph_cal.offset); if (bid >= PH_CAL_BUFFERS || tsl >= PH_CAL_TEMPS) break;
float buf_ph = eis_ph_cal_buffer_ph(bid);
printf("pH cal: buffer %u slot %u (pH %.2f)\n", bid, tsl, buf_ph);
PhConfig ph_cfg;
ph_cfg.stabilize_s = cmd.ph_cal_point.stabilize_s;
ph_cfg.temp_c = temp_get();
PhResult ph_result;
echem_ph_ocp(&ph_cfg, &ph_result);
eis_ph_cal_set_point(bid, tsl, ph_result.v_ocp_mv, ph_result.temp_c);
int baseline_n = 0;
for (int i = 0; i < PH_CAL_BUFFERS; i++)
if (eis_ph_cal_get_point(i, PH_TEMP_BASE, NULL, NULL)) baseline_n++;
printf("pH cal: [%u][%u] OCP=%.1f mV T=%.1f C (%d/%d)\n",
bid, tsl, ph_result.v_ocp_mv, ph_result.temp_c,
baseline_n, eis_ph_cal_count());
send_ph_cal_point(bid, tsl, ph_result.v_ocp_mv, ph_result.temp_c,
buf_ph, (uint8_t)baseline_n);
break;
}
case CMD_PH_CAL_CLEAR: {
uint8_t bid = cmd.ph_cal_clear.buffer_id;
uint8_t tsl = cmd.ph_cal_clear.temp_slot;
if (bid == 0x7F) {
eis_ph_cal_clear_all();
printf("pH cal: all points cleared\n");
} else if (bid < PH_CAL_BUFFERS && tsl < PH_CAL_TEMPS) {
eis_ph_cal_clear_point(bid, tsl);
printf("pH cal: [%u][%u] cleared\n", bid, tsl);
}
send_ph_cal_status();
break;
}
case CMD_PH_CAL_STATUS:
send_ph_cal_status();
break; break;
case CMD_GET_PH_CAL: case CMD_GET_PH_CAL:

View File

@ -1,4 +1,5 @@
#include "protocol.h" #include "protocol.h"
#include "eis.h"
#include "wifi_transport.h" #include "wifi_transport.h"
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
@ -357,6 +358,44 @@ int send_ph_cal(float slope, float offset)
return send_sysex(sx, p); return send_sysex(sx, p);
} }
int send_ph_cal_point(uint8_t buf, uint8_t tslot, float ocp_mv, float temp_c,
float buffer_ph, uint8_t baseline_count)
{
uint8_t sx[24];
uint16_t p = 0;
sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_PH_CAL_POINT;
sx[p++] = buf & 0x7F;
sx[p++] = tslot & 0x7F;
encode_float(ocp_mv, &sx[p]); p += 5;
encode_float(temp_c, &sx[p]); p += 5;
encode_float(buffer_ph, &sx[p]); p += 5;
sx[p++] = baseline_count & 0x7F;
sx[p++] = 0xF7;
return send_sysex(sx, p);
}
int send_ph_cal_status(void)
{
uint8_t sx[32];
uint16_t p = 0;
sx[p++] = 0xF0; sx[p++] = 0x7D; sx[p++] = RSP_PH_CAL_STATUS;
uint16_t mask = 0;
for (int b = 0; b < PH_CAL_BUFFERS; b++)
for (int t = 0; t < PH_CAL_TEMPS; t++)
if (eis_ph_cal_get_point(b, t, NULL, NULL))
mask |= (1 << (b * PH_CAL_TEMPS + t));
sx[p++] = mask & 0x7F;
sx[p++] = (mask >> 7) & 0x7F;
encode_float(eis_get_ph_slope(), &sx[p]); p += 5;
encode_float(eis_get_ph_offset(), &sx[p]); p += 5;
encode_float(eis_get_ph_temp_slope_cold(), &sx[p]); p += 5;
encode_float(eis_get_ph_temp_slope_hot(), &sx[p]); p += 5;
sx[p++] = 0xF7;
return send_sysex(sx, p);
}
/* ---- outbound: pH ---- */ /* ---- outbound: pH ---- */
int send_ph_result(float v_ocp_mv, float ph, float temp_c, int send_ph_result(float v_ocp_mv, float ph, float temp_c,

View File

@ -28,8 +28,10 @@
#define CMD_CLEAR_REFS 0x32 #define CMD_CLEAR_REFS 0x32
#define CMD_SET_CL_FACTOR 0x33 #define CMD_SET_CL_FACTOR 0x33
#define CMD_GET_CL_FACTOR 0x34 #define CMD_GET_CL_FACTOR 0x34
#define CMD_SET_PH_CAL 0x35
#define CMD_GET_PH_CAL 0x36 #define CMD_GET_PH_CAL 0x36
#define CMD_PH_CAL_POINT 0x37
#define CMD_PH_CAL_CLEAR 0x38
#define CMD_PH_CAL_STATUS 0x39
/* Session sync commands (0x4x) */ /* Session sync commands (0x4x) */
#define CMD_SESSION_CREATE 0x40 #define CMD_SESSION_CREATE 0x40
@ -38,13 +40,6 @@
#define CMD_SESSION_RENAME 0x43 #define CMD_SESSION_RENAME 0x43
#define CMD_HEARTBEAT 0x44 #define CMD_HEARTBEAT 0x44
/* Provisioning commands (0x6x, over BLE) */
#define CMD_GET_WIFI_CFG 0x60
#define CMD_SET_WIFI_STA 0x61
#define CMD_SET_WIFI_AP 0x62
#define CMD_GET_DEVICE_INFO 0x63
#define CMD_WIFI_STATUS 0x64
/* Responses: Firmware -> Client (0x0x, 0x2x) */ /* Responses: Firmware -> Client (0x0x, 0x2x) */
#define RSP_SWEEP_START 0x01 #define RSP_SWEEP_START 0x01
#define RSP_DATA_POINT 0x02 #define RSP_DATA_POINT 0x02
@ -69,6 +64,8 @@
#define RSP_REF_STATUS 0x23 #define RSP_REF_STATUS 0x23
#define RSP_CL_FACTOR 0x24 #define RSP_CL_FACTOR 0x24
#define RSP_PH_CAL 0x25 #define RSP_PH_CAL 0x25
#define RSP_PH_CAL_POINT 0x26
#define RSP_PH_CAL_STATUS 0x27
#define RSP_KEEPALIVE 0x50 #define RSP_KEEPALIVE 0x50
/* Session sync responses (0x4x) */ /* Session sync responses (0x4x) */
@ -78,13 +75,7 @@
#define RSP_SESSION_RENAMED 0x43 #define RSP_SESSION_RENAMED 0x43
#define RSP_CLIENT_LIST 0x44 #define RSP_CLIENT_LIST 0x44
/* Provisioning responses (0x6x, over BLE) */ /* WiFi STA state values */
#define RSP_WIFI_CFG 0x60
#define RSP_SET_WIFI_RESULT 0x61
#define RSP_DEVICE_INFO 0x63
#define RSP_WIFI_STATUS 0x64
/* WiFi STA state values (for RSP_WIFI_STATUS) */
#define WIFI_STATE_DISCONNECTED 0 #define WIFI_STATE_DISCONNECTED 0
#define WIFI_STATE_CONNECTING 1 #define WIFI_STATE_CONNECTING 1
#define WIFI_STATE_CONNECTED 2 #define WIFI_STATE_CONNECTED 2
@ -107,7 +98,8 @@ typedef struct {
struct { float v_mv; float duration_s; } clean; struct { float v_mv; float duration_s; } clean;
float cell_k; float cell_k;
float cl_factor; float cl_factor;
struct { float slope; float offset; } ph_cal; struct { uint8_t buffer_id; uint8_t temp_slot; float stabilize_s; } ph_cal_point;
struct { uint8_t buffer_id; uint8_t temp_slot; } ph_cal_clear;
struct { uint8_t name_len; char name[MAX_SESSION_NAME]; } session_create; struct { uint8_t name_len; char name[MAX_SESSION_NAME]; } session_create;
struct { uint8_t id; } session_switch; struct { uint8_t id; } session_switch;
struct { uint8_t id; uint8_t name_len; char name[MAX_SESSION_NAME]; } session_rename; struct { uint8_t id; uint8_t name_len; char name[MAX_SESSION_NAME]; } session_rename;
@ -169,6 +161,9 @@ int send_cl_factor(float f);
/* outbound: pH calibration */ /* outbound: pH calibration */
int send_ph_cal(float slope, float offset); int send_ph_cal(float slope, float offset);
int send_ph_cal_point(uint8_t buf, uint8_t tslot, float ocp_mv, float temp_c,
float buffer_ph, uint8_t baseline_count);
int send_ph_cal_status(void);
/* outbound: reference collection */ /* outbound: reference collection */
int send_ref_frame(uint8_t mode, uint8_t rtia_idx); int send_ref_frame(uint8_t mode, uint8_t rtia_idx);

View File

@ -10,7 +10,7 @@
#define KEY_AP_PASS "ap_pass" #define KEY_AP_PASS "ap_pass"
#define DEFAULT_AP_SSID "EIS4" #define DEFAULT_AP_SSID "EIS4"
#define DEFAULT_AP_PASS "eis4data" #define DEFAULT_AP_PASS ""
static char sta_ssid[WIFI_CFG_SSID_MAX]; static char sta_ssid[WIFI_CFG_SSID_MAX];
static char sta_pass[WIFI_CFG_PASS_MAX]; static char sta_pass[WIFI_CFG_PASS_MAX];

View File

@ -1,6 +1,6 @@
#include "wifi_transport.h" #include "wifi_transport.h"
#include "wifi_cfg.h" #include "wifi_cfg.h"
#include "ble_prov.h" #include "captive_portal.h"
#include "protocol.h" #include "protocol.h"
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
@ -46,6 +46,7 @@ static SemaphoreHandle_t client_mutex;
static uint8_t s_sta_state = WIFI_STATE_DISCONNECTED; static uint8_t s_sta_state = WIFI_STATE_DISCONNECTED;
static uint32_t s_sta_ip; static uint32_t s_sta_ip;
static int s_sta_retries; static int s_sta_retries;
static bool s_sta_give_up;
static EventGroupHandle_t s_reconnect_eg; static EventGroupHandle_t s_reconnect_eg;
static void client_touch(const struct sockaddr_in *addr) static void client_touch(const struct sockaddr_in *addr)
@ -185,10 +186,16 @@ static void parse_udp_sysex(const uint8_t *data, uint16_t len)
if (len < 8) return; if (len < 8) return;
cmd.cl_factor = decode_float(&data[3]); cmd.cl_factor = decode_float(&data[3]);
break; break;
case CMD_SET_PH_CAL: case CMD_PH_CAL_POINT:
if (len < 13) return; if (len < 10) return;
cmd.ph_cal.slope = decode_float(&data[3]); cmd.ph_cal_point.buffer_id = data[3] & 0x7F;
cmd.ph_cal.offset = decode_float(&data[8]); cmd.ph_cal_point.temp_slot = data[4] & 0x7F;
cmd.ph_cal_point.stabilize_s = decode_float(&data[5]);
break;
case CMD_PH_CAL_CLEAR:
if (len < 5) return;
cmd.ph_cal_clear.buffer_id = data[3] & 0x7F;
cmd.ph_cal_clear.temp_slot = data[4] & 0x7F;
break; break;
case CMD_SESSION_CREATE: case CMD_SESSION_CREATE:
if (len < 5) return; if (len < 5) return;
@ -218,6 +225,7 @@ static void parse_udp_sysex(const uint8_t *data, uint16_t len)
case CMD_GET_CELL_K: case CMD_GET_CELL_K:
case CMD_GET_CL_FACTOR: case CMD_GET_CL_FACTOR:
case CMD_GET_PH_CAL: case CMD_GET_PH_CAL:
case CMD_PH_CAL_STATUS:
case CMD_START_REFS: case CMD_START_REFS:
case CMD_GET_REFS: case CMD_GET_REFS:
case CMD_CLEAR_REFS: case CMD_CLEAR_REFS:
@ -320,6 +328,25 @@ static void wifi_event_handler(void *arg, esp_event_base_t base,
} }
} }
static void reconfigure_ap_open(void)
{
char ap_ssid[WIFI_CFG_SSID_MAX];
char ap_pass[WIFI_CFG_PASS_MAX];
wifi_cfg_get_ap(ap_ssid, sizeof(ap_ssid), ap_pass, sizeof(ap_pass));
wifi_config_t ap_cfg = {
.ap = {
.channel = WIFI_CHANNEL,
.max_connection = WIFI_MAX_CONN,
.authmode = WIFI_AUTH_OPEN,
},
};
strncpy((char *)ap_cfg.ap.ssid, ap_ssid, sizeof(ap_cfg.ap.ssid) - 1);
ap_cfg.ap.ssid_len = strlen(ap_ssid);
esp_wifi_set_config(WIFI_IF_AP, &ap_cfg);
printf("WiFi: AP reconfigured as open\n");
}
static void sta_event_handler(void *arg, esp_event_base_t base, static void sta_event_handler(void *arg, esp_event_base_t base,
int32_t id, void *data) int32_t id, void *data)
{ {
@ -333,10 +360,13 @@ static void sta_event_handler(void *arg, esp_event_base_t base,
if (s_sta_retries >= STA_RETRY_MAX) { if (s_sta_retries >= STA_RETRY_MAX) {
if (s_reconnect_eg) if (s_reconnect_eg)
xEventGroupSetBits(s_reconnect_eg, RECONNECT_BIT_FAIL); xEventGroupSetBits(s_reconnect_eg, RECONNECT_BIT_FAIL);
if (!ble_prov_is_active()) { if (!s_sta_give_up) {
printf("WiFi: STA retries exhausted, starting BLE provisioning\n"); s_sta_give_up = true;
ble_prov_start(); printf("WiFi: STA retries exhausted, starting captive portal\n");
reconfigure_ap_open();
captive_portal_start();
} }
return;
} }
s_sta_state = WIFI_STATE_CONNECTING; s_sta_state = WIFI_STATE_CONNECTING;
@ -346,13 +376,14 @@ static void sta_event_handler(void *arg, esp_event_base_t base,
s_sta_state = WIFI_STATE_CONNECTED; s_sta_state = WIFI_STATE_CONNECTED;
s_sta_ip = evt->ip_info.ip.addr; s_sta_ip = evt->ip_info.ip.addr;
s_sta_retries = 0; s_sta_retries = 0;
s_sta_give_up = false;
printf("WiFi: STA connected, IP " IPSTR "\n", IP2STR(&evt->ip_info.ip)); printf("WiFi: STA connected, IP " IPSTR "\n", IP2STR(&evt->ip_info.ip));
if (s_reconnect_eg) if (s_reconnect_eg)
xEventGroupSetBits(s_reconnect_eg, RECONNECT_BIT_OK); xEventGroupSetBits(s_reconnect_eg, RECONNECT_BIT_OK);
if (ble_prov_is_active()) if (captive_portal_is_active())
ble_prov_stop(); captive_portal_stop();
} }
} }
@ -365,6 +396,7 @@ void wifi_transport_get_sta_state(uint8_t *state, uint32_t *ip)
esp_err_t wifi_transport_reconnect_sta(const char *ssid, const char *pass) esp_err_t wifi_transport_reconnect_sta(const char *ssid, const char *pass)
{ {
s_sta_retries = 0; s_sta_retries = 0;
s_sta_give_up = false;
s_sta_state = WIFI_STATE_CONNECTING; s_sta_state = WIFI_STATE_CONNECTING;
esp_wifi_disconnect(); esp_wifi_disconnect();
@ -415,12 +447,16 @@ static int wifi_ap_init(void)
.ap = { .ap = {
.channel = WIFI_CHANNEL, .channel = WIFI_CHANNEL,
.max_connection = WIFI_MAX_CONN, .max_connection = WIFI_MAX_CONN,
.authmode = WIFI_AUTH_WPA2_PSK,
}, },
}; };
strncpy((char *)ap_cfg.ap.ssid, ap_ssid, sizeof(ap_cfg.ap.ssid) - 1); strncpy((char *)ap_cfg.ap.ssid, ap_ssid, sizeof(ap_cfg.ap.ssid) - 1);
ap_cfg.ap.ssid_len = strlen(ap_ssid); ap_cfg.ap.ssid_len = strlen(ap_ssid);
if (ap_pass[0]) {
strncpy((char *)ap_cfg.ap.password, ap_pass, sizeof(ap_cfg.ap.password) - 1); strncpy((char *)ap_cfg.ap.password, ap_pass, sizeof(ap_cfg.ap.password) - 1);
ap_cfg.ap.authmode = WIFI_AUTH_WPA2_PSK;
} else {
ap_cfg.ap.authmode = WIFI_AUTH_OPEN;
}
esp_wifi_set_mode(WIFI_MODE_APSTA); esp_wifi_set_mode(WIFI_MODE_APSTA);
esp_wifi_set_config(WIFI_IF_AP, &ap_cfg); esp_wifi_set_config(WIFI_IF_AP, &ap_cfg);
@ -448,11 +484,12 @@ static int wifi_ap_init(void)
esp_wifi_connect(); esp_wifi_connect();
printf("WiFi: STA connecting to \"%s\"\n", sta_ssid); printf("WiFi: STA connecting to \"%s\"\n", sta_ssid);
} else { } else {
printf("WiFi: no STA credentials, starting BLE provisioning\n"); printf("WiFi: no STA credentials, starting captive portal\n");
ble_prov_start(); captive_portal_start();
} }
printf("WiFi: AP \"%s\" on channel %d\n", ap_ssid, WIFI_CHANNEL); printf("WiFi: AP \"%s\" %s on channel %d\n",
ap_ssid, ap_pass[0] ? "(secured)" : "(open)", WIFI_CHANNEL);
return 0; return 0;
} }

View File

@ -1,6 +1,5 @@
CONFIG_IDF_TARGET="esp32s3" CONFIG_IDF_TARGET="esp32s3"
CONFIG_LWIP_DHCPS_MAX_STATION_NUM=12 CONFIG_LWIP_DHCPS_MAX_STATION_NUM=12
CONFIG_ESP_WIFI_SLP_IRAM_OPT=n CONFIG_ESP_WIFI_SLP_IRAM_OPT=n
CONFIG_BT_ENABLED=y CONFIG_BT_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_ESP_WIFI_IRAM_OPT=n CONFIG_ESP_WIFI_IRAM_OPT=n