Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/ts_api/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ set(TS_API_SRCS

set(TS_API_EMBED_FILES
"assets/lpmu-agx-network-setup.tar.gz"
"assets/lpmu-agx-network-setup.sha256"
)

idf_component_register(
Expand Down
8 changes: 4 additions & 4 deletions components/ts_api/assets/lpmu-agx-network-setup.manifest
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repo: RMinte/lpmu-agx-network-setup
ref: main
commit: 9ee3c51de5c9612e6e2c68d271ae329bd67b66ce
ref: fix/detect-lpmu-internal-interface
commit: 3faa1ef4bc144843f5c8b136c095dc2dc79b6f2b
archive: lpmu-agx-network-setup.tar.gz
sha256: 77ac9410a62eea50b2a5079d3a7adb1012deee1188c03e0a5cfb4609e4e5f636
size: 12874
sha256: b53c20ffd4651a58246a6f06e1ce3f275d0768c1627e80acdcdae7bdc823faf5
size: 7454
8 changes: 8 additions & 0 deletions components/ts_api/assets/lpmu-agx-network-setup.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
98370ee1bfadbb6a706b23b9f6699b58b26bb2283da48a2904c5eedd8f3c48e6 lpmu-agx-network-setup/lpmu/install-nat-service.sh
3216b67162c673d4360b42f4a6be1a3b785b1301366f3373936da8f865f00047 lpmu-agx-network-setup/lpmu/install-persistent.sh
9e193cf1cfa2e9b4d0ae9308c847cc2b22a0b865b2fe95113bc600bff6a0980b lpmu-agx-network-setup/lpmu/install-smart-route-service.sh
09a43407e22050f40d9ba797078d5080d3c70c3997d78a096ae6c1332e978895 lpmu-agx-network-setup/lpmu/lpmu-network.sh
a6f6f122011aa40ba564cc760983f66dfc16c93f8ca2253c4a3ca69859aae56f lpmu-agx-network-setup/lpmu/monitor-smart-route.sh
5dc4418b80438a74113bd0dc284e9d40d37ce57c094b216e0f18815e9d594ff8 lpmu-agx-network-setup/lpmu/setup-nat.sh
e8914194864ec1a810877604c3015ccf95ea9578725adac5431ec05e1499be25 lpmu-agx-network-setup/lpmu/setup-smart-route.sh
f57b051fab2b172c42323d96a9f9e175e800f0ccc99e1da28ec066c20790c968 lpmu-agx-network-setup/lpmu/test-nat.sh
Binary file modified components/ts_api/assets/lpmu-agx-network-setup.tar.gz
Binary file not shown.
83 changes: 55 additions & 28 deletions components/ts_api/src/ts_api_lpmu_access.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@
#define LPMU_HOST "10.10.99.99"
#define LPMU_PORT 22
#define LPMU_REMOTE_TARBALL "lpmu-agx-network-setup.tar.gz"
#define LPMU_REMOTE_MISMATCH 1
#define LPMU_STACK_SIZE 8192
#define LPMU_OUTPUT_TAIL_MAX 1024
#define LPMU_LOG_PATH "/sdcard/logs/lpmu-access.log"
#define LPMU_LOG_CHUNK 1024

extern const uint8_t lpmu_archive_start[] asm("_binary_lpmu_agx_network_setup_tar_gz_start");
extern const uint8_t lpmu_archive_end[] asm("_binary_lpmu_agx_network_setup_tar_gz_end");
extern const char lpmu_checksums_start[] asm("_binary_lpmu_agx_network_setup_sha256_start");
extern const char lpmu_checksums_end[] asm("_binary_lpmu_agx_network_setup_sha256_end");

typedef enum {
LPMU_STAGE_IDLE = 0,
Expand All @@ -50,7 +53,6 @@ typedef enum {
LPMU_STAGE_CHECKING_REMOTE,
LPMU_STAGE_UPLOADING,
LPMU_STAGE_EXTRACTING,
LPMU_STAGE_CHMOD,
LPMU_STAGE_RUNNING_SCRIPT,
LPMU_STAGE_SUCCESS,
LPMU_STAGE_FAILED,
Expand Down Expand Up @@ -122,7 +124,6 @@ static const char *lpmu_stage_str(lpmu_stage_t stage)
case LPMU_STAGE_CHECKING_REMOTE: return "checking_remote";
case LPMU_STAGE_UPLOADING: return "uploading";
case LPMU_STAGE_EXTRACTING: return "extracting";
case LPMU_STAGE_CHMOD: return "chmod";
case LPMU_STAGE_RUNNING_SCRIPT: return "running_script";
case LPMU_STAGE_SUCCESS: return "success";
case LPMU_STAGE_FAILED: return "failed";
Expand Down Expand Up @@ -244,6 +245,21 @@ static void lpmu_append_tail_locked(const char *data, size_t len)
s_status.output_tail[s_status.output_len] = '\0';
}

static bool lpmu_utf8_continuation(char byte)
{
return ((unsigned char)byte & 0xc0) == 0x80;
}

static size_t lpmu_complete_utf8_prefix(const char *text, size_t len)
{
size_t start = len;
while (start && lpmu_utf8_continuation(text[start - 1])) start--;
if (!start) return 0;
unsigned char lead = (unsigned char)text[start - 1];
size_t width = lead >= 0xf0 ? 4 : lead >= 0xe0 ? 3 : lead >= 0xc0 ? 2 : 1;
return len - (start - 1) < width ? start - 1 : len;
}

static void lpmu_append_log(const char *data, size_t len)
{
// Only the running task owns this descriptor; another start is excluded.
Expand Down Expand Up @@ -312,6 +328,18 @@ static esp_err_t lpmu_run_command(ts_ssh_session_t session, const char *command,
return ret;
}

static esp_err_t lpmu_check_remote(ts_ssh_session_t session, int *exit_code)
{
// Compare the actual files, then check that the entry point and its NAT dependency can run.
const char *command = "cd \"$HOME\" || exit 2; sha256sum --check --status - && "
"test -x lpmu-agx-network-setup/lpmu/setup-smart-route.sh && "
"test -x lpmu-agx-network-setup/lpmu/setup-nat.sh";
lpmu_record_command(command);
return ts_ssh_exec_stream_input(session, command,
lpmu_checksums_start, lpmu_checksums_end - lpmu_checksums_start,
lpmu_output_cb, NULL, exit_code);
}

static const char *lpmu_verify_known_host(ts_ssh_session_t session, char *err, size_t err_size)
{
ts_host_verify_result_t verify_result = TS_HOST_VERIFY_ERROR;
Expand Down Expand Up @@ -415,15 +443,17 @@ static void lpmu_task(void *arg)
}

lpmu_set_stage(LPMU_STAGE_CHECKING_REMOTE);
ret = lpmu_run_command(session,
"test -f \"$HOME/lpmu-agx-network-setup/lpmu/setup-smart-route.sh\"",
&exit_code);
ret = lpmu_check_remote(session, &exit_code);
if (ret != ESP_OK) {
lpmu_fail(ret, exit_code, "access_remote_check_failed", "remote check failed: %s", ts_ssh_get_error(session));
goto cleanup;
}
if (exit_code != 0 && exit_code != LPMU_REMOTE_MISMATCH) {
lpmu_fail(ESP_FAIL, exit_code, "access_remote_check_failed", "remote checksum command failed (exit=%d)", exit_code);
goto cleanup;
}

if (exit_code != 0) {
if (exit_code == LPMU_REMOTE_MISMATCH) {
lpmu_set_stage(LPMU_STAGE_UPLOADING);
lpmu_lock();
s_status.bytes_transferred = 0;
Expand All @@ -444,7 +474,7 @@ static void lpmu_task(void *arg)
lpmu_set_stage(LPMU_STAGE_EXTRACTING);
ret = lpmu_run_command(session,
"mkdir -p \"$HOME/lpmu-agx-network-setup\" && "
"tar -xzf \"$HOME/lpmu-agx-network-setup.tar.gz\" "
"tar -xpzf \"$HOME/lpmu-agx-network-setup.tar.gz\" "
"-C \"$HOME/lpmu-agx-network-setup\" --strip-components=1",
&exit_code);
if (ret != ESP_OK || exit_code != 0) {
Expand All @@ -453,17 +483,14 @@ static void lpmu_task(void *arg)
ret != ESP_OK ? ts_ssh_get_error(session) : "tar command failed");
goto cleanup;
}
}

lpmu_set_stage(LPMU_STAGE_CHMOD);
ret = lpmu_run_command(session,
"chmod +x \"$HOME/lpmu-agx-network-setup/lpmu/\"*.sh",
&exit_code);
if (ret != ESP_OK || exit_code != 0) {
lpmu_fail(ret != ESP_OK ? ret : ESP_FAIL, exit_code,
"access_chmod_failed", "remote chmod failed (exit=%d): %s", exit_code,
ret != ESP_OK ? ts_ssh_get_error(session) : "chmod command failed");
goto cleanup;
ret = lpmu_check_remote(session, &exit_code);
if (ret != ESP_OK || exit_code != 0) {
lpmu_fail(ret != ESP_OK ? ret : ESP_FAIL, exit_code,
"access_unpack_failed", "remote checksum verification after extract failed (exit=%d): %s",
exit_code, ret != ESP_OK ? ts_ssh_get_error(session) : "installed files did not pass verification");
goto cleanup;
}
}

lpmu_set_stage(LPMU_STAGE_RUNNING_SCRIPT);
Expand Down Expand Up @@ -608,6 +635,13 @@ static esp_err_t api_lpmu_access_status(const cJSON *params, ts_api_result_t *re
lpmu_status_t snap = s_status;
lpmu_unlock();

// The byte cap can split the first character, and a live SSH chunk
// can end before the last character is complete.
size_t start = 0;
while (start < snap.output_len && lpmu_utf8_continuation(snap.output_tail[start])) start++;
char *output_tail = snap.output_tail + start;
output_tail[lpmu_complete_utf8_prefix(output_tail, snap.output_len - start)] = '\0';

cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "run_id", snap.run_id);
cJSON_AddBoolToObject(data, "running", snap.running);
Expand All @@ -623,7 +657,7 @@ static esp_err_t api_lpmu_access_status(const cJSON *params, ts_api_result_t *re
cJSON_AddStringToObject(data, "reason", snap.reason ? snap.reason : "");
cJSON_AddStringToObject(data, "failure_stage", snap.reason ? lpmu_stage_str(snap.failure_stage) : "");
cJSON_AddStringToObject(data, "last_error", snap.last_error);
cJSON_AddStringToObject(data, "output_tail", snap.output_tail);
cJSON_AddStringToObject(data, "output_tail", output_tail);
cJSON_AddStringToObject(data, "host", LPMU_HOST);
cJSON_AddNumberToObject(data, "port", LPMU_PORT);

Expand Down Expand Up @@ -705,16 +739,9 @@ static esp_err_t api_lpmu_access_log(const cJSON *params, ts_api_result_t *resul
size_t count = received > 0 ? (size_t)received : 0;
// Keep a UTF-8 character intact across JSON pages.
if (!read_failed && count) {
size_t start = count;
while (start && ((unsigned char)chunk[start - 1] & 0xc0) == 0x80) start--;
if (start) {
unsigned char lead = (unsigned char)chunk[start - 1];
size_t width = lead >= 0xf0 ? 4 : lead >= 0xe0 ? 3 : lead >= 0xc0 ? 2 : 1;
if (count - (start - 1) < width) {
if (position + count == log_len) read_failed = true;
else count = start - 1;
}
} else read_failed = true;
size_t complete = lpmu_complete_utf8_prefix(chunk, count);
if (complete != count && position + count == log_len) read_failed = true;
count = complete;
}
if (!count && position < log_len) read_failed = true;

Expand Down
62 changes: 44 additions & 18 deletions components/ts_api/src/ts_api_ota.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,33 @@
#include "ts_security.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "sdkconfig.h"

static const char *TAG = "ts_api_ota";
static atomic_flag s_ota_probe_busy = ATOMIC_FLAG_INIT;

// NVS 存储键
#define NVS_NAMESPACE_OTA "ota_config"
#define NVS_KEY_SERVER_URL "server_url"
#define LEGACY_OTA_SERVER_URL "http://a6000nvlink.aoseo.com:15207"

// 服务器 URL 缓存
// 用户配置的服务器 URL,空字符串表示使用固件默认地址。
static char s_ota_server_url[256] = {0};

static const char *ota_server_url(void)
{
#ifdef CONFIG_TS_OTA_DEFAULT_URL
if (!s_ota_server_url[0]) return CONFIG_TS_OTA_DEFAULT_URL;
#endif
return s_ota_server_url;
}

static bool ota_server_is_legacy(const char *url)
{
return strcmp(url, LEGACY_OTA_SERVER_URL) == 0 ||
strcmp(url, LEGACY_OTA_SERVER_URL "/") == 0;
}

// ============================================================================
// OTA Server Config
// ============================================================================
Expand Down Expand Up @@ -65,7 +81,7 @@ static esp_err_t load_ota_server_config(void)
/**
* @brief 保存 OTA 服务器配置到 NVS
*/
static esp_err_t save_ota_server_config(void)
static esp_err_t save_ota_server_config(const char *url)
{
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE_OTA, NVS_READWRITE, &handle);
Expand All @@ -74,8 +90,8 @@ static esp_err_t save_ota_server_config(void)
return ret;
}

if (s_ota_server_url[0]) {
ret = nvs_set_str(handle, NVS_KEY_SERVER_URL, s_ota_server_url);
if (url[0]) {
ret = nvs_set_str(handle, NVS_KEY_SERVER_URL, url);
} else {
ret = nvs_erase_key(handle, NVS_KEY_SERVER_URL);
if (ret == ESP_ERR_NVS_NOT_FOUND) {
Expand All @@ -91,16 +107,28 @@ static esp_err_t save_ota_server_config(void)
return ret;
}

static void init_ota_server_config(void)
{
if (load_ota_server_config() == ESP_OK && ota_server_is_legacy(s_ota_server_url)) {
s_ota_server_url[0] = '\0';
if (save_ota_server_config("") != ESP_OK) {
ESP_LOGW(TAG, "Could not clear legacy OTA server URL from NVS");
}
}
ESP_LOGI(TAG, "OTA server URL: %s", ota_server_url());
}

/**
* @brief API: ota.server.get - 获取 OTA 服务器地址
*/
static esp_err_t api_ota_server_get(const cJSON *params, ts_api_result_t *result)
{
(void)params;

const char *url = ota_server_url();
cJSON *json = cJSON_CreateObject();
if (!json || !cJSON_AddStringToObject(json, "url", s_ota_server_url) ||
!cJSON_AddBoolToObject(json, "configured", s_ota_server_url[0] != '\0')) {
if (!json || !cJSON_AddStringToObject(json, "url", url) ||
!cJSON_AddBoolToObject(json, "configured", url[0] != '\0')) {
cJSON_Delete(json);
ts_api_result_error(result, TS_API_ERR_NO_MEM, "内存不足");
return ESP_ERR_NO_MEM;
Expand Down Expand Up @@ -130,34 +158,32 @@ static esp_err_t api_ota_server_set(const cJSON *params, ts_api_result_t *result

// URL 可以是空字符串(表示清除)
const char *url = cJSON_IsString(url_item) ? url_item->valuestring : "";
if (ota_server_is_legacy(url)) url = "";

// 验证 URL 长度
if (strlen(url) >= sizeof(s_ota_server_url)) {
ts_api_result_error(result, TS_API_ERR_INVALID_ARG, "URL 过长");
return ESP_ERR_INVALID_ARG;
}

// 更新缓存
strncpy(s_ota_server_url, url, sizeof(s_ota_server_url) - 1);
s_ota_server_url[sizeof(s_ota_server_url) - 1] = '\0';

ESP_LOGI(TAG, "OTA server configuration updated (configured=%d)", s_ota_server_url[0] != 0);

// 检查是否需要持久化
cJSON *save_item = cJSON_GetObjectItem(params, "save");
bool do_save = save_item && cJSON_IsTrue(save_item);

if (do_save) {
esp_err_t ret = save_ota_server_config();
esp_err_t ret = save_ota_server_config(url);
if (ret != ESP_OK) {
ts_api_result_error(result, TS_API_ERR_INTERNAL, "保存到 NVS 失败");
return ret;
}
ESP_LOGI(TAG, "OTA server URL saved to NVS");
}

strcpy(s_ota_server_url, url);
ESP_LOGI(TAG, "OTA server configuration updated (configured=%d)", s_ota_server_url[0] != 0);

cJSON *json = cJSON_CreateObject();
cJSON_AddStringToObject(json, "url", s_ota_server_url);
cJSON_AddStringToObject(json, "url", ota_server_url());
cJSON_AddBoolToObject(json, "saved", do_save);

ts_api_result_ok(result, json);
Expand Down Expand Up @@ -688,16 +714,17 @@ static esp_err_t api_ota_www_start(const cJSON *params, ts_api_result_t *result)
// Get URL - either from params or use server URL with www.bin suffix
cJSON *url_item = cJSON_GetObjectItem(params, "url");
char url[300] = {0};
const char *server = ota_server_url();

if (url_item && cJSON_IsString(url_item) && strlen(url_item->valuestring) >= sizeof(url)) {
ts_api_result_error(result, TS_API_ERR_INVALID_ARG, "URL too long");
return ESP_ERR_INVALID_ARG;
}
if (url_item && cJSON_IsString(url_item) && url_item->valuestring[0]) {
strncpy(url, url_item->valuestring, sizeof(url) - 1);
} else if (s_ota_server_url[0]) {
} else if (server[0]) {
// Use server URL and replace firmware.bin with www.bin
strncpy(url, s_ota_server_url, sizeof(url) - 1);
strncpy(url, server, sizeof(url) - 1);
// Try to replace common firmware names
char *bin_pos = strstr(url, "firmware.bin");
if (bin_pos) {
Expand Down Expand Up @@ -1131,8 +1158,7 @@ esp_err_t ts_api_ota_register(void)
size_t num_endpoints = sizeof(endpoints) / sizeof(endpoints[0]);

// 加载保存的配置
load_ota_server_config();
ESP_LOGI(TAG, "Loaded OTA server URL: %s", s_ota_server_url[0] ? s_ota_server_url : "(not set)");
init_ota_server_config();

for (size_t i = 0; i < num_endpoints; i++) {
esp_err_t ret = ts_api_register(&endpoints[i]);
Expand Down
2 changes: 1 addition & 1 deletion components/ts_ota/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ menu "TianShanOS OTA Configuration"

config TS_OTA_DEFAULT_URL
string "Default OTA server URL"
default "https://ota.example.com/tianshanos"
default "https://ota.rminte.com"
depends on TS_OTA_HTTPS_ENABLED
help
Default URL for checking and downloading OTA updates.
Expand Down
Loading
Loading