chore: initial commit

This commit is contained in:
CSBOOK\44744
2026-06-25 00:34:34 +08:00
commit 3cce1be162
295 changed files with 30971 additions and 0 deletions
@@ -0,0 +1,257 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain.h"
#include "domain_internal.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "domain_cal";
#define DOMAIN_CALIBRATION_TIMEOUT_MS 20000
#define DOMAIN_CALIBRATION_STEP_TIMEOUT_MS 1500
#define DOMAIN_CALIBRATION_RELEASE_SWEEP_DEG 45.0f
static float clamp_positive_default(float value, float default_value)
{
if (value > 0.0f) {
return value;
}
return default_value;
}
static esp_err_t prepare_calibration_switch(const domain_direction_calibration_t *calibration)
{
if (calibration == NULL || !calibration->enabled) {
return ESP_ERR_INVALID_STATE;
}
return domain_runtime_calibration_switch_init();
}
static bool is_calibration_switch_active(const domain_direction_calibration_t *calibration)
{
if (calibration == NULL || !calibration->enabled) {
return false;
}
return domain_runtime_calibration_switch_is_active();
}
static bool calibration_timeout_reached(TickType_t start_tick, TickType_t timeout_ticks)
{
return (xTaskGetTickCount() - start_tick) >= timeout_ticks;
}
static int calibration_remaining_timeout_ms(TickType_t start_tick, TickType_t timeout_ticks)
{
TickType_t elapsed_ticks = xTaskGetTickCount() - start_tick;
if (elapsed_ticks >= timeout_ticks) {
return 0;
}
TickType_t remaining_ticks = timeout_ticks - elapsed_ticks;
int remaining_ms = (int)(remaining_ticks * portTICK_PERIOD_MS);
return remaining_ms > 0 ? remaining_ms : 1;
}
static int calibration_step_timeout_ms(TickType_t start_tick, TickType_t timeout_ticks)
{
int remaining_ms = calibration_remaining_timeout_ms(start_tick, timeout_ticks);
if (remaining_ms <= 0) {
return 0;
}
return remaining_ms < DOMAIN_CALIBRATION_STEP_TIMEOUT_MS ?
remaining_ms :
DOMAIN_CALIBRATION_STEP_TIMEOUT_MS;
}
static bool calibration_settle(const domain_direction_calibration_t *calibration,
TickType_t start_tick,
TickType_t timeout_ticks)
{
if (calibration != NULL && calibration->settle_delay_ms > 0) {
int remaining_ms = calibration_remaining_timeout_ms(start_tick, timeout_ticks);
if (remaining_ms <= 0) {
return false;
}
int delay_ms = calibration->settle_delay_ms < remaining_ms ?
calibration->settle_delay_ms :
remaining_ms;
TickType_t ticks = pdMS_TO_TICKS(delay_ms);
vTaskDelay(ticks > 0 ? ticks : 1);
}
return !calibration_timeout_reached(start_tick, timeout_ticks);
}
static esp_err_t calibration_rotate_and_track(float delta, int rotate_speed_us, int timeout_ms)
{
if (timeout_ms <= 0) {
return ESP_ERR_TIMEOUT;
}
esp_err_t err = domain_runtime_stepper_rotate_angle_with_accel_timeout(
delta,
rotate_speed_us,
timeout_ms);
if (err != ESP_OK) {
return err;
}
return domain_apply_rotation_delta(delta);
}
static esp_err_t calibration_release_active_switch(const domain_direction_calibration_t *calibration,
float step_angle,
float search_direction,
int rotate_speed_us,
TickType_t start_tick,
TickType_t timeout_ticks)
{
float release_sweep_limit = clamp_positive_default(DOMAIN_CALIBRATION_RELEASE_SWEEP_DEG,
step_angle);
float released_sweep = 0.0f;
float release_delta = -search_direction * step_angle;
while (released_sweep < release_sweep_limit) {
if (!is_calibration_switch_active(calibration)) {
ESP_LOGI(TAG, "Calibration switch released after %.1f deg", released_sweep);
return ESP_OK;
}
if (calibration_timeout_reached(start_tick, timeout_ticks)) {
return ESP_ERR_TIMEOUT;
}
int step_timeout_ms = calibration_step_timeout_ms(start_tick, timeout_ticks);
esp_err_t err = calibration_rotate_and_track(release_delta, rotate_speed_us, step_timeout_ms);
if (err != ESP_OK) {
return err;
}
released_sweep += step_angle;
if (!calibration_settle(calibration, start_tick, timeout_ticks)) {
return ESP_ERR_TIMEOUT;
}
}
if (is_calibration_switch_active(calibration)) {
ESP_LOGW(TAG,
"Calibration switch stayed active after %.1f deg release sweep",
released_sweep);
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "Calibration switch released after %.1f deg", released_sweep);
return ESP_OK;
}
esp_err_t domain_calibrate_with_timeout(const domain_direction_calibration_t *config, int timeout_ms)
{
int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : DOMAIN_CALIBRATION_TIMEOUT_MS;
TickType_t timeout_ticks = pdMS_TO_TICKS(effective_timeout_ms);
if (timeout_ticks == 0) {
timeout_ticks = 1;
}
TickType_t start_tick = xTaskGetTickCount();
if (!domain_direction_lock_state(effective_timeout_ms)) {
return ESP_ERR_TIMEOUT;
}
if (config != NULL) {
domain_set_calibration(config);
}
domain_direction_calibration_t calibration = {0};
if (!domain_get_calibration(&calibration)) {
ESP_LOGW(TAG, "Calibration not configured");
domain_direction_unlock_state();
return ESP_ERR_INVALID_STATE;
}
if (!calibration.enabled) {
ESP_LOGW(TAG, "Calibration disabled");
domain_direction_unlock_state();
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = prepare_calibration_switch(&calibration);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Prepare calibration switch failed");
domain_direction_unlock_state();
return err;
}
float step_angle = clamp_positive_default(calibration.step_angle_deg, 5.0f);
float max_sweep = clamp_positive_default(calibration.max_sweep_deg, 360.0f);
float total_sweep = 0.0f;
float direction = calibration.search_clockwise ? 1.0f : -1.0f;
int rotate_speed_us = domain_get_rotate_speed_us();
ESP_LOGI(TAG,
"Calibration start step=%.1f sweep=%.1f search=%s",
step_angle,
max_sweep,
calibration.search_clockwise ? "clockwise" : "counterclockwise");
if (is_calibration_switch_active(&calibration)) {
ESP_LOGI(TAG, "Calibration switch active at start, releasing before homing");
err = calibration_release_active_switch(&calibration,
step_angle,
direction,
rotate_speed_us,
start_tick,
timeout_ticks);
if (err != ESP_OK) {
domain_runtime_stepper_power_off();
domain_direction_unlock_state();
return err;
}
}
while (total_sweep < max_sweep) {
if (calibration_timeout_reached(start_tick, timeout_ticks)) {
domain_runtime_stepper_power_off();
domain_direction_unlock_state();
ESP_LOGW(TAG, "Calibration timed out after %.1f deg search", total_sweep);
return ESP_ERR_TIMEOUT;
}
float delta = direction * step_angle;
int step_timeout_ms = calibration_step_timeout_ms(start_tick, timeout_ticks);
err = calibration_rotate_and_track(delta, rotate_speed_us, step_timeout_ms);
if (err != ESP_OK) {
domain_runtime_stepper_power_off();
domain_direction_unlock_state();
return err;
}
total_sweep += step_angle;
if (!calibration_settle(&calibration, start_tick, timeout_ticks)) {
domain_runtime_stepper_power_off();
domain_direction_unlock_state();
ESP_LOGW(TAG, "Calibration timed out during settle after %.1f deg search", total_sweep);
return ESP_ERR_TIMEOUT;
}
if (is_calibration_switch_active(&calibration)) {
domain_set_current_angle(calibration.calibration_angle);
domain_runtime_stepper_power_off();
domain_direction_unlock_state();
ESP_LOGI(TAG, "Calibration switch detected after %.1f deg", total_sweep);
return ESP_OK;
}
}
domain_runtime_stepper_power_off();
domain_direction_unlock_state();
ESP_LOGW(TAG, "Calibration sweep exhausted after %.1f deg", total_sweep);
return ESP_ERR_TIMEOUT;
}
@@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain_internal.h"
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
static domain_direction_calibration_t s_calibration = {0};
static bool s_calibration_configured = false;
static portMUX_TYPE s_calibration_lock = portMUX_INITIALIZER_UNLOCKED;
void domain_calibration_store_set(const domain_direction_calibration_t *config)
{
portENTER_CRITICAL(&s_calibration_lock);
if (config == NULL) {
memset(&s_calibration, 0, sizeof(s_calibration));
s_calibration_configured = false;
portEXIT_CRITICAL(&s_calibration_lock);
return;
}
s_calibration = *config;
s_calibration_configured = true;
portEXIT_CRITICAL(&s_calibration_lock);
}
bool domain_calibration_store_get(domain_direction_calibration_t *out_config)
{
portENTER_CRITICAL(&s_calibration_lock);
if (!s_calibration_configured || out_config == NULL) {
portEXIT_CRITICAL(&s_calibration_lock);
return false;
}
*out_config = s_calibration;
portEXIT_CRITICAL(&s_calibration_lock);
return true;
}
@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain_internal.h"
float domain_angle_normalize(float angle)
{
while (angle > 180.0f) {
angle -= 360.0f;
}
while (angle <= -180.0f) {
angle += 360.0f;
}
return angle;
}
float domain_angle_shortest_rotation(float current, float target)
{
float diff = target - current;
while (diff > 180.0f) {
diff -= 360.0f;
}
while (diff < -180.0f) {
diff += 360.0f;
}
return diff;
}
@@ -0,0 +1,158 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain_internal.h"
#include <ctype.h>
#include <string.h>
static const domain_direction_entry_t s_default_directions[] = {
{"南", "S", 0.0f},
{"西南", "SW", 45.0f},
{"西", "W", 90.0f},
{"西北", "NW", 135.0f},
{"北", "N", 180.0f},
{"东北", "NE", -135.0f},
{"东", "E", -90.0f},
{"东南", "SE", -45.0f},
};
/*
* Aliases for cardinal directions. The server fortune API returns the four
* cardinals with a "正" prefix (正北/正南/正东/正西); map those to the catalog's
* canonical names. The four diagonals (东北/西北/东南/西南) already match.
*/
static const struct {
const char *alias;
const char *canonical_cn;
} s_direction_aliases[] = {
{"正南", "南"},
{"正西", "西"},
{"正北", "北"},
{"正东", "东"},
};
static void trimmed_span(const char *input, const char **start_out, size_t *len_out)
{
const char *start = input;
const char *end = NULL;
if (input == NULL) {
*start_out = NULL;
*len_out = 0;
return;
}
while (*start != '\0' && isspace((unsigned char)*start)) {
start++;
}
end = start + strlen(start);
while (end > start && isspace((unsigned char)*(end - 1))) {
end--;
}
*start_out = start;
*len_out = (size_t)(end - start);
}
static bool equals_trimmed(const char *input, const char *token, bool case_insensitive)
{
const char *start = NULL;
size_t len = 0;
size_t token_len = 0;
if (input == NULL || token == NULL) {
return false;
}
trimmed_span(input, &start, &len);
token_len = strlen(token);
if (len == 0 || len != token_len) {
return false;
}
for (size_t i = 0; i < len; i++) {
char left = start[i];
char right = token[i];
if (case_insensitive) {
left = (char)tolower((unsigned char)left);
right = (char)tolower((unsigned char)right);
}
if (left != right) {
return false;
}
}
return true;
}
size_t domain_direction_catalog_count(void)
{
return sizeof(s_default_directions) / sizeof(s_default_directions[0]);
}
const domain_direction_entry_t *domain_direction_catalog_get(size_t index)
{
if (index >= domain_direction_catalog_count()) {
return NULL;
}
return &s_default_directions[index];
}
static bool matches_direction_alias(const char *input, const char *canonical_cn)
{
for (size_t i = 0; i < sizeof(s_direction_aliases) / sizeof(s_direction_aliases[0]); i++) {
if (strcmp(s_direction_aliases[i].canonical_cn, canonical_cn) == 0 &&
equals_trimmed(input, s_direction_aliases[i].alias, false)) {
return true;
}
}
return false;
}
bool domain_direction_catalog_find_by_name(const char *input, size_t *out_index)
{
for (size_t i = 0; i < domain_direction_catalog_count(); i++) {
const domain_direction_entry_t *entry = domain_direction_catalog_get(i);
if (entry == NULL) {
return false;
}
if (equals_trimmed(input, entry->name_cn, false) ||
equals_trimmed(input, entry->name_en, true) ||
matches_direction_alias(input, entry->name_cn)) {
if (out_index != NULL) {
*out_index = i;
}
return true;
}
}
return false;
}
bool domain_direction_catalog_find_by_angle(float angle, size_t *out_index)
{
for (size_t i = 0; i < domain_direction_catalog_count(); i++) {
const domain_direction_entry_t *entry = domain_direction_catalog_get(i);
if (entry == NULL) {
return false;
}
if (entry->angle == angle) {
if (out_index != NULL) {
*out_index = i;
}
return true;
}
}
return false;
}
@@ -0,0 +1,317 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain.h"
#include "domain_internal.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
static const char *TAG = "domain";
#define DOMAIN_DEFAULT_ROTATE_SPEED_US 1500
#define DOMAIN_ROTATE_TIMEOUT_MS 8000
#define DOMAIN_STATE_LOCK_TIMEOUT_MS 2000
#define DOMAIN_DEFAULT_CALIBRATION_ENABLED 1
#define DOMAIN_DEFAULT_CALIBRATION_ANGLE_DEG 180.0f
#define DOMAIN_DEFAULT_CALIBRATION_STEP_ANGLE_DEG 5.0f
#define DOMAIN_DEFAULT_CALIBRATION_MAX_SWEEP_DEG 360.0f
#define DOMAIN_DEFAULT_CALIBRATION_SEARCH_CLOCKWISE 1
#define DOMAIN_DEFAULT_CALIBRATION_SETTLE_DELAY_MS 10
static float s_current_angle = 0.0f;
static int s_rotate_speed_us = DOMAIN_DEFAULT_ROTATE_SPEED_US;
static bool s_initialized;
static SemaphoreHandle_t s_state_mutex;
static portMUX_TYPE s_state_mutex_init_lock = portMUX_INITIALIZER_UNLOCKED;
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
static StaticSemaphore_t s_state_mutex_buf;
#endif
static const domain_direction_axis_config_t s_default_axis_config = {
.initial_angle = 0.0f,
.rotate_speed_us = 0
};
static const domain_direction_calibration_t s_default_calibration = {
.enabled = DOMAIN_DEFAULT_CALIBRATION_ENABLED,
.calibration_angle = DOMAIN_DEFAULT_CALIBRATION_ANGLE_DEG,
.step_angle_deg = DOMAIN_DEFAULT_CALIBRATION_STEP_ANGLE_DEG,
.max_sweep_deg = DOMAIN_DEFAULT_CALIBRATION_MAX_SWEEP_DEG,
.search_clockwise = DOMAIN_DEFAULT_CALIBRATION_SEARCH_CLOCKWISE,
.settle_delay_ms = DOMAIN_DEFAULT_CALIBRATION_SETTLE_DELAY_MS
};
static int remaining_timeout_ms(TickType_t start_tick, int timeout_ms)
{
int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : DOMAIN_ROTATE_TIMEOUT_MS;
TickType_t timeout_ticks = pdMS_TO_TICKS(effective_timeout_ms);
if (timeout_ticks == 0) {
timeout_ticks = 1;
}
TickType_t elapsed_ticks = xTaskGetTickCount() - start_tick;
if (elapsed_ticks >= timeout_ticks) {
return 0;
}
TickType_t remaining_ticks = timeout_ticks - elapsed_ticks;
int remaining_ms = (int)(remaining_ticks * portTICK_PERIOD_MS);
return remaining_ms > 0 ? remaining_ms : 1;
}
static bool ensure_state_mutex(void)
{
if (s_state_mutex != NULL) {
return true;
}
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
portENTER_CRITICAL(&s_state_mutex_init_lock);
if (s_state_mutex == NULL) {
s_state_mutex = xSemaphoreCreateRecursiveMutexStatic(&s_state_mutex_buf);
}
portEXIT_CRITICAL(&s_state_mutex_init_lock);
#else
SemaphoreHandle_t created_mutex = xSemaphoreCreateRecursiveMutex();
portENTER_CRITICAL(&s_state_mutex_init_lock);
if (s_state_mutex == NULL) {
s_state_mutex = created_mutex;
created_mutex = NULL;
}
portEXIT_CRITICAL(&s_state_mutex_init_lock);
if (created_mutex != NULL) {
vSemaphoreDelete(created_mutex);
}
#endif
return (s_state_mutex != NULL);
}
bool domain_direction_lock_state(int timeout_ms)
{
if (!ensure_state_mutex()) {
return false;
}
int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : DOMAIN_STATE_LOCK_TIMEOUT_MS;
TickType_t timeout_ticks = pdMS_TO_TICKS(effective_timeout_ms);
if (timeout_ticks == 0) {
timeout_ticks = 1;
}
return (xSemaphoreTakeRecursive(s_state_mutex, timeout_ticks) == pdTRUE);
}
void domain_direction_unlock_state(void)
{
if (s_state_mutex != NULL) {
(void)xSemaphoreGiveRecursive(s_state_mutex);
}
}
esp_err_t domain_init(const domain_direction_axis_config_t *config,
const domain_direction_calibration_t *calibration)
{
esp_err_t runtime_err = domain_runtime_platform_init();
if (runtime_err != ESP_OK) {
return runtime_err;
}
if (!domain_direction_lock_state(DOMAIN_STATE_LOCK_TIMEOUT_MS)) {
return ESP_ERR_TIMEOUT;
}
if (s_initialized) {
domain_direction_unlock_state();
return ESP_OK;
}
s_current_angle = 0.0f;
s_rotate_speed_us = DOMAIN_DEFAULT_ROTATE_SPEED_US;
if (config != NULL) {
s_current_angle = domain_angle_normalize(config->initial_angle);
if (config->rotate_speed_us > 0) {
s_rotate_speed_us = config->rotate_speed_us;
}
}
s_initialized = true;
domain_direction_unlock_state();
domain_calibration_store_set(calibration);
ESP_LOGI(TAG, "Direction axis initialized (angle=%.1f, speed=%dus)",
s_current_angle, s_rotate_speed_us);
return ESP_OK;
}
const domain_direction_axis_config_t *domain_direction_default_axis_config(void)
{
return &s_default_axis_config;
}
const domain_direction_calibration_t *domain_direction_default_calibration(void)
{
return &s_default_calibration;
}
bool domain_is_initialized(void)
{
bool initialized = false;
if (domain_direction_lock_state(DOMAIN_STATE_LOCK_TIMEOUT_MS)) {
initialized = s_initialized;
domain_direction_unlock_state();
}
return initialized;
}
size_t domain_get_direction_count(void)
{
return domain_direction_catalog_count();
}
bool domain_get_direction_view(size_t index,
const char **out_name_cn,
const char **out_name_en,
float *out_angle)
{
const domain_direction_entry_t *entry = domain_direction_catalog_get(index);
if (entry == NULL) {
return false;
}
if (out_name_cn != NULL) {
*out_name_cn = entry->name_cn;
}
if (out_name_en != NULL) {
*out_name_en = entry->name_en;
}
if (out_angle != NULL) {
*out_angle = entry->angle;
}
return true;
}
bool domain_find_direction_by_name(const char *input, size_t *out_index)
{
return domain_direction_catalog_find_by_name(input, out_index);
}
bool domain_get_current_direction_index(size_t *out_index)
{
if (!domain_direction_lock_state(DOMAIN_STATE_LOCK_TIMEOUT_MS)) {
return false;
}
if (!s_initialized) {
domain_direction_unlock_state();
return false;
}
float angle = s_current_angle;
domain_direction_unlock_state();
return domain_direction_catalog_find_by_angle(angle, out_index);
}
float domain_get_current_angle(void)
{
if (!domain_direction_lock_state(DOMAIN_STATE_LOCK_TIMEOUT_MS)) {
return 0.0f;
}
float angle = s_current_angle;
domain_direction_unlock_state();
return angle;
}
void domain_set_current_angle(float angle)
{
if (!domain_direction_lock_state(DOMAIN_STATE_LOCK_TIMEOUT_MS)) {
return;
}
s_current_angle = domain_angle_normalize(angle);
domain_direction_unlock_state();
}
int domain_get_rotate_speed_us(void)
{
if (!domain_direction_lock_state(DOMAIN_STATE_LOCK_TIMEOUT_MS)) {
return 0;
}
int speed = s_rotate_speed_us;
domain_direction_unlock_state();
return speed;
}
esp_err_t domain_rotate_to_angle_with_timeout(float target_angle, int timeout_ms)
{
TickType_t start_tick = xTaskGetTickCount();
int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : DOMAIN_ROTATE_TIMEOUT_MS;
if (!domain_direction_lock_state(effective_timeout_ms)) {
return ESP_ERR_TIMEOUT;
}
if (!s_initialized) {
domain_direction_unlock_state();
return ESP_ERR_INVALID_STATE;
}
float normalized_target = domain_angle_normalize(target_angle);
float rotation = domain_angle_shortest_rotation(s_current_angle, normalized_target);
if (rotation == 0.0f) {
domain_direction_unlock_state();
return ESP_OK;
}
int motor_timeout_ms = remaining_timeout_ms(start_tick, effective_timeout_ms);
if (motor_timeout_ms <= 0) {
domain_direction_unlock_state();
return ESP_ERR_TIMEOUT;
}
esp_err_t motor_err = domain_runtime_stepper_rotate_angle_with_accel_timeout(
rotation,
s_rotate_speed_us,
motor_timeout_ms);
if (motor_err != ESP_OK) {
domain_direction_unlock_state();
return motor_err;
}
s_current_angle = domain_angle_normalize(s_current_angle + rotation);
domain_direction_unlock_state();
return ESP_OK;
}
esp_err_t domain_apply_rotation_delta(float delta)
{
if (!domain_direction_lock_state(DOMAIN_STATE_LOCK_TIMEOUT_MS)) {
return ESP_ERR_TIMEOUT;
}
if (!s_initialized) {
domain_direction_unlock_state();
return ESP_ERR_INVALID_STATE;
}
if (delta == 0.0f) {
domain_direction_unlock_state();
return ESP_OK;
}
s_current_angle = domain_angle_normalize(s_current_angle + delta);
domain_direction_unlock_state();
return ESP_OK;
}
void domain_set_calibration(const domain_direction_calibration_t *config)
{
domain_calibration_store_set(config);
}
bool domain_get_calibration(domain_direction_calibration_t *out_config)
{
return domain_calibration_store_get(out_config);
}
@@ -0,0 +1,237 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain.h"
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "platform.h"
static const char *TAG = "domain_fortune_store";
/* NVS namespace + keys (<=15 chars). The platform layer owns the raw storage;
* the domain serializes its typed records as opaque struct blobs. */
#define DOMAIN_FORTUNE_NVS_NAMESPACE "fortune"
#define DOMAIN_FORTUNE_KEY_PROFILE "profile"
#define DOMAIN_FORTUNE_KEY_RESULT "result"
static domain_birth_profile_observer_t s_birth_observer;
static void *s_birth_observer_user_data;
static domain_birth_profile_t s_birth_profile;
static bool s_birth_profile_loaded;
static bool s_birth_profile_valid;
static portMUX_TYPE s_birth_profile_lock = portMUX_INITIALIZER_UNLOCKED;
static domain_fortune_result_t s_fortune_result;
static bool s_fortune_result_loaded;
static bool s_fortune_result_valid;
static portMUX_TYPE s_fortune_result_lock = portMUX_INITIALIZER_UNLOCKED;
static bool domain_birth_profile_is_valid(const domain_birth_profile_t *profile)
{
return profile != NULL &&
profile->valid &&
profile->year > 0 &&
profile->month >= 1 && profile->month <= 12 &&
profile->day >= 1 && profile->day <= 31 &&
profile->hour >= 0 && profile->hour <= 23 &&
profile->minute >= 0 && profile->minute <= 59 &&
profile->birth_place[0] != '\0';
}
static bool domain_fortune_result_is_valid(const domain_fortune_result_t *result)
{
return result != NULL &&
result->valid &&
result->bazi[0] != '\0' &&
result->element[0] != '\0' &&
result->lucky_color[0] != '\0' &&
result->wealth_direction[0] != '\0' &&
result->target_date[0] != '\0' &&
result->wealth_direction_index < domain_get_direction_count();
}
void domain_runtime_register_birth_profile_observer(domain_birth_profile_observer_t cb, void *user_data)
{
portENTER_CRITICAL(&s_birth_profile_lock);
s_birth_observer = cb;
s_birth_observer_user_data = user_data;
portEXIT_CRITICAL(&s_birth_profile_lock);
}
esp_err_t domain_birth_profile_store_set(const domain_birth_profile_t *profile)
{
if (!domain_birth_profile_is_valid(profile)) {
return ESP_ERR_INVALID_ARG;
}
domain_birth_profile_t snapshot = *profile;
snapshot.birth_place[sizeof(snapshot.birth_place) - 1U] = '\0';
snapshot.current_residence[sizeof(snapshot.current_residence) - 1U] = '\0';
snapshot.mbti[sizeof(snapshot.mbti) - 1U] = '\0';
snapshot.timezone[sizeof(snapshot.timezone) - 1U] = '\0';
esp_err_t err = platform_storage_set_blob(DOMAIN_FORTUNE_NVS_NAMESPACE,
DOMAIN_FORTUNE_KEY_PROFILE,
&snapshot,
sizeof(snapshot));
if (err != ESP_OK) {
return err;
}
domain_birth_profile_observer_t cb = NULL;
void *user_data = NULL;
portENTER_CRITICAL(&s_birth_profile_lock);
s_birth_profile = snapshot;
s_birth_profile_loaded = true;
s_birth_profile_valid = true;
cb = s_birth_observer;
user_data = s_birth_observer_user_data;
portEXIT_CRITICAL(&s_birth_profile_lock);
if (cb != NULL) {
cb(&snapshot, user_data);
}
ESP_LOGI(TAG, "Birth profile stored (%04d-%02d-%02d, %s)",
snapshot.year, snapshot.month, snapshot.day, snapshot.birth_place);
return ESP_OK;
}
bool domain_birth_profile_store_get(domain_birth_profile_t *out_profile)
{
if (out_profile == NULL) {
return false;
}
portENTER_CRITICAL(&s_birth_profile_lock);
bool loaded = s_birth_profile_loaded;
bool valid = s_birth_profile_valid;
if (loaded && valid) {
*out_profile = s_birth_profile;
}
portEXIT_CRITICAL(&s_birth_profile_lock);
if (loaded) {
return valid;
}
domain_birth_profile_t persisted = {0};
size_t actual = 0U;
esp_err_t err = platform_storage_get_blob(DOMAIN_FORTUNE_NVS_NAMESPACE,
DOMAIN_FORTUNE_KEY_PROFILE,
&persisted,
sizeof(persisted),
&actual);
bool persisted_valid = err == ESP_OK &&
actual == sizeof(persisted) &&
domain_birth_profile_is_valid(&persisted);
portENTER_CRITICAL(&s_birth_profile_lock);
if (!s_birth_profile_loaded) {
s_birth_profile_loaded = true;
s_birth_profile_valid = persisted_valid;
if (persisted_valid) {
s_birth_profile = persisted;
}
}
valid = s_birth_profile_valid;
if (valid) {
*out_profile = s_birth_profile;
}
portEXIT_CRITICAL(&s_birth_profile_lock);
return valid;
}
esp_err_t domain_fortune_result_store_set(const domain_fortune_result_t *result)
{
if (!domain_fortune_result_is_valid(result)) {
return ESP_ERR_INVALID_ARG;
}
domain_fortune_result_t snapshot = *result;
snapshot.bazi[sizeof(snapshot.bazi) - 1U] = '\0';
snapshot.element[sizeof(snapshot.element) - 1U] = '\0';
snapshot.lucky_color[sizeof(snapshot.lucky_color) - 1U] = '\0';
snapshot.wealth_direction[sizeof(snapshot.wealth_direction) - 1U] = '\0';
snapshot.target_date[sizeof(snapshot.target_date) - 1U] = '\0';
esp_err_t err = platform_storage_set_blob(DOMAIN_FORTUNE_NVS_NAMESPACE,
DOMAIN_FORTUNE_KEY_RESULT,
&snapshot,
sizeof(snapshot));
if (err != ESP_OK) {
return err;
}
portENTER_CRITICAL(&s_fortune_result_lock);
s_fortune_result = snapshot;
s_fortune_result_loaded = true;
s_fortune_result_valid = true;
portEXIT_CRITICAL(&s_fortune_result_lock);
return ESP_OK;
}
bool domain_fortune_result_store_get(domain_fortune_result_t *out_result)
{
if (out_result == NULL) {
return false;
}
portENTER_CRITICAL(&s_fortune_result_lock);
bool loaded = s_fortune_result_loaded;
bool valid = s_fortune_result_valid;
if (loaded && valid) {
*out_result = s_fortune_result;
}
portEXIT_CRITICAL(&s_fortune_result_lock);
if (loaded) {
return valid;
}
domain_fortune_result_t persisted = {0};
size_t actual = 0U;
esp_err_t err = platform_storage_get_blob(DOMAIN_FORTUNE_NVS_NAMESPACE,
DOMAIN_FORTUNE_KEY_RESULT,
&persisted,
sizeof(persisted),
&actual);
bool persisted_valid = err == ESP_OK &&
actual == sizeof(persisted) &&
domain_fortune_result_is_valid(&persisted);
portENTER_CRITICAL(&s_fortune_result_lock);
if (!s_fortune_result_loaded) {
s_fortune_result_loaded = true;
s_fortune_result_valid = persisted_valid;
if (persisted_valid) {
s_fortune_result = persisted;
}
}
valid = s_fortune_result_valid;
if (valid) {
*out_result = s_fortune_result;
}
portEXIT_CRITICAL(&s_fortune_result_lock);
return valid;
}
esp_err_t domain_fortune_result_store_clear(void)
{
esp_err_t err = platform_storage_erase(DOMAIN_FORTUNE_NVS_NAMESPACE,
DOMAIN_FORTUNE_KEY_RESULT);
if (err != ESP_OK) {
return err;
}
portENTER_CRITICAL(&s_fortune_result_lock);
memset(&s_fortune_result, 0, sizeof(s_fortune_result));
s_fortune_result_loaded = true;
s_fortune_result_valid = false;
portEXIT_CRITICAL(&s_fortune_result_lock);
return ESP_OK;
}
@@ -0,0 +1,417 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain.h"
#include <ctype.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
#include "esp_log.h"
#include "platform.h"
static const char *TAG = "domain_user_web";
static const char *kFortuneTimezone = "Asia/Shanghai";
extern const uint8_t user_config_html_start[]
asm("_binary_user_config_html_start");
extern const uint8_t user_config_html_end[]
asm("_binary_user_config_html_end");
static bool is_leap_year(int year)
{
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
static bool is_valid_date(int year, int month, int day)
{
static const uint8_t days_per_month[] = {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
};
if (year < 1900 || year > 2100 || month < 1 || month > 12 || day < 1) {
return false;
}
int max_day = days_per_month[month - 1];
if (month == 2 && is_leap_year(year)) {
max_day = 29;
}
return day <= max_day;
}
static void trim_ascii_whitespace(char *value)
{
if (value == NULL || value[0] == '\0') {
return;
}
char *start = value;
while (*start != '\0' && isspace((unsigned char)*start)) {
start++;
}
if (start != value) {
memmove(value, start, strlen(start) + 1U);
}
size_t len = strlen(value);
while (len > 0U && isspace((unsigned char)value[len - 1U])) {
value[--len] = '\0';
}
}
static bool json_get_required_int(const cJSON *root, const char *key, int *out_value)
{
const cJSON *item = cJSON_GetObjectItemCaseSensitive(root, key);
if (!cJSON_IsNumber(item)) {
return false;
}
*out_value = item->valueint;
return true;
}
static bool json_copy_string(const cJSON *root,
const char *key,
char *out,
size_t out_len,
bool required)
{
const cJSON *item = cJSON_GetObjectItemCaseSensitive(root, key);
if (item == NULL && !required) {
out[0] = '\0';
return true;
}
if (!cJSON_IsString(item) || item->valuestring == NULL) {
return false;
}
size_t len = strlen(item->valuestring);
if (len >= out_len) {
return false;
}
memcpy(out, item->valuestring, len + 1U);
trim_ascii_whitespace(out);
return !required || out[0] != '\0';
}
static esp_err_t response_copy(platform_lan_http_response_t *response,
platform_lan_http_status_t status,
platform_lan_http_content_type_t content_type,
const uint8_t *body,
size_t body_len)
{
if (body_len > response->body_capacity) {
return ESP_ERR_INVALID_SIZE;
}
if (body_len > 0U) {
memcpy(response->body, body, body_len);
}
response->status = status;
response->content_type = content_type;
response->body_len = body_len;
return ESP_OK;
}
static esp_err_t response_json_object(platform_lan_http_response_t *response,
platform_lan_http_status_t status,
cJSON *root)
{
if (response == NULL ||
response->body == NULL ||
response->body_capacity == 0U ||
response->body_capacity > (size_t)INT_MAX) {
return ESP_ERR_INVALID_ARG;
}
if (!cJSON_PrintPreallocated(root,
(char *)response->body,
(int)response->body_capacity,
false)) {
response->body_len = 0U;
return ESP_ERR_INVALID_SIZE;
}
response->status = status;
response->content_type = PLATFORM_LAN_HTTP_CONTENT_JSON;
response->body_len = strlen((const char *)response->body);
return ESP_OK;
}
static esp_err_t response_error(platform_lan_http_response_t *response,
platform_lan_http_status_t status,
const char *message)
{
cJSON *root = cJSON_CreateObject();
if (root == NULL) {
return ESP_ERR_NO_MEM;
}
cJSON_AddBoolToObject(root, "success", false);
cJSON_AddStringToObject(root, "error", message);
esp_err_t err = response_json_object(response, status, root);
cJSON_Delete(root);
return err;
}
static esp_err_t handle_index(platform_lan_http_response_t *response)
{
size_t html_len = (size_t)(user_config_html_end - user_config_html_start);
return response_copy(response,
PLATFORM_LAN_HTTP_STATUS_OK,
PLATFORM_LAN_HTTP_CONTENT_HTML,
user_config_html_start,
html_len);
}
static esp_err_t handle_get_profile(platform_lan_http_response_t *response)
{
cJSON *root = cJSON_CreateObject();
if (root == NULL) {
return ESP_ERR_NO_MEM;
}
domain_birth_profile_t profile = {0};
bool configured = domain_birth_profile_store_get(&profile);
cJSON_AddBoolToObject(root, "configured", configured);
if (configured) {
cJSON_AddNumberToObject(root, "birthYear", profile.year);
cJSON_AddNumberToObject(root, "birthMonth", profile.month);
cJSON_AddNumberToObject(root, "birthDay", profile.day);
cJSON_AddNumberToObject(root, "birthHour", profile.hour);
cJSON_AddNumberToObject(root, "birthMinute", profile.minute);
cJSON_AddStringToObject(root, "birthPlace", profile.birth_place);
cJSON_AddStringToObject(root, "currentResidence", profile.current_residence);
cJSON_AddStringToObject(root, "mbti", profile.mbti);
cJSON_AddStringToObject(root, "timezone", profile.timezone);
}
esp_err_t err = response_json_object(response,
PLATFORM_LAN_HTTP_STATUS_OK,
root);
cJSON_Delete(root);
return err;
}
static bool parse_profile(const uint8_t *body,
size_t body_len,
domain_birth_profile_t *out_profile,
const char **out_error)
{
if (body == NULL || body_len == 0U) {
*out_error = "Request body is empty";
return false;
}
cJSON *root = cJSON_ParseWithLength((const char *)body, body_len);
if (root == NULL) {
*out_error = "Invalid JSON";
return false;
}
memset(out_profile, 0, sizeof(*out_profile));
bool valid = json_get_required_int(root, "birthYear", &out_profile->year) &&
json_get_required_int(root, "birthMonth", &out_profile->month) &&
json_get_required_int(root, "birthDay", &out_profile->day) &&
json_get_required_int(root, "birthHour", &out_profile->hour) &&
json_get_required_int(root, "birthMinute", &out_profile->minute) &&
json_copy_string(root,
"birthPlace",
out_profile->birth_place,
sizeof(out_profile->birth_place),
true) &&
json_copy_string(root,
"currentResidence",
out_profile->current_residence,
sizeof(out_profile->current_residence),
false) &&
json_copy_string(root,
"mbti",
out_profile->mbti,
sizeof(out_profile->mbti),
false);
cJSON_Delete(root);
if (!valid) {
*out_error = "Missing or oversized profile field";
return false;
}
if (!is_valid_date(out_profile->year, out_profile->month, out_profile->day)) {
*out_error = "Invalid birth date";
return false;
}
if (out_profile->hour < 0 || out_profile->hour > 23 ||
out_profile->minute < 0 || out_profile->minute > 59) {
*out_error = "Invalid birth time";
return false;
}
for (char *cursor = out_profile->mbti; *cursor != '\0'; ++cursor) {
*cursor = (char)toupper((unsigned char)*cursor);
}
strlcpy(out_profile->timezone, kFortuneTimezone, sizeof(out_profile->timezone));
out_profile->valid = true;
return true;
}
static esp_err_t handle_post_profile(const platform_lan_http_request_t *request,
platform_lan_http_response_t *response)
{
domain_birth_profile_t profile = {0};
const char *error_message = NULL;
if (!parse_profile(request->body,
request->body_len,
&profile,
&error_message)) {
return response_error(response,
PLATFORM_LAN_HTTP_STATUS_BAD_REQUEST,
error_message);
}
esp_err_t err = domain_birth_profile_store_set(&profile);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Persist birth profile failed: %s", esp_err_to_name(err));
return response_error(response,
PLATFORM_LAN_HTTP_STATUS_INTERNAL_ERROR,
"Failed to persist profile");
}
err = domain_fortune_result_store_clear();
if (err != ESP_OK) {
ESP_LOGW(TAG, "Invalidate previous fortune failed: %s", esp_err_to_name(err));
return response_error(response,
PLATFORM_LAN_HTTP_STATUS_INTERNAL_ERROR,
"Profile saved but previous result could not be invalidated");
}
cJSON *root = cJSON_CreateObject();
if (root == NULL) {
return ESP_ERR_NO_MEM;
}
cJSON_AddBoolToObject(root, "success", true);
cJSON_AddStringToObject(root, "status", "pending");
err = response_json_object(response,
PLATFORM_LAN_HTTP_STATUS_ACCEPTED,
root);
cJSON_Delete(root);
return err;
}
static esp_err_t handle_get_fortune(platform_lan_http_response_t *response)
{
cJSON *root = cJSON_CreateObject();
if (root == NULL) {
return ESP_ERR_NO_MEM;
}
domain_birth_profile_t profile = {0};
domain_fortune_result_t result = {0};
if (!domain_birth_profile_store_get(&profile)) {
cJSON_AddStringToObject(root, "status", "not_configured");
} else if (!domain_fortune_result_store_get(&result)) {
cJSON_AddStringToObject(root, "status", "pending");
} else {
cJSON_AddStringToObject(root, "status", "ready");
cJSON_AddStringToObject(root, "bazi", result.bazi);
cJSON_AddStringToObject(root, "element", result.element);
cJSON_AddStringToObject(root, "luckyColor", result.lucky_color);
cJSON_AddStringToObject(root, "wealthDirection", result.wealth_direction);
cJSON_AddStringToObject(root, "targetDate", result.target_date);
}
esp_err_t err = response_json_object(response,
PLATFORM_LAN_HTTP_STATUS_OK,
root);
cJSON_Delete(root);
return err;
}
static esp_err_t user_config_http_handler(
const platform_lan_http_request_t *request,
platform_lan_http_response_t *response,
void *user_data)
{
(void)user_data;
if (strcmp(request->path, "/") == 0) {
if (request->method != PLATFORM_LAN_HTTP_METHOD_GET) {
return response_error(response,
PLATFORM_LAN_HTTP_STATUS_METHOD_NOT_ALLOWED,
"Method not allowed");
}
return handle_index(response);
}
if (strcmp(request->path, "/api/profile") == 0) {
if (request->method == PLATFORM_LAN_HTTP_METHOD_GET) {
return handle_get_profile(response);
}
if (request->method == PLATFORM_LAN_HTTP_METHOD_POST) {
return handle_post_profile(request, response);
}
return response_error(response,
PLATFORM_LAN_HTTP_STATUS_METHOD_NOT_ALLOWED,
"Method not allowed");
}
if (strcmp(request->path, "/api/fortune") == 0) {
if (request->method != PLATFORM_LAN_HTTP_METHOD_GET) {
return response_error(response,
PLATFORM_LAN_HTTP_STATUS_METHOD_NOT_ALLOWED,
"Method not allowed");
}
return handle_get_fortune(response);
}
return response_error(response,
PLATFORM_LAN_HTTP_STATUS_NOT_FOUND,
"Route not found");
}
esp_err_t domain_user_config_server_start(
const domain_user_config_server_config_t *config)
{
if (config == NULL ||
config->port == 0U ||
config->hostname[0] == '\0' ||
strnlen(config->hostname, sizeof(config->hostname)) >= sizeof(config->hostname)) {
return ESP_ERR_INVALID_ARG;
}
platform_lan_http_server_config_t platform_config = {
.port = config->port,
.handler = user_config_http_handler,
.user_data = NULL,
};
strlcpy(platform_config.hostname,
config->hostname,
sizeof(platform_config.hostname));
return platform_lan_http_server_start(&platform_config);
}
esp_err_t domain_user_config_server_stop(int timeout_ms)
{
return platform_lan_http_server_stop(timeout_ms);
}
esp_err_t domain_user_config_server_get_state(
domain_user_config_server_state_t *out_state)
{
if (out_state == NULL) {
return ESP_ERR_INVALID_ARG;
}
platform_lan_http_server_state_t platform_state = {0};
esp_err_t err = platform_lan_http_server_get_state(&platform_state);
if (err != ESP_OK) {
return err;
}
memset(out_state, 0, sizeof(*out_state));
out_state->running = platform_state.running;
out_state->port = platform_state.port;
strlcpy(out_state->hostname,
platform_state.hostname,
sizeof(out_state->hostname));
return ESP_OK;
}
File diff suppressed because it is too large Load Diff