/** * @file web_ctrl.c * @brief WiFi STA + HTTP 服务器 + 网页控制面板 */ #include "web_ctrl.h" #include "emm_v5.h" #include "magnet.h" #include "servo.h" #include "protocol.h" #include "actuator.h" #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_http_server.h" #include "esp_log.h" #include "esp_netif.h" #include "nvs_flash.h" static const char *TAG = "WEB_CTRL"; /* ---- WiFi 配置 ---- */ #define WIFI_SSID "ICE" #define WIFI_PASS "12345678" #define WIFI_MAX_RETRY 5 #define WIFI_CONNECT_TIMEOUT_MS 30000 static EventGroupHandle_t s_wifi_event_group; #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 static int s_retry_num = 0; /* 保存 IP 供 /api/status 使用 */ static char s_ip_str[16] = "0.0.0.0"; /* ---- HTML 页面 ---- */ static const char INDEX_HTML[] = R"raw( EDC 步进电机控制

EDC 步进电机控制

WiFi: -- IP: --

轴控制 (XYZA)

→电机2 (CW) 0~210
→电机3+4 (CCW) 0~335
→电机1 (±: CW/CCW)
°
RPM

电机使能

速度模式

RPM

位置模式

RPM

角度控制

°
RPM

舵机控制

90°

电磁铁

读取参数

回零校准 (Origin)

RPM
ms
RPM
mA
ms

通讯协议

I2C: - UART: - 通道: -
等待 Cycle...

运行日志

等待指令...
)raw"; /* ============================================================ * WiFi 事件处理 * ============================================================ */ static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { wifi_event_sta_disconnected_t *ev = (wifi_event_sta_disconnected_t *)data; ESP_LOGW(TAG, "WiFi 断开 (reason=%d), 重试 %d/%d", ev->reason, s_retry_num + 1, WIFI_MAX_RETRY); if (s_retry_num < WIFI_MAX_RETRY) { s_retry_num++; esp_wifi_connect(); } else { xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *ev = (ip_event_got_ip_t *)data; snprintf(s_ip_str, sizeof(s_ip_str), IPSTR, IP2STR(&ev->ip_info.ip)); ESP_LOGI(TAG, "========================================"); ESP_LOGI(TAG, " WiFi 已连接!"); ESP_LOGI(TAG, " SSID: %s", WIFI_SSID); ESP_LOGI(TAG, " IP: %s", s_ip_str); ESP_LOGI(TAG, " 浏览器打开 http://%s", s_ip_str); ESP_LOGI(TAG, "========================================"); s_retry_num = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } } static void wifi_init_sta(void) { s_wifi_event_group = xEventGroupCreate(); ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); esp_event_handler_instance_t inst_any_id, inst_got_ip; ESP_ERROR_CHECK(esp_event_handler_instance_register( WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, &inst_any_id)); ESP_ERROR_CHECK(esp_event_handler_instance_register( IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, &inst_got_ip)); wifi_config_t wifi_cfg = { .sta = { .ssid = WIFI_SSID, .password = WIFI_PASS, }, }; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_LOGI(TAG, "正在连接 WiFi: %s ...", WIFI_SSID); /* 等待连接成功或失败 (30 秒超时) */ EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, pdMS_TO_TICKS(WIFI_CONNECT_TIMEOUT_MS)); if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "WiFi 就绪 (IP: %s)", s_ip_str); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGE(TAG, "WiFi 连接失败! 请检查 SSID/密码"); } else { ESP_LOGE(TAG, "WiFi 连接超时 (%d秒)!", WIFI_CONNECT_TIMEOUT_MS / 1000); } /* 无论 WiFi 是否成功, 都继续启动 HTTP 服务器 */ } /* ============================================================ * HTTP 查询字符串解析 * ============================================================ */ static int get_query_int(httpd_req_t *req, const char *key, int def) { char buf[128]; size_t len = httpd_req_get_url_query_len(req) + 1; if (len > sizeof(buf)) len = sizeof(buf); if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) { char val[16]; if (httpd_query_key_value(buf, key, val, sizeof(val)) == ESP_OK) { return atoi(val); } } return def; } static float get_query_float(httpd_req_t *req, const char *key, float def) { char buf[128]; size_t len = httpd_req_get_url_query_len(req) + 1; if (len > sizeof(buf)) len = sizeof(buf); if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) { char val[16]; if (httpd_query_key_value(buf, key, val, sizeof(val)) == ESP_OK) { return (float)atof(val); } } return def; } /* ============================================================ * HTTP 处理器 * ============================================================ */ static esp_err_t root_handler(httpd_req_t *req) { const char *html = INDEX_HTML; size_t total = strlen(html); size_t offset = 0; httpd_resp_set_type(req, "text/html; charset=utf-8"); httpd_resp_set_hdr(req, "Connection", "close"); while (offset < total) { size_t chunk = total - offset; if (chunk > 1024) chunk = 1024; esp_err_t err = httpd_resp_send_chunk(req, html + offset, (int)chunk); if (err != ESP_OK) { ESP_LOGE(TAG, "发送 HTML 失败: %d", err); return err; } offset += chunk; } httpd_resp_send_chunk(req, NULL, 0); /* 结束分块 */ return ESP_OK; } static esp_err_t api_enable_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); int state = get_query_int(req, "state", 0); Emm_V5_En_Control((uint8_t)addr, (bool)state, false); char resp[64]; snprintf(resp, sizeof(resp), "电机 %d %s", addr, state ? "使能" : "关闭"); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t api_velocity_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); int dir = get_query_int(req, "dir", 0); int vel = get_query_int(req, "vel", 100); int acc = get_query_int(req, "acc", 10); Emm_V5_Vel_Control((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc, false); char resp[64]; snprintf(resp, sizeof(resp), "速度模式: 地址%d %s %dRPM acc=%d", addr, dir ? "CCW" : "CW", vel, acc); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t api_position_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); int dir = get_query_int(req, "dir", 0); int vel = get_query_int(req, "vel", 100); int acc = get_query_int(req, "acc", 10); int clk = get_query_int(req, "clk", 2000); int raf = get_query_int(req, "raf", 0); Emm_V5_Pos_Control((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc, (uint32_t)clk, (bool)raf, false); char resp[80]; snprintf(resp, sizeof(resp), "位置模式: 地址%d %s %dRPM %d脉冲 %s", addr, dir ? "CCW" : "CW", vel, clk, raf ? "绝对" : "相对"); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t api_stop_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); Emm_V5_Stop_Now((uint8_t)addr, false); char resp[32]; snprintf(resp, sizeof(resp), "电机 %d 已停止", addr); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t api_magnet_handler(httpd_req_t *req) { int state = get_query_int(req, "state", 0); magnet_set((bool)state); char resp[32]; snprintf(resp, sizeof(resp), "电磁铁 %s", state ? "吸合" : "释放"); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t api_status_handler(httpd_req_t *req) { char json[128]; wifi_ap_record_t ap; esp_err_t err = esp_wifi_sta_get_ap_info(&ap); snprintf(json, sizeof(json), "{\"ssid\":\"%s\",\"ip\":\"%s\"}", (err == ESP_OK) ? (char *)ap.ssid : "N/A", s_ip_str); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t api_servo_handler(httpd_req_t *req) { char buf[128]; size_t len = httpd_req_get_url_query_len(req) + 1; if (len > sizeof(buf)) len = sizeof(buf); if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) { char val[16]; if (httpd_query_key_value(buf, "pos", val, sizeof(val)) == ESP_OK) { if (strcmp(val, "up") == 0) { servo_set_up(); } else { servo_set_down(); } char resp[32]; snprintf(resp, sizeof(resp), "舵机: %s", val); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } if (httpd_query_key_value(buf, "angle", val, sizeof(val)) == ESP_OK) { float angle = atof(val); servo_set_angle(angle); char resp[32]; snprintf(resp, sizeof(resp), "舵机: %.1f°", (double)angle); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } } httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 angle 或 pos 参数"); return ESP_FAIL; } /* ---- 轴联动: XYZA 同时驱动 (mm + °) ---- */ static esp_err_t api_axis_handler(httpd_req_t *req) { float x_mm = get_query_float(req, "x", 0.0f); float y_mm = get_query_float(req, "y", 0.0f); float z_deg = get_query_float(req, "zrot", 0.0f); float s_ang = get_query_float(req, "servo", -1.0f); int vel = get_query_int(req, "vel", ACTUATOR_DEFAULT_VEL); int acc = get_query_int(req, "acc", ACTUATOR_DEFAULT_ACC); actuator_motor_mm(ACTUATOR_ADDR_X, x_mm, (uint16_t)vel, (uint8_t)acc, true); actuator_motor_mm(ACTUATOR_ADDR_Y1, y_mm, (uint16_t)vel, (uint8_t)acc, true); actuator_motor_angle(ACTUATOR_ADDR_ANGLE, z_deg, (uint16_t)vel, (uint8_t)acc, true); if (s_ang >= 0.0f) actuator_servo_set_angle(s_ang); char resp[128]; snprintf(resp, sizeof(resp), "轴联动: X=%.1fmm Y=%.1fmm Z=%.1f° Servo=%.1f° vel=%d acc=%d", (double)x_mm, (double)y_mm, (double)z_deg, (double)s_ang, vel, acc); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* ---- 直线距离控制: mm → 脉冲 ---- */ static esp_err_t api_mm_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 2); float mm = get_query_float(req, "mm", 0.0f); int vel = get_query_int(req, "vel", ACTUATOR_DEFAULT_VEL); int acc = get_query_int(req, "acc", ACTUATOR_DEFAULT_ACC); int raf = get_query_int(req, "raf", 1); actuator_motor_mm((uint8_t)addr, mm, (uint16_t)vel, (uint8_t)acc, (bool)raf); uint32_t clk = ACTUATOR_MM_TO_PULSES(mm); char resp[96]; snprintf(resp, sizeof(resp), "直线模式: 地址%d %.2fmm → %lu脉冲 %s vel=%dRPM acc=%d", addr, (double)mm, (unsigned long)clk, raf ? "绝对" : "相对", vel, acc); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* ---- 角度直控: ° → 脉冲 ---- */ static esp_err_t api_angle_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); float angle = get_query_float(req, "angle", 0.0f); int vel = get_query_int(req, "vel", ACTUATOR_DEFAULT_VEL); int acc = get_query_int(req, "acc", ACTUATOR_DEFAULT_ACC); int raf = get_query_int(req, "raf", 1); actuator_motor_angle((uint8_t)addr, angle, (uint16_t)vel, (uint8_t)acc, (bool)raf); uint32_t clk = EMM_V5_DEG_TO_CLK(angle); char resp[96]; snprintf(resp, sizeof(resp), "角度模式: 地址%d %.1f° → %lu脉冲 %s vel=%dRPM acc=%d", addr, (double)angle, (unsigned long)clk, raf ? "绝对" : "相对", vel, acc); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* ---- 当前位置清零 ---- */ static esp_err_t api_reset_pos_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); Emm_V5_Reset_CurPos_To_Zero((uint8_t)addr); char resp[32]; snprintf(resp, sizeof(resp), "电机 %d 位置已清零", addr); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* ---- 解除堵转保护 ---- */ static esp_err_t api_reset_clog_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); Emm_V5_Reset_Clog_Pro((uint8_t)addr); char resp[32]; snprintf(resp, sizeof(resp), "电机 %d 堵转保护已解除", addr); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* ---- 读取系统参数 ---- */ static esp_err_t api_read_param_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); int param = get_query_int(req, "param", 0); Emm_V5_Read_Sys_Params((uint8_t)addr, (EmmSysParams_t)param); uint8_t rx[128]; uint8_t len = Emm_V5_Receive_Data(rx, sizeof(rx)); uint32_t val32 = 0; uint16_t val16 = 0; int sign = 0; float angle = 0.0f; if (len >= 8) { sign = rx[2]; val32 = ((uint32_t)rx[3] << 24) | ((uint32_t)rx[4] << 16) | ((uint32_t)rx[5] << 8) | ((uint32_t)rx[6]); uint16_t pos16 = (uint16_t)(val32 & 0xFFFF); angle = (sign ? -1.0f : 1.0f) * pos16 * 360.0f / 65536.0f; } if (len >= 6) { val16 = ((uint16_t)rx[3] << 8) | ((uint16_t)rx[4]); } char json[512]; int off = snprintf(json, sizeof(json), "{\"addr\":%d,\"param\":%d,\"len\":%d,\"sign\":%d" ",\"val32\":%lu,\"val16\":%u,\"angle\":%.2f", addr, param, len, sign, (unsigned long)val32, (unsigned)val16, (double)angle); off += snprintf(json + off, sizeof(json) - off, ",\"hex\":\""); for (int i = 0; i < len && off < (int)sizeof(json) - 4; i++) { off += snprintf(json + off, sizeof(json) - off, "%02X", rx[i]); } off += snprintf(json + off, sizeof(json) - off, "\"}"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* ---- 回零: 设置零点 ---- */ static esp_err_t api_origin_set_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); int svf = get_query_int(req, "svf", 0); Emm_V5_Origin_Set_O((uint8_t)addr, (bool)svf); char resp[48]; snprintf(resp, sizeof(resp), "电机 %d 零点已设置 (svF=%d)", addr, svf); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* ---- 回零: 配置参数 ---- */ static esp_err_t api_origin_config_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); int svf = get_query_int(req, "svf", 0); int omode = get_query_int(req, "omode", 2); /* 默认: 多圈无限位碰撞 */ int odir = get_query_int(req, "odir", 0); int ovel = get_query_int(req, "ovel", 200); int otm = get_query_int(req, "otm", 10000); int slvel = get_query_int(req, "slvel", 100); int slma = get_query_int(req, "slma", 500); int slms = get_query_int(req, "slms", 100); int potf = get_query_int(req, "potf", 0); Emm_V5_Origin_Modify_Params((uint8_t)addr, (bool)svf, (uint8_t)omode, (uint8_t)odir, (uint16_t)ovel, (uint32_t)otm, (uint16_t)slvel, (uint16_t)slma, (uint16_t)slms, (bool)potf); char resp[128]; snprintf(resp, sizeof(resp), "电机 %d 回零参数已配置 (mode=%d dir=%d vel=%d tm=%lu svF=%d potF=%d)", addr, omode, odir, ovel, (unsigned long)otm, svf, potf); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* ---- 回零: 触发回零 ---- */ static esp_err_t api_origin_go_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); int omode = get_query_int(req, "omode", 2); /* 默认: 多圈无限位碰撞 */ Emm_V5_Origin_Trigger_Return((uint8_t)addr, (uint8_t)omode, false); char resp[48]; snprintf(resp, sizeof(resp), "电机 %d 回零已触发 (mode=%d)", addr, omode); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* ---- 回零: 强制中断 ---- */ static esp_err_t api_origin_stop_handler(httpd_req_t *req) { int addr = get_query_int(req, "addr", 1); Emm_V5_Origin_Interrupt((uint8_t)addr); char resp[32]; snprintf(resp, sizeof(resp), "电机 %d 回零已中断", addr); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } /* favicon — 204 No Content, 避免浏览器反复请求 */ static esp_err_t favicon_handler(httpd_req_t *req) { httpd_resp_set_status(req, "204 No Content"); httpd_resp_send(req, NULL, 0); return ESP_OK; } /* ============================================================ * 协议状态 API * ============================================================ */ /* 最近收到的 Cycle 快照 (由 edc_proto_on_cycle 更新, web 读取) */ static edc_cycle_t s_web_cycle; static volatile bool s_web_cycle_valid; static portMUX_TYPE s_web_lock = portMUX_INITIALIZER_UNLOCKED; void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch) { static uint16_t s_last_done_id = 0xFFFF; portENTER_CRITICAL(&s_web_lock); memcpy(&s_web_cycle, cycle, sizeof(edc_cycle_t)); s_web_cycle_valid = true; portEXIT_CRITICAL(&s_web_lock); ESP_LOGI(TAG, "Cycle #%u via %s, %u frags", cycle->cycle_id, ch == EDC_CH_I2C ? "I2C" : "UART", cycle->frag_count); /* I2C / UART 冗余下发, 同一 cycle_id 只回一次 DONE */ if (cycle->cycle_id == s_last_done_id) { return; } s_last_done_id = cycle->cycle_id; /* 回复 MPU: Cycle 已收到 (后续实际执行电机后应移到执行完毕处) */ edc_proto_send_done(cycle->cycle_id); } static esp_err_t api_proto_status_handler(httpd_req_t *req) { edc_proto_stat_t stat; edc_proto_get_stats(&stat); char json[1024]; int off = snprintf(json, sizeof(json), "{\"i2c_ok\":%lu,\"i2c_err\":%lu,\"uart_ok\":%lu,\"uart_err\":%lu," "\"last_ch\":\"%s\",\"done\":%lu,\"req\":%lu", stat.i2c.cycles_ok, stat.i2c.cycles_err, stat.uart.cycles_ok, stat.uart.cycles_err, (stat.last_channel == EDC_CH_I2C) ? "I2C" : (stat.last_channel == EDC_CH_UART) ? "UART" : "none", stat.done_sent, stat.request_sent); portENTER_CRITICAL(&s_web_lock); bool valid = s_web_cycle_valid; edc_cycle_t cycle; if (valid) memcpy(&cycle, &s_web_cycle, sizeof(edc_cycle_t)); portEXIT_CRITICAL(&s_web_lock); if (valid) { off += snprintf(json + off, sizeof(json) - off, ",\"cycle\":{\"id\":%u,\"frags\":%u,\"data\":[", cycle.cycle_id, cycle.frag_count); for (int i = 0; i < cycle.frag_count; i++) { edc_fragment_t *f = &cycle.fragments[i]; off += snprintf(json + off, sizeof(json) - off, "%s{\"sx\":%u,\"sy\":%u,\"sa\":%.1f,\"tx\":%u,\"ty\":%u,\"ta\":%.1f}", (i > 0) ? "," : "", f->source.x, f->source.y, f->source.angle_x10 / 10.0f, f->target.x, f->target.y, f->target.angle_x10 / 10.0f); } off += snprintf(json + off, sizeof(json) - off, "]}"); } off += snprintf(json + off, sizeof(json) - off, "}"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t api_proto_request_handler(httpd_req_t *req) { edc_proto_send_request(); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, "已发送 CYCLE_REQUEST", HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t api_proto_done_handler(httpd_req_t *req) { edc_cycle_t cycle; edc_proto_stat_t stat; edc_proto_get_stats(&stat); /* 对最近收到的 cycle 发送 DONE */ if (stat.last_channel != EDC_CH_NONE) { char buf[128]; size_t len = httpd_req_get_url_query_len(req) + 1; if (len > sizeof(buf)) len = sizeof(buf); uint16_t cid = 0; if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) { char val[16]; if (httpd_query_key_value(buf, "id", val, sizeof(val)) == ESP_OK) { cid = (uint16_t)atoi(val); } } edc_proto_send_done(cid); char resp[64]; snprintf(resp, sizeof(resp), "已发送 CYCLE_DONE (id=%u)", cid); httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "尚无收到 Cycle"); return ESP_FAIL; } static void http_server_init(void) { httpd_handle_t server = NULL; httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); cfg.stack_size = 8192; /* 默认 4096 不够, 增大 */ cfg.max_uri_handlers = 30; cfg.max_open_sockets = 4; cfg.lru_purge_enable = true; ESP_ERROR_CHECK(httpd_start(&server, &cfg)); httpd_uri_t uri_root = { .uri = "/", .method = HTTP_GET, .handler = root_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_root); httpd_uri_t uri_enable = { .uri = "/api/enable", .method = HTTP_GET, .handler = api_enable_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_enable); httpd_uri_t uri_velocity = { .uri = "/api/velocity", .method = HTTP_GET, .handler = api_velocity_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_velocity); httpd_uri_t uri_position = { .uri = "/api/position", .method = HTTP_GET, .handler = api_position_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_position); httpd_uri_t uri_stop = { .uri = "/api/stop", .method = HTTP_GET, .handler = api_stop_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_stop); httpd_uri_t uri_magnet = { .uri = "/api/magnet", .method = HTTP_GET, .handler = api_magnet_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_magnet); httpd_uri_t uri_status = { .uri = "/api/status", .method = HTTP_GET, .handler = api_status_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_status); httpd_uri_t uri_servo = { .uri = "/api/servo", .method = HTTP_GET, .handler = api_servo_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_servo); /* ---- 轴联动 + 直线/角度 ---- */ httpd_uri_t uri_axis = { .uri = "/api/axis", .method = HTTP_GET, .handler = api_axis_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_axis); httpd_uri_t uri_mm = { .uri = "/api/mm", .method = HTTP_GET, .handler = api_mm_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_mm); httpd_uri_t uri_angle = { .uri = "/api/angle", .method = HTTP_GET, .handler = api_angle_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_angle); /* ---- 底层调试 API ---- */ httpd_uri_t uri_reset_pos = { .uri = "/api/reset_pos", .method = HTTP_GET, .handler = api_reset_pos_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_reset_pos); httpd_uri_t uri_reset_clog = { .uri = "/api/reset_clog", .method = HTTP_GET, .handler = api_reset_clog_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_reset_clog); httpd_uri_t uri_read_param = { .uri = "/api/read_param", .method = HTTP_GET, .handler = api_read_param_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_read_param); /* favicon.ico — 返回空, 避免 404 警告 */ httpd_uri_t uri_favicon = { .uri = "/favicon.ico", .method = HTTP_GET, .handler = favicon_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_favicon); /* ---- 回零校准 API ---- */ httpd_uri_t uri_origin_set = { .uri = "/api/origin_set", .method = HTTP_GET, .handler = api_origin_set_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_origin_set); httpd_uri_t uri_origin_config = { .uri = "/api/origin_config", .method = HTTP_GET, .handler = api_origin_config_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_origin_config); httpd_uri_t uri_origin_go = { .uri = "/api/origin_go", .method = HTTP_GET, .handler = api_origin_go_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_origin_go); httpd_uri_t uri_origin_stop = { .uri = "/api/origin_stop", .method = HTTP_GET, .handler = api_origin_stop_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_origin_stop); /* ---- 协议状态 API ---- */ httpd_uri_t uri_proto_status = { .uri = "/api/proto/status", .method = HTTP_GET, .handler = api_proto_status_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_proto_status); httpd_uri_t uri_proto_request = { .uri = "/api/proto/request", .method = HTTP_GET, .handler = api_proto_request_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_proto_request); httpd_uri_t uri_proto_done = { .uri = "/api/proto/done", .method = HTTP_GET, .handler = api_proto_done_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &uri_proto_done); ESP_LOGI(TAG, "HTTP 服务器已启动 (端口 %d)", cfg.server_port); } /* ============================================================ * 公开接口 * ============================================================ */ void web_ctrl_init(void) { wifi_init_sta(); http_server_init(); }