Files
wpa_ctrl/jl_wifi_ctrl.c
2026-10-30 13:55:31 +04:00

604 lines
16 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <poll.h>
#include "jl_modbus.h"
#include "jl_wifi_ctrl.h"
extern uint32_t custom_tick_get(void);
#ifndef JL_WIFI_CTRL_PATH
#define JL_WIFI_CTRL_PATH "/var/run/wpa_supplicant/wlan0"
#endif
#define JL_WIFI_CTRL_RECONNECT_COOLDOWN_MS 2000
/* Used by the periodic SCAN trigger (JLV_WLAN_SCAN_INTERVAL_MS apart, so a
* blocking wait here is cheap) and other short one-shot commands. STATUS and
* SCAN_RESULTS reads no longer go through this: they're polled on a tight UI
* cycle (every 1-2s) and are handled asynchronously instead (see
* jl_wifi_ctrl_request() / jl_wifi_ctrl_poll()) so they never block the UI
* task thread. */
#define JL_WIFI_CTRL_TXN_TIMEOUT_MS 250
/* Used by one-shot config/action commands (SET_NETWORK, ENABLE_NETWORK,
* RECONNECT, ...) triggered from a button press rather than a poll tick.
* wpa_supplicant can take a while to reply while it's busy processing a
* just-finished scan, which is exactly when the user tends to fire these
* (right after picking a network from the scan list) - 250ms was too
* tight and made the command silently no-op, leaving the network half
* configured. */
#define JL_WIFI_CTRL_ACTION_TIMEOUT_MS 1500
/* Safety net for an async STATUS/SCAN_RESULTS request that never gets a
* reply (e.g. wpa_supplicant wedged) - without this pending_req would stay
* stuck forever and every future request would silently no-op. */
#define JL_WIFI_CTRL_REQ_TIMEOUT_MS 2000
#define JL_WIFI_CTRL_SSID_RAW_MAX (JLV_WIFI_SSID_BYTES*4+1)
#define JL_WIFI_CTRL_SCAN_CACHE_MAX 32
typedef enum {
JL_WIFI_CTRL_REQ_NONE = 0,
JL_WIFI_CTRL_REQ_STATUS,
JL_WIFI_CTRL_REQ_SCAN_RESULTS,
} jl_wifi_ctrl_req_t;
static struct wpa_ctrl* ctrl_cmd;
static struct wpa_ctrl* ctrl_mon;
static uint32_t last_connect_attempt_ms;
static uint32_t pending_events;
/* STATUS/SCAN_RESULTS are request/response commands on the single ctrl_cmd
* socket, so only one can be in flight at a time. jl_wifi_ctrl_request()
* sends without waiting; the reply is picked up later, non-blocking, from
* jl_wifi_ctrl_poll() (already ticking every 100ms) and cached below. */
static jl_wifi_ctrl_req_t pending_req = JL_WIFI_CTRL_REQ_NONE;
static uint32_t pending_req_sent_ms;
static jl_wifi_ctrl_status_t cached_status;
static bool cached_status_valid;
static jl_wifi_ctrl_scan_result_t cached_scan[JL_WIFI_CTRL_SCAN_CACHE_MAX];
static int cached_scan_count;
static void jl_wifi_ctrl_apply_reply(jl_wifi_ctrl_req_t req, char* reply, size_t reply_len);
static void jl_wifi_ctrl_teardown(void)
{
if (ctrl_cmd) {
wpa_ctrl_close(ctrl_cmd);
ctrl_cmd = NULL;
}
if (ctrl_mon) {
wpa_ctrl_close(ctrl_mon);
ctrl_mon = NULL;
}
/* Any request in flight belonged to the now-closed socket; its reply,
* if it ever arrives, is not coming back to us. Drop cached data too,
* so callers see "unavailable" instead of stale pre-disconnect state. */
pending_req = JL_WIFI_CTRL_REQ_NONE;
cached_status_valid = false;
cached_scan_count = 0;
}
static bool jl_wifi_ctrl_ensure_connected(void)
{
if (ctrl_cmd && ctrl_mon) {
return true;
}
uint32_t now = custom_tick_get();
if (last_connect_attempt_ms != 0 &&
(uint32_t)(now - last_connect_attempt_ms) < JL_WIFI_CTRL_RECONNECT_COOLDOWN_MS) {
return false;
}
last_connect_attempt_ms = now;
jl_wifi_ctrl_teardown();
ctrl_cmd = wpa_ctrl_open(JL_WIFI_CTRL_PATH);
if (!ctrl_cmd) {
return false;
}
ctrl_mon = wpa_ctrl_open(JL_WIFI_CTRL_PATH);
if (!ctrl_mon || wpa_ctrl_attach(ctrl_mon) != 0) {
log_debug("jl_wifi_ctrl: attach failed");
jl_wifi_ctrl_teardown();
return false;
}
log_info("jl_wifi_ctrl: connected to %s", JL_WIFI_CTRL_PATH);
return true;
}
/* Bounded (~JL_WIFI_CTRL_TXN_TIMEOUT_MS) request/response, bypassing
* wpa_ctrl_request()'s internal 10s blocking retry loop - it's fine for the
* rare ATTACH/DETACH call (done via wpa_ctrl_attach() above), but far too
* coarse for commands issued from a UI poll tick. */
static bool jl_wifi_ctrl_txn_timeout(struct wpa_ctrl* ctrl, const char* cmd, char* reply, size_t* reply_len, int timeout_ms)
{
int fd = wpa_ctrl_get_fd(ctrl);
if (fd < 0) {
return false;
}
if (send(fd, cmd, strlen(cmd), 0) < 0) {
log_debug("jl_wifi_ctrl: send failed, reconnecting");
jl_wifi_ctrl_teardown();
return false;
}
struct pollfd pfd = { .fd = fd, .events = POLLIN };
if (poll(&pfd, 1, timeout_ms) <= 0 || !(pfd.revents & POLLIN)) {
return false;
}
return wpa_ctrl_recv(ctrl, reply, reply_len) == 0;
}
/* Waits (up to timeout_ms) for a STATUS/SCAN_RESULTS request already in
* flight to be answered, before a synchronous command is allowed to send on
* the same socket - otherwise the two replies could arrive out of order and
* get attributed to the wrong request. Rare in practice (async requests are
* answered within a poll tick or two), so this basically never actually
* waits. */
static void jl_wifi_ctrl_drain_pending(int timeout_ms)
{
if (pending_req == JL_WIFI_CTRL_REQ_NONE) {
return;
}
int fd = wpa_ctrl_get_fd(ctrl_cmd);
struct pollfd pfd = { .fd = fd, .events = POLLIN };
if (fd >= 0 && poll(&pfd, 1, timeout_ms) > 0 && (pfd.revents & POLLIN)) {
static char reply[8192];
size_t reply_len = sizeof(reply) - 1;
if (wpa_ctrl_recv(ctrl_cmd, reply, &reply_len) == 0) {
jl_wifi_ctrl_apply_reply(pending_req, reply, reply_len);
}
}
pending_req = JL_WIFI_CTRL_REQ_NONE;
}
static bool jl_wifi_ctrl_cmd_ok_timeout(const char* cmd, int timeout_ms)
{
if (!jl_wifi_ctrl_ensure_connected()) {
return false;
}
jl_wifi_ctrl_drain_pending(timeout_ms);
char reply[64];
size_t reply_len = sizeof(reply) - 1;
if (!jl_wifi_ctrl_txn_timeout(ctrl_cmd, cmd, reply, &reply_len, timeout_ms)) {
log_warn("jl_wifi_ctrl: '%s' timed out after %dms", cmd, timeout_ms);
return false;
}
reply[reply_len] = '\0';
bool ok = strncmp(reply, "OK", 2) == 0;
if (!ok) {
log_warn("jl_wifi_ctrl: '%s' -> '%s'", cmd, reply);
}
return ok;
}
static bool jl_wifi_ctrl_cmd_ok(const char* cmd)
{
return jl_wifi_ctrl_cmd_ok_timeout(cmd, JL_WIFI_CTRL_TXN_TIMEOUT_MS);
}
static bool jl_wifi_ctrl_action_ok(const char* cmd)
{
return jl_wifi_ctrl_cmd_ok_timeout(cmd, JL_WIFI_CTRL_ACTION_TIMEOUT_MS);
}
static const char* jl_wifi_ctrl_find_field(const char* buf, const char* key)
{
size_t keylen = strlen(key);
if (strncmp(buf, key, keylen) == 0) {
return buf + keylen;
}
const char* p = buf;
while ((p = strstr(p, key)) != NULL) {
if (p[-1] == '\n') {
return p + keylen;
}
p++;
}
return NULL;
}
static void jl_wifi_ctrl_copy_field(const char* val, char* out, size_t outsz)
{
const char* nl = strchr(val, '\n');
size_t len = nl ? (size_t)(nl - val) : strlen(val);
if (len >= outsz) {
len = outsz - 1;
}
memcpy(out, val, len);
out[len] = '\0';
}
static jl_wifi_ctrl_state_t jl_wifi_ctrl_map_state(const char* s)
{
if (strcmp(s, "COMPLETED") == 0) return JL_WIFI_CTRL_STATE_COMPLETED;
if (strcmp(s, "DISCONNECTED") == 0) return JL_WIFI_CTRL_STATE_DISCONNECTED;
if (strcmp(s, "SCANNING") == 0) return JL_WIFI_CTRL_STATE_SCANNING;
if (strcmp(s, "ASSOCIATED") == 0) return JL_WIFI_CTRL_STATE_ASSOCIATED;
if (strcmp(s, "4WAY_HANDSHAKE") == 0) return JL_WIFI_CTRL_STATE_4WAY_HANDSHAKE;
if (strcmp(s, "INACTIVE") == 0) return JL_WIFI_CTRL_STATE_INACTIVE;
return JL_WIFI_CTRL_STATE_UNKNOWN;
}
static void jl_wifi_ctrl_parse_status(const char* reply, jl_wifi_ctrl_status_t* out)
{
const char* val;
if ((val = jl_wifi_ctrl_find_field(reply, "ssid=")) != NULL) {
char tmp[JL_WIFI_CTRL_SSID_RAW_MAX];
jl_wifi_ctrl_copy_field(val, tmp, sizeof(tmp));
jl_wifi_ssid_decode(out->ssid, sizeof(out->ssid), tmp);
}
if ((val = jl_wifi_ctrl_find_field(reply, "id=")) != NULL) {
out->id_bss = (int)strtol(val, NULL, 10);
}
if ((val = jl_wifi_ctrl_find_field(reply, "wpa_state=")) != NULL) {
char tmp[32];
jl_wifi_ctrl_copy_field(val, tmp, sizeof(tmp));
out->state = jl_wifi_ctrl_map_state(tmp);
}
if ((val = jl_wifi_ctrl_find_field(reply, "ip_address=")) != NULL) {
jl_wifi_ctrl_copy_field(val, out->ip_address, sizeof(out->ip_address));
}
if ((val = jl_wifi_ctrl_find_field(reply, "address=")) != NULL) {
jl_wifi_ctrl_copy_field(val, out->mac_address, sizeof(out->mac_address));
}
}
static int jl_wifi_ctrl_parse_scan_results(char* reply, jl_wifi_ctrl_scan_result_t* out, int max)
{
int count = 0;
bool header_skipped = false;
char* line_save = NULL;
char* line = strtok_r(reply, "\n", &line_save);
while (line && count < max) {
if (!header_skipped) {
header_skipped = true;
line = strtok_r(NULL, "\n", &line_save);
continue;
}
jl_wifi_ctrl_scan_result_t* ap = &out[count];
memset(ap, 0, sizeof(*ap));
char* field_save = NULL;
char* field = strtok_r(line, "\t", &field_save);
int step = 0;
while (field) {
switch (step) {
case 0:
snprintf(ap->bssid, sizeof(ap->bssid), "%s", field);
break;
case 1:
ap->freq = atoi(field);
break;
case 2:
ap->level = strtol(field, NULL, 10);
break;
case 3:
ap->wpa2 = true;
break;
case 4:
jl_wifi_ssid_decode(ap->ssid, sizeof(ap->ssid), field);
break;
}
field = strtok_r(NULL, "\t", &field_save);
step++;
}
if (step == 5) {
count++;
}
line = strtok_r(NULL, "\n", &line_save);
}
return count;
}
/* Parses a completed async reply into the cache matching the request that
* produced it. Shared by jl_wifi_ctrl_poll() (normal, non-blocking path) and
* jl_wifi_ctrl_drain_pending() (the rare fallback used by synchronous action
* commands). */
static void jl_wifi_ctrl_apply_reply(jl_wifi_ctrl_req_t req, char* reply, size_t reply_len)
{
reply[reply_len] = '\0';
if (req == JL_WIFI_CTRL_REQ_STATUS) {
jl_wifi_ctrl_status_t st = {0};
st.state = JL_WIFI_CTRL_STATE_UNKNOWN;
jl_wifi_ctrl_parse_status(reply, &st);
st.valid = true;
cached_status = st;
cached_status_valid = true;
} else if (req == JL_WIFI_CTRL_REQ_SCAN_RESULTS) {
cached_scan_count = jl_wifi_ctrl_parse_scan_results(reply, cached_scan, JL_WIFI_CTRL_SCAN_CACHE_MAX);
}
}
/* Fires a STATUS/SCAN_RESULTS request without waiting for the reply. A
* no-op if one is already in flight - the caller just keeps seeing the last
* cached result until it resolves. */
static void jl_wifi_ctrl_request(jl_wifi_ctrl_req_t req, const char* cmd)
{
if (pending_req != JL_WIFI_CTRL_REQ_NONE) {
return;
}
int fd = wpa_ctrl_get_fd(ctrl_cmd);
if (fd < 0) {
return;
}
if (send(fd, cmd, strlen(cmd), 0) < 0) {
log_debug("jl_wifi_ctrl: send failed, reconnecting");
jl_wifi_ctrl_teardown();
return;
}
pending_req = req;
pending_req_sent_ms = custom_tick_get();
}
static void jl_wifi_ctrl_handle_event(const char* msg)
{
const char* p = msg;
if (*p == '<') {
const char* gt = strchr(p, '>');
if (gt) {
p = gt + 1;
}
}
if (strncmp(p, WPA_EVENT_SCAN_RESULTS, strlen(WPA_EVENT_SCAN_RESULTS)) == 0) {
pending_events |= JL_WIFI_EVT_SCAN_RESULTS;
} else if (strncmp(p, WPA_EVENT_CONNECTED, strlen(WPA_EVENT_CONNECTED)) == 0) {
pending_events |= JL_WIFI_EVT_CONNECTED;
} else if (strncmp(p, WPA_EVENT_DISCONNECTED, strlen(WPA_EVENT_DISCONNECTED)) == 0) {
pending_events |= JL_WIFI_EVT_DISCONNECTED;
} else if (strncmp(p, WPA_EVENT_TERMINATING, strlen(WPA_EVENT_TERMINATING)) == 0) {
pending_events |= JL_WIFI_EVT_TERMINATING;
} else if (strncmp(p, WPA_EVENT_TEMP_DISABLED, strlen(WPA_EVENT_TEMP_DISABLED)) == 0) {
pending_events |= JL_WIFI_EVT_AUTH_FAILED;
}
}
void jl_wifi_ctrl_poll(void)
{
if (!jl_wifi_ctrl_ensure_connected()) {
return;
}
if (pending_req != JL_WIFI_CTRL_REQ_NONE) {
int cmd_fd = wpa_ctrl_get_fd(ctrl_cmd);
struct pollfd pfd_cmd = { .fd = cmd_fd, .events = POLLIN };
if (poll(&pfd_cmd, 1, 0) > 0 && (pfd_cmd.revents & POLLIN)) {
static char reply[8192];
size_t reply_len = sizeof(reply) - 1;
if (wpa_ctrl_recv(ctrl_cmd, reply, &reply_len) == 0) {
jl_wifi_ctrl_apply_reply(pending_req, reply, reply_len);
}
pending_req = JL_WIFI_CTRL_REQ_NONE;
} else if ((uint32_t)(custom_tick_get() - pending_req_sent_ms) > JL_WIFI_CTRL_REQ_TIMEOUT_MS) {
log_warn("jl_wifi_ctrl: async request timed out after %dms", JL_WIFI_CTRL_REQ_TIMEOUT_MS);
pending_req = JL_WIFI_CTRL_REQ_NONE;
}
}
struct pollfd pfd_mon = { .fd = wpa_ctrl_get_fd(ctrl_mon), .events = POLLIN };
while (poll(&pfd_mon, 1, 0) > 0 && (pfd_mon.revents & POLLIN)) {
static char buf[4096];
size_t len = sizeof(buf) - 1;
if (wpa_ctrl_recv(ctrl_mon, buf, &len) < 0) {
log_debug("jl_wifi_ctrl: monitor recv failed, reconnecting");
jl_wifi_ctrl_teardown();
return;
}
buf[len] = '\0';
jl_wifi_ctrl_handle_event(buf);
}
}
uint32_t jl_wifi_ctrl_events_get_and_clear(void)
{
uint32_t events = pending_events;
pending_events = 0;
return events;
}
bool jl_wifi_ctrl_status_get(jl_wifi_ctrl_status_t* out)
{
if (!jl_wifi_ctrl_ensure_connected()) {
memset(out, 0, sizeof(*out));
out->state = JL_WIFI_CTRL_STATE_UNKNOWN;
return false;
}
jl_wifi_ctrl_request(JL_WIFI_CTRL_REQ_STATUS, "STATUS");
if (!cached_status_valid) {
memset(out, 0, sizeof(*out));
out->state = JL_WIFI_CTRL_STATE_UNKNOWN;
return false;
}
*out = cached_status;
return true;
}
bool jl_wifi_ctrl_enable_network(int id)
{
char cmd[32];
snprintf(cmd, sizeof(cmd), "ENABLE_NETWORK %d", id);
return jl_wifi_ctrl_action_ok(cmd);
}
bool jl_wifi_ctrl_disable_network(int id)
{
char cmd[32];
snprintf(cmd, sizeof(cmd), "DISABLE_NETWORK %d", id);
return jl_wifi_ctrl_action_ok(cmd);
}
bool jl_wifi_ctrl_reconnect(void)
{
return jl_wifi_ctrl_action_ok("RECONNECT");
}
bool jl_wifi_ctrl_disconnect(void)
{
return jl_wifi_ctrl_action_ok("DISCONNECT");
}
static bool jl_wifi_ctrl_txn_reply(const char* cmd, char* reply, size_t reply_sz, int timeout_ms)
{
if (!jl_wifi_ctrl_ensure_connected()) {
return false;
}
jl_wifi_ctrl_drain_pending(timeout_ms);
size_t reply_len = reply_sz - 1;
if (!jl_wifi_ctrl_txn_timeout(ctrl_cmd, cmd, reply, &reply_len, timeout_ms)) {
log_warn("jl_wifi_ctrl: '%s' timed out after %dms", cmd, timeout_ms);
return false;
}
reply[reply_len] = '\0';
return true;
}
int jl_wifi_ctrl_add_network(void)
{
char reply[32];
if (!jl_wifi_ctrl_txn_reply("ADD_NETWORK", reply, sizeof(reply),
JL_WIFI_CTRL_ACTION_TIMEOUT_MS)) {
return -1;
}
if (strncmp(reply, "FAIL", 4) == 0) {
log_warn("jl_wifi_ctrl: ADD_NETWORK -> '%s'", reply);
return -1;
}
int id = (int)strtol(reply, NULL, 10);
log_info("jl_wifi_ctrl: ADD_NETWORK -> %d", id);
return id;
}
static bool jl_wifi_ctrl_list_has_id0(void)
{
char reply[512];
if (!jl_wifi_ctrl_txn_reply("LIST_NETWORKS", reply, sizeof(reply),
JL_WIFI_CTRL_ACTION_TIMEOUT_MS)) {
return false;
}
char* line = strchr(reply, '\n');
if (!line) {
return false;
}
for (line++; line && *line; ) {
if (line[0] == '0' && (line[1] == '\t' || line[1] == ' ' ||
line[1] == '\n' || line[1] == '\0')) {
return true;
}
char* nl = strchr(line, '\n');
line = nl ? nl + 1 : NULL;
}
return false;
}
bool jl_wifi_ctrl_ensure_network0(void)
{
if (jl_wifi_ctrl_list_has_id0()) {
return true;
}
if (jl_wifi_ctrl_add_network() != 0) {
return false;
}
jl_wifi_ctrl_save_config();
return true;
}
bool jl_wifi_ctrl_set_network_ssid_hex(int id, const char* ssid_hex)
{
char cmd[JL_WIFI_CTRL_SSID_RAW_MAX + 32];
int n = snprintf(cmd, sizeof(cmd), "SET_NETWORK %d ssid %s", id, ssid_hex);
if (n < 0 || (size_t)n >= sizeof(cmd)) {
return false;
}
if (jl_wifi_ctrl_action_ok(cmd)) {
return true;
}
if (id != 0) {
return false;
}
/* Empty factory network={} can occupy id 0 and still reject SET. */
jl_wifi_ctrl_action_ok("REMOVE_NETWORK 0");
if (jl_wifi_ctrl_add_network() != 0) {
return false;
}
return jl_wifi_ctrl_action_ok(cmd);
}
bool jl_wifi_ctrl_set_network_psk(int id, const char* psk_plaintext)
{
char cmd[128];
int n = snprintf(cmd, sizeof(cmd), "SET_NETWORK %d psk \"%s\"", id, psk_plaintext);
if (n < 0 || (size_t)n >= sizeof(cmd)) {
return false;
}
return jl_wifi_ctrl_action_ok(cmd);
}
bool jl_wifi_ctrl_set_network_scan_ssid(int id, bool enable)
{
char cmd[32];
snprintf(cmd, sizeof(cmd), "SET_NETWORK %d scan_ssid %d", id, enable ? 1 : 0);
return jl_wifi_ctrl_action_ok(cmd);
}
bool jl_wifi_ctrl_save_config(void)
{
return jl_wifi_ctrl_action_ok("SAVE_CONFIG");
}
bool jl_wifi_ctrl_scan(void)
{
return jl_wifi_ctrl_cmd_ok("SCAN");
}
int jl_wifi_ctrl_scan_results_get(jl_wifi_ctrl_scan_result_t* out, int max)
{
if (!jl_wifi_ctrl_ensure_connected()) {
return 0;
}
jl_wifi_ctrl_request(JL_WIFI_CTRL_REQ_SCAN_RESULTS, "SCAN_RESULTS");
int n = cached_scan_count < max ? cached_scan_count : max;
if (n > 0) {
memcpy(out, cached_scan, n * sizeof(*out));
}
return n;
}