feat(factory-cli): add Wi-Fi credential management commands

This commit is contained in:
chenjintao
2026-08-25 14:09:25 +08:00
parent 63df9f91b3
commit 140e90447c
8 changed files with 362 additions and 13 deletions
@@ -13,6 +13,7 @@
#include "control_plane_internal.h"
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -29,7 +30,9 @@
#define FACTORY_CLI_DEVICE_ID_MAX_LEN 64
#define FACTORY_CLI_SERIAL_MAX_LEN 64
#define FACTORY_CLI_JSON_MAX_LEN 640
#define FACTORY_CLI_WIFI_JSON_MAX_LEN 3072
#define FACTORY_CLI_READ_TIMEOUT_MS 100
#define FACTORY_CLI_RESET_REPLY_DELAY_MS 100
#ifndef CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES
#define CONFIG_TQ_FACTORY_CLI_TASK_STACK_BYTES (10U * 1024U)
#endif
@@ -184,23 +187,56 @@ static void trim_text(char *value)
static size_t split_args(char *line, char *argv[], size_t max_args)
{
size_t argc = 0;
char *cursor = line;
while (*cursor != '\0' && argc < max_args) {
while (*cursor != '\0' && isspace((unsigned char)(*cursor))) {
cursor++;
char *read_cursor = line;
char *write_cursor = line;
while (*read_cursor != '\0') {
while (*read_cursor != '\0' && isspace((unsigned char)(*read_cursor))) {
read_cursor++;
}
if (*cursor == '\0') {
if (*read_cursor == '\0') {
break;
}
argv[argc++] = cursor;
while (*cursor != '\0' && !isspace((unsigned char)(*cursor))) {
cursor++;
if (argc >= max_args) {
return SIZE_MAX;
}
if (*cursor == '\0') {
break;
argv[argc++] = write_cursor;
char quote = '\0';
while (*read_cursor != '\0') {
char ch = *read_cursor;
if (quote != '\0') {
if (ch == quote) {
quote = '\0';
read_cursor++;
continue;
}
} else {
if (isspace((unsigned char)ch)) {
break;
}
if (ch == '\'' || ch == '"') {
quote = ch;
read_cursor++;
continue;
}
}
if (ch == '\\' && read_cursor[1] != '\0') {
read_cursor++;
ch = *read_cursor;
}
*write_cursor++ = ch;
read_cursor++;
}
*cursor = '\0';
cursor++;
if (quote != '\0') {
return SIZE_MAX;
}
while (*read_cursor != '\0' && isspace((unsigned char)(*read_cursor))) {
read_cursor++;
}
*write_cursor++ = '\0';
}
return argc;
}
@@ -226,6 +262,150 @@ static void cli_write_error(const char *message)
cli_write_line(line);
}
static bool json_append_text(char *out,
size_t out_len,
size_t *offset,
const char *text)
{
if (out == NULL || offset == NULL || text == NULL || *offset >= out_len) {
return false;
}
size_t text_len = strlen(text);
if (text_len >= out_len - *offset) {
return false;
}
memcpy(out + *offset, text, text_len);
*offset += text_len;
out[*offset] = '\0';
return true;
}
static bool json_append_escaped_string(char *out,
size_t out_len,
size_t *offset,
const char *value)
{
if (!json_append_text(out, out_len, offset, "\"")) {
return false;
}
for (const unsigned char *cursor = (const unsigned char *)value;
*cursor != '\0';
++cursor) {
char escape[8] = {0};
const char *encoded = escape;
switch (*cursor) {
case '"':
encoded = "\\\"";
break;
case '\\':
encoded = "\\\\";
break;
case '\b':
encoded = "\\b";
break;
case '\f':
encoded = "\\f";
break;
case '\n':
encoded = "\\n";
break;
case '\r':
encoded = "\\r";
break;
case '\t':
encoded = "\\t";
break;
default:
if (*cursor < 0x20U) {
snprintf(escape, sizeof(escape), "\\u%04x", (unsigned)*cursor);
} else {
escape[0] = (char)*cursor;
escape[1] = '\0';
}
break;
}
if (!json_append_text(out, out_len, offset, encoded)) {
return false;
}
}
return json_append_text(out, out_len, offset, "\"");
}
static void handle_get_wifi(void)
{
domain_wifi_saved_networks_t networks = {0};
esp_err_t err = domain_runtime_wifi_portal_get_saved_networks(&networks);
if (err != ESP_OK) {
cli_write_error("wifi_read_failed");
return;
}
char json[FACTORY_CLI_WIFI_JSON_MAX_LEN] = {0};
size_t offset = 0;
char prefix[48] = {0};
snprintf(prefix, sizeof(prefix), "{\"count\":%u,\"networks\":[", (unsigned)networks.count);
bool valid = json_append_text(json, sizeof(json), &offset, prefix);
for (size_t i = 0; valid && i < networks.count; ++i) {
char item_prefix[40] = {0};
snprintf(item_prefix,
sizeof(item_prefix),
"%s{\"priority\":%u,\"ssid\":",
i == 0U ? "" : ",",
(unsigned)i);
valid = json_append_text(json, sizeof(json), &offset, item_prefix) &&
json_append_escaped_string(json,
sizeof(json),
&offset,
networks.ssids[i]) &&
json_append_text(json, sizeof(json), &offset, "}");
}
valid = valid && json_append_text(json, sizeof(json), &offset, "]}");
if (!valid) {
cli_write_error("wifi_response_too_large");
return;
}
cli_write_line(json);
cli_write_ok();
}
static void handle_set_wifi(size_t argc, char *argv[])
{
if (argc < 2 || is_text_empty(argv[1])) {
cli_write_error("missing_wifi_ssid");
return;
}
if (argc > 3) {
cli_write_error("invalid_wifi_arguments");
return;
}
const char *password = argc >= 3 ? argv[2] : "";
esp_err_t err = domain_runtime_wifi_portal_save_credentials(argv[1], password);
if (err != ESP_OK) {
cli_write_error(err == ESP_ERR_INVALID_ARG ?
"invalid_wifi_credentials" :
"wifi_save_failed");
return;
}
cli_write_ok();
}
static void handle_reset_wifi(void)
{
esp_err_t err = domain_runtime_wifi_portal_clear_saved_credentials();
if (err != ESP_OK) {
cli_write_error("wifi_reset_failed");
return;
}
cli_write_ok();
vTaskDelay(pdMS_TO_TICKS(FACTORY_CLI_RESET_REPLY_DELAY_MS));
domain_runtime_restart();
}
static esp_err_t resolve_device_identity(char *device_id, size_t device_id_len)
{
char serial_number[FACTORY_CLI_SERIAL_MAX_LEN] = {0};
@@ -406,6 +586,10 @@ static void handle_command_line(char *line)
char *argv[FACTORY_CLI_MAX_ARGC] = {0};
size_t argc = split_args(line, argv, FACTORY_CLI_MAX_ARGC);
if (argc == SIZE_MAX) {
cli_write_error("invalid_command_syntax");
return;
}
if (argc == 0) {
return;
}
@@ -443,6 +627,12 @@ static void handle_command_line(char *line)
cli_write_ok();
} else if (strcmp(cmd, "save_config") == 0) {
handle_save_config();
} else if (strcmp(cmd, "get_wifi") == 0) {
handle_get_wifi();
} else if (strcmp(cmd, "set_wifi") == 0) {
handle_set_wifi(argc, argv);
} else if (strcmp(cmd, "reset_wifi") == 0) {
handle_reset_wifi();
} else if (strcmp(cmd, "get_auth_material") == 0 ||
strcmp(cmd, "auth_material") == 0 ||
strcmp(cmd, "get_device_auth_material") == 0 ||
+13 -1
View File
@@ -548,6 +548,18 @@ esp_err_t domain_runtime_behavior_play_prompt_file(const char *path);
void domain_runtime_wifi_portal_set_behavior_hooks(const domain_wifi_behavior_hooks_t *hooks);
esp_err_t domain_runtime_wifi_portal_init_defaults(const char *seed_ssid, const char *seed_password);
bool domain_runtime_wifi_portal_has_saved_credentials(void);
#define DOMAIN_WIFI_SSID_MAX_LEN 33
#define DOMAIN_WIFI_SAVED_NETWORK_MAX_COUNT 10
typedef struct {
size_t count;
char ssids[DOMAIN_WIFI_SAVED_NETWORK_MAX_COUNT][DOMAIN_WIFI_SSID_MAX_LEN];
} domain_wifi_saved_networks_t;
/** Copy saved SSIDs in persistent priority order. Passwords are not exposed. */
esp_err_t domain_runtime_wifi_portal_get_saved_networks(
domain_wifi_saved_networks_t *out_networks);
bool domain_runtime_wifi_portal_sta_is_connected(void);
esp_err_t domain_runtime_wifi_portal_start_config_ap(const char *ssid_prefix);
bool domain_runtime_wifi_connect_error_keeps_credentials(esp_err_t err);
@@ -579,7 +591,7 @@ bool domain_runtime_is_network_connect_error(esp_err_t err);
esp_err_t domain_runtime_wifi_portal_save_credentials(const char *ssid, const char *password);
esp_err_t domain_runtime_wifi_portal_clear_saved_credentials(void);
#define DOMAIN_WIFI_PROVISIONING_SSID_MAX_LEN 33
#define DOMAIN_WIFI_PROVISIONING_SSID_MAX_LEN DOMAIN_WIFI_SSID_MAX_LEN
#define DOMAIN_WIFI_PROVISIONING_PASSWORD_MAX_LEN 65
typedef struct {
@@ -1286,6 +1286,21 @@ bool domain_runtime_wifi_portal_has_saved_credentials(void)
return platform_wifi_portal_has_saved_credentials();
}
esp_err_t domain_runtime_wifi_portal_get_saved_networks(
domain_wifi_saved_networks_t *out_networks)
{
if (out_networks == NULL) {
return ESP_ERR_INVALID_ARG;
}
memset(out_networks, 0, sizeof(*out_networks));
return platform_wifi_portal_get_saved_ssids(
&out_networks->ssids[0][0],
sizeof(out_networks->ssids[0]),
DOMAIN_WIFI_SAVED_NETWORK_MAX_COUNT,
&out_networks->count);
}
bool domain_runtime_wifi_portal_sta_is_connected(void)
{
return platform_wifi_portal_sta_is_connected();
+8
View File
@@ -359,6 +359,14 @@ esp_err_t platform_behavior_play_prompt_file(const char *path);
void platform_wifi_portal_set_behavior_hooks(const platform_wifi_behavior_hooks_t *hooks);
esp_err_t platform_wifi_portal_init_defaults(const char *seed_ssid, const char *seed_password);
bool platform_wifi_portal_has_saved_credentials(void);
/**
* Copy saved SSIDs in persistent priority order without exposing passwords.
* `out_ssids` contains `capacity` rows of `ssid_stride` bytes.
*/
esp_err_t platform_wifi_portal_get_saved_ssids(char *out_ssids,
size_t ssid_stride,
size_t capacity,
size_t *out_count);
bool platform_wifi_portal_sta_is_connected(void);
/** Apply standby modem power save, or restore the configured active policy. */
esp_err_t platform_wifi_portal_set_light_standby(bool standby);
@@ -2,6 +2,7 @@
#include "esp_err.h"
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
@@ -26,6 +27,17 @@ esp_err_t wifi_portal_init_defaults(const char *seed_ssid, const char *seed_pass
*/
bool wifi_portal_has_saved_credentials(void);
/**
* @brief Copy saved SSIDs in persistent priority order.
*
* Passwords are intentionally not exposed. `out_ssids` contains `capacity`
* rows of `ssid_stride` bytes. `out_count` receives the total saved count.
*/
esp_err_t wifi_portal_get_saved_ssids(char *out_ssids,
size_t ssid_stride,
size_t capacity,
size_t *out_count);
/**
* @brief Check whether the STA interface is connected (has an IP).
*
@@ -128,6 +128,17 @@ bool platform_wifi_portal_has_saved_credentials(void)
return wifi_portal_has_saved_credentials();
}
esp_err_t platform_wifi_portal_get_saved_ssids(char *out_ssids,
size_t ssid_stride,
size_t capacity,
size_t *out_count)
{
return wifi_portal_get_saved_ssids(out_ssids,
ssid_stride,
capacity,
out_count);
}
bool platform_wifi_portal_sta_is_connected(void)
{
return wifi_portal_sta_is_connected();
@@ -298,6 +298,34 @@ bool wifi_portal_has_saved_credentials(void)
return !SsidManager::GetInstance().GetSsidList().empty();
}
esp_err_t wifi_portal_get_saved_ssids(char *out_ssids,
size_t ssid_stride,
size_t capacity,
size_t *out_count)
{
if (out_count == nullptr ||
(capacity > 0U && (out_ssids == nullptr || ssid_stride < 33U))) {
return ESP_ERR_INVALID_ARG;
}
install_wifi_storage_hooks();
ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal");
PortalUnlockGuard guard;
const auto &ssid_list = SsidManager::GetInstance().GetSsidList();
*out_count = ssid_list.size();
if (ssid_list.size() > capacity) {
return ESP_ERR_INVALID_SIZE;
}
for (size_t i = 0; i < ssid_list.size(); ++i) {
strlcpy(out_ssids + (i * ssid_stride),
ssid_list[i].ssid.c_str(),
ssid_stride);
}
return ESP_OK;
}
bool wifi_portal_sta_is_connected(void)
{
if (!s_wifi_manager_initialized) {
+73
View File
@@ -0,0 +1,73 @@
# Factory CLI Wi-Fi Commands
The Factory CLI accepts commands terminated by CR, LF, or CRLF on UART0. A
successful command returns `OK`. Commands that return data emit one JSON line
before `OK`. Failures return `ERROR <reason>`.
## Read Saved Networks
```text
get_wifi
```
The response lists saved SSIDs in connection-priority order. Passwords are
never returned. Up to 10 networks can be stored.
```text
{"count":2,"networks":[{"priority":0,"ssid":"Office WiFi"},{"priority":1,"ssid":"Backup"}]}
OK
```
An empty list is returned as:
```text
{"count":0,"networks":[]}
OK
```
## Save a Network
```text
set_wifi "Office WiFi" "network-password"
```
The SSID is required and may contain up to 32 bytes. The password may contain
up to 64 bytes. Quote arguments that contain spaces. A missing or explicitly
empty password saves an open network:
```text
set_wifi "Guest Network"
set_wifi "Guest Network" ""
```
Backslash escapes the following character inside or outside quoted arguments.
To include a literal backslash, enter it twice.
Saving a new SSID inserts it at priority 0. Saving an existing SSID replaces
its password and moves it to priority 0. If 10 networks are already saved,
adding a new SSID removes the lowest-priority entry. This command only updates
persistent credentials; it does not disconnect or immediately reconnect the
current station.
## Clear Saved Networks
```text
reset_wifi
```
This command clears all saved Wi-Fi credentials, returns `OK`, allows 100 ms
for the UART reply to be transmitted, and restarts the device. It has the same
persistence effect as the physical Wi-Fi reset action.
## Error Responses
| Response | Meaning |
| --- | --- |
| `ERROR invalid_command_syntax` | An argument quote was not closed or the command contains too many arguments. |
| `ERROR wifi_read_failed` | Saved networks could not be read. |
| `ERROR wifi_response_too_large` | The saved network response exceeded the internal reply buffer. |
| `ERROR missing_wifi_ssid` | `set_wifi` did not receive a non-empty SSID. |
| `ERROR invalid_wifi_arguments` | `set_wifi` received more than an SSID and password. |
| `ERROR invalid_wifi_credentials` | The SSID or password exceeded its supported length. |
| `ERROR wifi_save_failed` | Credentials could not be saved. |
| `ERROR wifi_reset_failed` | Saved credentials could not be cleared. |