feat(factory-cli): add Wi-Fi credential management commands
This commit is contained in:
@@ -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 ||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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. |
|
||||
Reference in New Issue
Block a user