replace BLE provisioning with captive portal, add 9-point pH calibration with temperature correction
This commit is contained in:
parent
ff51b9ef23
commit
80319bb259
|
|
@ -1,4 +1,4 @@
|
|||
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 "."
|
||||
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)
|
||||
|
|
|
|||
391
main/ble_prov.c
391
main/ble_prov.c
|
|
@ -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,
|
||||
¶ms, 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 & 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
14
main/echem.c
14
main/echem.c
|
|
@ -1,4 +1,5 @@
|
|||
#include "echem.h"
|
||||
#include "eis.h"
|
||||
#include "ad5940.h"
|
||||
#include "protocol.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 ocp = v_se0 - v_re0;
|
||||
|
||||
float t_k = cfg->temp_c + 273.15f;
|
||||
float slope = 0.1984f * t_k; /* mV/pH at temperature */
|
||||
float ocp_corrected = ocp;
|
||||
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->ph = 7.0f - ocp / slope;
|
||||
result->ph = eis_get_ph_slope() * ocp_corrected + eis_get_ph_offset();
|
||||
result->temp_c = cfg->temp_c;
|
||||
|
||||
printf("pH: SE0=%.1f mV, RE0=%.1f mV, OCP=%.1f mV, pH=%.2f\n",
|
||||
|
|
|
|||
164
main/eis.c
164
main/eis.c
|
|
@ -651,30 +651,168 @@ void eis_load_cl_factor(void)
|
|||
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;
|
||||
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_OFFSET_KEY, &offset, sizeof(offset));
|
||||
nvs_set_blob(h, NVS_PH_CAL_PTS_KEY, &ph_cal, sizeof(ph_cal));
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
}
|
||||
|
||||
float eis_get_ph_slope(void) { return ph_slope_cached; }
|
||||
float eis_get_ph_offset(void) { return ph_offset_cached; }
|
||||
float eis_get_ph_temp_slope_cold(void) { return ph_temp_slope_cold; }
|
||||
float eis_get_ph_temp_slope_hot(void) { return ph_temp_slope_hot; }
|
||||
|
||||
void eis_load_ph_cal(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_OCAL_NS, NVS_READONLY, &h) != ESP_OK) return;
|
||||
size_t len = sizeof(ph_slope_cached);
|
||||
if (nvs_get_blob(h, NVS_PH_SLOPE_KEY, &ph_slope_cached, &len) != ESP_OK || len != sizeof(ph_slope_cached))
|
||||
ph_slope_cached = 0.0f;
|
||||
len = sizeof(ph_offset_cached);
|
||||
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;
|
||||
size_t len = sizeof(ph_cal);
|
||||
if (nvs_get_blob(h, NVS_PH_CAL_PTS_KEY, &ph_cal, &len) != ESP_OK
|
||||
|| len != sizeof(ph_cal)) {
|
||||
memset(&ph_cal, 0, sizeof(ph_cal));
|
||||
}
|
||||
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];
|
||||
}
|
||||
|
|
|
|||
17
main/eis.h
17
main/eis.h
|
|
@ -2,6 +2,7 @@
|
|||
#define EIS_H
|
||||
|
||||
#include "ad5940.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
#define EIS_MAX_POINTS 100
|
||||
|
||||
|
|
@ -71,9 +72,23 @@ void eis_set_cl_factor(float f);
|
|||
float eis_get_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_offset(void);
|
||||
float eis_get_ph_temp_slope_cold(void);
|
||||
float eis_get_ph_temp_slope_hot(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
|
||||
|
|
|
|||
48
main/eis4.c
48
main/eis4.c
|
|
@ -250,10 +250,50 @@ void app_main(void)
|
|||
send_cl_factor(eis_get_cl_factor());
|
||||
break;
|
||||
|
||||
case CMD_SET_PH_CAL:
|
||||
eis_set_ph_cal(cmd.ph_cal.slope, cmd.ph_cal.offset);
|
||||
send_ph_cal(cmd.ph_cal.slope, cmd.ph_cal.offset);
|
||||
printf("pH cal set: slope=%.4f offset=%.4f\n", cmd.ph_cal.slope, cmd.ph_cal.offset);
|
||||
case CMD_PH_CAL_POINT: {
|
||||
uint8_t bid = cmd.ph_cal_point.buffer_id;
|
||||
uint8_t tsl = cmd.ph_cal_point.temp_slot;
|
||||
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;
|
||||
|
||||
case CMD_GET_PH_CAL:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "protocol.h"
|
||||
#include "eis.h"
|
||||
#include "wifi_transport.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -357,6 +358,44 @@ int send_ph_cal(float slope, float offset)
|
|||
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 ---- */
|
||||
|
||||
int send_ph_result(float v_ocp_mv, float ph, float temp_c,
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@
|
|||
#define CMD_CLEAR_REFS 0x32
|
||||
#define CMD_SET_CL_FACTOR 0x33
|
||||
#define CMD_GET_CL_FACTOR 0x34
|
||||
#define CMD_SET_PH_CAL 0x35
|
||||
#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) */
|
||||
#define CMD_SESSION_CREATE 0x40
|
||||
|
|
@ -38,13 +40,6 @@
|
|||
#define CMD_SESSION_RENAME 0x43
|
||||
#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) */
|
||||
#define RSP_SWEEP_START 0x01
|
||||
#define RSP_DATA_POINT 0x02
|
||||
|
|
@ -68,7 +63,9 @@
|
|||
#define RSP_REFS_DONE 0x22
|
||||
#define RSP_REF_STATUS 0x23
|
||||
#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
|
||||
|
||||
/* Session sync responses (0x4x) */
|
||||
|
|
@ -78,13 +75,7 @@
|
|||
#define RSP_SESSION_RENAMED 0x43
|
||||
#define RSP_CLIENT_LIST 0x44
|
||||
|
||||
/* Provisioning responses (0x6x, over BLE) */
|
||||
#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) */
|
||||
/* WiFi STA state values */
|
||||
#define WIFI_STATE_DISCONNECTED 0
|
||||
#define WIFI_STATE_CONNECTING 1
|
||||
#define WIFI_STATE_CONNECTED 2
|
||||
|
|
@ -107,7 +98,8 @@ typedef struct {
|
|||
struct { float v_mv; float duration_s; } clean;
|
||||
float cell_k;
|
||||
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 id; } session_switch;
|
||||
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 */
|
||||
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 */
|
||||
int send_ref_frame(uint8_t mode, uint8_t rtia_idx);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
#define KEY_AP_PASS "ap_pass"
|
||||
|
||||
#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_pass[WIFI_CFG_PASS_MAX];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#include "wifi_transport.h"
|
||||
#include "wifi_cfg.h"
|
||||
#include "ble_prov.h"
|
||||
#include "captive_portal.h"
|
||||
#include "protocol.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -46,6 +46,7 @@ static SemaphoreHandle_t client_mutex;
|
|||
static uint8_t s_sta_state = WIFI_STATE_DISCONNECTED;
|
||||
static uint32_t s_sta_ip;
|
||||
static int s_sta_retries;
|
||||
static bool s_sta_give_up;
|
||||
static EventGroupHandle_t s_reconnect_eg;
|
||||
|
||||
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;
|
||||
cmd.cl_factor = decode_float(&data[3]);
|
||||
break;
|
||||
case CMD_SET_PH_CAL:
|
||||
if (len < 13) return;
|
||||
cmd.ph_cal.slope = decode_float(&data[3]);
|
||||
cmd.ph_cal.offset = decode_float(&data[8]);
|
||||
case CMD_PH_CAL_POINT:
|
||||
if (len < 10) return;
|
||||
cmd.ph_cal_point.buffer_id = data[3] & 0x7F;
|
||||
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;
|
||||
case CMD_SESSION_CREATE:
|
||||
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_CL_FACTOR:
|
||||
case CMD_GET_PH_CAL:
|
||||
case CMD_PH_CAL_STATUS:
|
||||
case CMD_START_REFS:
|
||||
case CMD_GET_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,
|
||||
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_reconnect_eg)
|
||||
xEventGroupSetBits(s_reconnect_eg, RECONNECT_BIT_FAIL);
|
||||
if (!ble_prov_is_active()) {
|
||||
printf("WiFi: STA retries exhausted, starting BLE provisioning\n");
|
||||
ble_prov_start();
|
||||
if (!s_sta_give_up) {
|
||||
s_sta_give_up = true;
|
||||
printf("WiFi: STA retries exhausted, starting captive portal\n");
|
||||
reconfigure_ap_open();
|
||||
captive_portal_start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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_ip = evt->ip_info.ip.addr;
|
||||
s_sta_retries = 0;
|
||||
s_sta_give_up = false;
|
||||
printf("WiFi: STA connected, IP " IPSTR "\n", IP2STR(&evt->ip_info.ip));
|
||||
|
||||
if (s_reconnect_eg)
|
||||
xEventGroupSetBits(s_reconnect_eg, RECONNECT_BIT_OK);
|
||||
|
||||
if (ble_prov_is_active())
|
||||
ble_prov_stop();
|
||||
if (captive_portal_is_active())
|
||||
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)
|
||||
{
|
||||
s_sta_retries = 0;
|
||||
s_sta_give_up = false;
|
||||
s_sta_state = WIFI_STATE_CONNECTING;
|
||||
|
||||
esp_wifi_disconnect();
|
||||
|
|
@ -415,12 +447,16 @@ static int wifi_ap_init(void)
|
|||
.ap = {
|
||||
.channel = WIFI_CHANNEL,
|
||||
.max_connection = WIFI_MAX_CONN,
|
||||
.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
},
|
||||
};
|
||||
strncpy((char *)ap_cfg.ap.ssid, ap_ssid, sizeof(ap_cfg.ap.ssid) - 1);
|
||||
ap_cfg.ap.ssid_len = strlen(ap_ssid);
|
||||
strncpy((char *)ap_cfg.ap.password, ap_pass, sizeof(ap_cfg.ap.password) - 1);
|
||||
if (ap_pass[0]) {
|
||||
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_config(WIFI_IF_AP, &ap_cfg);
|
||||
|
|
@ -448,11 +484,12 @@ static int wifi_ap_init(void)
|
|||
esp_wifi_connect();
|
||||
printf("WiFi: STA connecting to \"%s\"\n", sta_ssid);
|
||||
} else {
|
||||
printf("WiFi: no STA credentials, starting BLE provisioning\n");
|
||||
ble_prov_start();
|
||||
printf("WiFi: no STA credentials, starting captive portal\n");
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
CONFIG_IDF_TARGET="esp32s3"
|
||||
CONFIG_LWIP_DHCPS_MAX_STATION_NUM=12
|
||||
CONFIG_ESP_WIFI_SLP_IRAM_OPT=n
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_ENABLED=n
|
||||
CONFIG_ESP_WIFI_IRAM_OPT=n
|
||||
|
|
|
|||
Loading…
Reference in New Issue