Add Wi-Fi next-profile rotation control
This commit is contained in:
@@ -55,6 +55,7 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
|
||||
|---|---|
|
||||
| `wifi status` / `wifi profiles` | Show Wi-Fi state or configured station profiles. |
|
||||
| `wifi start` / `wifi stop` / `wifi reconnect` | Start, stop, or reconnect Wi-Fi. |
|
||||
| `wifi next-profile` | Queue a switch to the enabled station profile after the currently active profile in priority order; wraps safely. |
|
||||
| `wifi profile set <slot> <priority> <mixed|wpa3> <ssid>` | Set a station profile. |
|
||||
| `wifi profile secret <slot>` | Set a profile password through a no-echo prompt. |
|
||||
| `wifi profile enable|disable|delete <slot>` | Manage a station-profile slot. |
|
||||
|
||||
@@ -142,9 +142,9 @@ For the OLED-aging policy, leave all three buttons untouched and verify:
|
||||
|
||||
### 9. Local controls (Phase 7D)
|
||||
|
||||
From a status page, use a short Select press to open **Controls**. Previous/next selects an item; Select activates it. The menu includes serial start/stop, Wi-Fi start/stop/reconnect, HTTPS start/stop, SSH start/stop, writer revocation, display off, and reboot. It must never show or change credentials, Wi-Fi profiles, serial framing, TLS/SSH keys, or I²C scan controls.
|
||||
From a status page, use a short Select press to open **Controls**. Previous/next selects an item; Select activates it. The menu includes serial start/stop, Wi-Fi start/stop/reconnect/next-profile, HTTPS start/stop, SSH start/stop, writer revocation, display off, and reboot. `WiFi:next profile` requests the enabled station profile after the active one in priority order and wraps safely; it must never show or change credentials, Wi-Fi profile configuration, serial framing, TLS/SSH keys, or I²C scan controls.
|
||||
|
||||
Stopping a service, Wi-Fi reconnect, writer revocation, and reboot open a confirmation page. Verify that:
|
||||
Stopping a service, Wi-Fi reconnect, Wi-Fi next-profile, writer revocation, and reboot open a confirmation page. Verify that:
|
||||
|
||||
1. Previous/back cancels the confirmation without changing the selected service or writer.
|
||||
2. A short Select press does not execute the action.
|
||||
@@ -153,7 +153,7 @@ Stopping a service, Wi-Fi reconnect, writer revocation, and reboot open a confir
|
||||
5. An untouched confirmation expires to the menu after 30 seconds.
|
||||
6. A dimmed/off OLED consumes the first button press for wake only; its later release and hold must not navigate or confirm an action.
|
||||
|
||||
For Wi-Fi lifecycle calls, confirm the immediate result says `Requested`, then use the status pages to observe the asynchronous state change. HTTPS and SSH starts must fail cleanly when neither station nor AP networking is available. Writer revocation must only release the current writer—never assign a replacement—and the UI must never appear as a broker client. For reboot, observe `Restarting...`, then verify all normal boot services and UART0 recovery return.
|
||||
For Wi-Fi lifecycle calls, confirm the immediate result says `Requested`, then use the status pages to observe the asynchronous state change. For `WiFi:next profile`, configure at least two enabled station profiles, cancel once with Previous/back, then hold Select for two seconds; verify the current station disconnects, the next enabled profile is attempted, and the Overview/Network `WiFi:<SSID> Ch:<channel>` value (or `wifi status`) changes. Repeat until the selection wraps to the first profile. HTTPS and SSH starts must fail cleanly when neither station nor AP networking is available. Writer revocation must only release the current writer—never assign a replacement—and the UI must never appear as a broker client. For reboot, observe `Restarting...`, then verify all normal boot services and UART0 recovery return.
|
||||
|
||||
Run these checks with UART0 available. Repeat appropriate stop/revoke cases with USB CDC, WebSocket, and SSH clients connected; verify the intended session/service is interrupted, unrelated recovery paths remain responsive, and no action injects serial data.
|
||||
|
||||
|
||||
+2
-2
@@ -222,8 +222,8 @@ The persistent yellow strip uses fixed-position serial, Wi-Fi-strength, USB, Web
|
||||
- Target-hardware validation passed for the four pages, fixed active-only status icons, lowercase layout, diagnostics hold, dim/off/wake behavior, all available concurrent client interfaces, UART0 recovery, and serial traffic from 9600 through 230400 baud.
|
||||
4. **Phase 7D — Local controls — Implemented; validation pending**
|
||||
- A shallow local `Controls` menu directly invokes public serial-service, Wi-Fi, HTTPS, SSH, broker, display, and restart APIs; it never parses or injects CLI text.
|
||||
- The menu provides serial start/stop, Wi-Fi start/stop/reconnect, HTTPS start/stop, SSH start/stop, emergency current-writer revocation, display off, and reboot. It deliberately excludes serial configuration editing, credentials, Wi-Fi profile/AP changes, host-key/certificate changes, I²C scans, and assignment of the writer to another client.
|
||||
- A visible confirmation screen and one continuous two-second Select hold are required for service stops, Wi-Fi reconnect, writer revocation, and reboot. The confirmation times out after 30 seconds; previous/back cancels. A wake press is consumed and cannot navigate or confirm an action.
|
||||
- The menu provides serial start/stop, Wi-Fi start/stop/reconnect/next-profile, HTTPS start/stop, SSH start/stop, emergency current-writer revocation, display off, and reboot. `WiFi:next profile` queues a rotation to the enabled profile after the currently active one in priority order, wrapping safely; profile configuration and credentials remain unavailable to the UI. The menu deliberately excludes serial configuration editing, Wi-Fi profile/AP changes, host-key/certificate changes, I²C scans, and assignment of the writer to another client.
|
||||
- A visible confirmation screen and one continuous two-second Select hold are required for service stops, Wi-Fi reconnect, Wi-Fi next-profile, writer revocation, and reboot. The confirmation times out after 30 seconds; previous/back cancels. A wake press is consumed and cannot navigate or confirm an action.
|
||||
- Wi-Fi lifecycle results report a queued request rather than falsely claiming an asynchronous transition has completed. HTTPS/SSH start requests require a connected station or active AP. The current writer is only ever force-released to no writer; the UI is not a broker client and cannot acquire or assign a writer lease.
|
||||
- Pending target-hardware validation: menu navigation/timeouts, each enabled action and error result, confirmation cancellation/chord rejection, wake-press suppression, and preservation of UART0 plus transport recovery.
|
||||
5. **Phase 7E — Reliability, persistence, and documentation — Planned**
|
||||
|
||||
@@ -80,6 +80,7 @@ typedef enum {
|
||||
LOCAL_STATUS_ACTION_SERIAL = 0,
|
||||
LOCAL_STATUS_ACTION_WIFI_POWER,
|
||||
LOCAL_STATUS_ACTION_WIFI_RECONNECT,
|
||||
LOCAL_STATUS_ACTION_WIFI_NEXT_PROFILE,
|
||||
LOCAL_STATUS_ACTION_WEB,
|
||||
LOCAL_STATUS_ACTION_SSH,
|
||||
LOCAL_STATUS_ACTION_REVOKE_WRITER,
|
||||
@@ -695,6 +696,8 @@ static const char *action_label(local_status_action_t action,
|
||||
return snapshot->wifi_available && snapshot->wifi.started ? "WiFi:stop" : "WiFi:start";
|
||||
case LOCAL_STATUS_ACTION_WIFI_RECONNECT:
|
||||
return "WiFi:reconnect";
|
||||
case LOCAL_STATUS_ACTION_WIFI_NEXT_PROFILE:
|
||||
return "WiFi:next profile";
|
||||
case LOCAL_STATUS_ACTION_WEB:
|
||||
return snapshot->web_available && snapshot->web.running ? "HTTPS:stop" : "HTTPS:start";
|
||||
case LOCAL_STATUS_ACTION_SSH:
|
||||
@@ -736,6 +739,8 @@ static const char *confirmed_action_label(const local_status_ui_state_t *state)
|
||||
return state->confirmed_stop ? "WiFi:stop" : "WiFi:start";
|
||||
case LOCAL_STATUS_ACTION_WIFI_RECONNECT:
|
||||
return "WiFi:reconnect";
|
||||
case LOCAL_STATUS_ACTION_WIFI_NEXT_PROFILE:
|
||||
return "WiFi:next profile";
|
||||
case LOCAL_STATUS_ACTION_WEB:
|
||||
return state->confirmed_stop ? "HTTPS:stop" : "HTTPS:start";
|
||||
case LOCAL_STATUS_ACTION_SSH:
|
||||
@@ -760,6 +765,7 @@ static bool action_requires_confirmation(local_status_action_t action,
|
||||
case LOCAL_STATUS_ACTION_WIFI_POWER:
|
||||
return snapshot->wifi_available && snapshot->wifi.started;
|
||||
case LOCAL_STATUS_ACTION_WIFI_RECONNECT:
|
||||
case LOCAL_STATUS_ACTION_WIFI_NEXT_PROFILE:
|
||||
return true;
|
||||
case LOCAL_STATUS_ACTION_WEB:
|
||||
return snapshot->web_available && snapshot->web.running;
|
||||
@@ -1047,6 +1053,12 @@ static void execute_action(local_status_ui_state_t *state,
|
||||
: ESP_ERR_INVALID_STATE;
|
||||
requested = error == ESP_OK;
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_WIFI_NEXT_PROFILE:
|
||||
error = snapshot->wifi_available && snapshot->wifi.started
|
||||
? wifi_manager_next_profile()
|
||||
: ESP_ERR_INVALID_STATE;
|
||||
requested = error == ESP_OK;
|
||||
break;
|
||||
case LOCAL_STATUS_ACTION_WEB:
|
||||
if (!snapshot->web_available || snapshot->web.running != state->confirmed_stop) {
|
||||
error = ESP_ERR_INVALID_STATE;
|
||||
|
||||
+6
-3
@@ -27,7 +27,7 @@ static void print_usage(void)
|
||||
{
|
||||
printf("Usage:\n");
|
||||
printf(" wifi status|profiles|counters|clear-counters\n");
|
||||
printf(" wifi start|stop|reconnect\n");
|
||||
printf(" wifi start|stop|reconnect|next-profile\n");
|
||||
printf(" wifi profile set <slot> <priority> <mixed|wpa3> <ssid>\n");
|
||||
printf(" wifi profile secret <slot>\n");
|
||||
printf(" wifi profile enable|disable|delete <slot>\n");
|
||||
@@ -580,8 +580,10 @@ static int queue_lifecycle(const char *operation)
|
||||
error = wifi_manager_start();
|
||||
} else if (strcmp(operation, "stop") == 0) {
|
||||
error = wifi_manager_stop();
|
||||
} else {
|
||||
} else if (strcmp(operation, "reconnect") == 0) {
|
||||
error = wifi_manager_reconnect();
|
||||
} else {
|
||||
error = wifi_manager_next_profile();
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
printf("Could not queue Wi-Fi %s: %s\n", operation, esp_err_to_name(error));
|
||||
@@ -617,7 +619,8 @@ static int command_wifi(int argc, char **argv)
|
||||
}
|
||||
if (argc == 2 && (strcmp(argv[1], "start") == 0 ||
|
||||
strcmp(argv[1], "stop") == 0 ||
|
||||
strcmp(argv[1], "reconnect") == 0)) {
|
||||
strcmp(argv[1], "reconnect") == 0 ||
|
||||
strcmp(argv[1], "next-profile") == 0)) {
|
||||
return queue_lifecycle(argv[1]);
|
||||
}
|
||||
if (argc == 7 && strcmp(argv[1], "profile") == 0 &&
|
||||
|
||||
@@ -34,6 +34,7 @@ typedef enum {
|
||||
MESSAGE_COMMAND_STOP,
|
||||
MESSAGE_COMMAND_APPLY,
|
||||
MESSAGE_COMMAND_RECONNECT,
|
||||
MESSAGE_COMMAND_NEXT_PROFILE,
|
||||
MESSAGE_STA_CONNECTED,
|
||||
MESSAGE_STA_DISCONNECTED,
|
||||
MESSAGE_STA_GOT_IP,
|
||||
@@ -488,6 +489,58 @@ static void start_next_profile(manager_runtime_t *runtime)
|
||||
schedule_cycle_retry(runtime);
|
||||
}
|
||||
|
||||
static void start_next_profile_after_current(manager_runtime_t *runtime)
|
||||
{
|
||||
wifi_app_config_t config;
|
||||
copy_working_config(&config);
|
||||
uint8_t order[WIFI_CONFIG_STA_PROFILE_COUNT];
|
||||
uint8_t count = build_profile_order(order, &config);
|
||||
int8_t active_profile;
|
||||
lock_shared();
|
||||
active_profile = s_shared.snapshot.active_profile;
|
||||
unlock_shared();
|
||||
|
||||
if (count == 0U) {
|
||||
wifi_config_secure_wipe(&config, sizeof(config));
|
||||
set_last_error(ESP_ERR_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t active_index = 0U;
|
||||
bool found_active = false;
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
if (active_profile >= 0 && order[index] == (uint8_t)active_profile) {
|
||||
active_index = index;
|
||||
found_active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
uint8_t source = found_active ? (uint8_t)((active_index + 1U + index) % count)
|
||||
: index;
|
||||
runtime->profile_order[index] = order[source];
|
||||
}
|
||||
runtime->profile_count = count;
|
||||
runtime->next_profile = 0U;
|
||||
runtime->attempt_deadline = 0;
|
||||
runtime->disconnect_deadline = 0;
|
||||
runtime->backoff_deadline = 0;
|
||||
runtime->stable_deadline = 0;
|
||||
runtime->advance_after_disconnect = true;
|
||||
|
||||
uint32_t intentional_disconnects = runtime->intentional_disconnects;
|
||||
mark_intentional_disconnect(runtime);
|
||||
if (runtime->intentional_disconnects == intentional_disconnects) {
|
||||
runtime->advance_after_disconnect = false;
|
||||
start_next_profile(runtime);
|
||||
} else {
|
||||
/* Recover if the bounded queue drops the matching disconnect event. */
|
||||
runtime->disconnect_deadline = esp_timer_get_time() +
|
||||
WIFI_MANAGER_DISCONNECT_SETTLE_US;
|
||||
}
|
||||
wifi_config_secure_wipe(&config, sizeof(config));
|
||||
}
|
||||
|
||||
static void start_profile_cycle(manager_runtime_t *runtime)
|
||||
{
|
||||
wifi_app_config_t config;
|
||||
@@ -767,6 +820,12 @@ static void handle_message(manager_runtime_t *runtime,
|
||||
}
|
||||
break;
|
||||
|
||||
case MESSAGE_COMMAND_NEXT_PROFILE:
|
||||
if (manager_is_started()) {
|
||||
start_next_profile_after_current(runtime);
|
||||
}
|
||||
break;
|
||||
|
||||
case MESSAGE_STA_CONNECTED:
|
||||
if (!manager_is_started() ||
|
||||
!connected_event_matches_active_profile(message)) {
|
||||
@@ -1356,6 +1415,11 @@ esp_err_t wifi_manager_reconnect(void)
|
||||
return enqueue_lifecycle_command(MESSAGE_COMMAND_RECONNECT, -1);
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_next_profile(void)
|
||||
{
|
||||
return enqueue_lifecycle_command(MESSAGE_COMMAND_NEXT_PROFILE, -1);
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_get_snapshot(wifi_manager_snapshot_t *snapshot)
|
||||
{
|
||||
if (snapshot == NULL) {
|
||||
|
||||
@@ -92,6 +92,8 @@ esp_err_t wifi_manager_apply_working_config(const wifi_app_config_t *config);
|
||||
esp_err_t wifi_manager_start(void);
|
||||
esp_err_t wifi_manager_stop(void);
|
||||
esp_err_t wifi_manager_reconnect(void);
|
||||
/* Advance to the next enabled station profile in priority order, wrapping safely. */
|
||||
esp_err_t wifi_manager_next_profile(void);
|
||||
|
||||
/* Snapshot data never contains station or AP passwords. */
|
||||
esp_err_t wifi_manager_get_snapshot(wifi_manager_snapshot_t *snapshot);
|
||||
|
||||
Reference in New Issue
Block a user