diff --git a/components/actuator/CMakeLists.txt b/components/actuator/CMakeLists.txt new file mode 100644 index 0000000..f1b60f3 --- /dev/null +++ b/components/actuator/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "actuator.c" + INCLUDE_DIRS "." + REQUIRES emm_v5 servo protocol driver freertos) diff --git a/components/actuator/actuator.c b/components/actuator/actuator.c new file mode 100644 index 0000000..15e4662 --- /dev/null +++ b/components/actuator/actuator.c @@ -0,0 +1,364 @@ +/** + * @file actuator.c + * @brief 执行器应用层封装实现 + */ + +#include "actuator.h" +#include "emm_v5.h" +#include "servo.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include + +static const char *TAG = "ACTUATOR"; + +/* ================================================================ + * 运行时状态 + * ================================================================ */ + +static float s_scale_x = ACTUATOR_SCALE_X; +static float s_scale_y = ACTUATOR_SCALE_Y; +static float s_scale_angle = ACTUATOR_SCALE_ANGLE; + +static bool s_initialized = false; + +/* ================================================================ + * 内部: 帧间间隔 + * + * Emm_V5 驱动板无法解析 UART 上零间隔的背靠背连续帧 (会丢字节, + * 导致本帧与后续帧全部失效), 凡连发多帧必须插入该延时。 + * 曾导致故障: Y 轴 3/4 号电机使能 (单帧) 正常, 速度/位置 (连发多帧) 无反应 + * ================================================================ */ + +static inline void _frame_gap(void) +{ + vTaskDelay(pdMS_TO_TICKS(ACTUATOR_FRAME_GAP_MS)); +} + +/* ================================================================ + * 初始化 + * ================================================================ */ + +void actuator_init(void) +{ + /* 初始化电机驱动 UART */ + Emm_V5_Init(ACTUATOR_UART_PORT, ACTUATOR_UART_TX, ACTUATOR_UART_RX); + + /* 初始化舵机 PWM */ + servo_init(ACTUATOR_SERVO_GPIO); + + /* 配置 4 路电机: 闭环位置模式 + 使能 */ + const uint8_t addrs[] = { + ACTUATOR_ADDR_ANGLE, ACTUATOR_ADDR_X, + ACTUATOR_ADDR_Y1, ACTUATOR_ADDR_Y2 + }; + for (int i = 0; i < 4; i++) { + uint8_t addr = addrs[i]; + Emm_V5_Modify_Ctrl_Mode(addr, false, ACTUATOR_CTRL_MODE); + vTaskDelay(pdMS_TO_TICKS(20)); + Emm_V5_En_Control(addr, true, false); + vTaskDelay(pdMS_TO_TICKS(20)); + } + + s_initialized = true; + ESP_LOGI(TAG, "初始化完成 (Angle=addr%d X=addr%d Y=addr%d/%d, servo=GPIO%d)", + ACTUATOR_ADDR_ANGLE, + ACTUATOR_ADDR_X, + ACTUATOR_ADDR_Y1, ACTUATOR_ADDR_Y2, + ACTUATOR_SERVO_GPIO); +} + +/* ================================================================ + * 脉冲换算 + * ================================================================ */ + +void actuator_set_scale(float x_scale, float y_scale, float angle_scale) +{ + s_scale_x = x_scale; + s_scale_y = y_scale; + s_scale_angle = angle_scale; + ESP_LOGI(TAG, "脉冲比例: X=%.3f Y=%.3f Angle=%.3f", + s_scale_x, s_scale_y, s_scale_angle); +} + +/* ================================================================ + * 内部: 单轴绝对位置运动 + * ================================================================ */ + +static void _actuator_move_abs(uint8_t addr, uint8_t dir, uint32_t pulses, + uint16_t vel, uint8_t acc, bool snF) +{ + Emm_V5_Pos_Control(addr, dir, vel, acc, + pulses, + true, /* raF=true: 绝对运动 */ + snF); /* 多机同步标志 */ +} + +/* ================================================================ + * 内部: Y 轴双电机同步运动 + * ================================================================ */ + +static void _actuator_move_y_sync(uint32_t pulses_y) +{ + uint16_t vel = ACTUATOR_DEFAULT_VEL; + uint8_t acc = ACTUATOR_DEFAULT_ACC; + + /* Y 轴正向 = CCW (dir=1), 双电机同步 */ + _actuator_move_abs(ACTUATOR_ADDR_Y1, ACTUATOR_DIR_Y, pulses_y, vel, acc, true); + _frame_gap(); + _actuator_move_abs(ACTUATOR_ADDR_Y2, ACTUATOR_DIR_Y, pulses_y, vel, acc, true); + _frame_gap(); + + /* 广播地址触发一次, 两台电机同时开始运动 */ + Emm_V5_Synchronous_motion(0); + + ESP_LOGD(TAG, "Y 同步(CCW): addr%d+%d -> %lu pulses (%.2fmm)", + ACTUATOR_ADDR_Y1, ACTUATOR_ADDR_Y2, pulses_y, + (double)pulses_y / (double)ACTUATOR_SCALE_Y); +} + +/* ================================================================ + * 内部: 执行单个 Fragment + * ================================================================ */ + +static void _actuator_move_fragment(const edc_fragment_t *frag) +{ + uint16_t vel = ACTUATOR_DEFAULT_VEL; + uint8_t acc = ACTUATOR_DEFAULT_ACC; + + /* target 坐标 → 脉冲数 */ + uint32_t pulses_x = (uint32_t)(frag->target.x * s_scale_x); + uint32_t pulses_y = (uint32_t)(frag->target.y * s_scale_y); + uint32_t pulses_angle = (uint32_t)(frag->target.angle_x10 * s_scale_angle); + + ESP_LOGI(TAG, "Fragment: X=%lu Y=%lu Angle=%lu (raw x=%u y=%u a=%u)", + pulses_x, pulses_y, pulses_angle, + frag->target.x, frag->target.y, frag->target.angle_x10); + + /* Angle / Z 旋转轴 (独立, CW) */ + _actuator_move_abs(ACTUATOR_ADDR_ANGLE, ACTUATOR_DIR_ANGLE, pulses_angle, vel, acc, false); + _frame_gap(); + + /* X 轴 (独立, CW) */ + _actuator_move_abs(ACTUATOR_ADDR_X, ACTUATOR_DIR_X, pulses_x, vel, acc, false); + _frame_gap(); + + /* Y 轴 (双电机同步) */ + _actuator_move_y_sync(pulses_y); +} + +/* ================================================================ + * 执行完整 Cycle + * ================================================================ */ + +int actuator_execute_cycle(const edc_cycle_t *cycle) +{ + if (!s_initialized) { + ESP_LOGE(TAG, "未初始化, 无法执行 Cycle"); + return -1; + } + if (!cycle) { + ESP_LOGE(TAG, "Cycle 指针为空"); + return -1; + } + if (cycle->frag_count < EDC_FRAG_MIN || cycle->frag_count > EDC_FRAG_MAX) { + ESP_LOGE(TAG, "Fragment 数量无效: %u (需 %d~%d)", + cycle->frag_count, EDC_FRAG_MIN, EDC_FRAG_MAX); + return -1; + } + + ESP_LOGI(TAG, "=== Cycle #%u 开始 (%u fragments) ===", + cycle->cycle_id, cycle->frag_count); + + for (int i = 0; i < cycle->frag_count; i++) { + ESP_LOGI(TAG, "-- Fragment %d/%d --", i + 1, cycle->frag_count); + _actuator_move_fragment(&cycle->fragments[i]); + + /* 等待当前片段运动完成 (TODO: 改为轮询到位标志) */ + if (i < cycle->frag_count - 1) { + vTaskDelay(pdMS_TO_TICKS(ACTUATOR_FRAG_DELAY_MS)); + } + } + + /* 最后一个 fragment 也等待 */ + vTaskDelay(pdMS_TO_TICKS(ACTUATOR_FRAG_DELAY_MS)); + + ESP_LOGI(TAG, "=== Cycle #%u 完成 ===", cycle->cycle_id); + return 0; +} + +/* ================================================================ + * 急停 + * ================================================================ */ + +void actuator_stop_all(void) +{ + const uint8_t addrs[] = { + ACTUATOR_ADDR_ANGLE, ACTUATOR_ADDR_X, + ACTUATOR_ADDR_Y1, ACTUATOR_ADDR_Y2 + }; + _frame_gap(); /* 防止与上一帧 UART 冲突 */ + for (int i = 0; i < 4; i++) { + Emm_V5_Stop_Now(addrs[i], false); + _frame_gap(); + } + ESP_LOGW(TAG, "全部电机急停"); +} + +/* ================================================================ + * 舵机控制 + * ================================================================ */ + +void actuator_servo_up(void) +{ + servo_set_up(); + ESP_LOGD(TAG, "舵机: 抬起"); +} + +void actuator_servo_down(void) +{ + servo_set_down(); + ESP_LOGD(TAG, "舵机: 放下"); +} + +void actuator_servo_set_angle(float angle) +{ + servo_set_angle(angle); + ESP_LOGD(TAG, "舵机: %.1f°", (double)angle); +} + +/* ================================================================ + * 单电机手动控制 (Y 轴自动同步) + * ================================================================ */ + +static inline bool _is_y_motor(uint8_t addr) +{ + return (addr == ACTUATOR_ADDR_Y1 || addr == ACTUATOR_ADDR_Y2); +} + +void actuator_motor_enable(uint8_t addr, bool state) +{ + _frame_gap(); /* 防止与上一帧 UART 冲突 */ + Emm_V5_En_Control(addr, state, false); + ESP_LOGI(TAG, "电机 %d %s", addr, state ? "使能" : "关闭"); +} + +void actuator_motor_vel(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc) +{ + _frame_gap(); /* 防止与上一帧 UART 冲突 */ + Emm_V5_Vel_Control(addr, dir, vel, acc, false); + ESP_LOGI(TAG, "速度: 电机%d %s %dRPM acc=%d", + addr, dir ? "CCW" : "CW", vel, acc); + + /* Y 轴同步: 对端电机同样命令 (帧间留间隔) */ + if (_is_y_motor(addr)) { + uint8_t peer = (addr == ACTUATOR_ADDR_Y1) + ? ACTUATOR_ADDR_Y2 : ACTUATOR_ADDR_Y1; + _frame_gap(); + Emm_V5_Vel_Control(peer, dir, vel, acc, false); + ESP_LOGD(TAG, "Y 同步: 电机%d 同速", peer); + } +} + +void actuator_motor_pos(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc, + uint32_t clk, bool raF) +{ + _frame_gap(); /* 防止与上一帧 UART 冲突 */ + if (_is_y_motor(addr)) { + /* Y 轴双电机同步位置 (帧间留间隔, 广播地址触发一次) */ + uint8_t peer = (addr == ACTUATOR_ADDR_Y1) + ? ACTUATOR_ADDR_Y2 : ACTUATOR_ADDR_Y1; + Emm_V5_Pos_Control(addr, dir, vel, acc, clk, raF, true); + _frame_gap(); + Emm_V5_Pos_Control(peer, dir, vel, acc, clk, raF, true); + _frame_gap(); + Emm_V5_Synchronous_motion(0); + ESP_LOGI(TAG, "位置(Y同步): 电机%d+%d %s %dRPM %lu脉冲 %s", + addr, peer, dir ? "CCW" : "CW", vel, clk, + raF ? "绝对" : "相对"); + } else { + Emm_V5_Pos_Control(addr, dir, vel, acc, clk, raF, false); + ESP_LOGI(TAG, "位置: 电机%d %s %dRPM %lu脉冲 %s", + addr, dir ? "CCW" : "CW", vel, clk, + raF ? "绝对" : "相对"); + } +} + +void actuator_motor_angle(uint8_t addr, float angle_deg, uint16_t vel, + uint8_t acc, bool raF) +{ + _frame_gap(); /* 防止与上一帧 UART 冲突 */ + /* 方向由 Emm_V5_Pos_Control_Angle 根据角度正负自动翻转 */ + uint8_t dir = ACTUATOR_DIR_ANGLE; + + if (_is_y_motor(addr)) { + uint8_t peer = (addr == ACTUATOR_ADDR_Y1) + ? ACTUATOR_ADDR_Y2 : ACTUATOR_ADDR_Y1; + Emm_V5_Pos_Control_Angle(addr, dir, vel, acc, angle_deg, raF, true); + _frame_gap(); + Emm_V5_Pos_Control_Angle(peer, dir, vel, acc, angle_deg, raF, true); + _frame_gap(); + Emm_V5_Synchronous_motion(0); + ESP_LOGI(TAG, "角度(Y同步): 电机%d+%d %.1f° %s vel=%dRPM", + addr, peer, (double)angle_deg, + raF ? "绝对" : "相对", vel); + } else { + Emm_V5_Pos_Control_Angle(addr, dir, vel, acc, angle_deg, raF, false); + ESP_LOGI(TAG, "角度: 电机%d %.1f° %s vel=%dRPM", + addr, (double)angle_deg, + raF ? "绝对" : "相对", vel); + } +} + +void actuator_motor_mm(uint8_t addr, float distance_mm, uint16_t vel, + uint8_t acc, bool raF) +{ + /* 软限位钳位 */ + if (_is_y_motor(addr)) { + if (distance_mm < ACTUATOR_LIMIT_Y_MIN) distance_mm = ACTUATOR_LIMIT_Y_MIN; + if (distance_mm > ACTUATOR_LIMIT_Y_MAX) distance_mm = ACTUATOR_LIMIT_Y_MAX; + } else { + if (distance_mm < ACTUATOR_LIMIT_X_MIN) distance_mm = ACTUATOR_LIMIT_X_MIN; + if (distance_mm > ACTUATOR_LIMIT_X_MAX) distance_mm = ACTUATOR_LIMIT_X_MAX; + } + + _frame_gap(); /* 防止与上一帧 UART 冲突 */ + uint32_t clk = ACTUATOR_MM_TO_PULSES(distance_mm); + /* Y 轴正向 = CCW, X 轴正向 = CW */ + uint8_t dir = _is_y_motor(addr) ? ACTUATOR_DIR_Y : ACTUATOR_DIR_X; + + if (_is_y_motor(addr)) { + uint8_t peer = (addr == ACTUATOR_ADDR_Y1) + ? ACTUATOR_ADDR_Y2 : ACTUATOR_ADDR_Y1; + Emm_V5_Pos_Control(addr, dir, vel, acc, clk, raF, true); + _frame_gap(); + Emm_V5_Pos_Control(peer, dir, vel, acc, clk, raF, true); + _frame_gap(); + Emm_V5_Synchronous_motion(0); + ESP_LOGI(TAG, "直线(Y同步 %s): 电机%d+%d %.2fmm (%lu脉冲) %s vel=%dRPM", + dir ? "CCW" : "CW", addr, peer, (double)distance_mm, + (unsigned long)clk, raF ? "绝对" : "相对", vel); + } else { + Emm_V5_Pos_Control(addr, dir, vel, acc, clk, raF, false); + ESP_LOGI(TAG, "直线(%s): 电机%d %.2fmm (%lu脉冲) %s vel=%dRPM", + dir ? "CCW" : "CW", addr, (double)distance_mm, + (unsigned long)clk, raF ? "绝对" : "相对", vel); + } +} + +void actuator_motor_stop(uint8_t addr) +{ + _frame_gap(); /* 防止与上一帧 UART 冲突 */ + Emm_V5_Stop_Now(addr, false); + ESP_LOGI(TAG, "停止: 电机%d", addr); + + /* Y 轴同步停止 (帧间留间隔) */ + if (_is_y_motor(addr)) { + uint8_t peer = (addr == ACTUATOR_ADDR_Y1) + ? ACTUATOR_ADDR_Y2 : ACTUATOR_ADDR_Y1; + _frame_gap(); + Emm_V5_Stop_Now(peer, false); + ESP_LOGD(TAG, "Y 同步: 电机%d 同停", peer); + } +} diff --git a/components/actuator/actuator.h b/components/actuator/actuator.h new file mode 100644 index 0000000..d6ba47d --- /dev/null +++ b/components/actuator/actuator.h @@ -0,0 +1,239 @@ +/** + * @file actuator.h + * @brief 执行器应用层封装 — 4 路步进电机 + 1 路舵机 + * + * 轴映射: + * 1 号电机 (addr=1) ── Angle/Z 旋转轴 + * 2 号电机 (addr=2) ── X 轴 + * 3 号电机 (addr=3) ── Y 轴 ─┐ 完全同步 + * 4 号电机 (addr=4) ── Y 轴 ─┘ + * 舵机 (PWM) ── Z 轴抬升 (仅抬起/放下两态) + * + * 运动模式: 位置模式, 绝对定位 (raF=true), 暂不标定 + */ + +#ifndef ACTUATOR_H +#define ACTUATOR_H + +#include +#include +#include "driver/gpio.h" +#include "protocol.h" +#include "emm_v5.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ================================================================ + * 可配置宏 (可在项目级或 -D 覆盖) + * ================================================================ */ + +/** 丝杆参数 (X / Y 轴直线运动) */ +#define ACTUATOR_SCREW_LEAD_MM 8.0f /* 导程: 一圈 = 8mm 直线位移 */ +#define ACTUATOR_SCREW_PITCH_MM 2.0f /* 螺距 */ + +/** 电机 Emm_V5 地址 */ +#ifndef ACTUATOR_ADDR_ANGLE +#define ACTUATOR_ADDR_ANGLE 1 /* Angle / Z 旋转轴 */ +#endif +#ifndef ACTUATOR_ADDR_X +#define ACTUATOR_ADDR_X 2 /* X 轴 */ +#endif +#ifndef ACTUATOR_ADDR_Y1 +#define ACTUATOR_ADDR_Y1 3 /* Y 轴 #1 (同步) */ +#endif +#ifndef ACTUATOR_ADDR_Y2 +#define ACTUATOR_ADDR_Y2 4 /* Y 轴 #2 (同步) */ +#endif + +/** 舵机 PWM GPIO */ +#ifndef ACTUATOR_SERVO_GPIO +#define ACTUATOR_SERVO_GPIO GPIO_NUM_4 +#endif + +/** 电机 UART 引脚 */ +#ifndef ACTUATOR_UART_PORT +#define ACTUATOR_UART_PORT UART_NUM_1 +#endif +#ifndef ACTUATOR_UART_TX +#define ACTUATOR_UART_TX GPIO_NUM_6 +#endif +#ifndef ACTUATOR_UART_RX +#define ACTUATOR_UART_RX GPIO_NUM_7 +#endif + +/** 默认速度 (RPM, 0~5000) */ +#ifndef ACTUATOR_DEFAULT_VEL +#define ACTUATOR_DEFAULT_VEL 500 +#endif + +/** 默认加速度 (0~255, 0=直接启动) */ +#ifndef ACTUATOR_DEFAULT_ACC +#define ACTUATOR_DEFAULT_ACC 50 +#endif + +/** 脉冲换算系数 (坐标值 × scale = 脉冲数) + * + * X/Y 轴: 丝杆导程 8mm → 一圈 8mm = 3200 脉冲 + * scale = 3200 / 8 = 400 脉冲/mm + * + * Z 旋转: angle_x10 单位为 0.1° + * 一圈 360° = 3600 (0.1°) = 3200 脉冲 + * scale = 3200 / 3600 ≈ 0.8889 脉冲/(0.1°) + */ +#ifndef ACTUATOR_SCALE_X +#define ACTUATOR_SCALE_X 400.0f /* mm → 脉冲 */ +#endif +#ifndef ACTUATOR_SCALE_Y +#define ACTUATOR_SCALE_Y 400.0f /* mm → 脉冲 */ +#endif +#ifndef ACTUATOR_SCALE_ANGLE +#define ACTUATOR_SCALE_ANGLE ((float)EMM_V5_PULSES_PER_REV / 3600.0f) /* 0.1° → 脉冲 */ +#endif + +/** 便捷宏: mm 直线距离 → 脉冲数 (四舍五入) */ +#define ACTUATOR_MM_TO_PULSES(mm) ((uint32_t)((float)(mm) * ACTUATOR_SCALE_X + 0.5f)) + +/** 软限位 (左上零点坐标系, 单位 mm) */ +#define ACTUATOR_LIMIT_X_MIN 0.0f +#define ACTUATOR_LIMIT_X_MAX 210.0f +#define ACTUATOR_LIMIT_Y_MIN 0.0f +#define ACTUATOR_LIMIT_Y_MAX 335.0f + +/** Fragment 间等待 (ms), 暂用固定延时替代到位检测 */ +#ifndef ACTUATOR_FRAG_DELAY_MS +#define ACTUATOR_FRAG_DELAY_MS 500 +#endif + +/** + * 连续多帧之间的间隔 (ms) + * Emm_V5 驱动板无法解析 UART 上零间隔的背靠背连续帧 (会丢字节), + * 凡是一次动作需要连发多帧 (如 Y 轴双电机), 帧间必须插入该延时 + */ +#ifndef ACTUATOR_FRAME_GAP_MS +#define ACTUATOR_FRAME_GAP_MS 10 +#endif + +/** 控制模式: 2 = 闭环位置模式 */ +#define ACTUATOR_CTRL_MODE 2 + +/** + * XY 平面左上零点坐标系 运动方向: + * X 轴正向 (→): CW (dir=0) ── 电机2 + * Y 轴正向 (↓): CCW (dir=1) ── 电机3+4 (双电机同步) + * Z 旋转正向 (↻): CW (dir=0) ── 电机1 + */ +#define ACTUATOR_DIR_X 0 /* X轴正向 = CW */ +#define ACTUATOR_DIR_Y 1 /* Y轴正向 = CCW */ +#define ACTUATOR_DIR_ANGLE 0 /* Z旋转正向 = CW */ + +/* ================================================================ + * API + * ================================================================ */ + +/** + * @brief 初始化所有执行器 + * @note 内部调用 Emm_V5_Init + servo_init + 4 路电机使能 + 闭环模式 + */ +void actuator_init(void); + +/** + * @brief 设置脉冲换算系数 + * @param x_scale X 轴: 坐标值 × x_scale = 脉冲数 + * @param y_scale Y 轴: 坐标值 × y_scale = 脉冲数 + * @param angle_scale Z 旋转: angle_x10 × angle_scale = 脉冲数 + */ +void actuator_set_scale(float x_scale, float y_scale, float angle_scale); + +/** + * @brief 执行一个完整 Cycle (遍历所有 Fragment, 逐段运动) + * @param cycle 协议下发的 Cycle 数据 + * @return 0 成功, -1 参数无效 + */ +int actuator_execute_cycle(const edc_cycle_t *cycle); + +/** + * @brief 急停全部 4 路电机 + */ +void actuator_stop_all(void); + +/** + * @brief 舵机: 抬起 (上限位) + */ +void actuator_servo_up(void); + +/** + * @brief 舵机: 放下 (下限位) + */ +void actuator_servo_down(void); + +/** + * @brief 舵机: 设置任意角度 + * @param angle 角度 0.0 ~ 180.0 + */ +void actuator_servo_set_angle(float angle); + +/* ---- 单电机手动控制 (Web 调试用, Y 轴自动同步) ---- */ + +/** + * @brief 单电机使能控制 (Y 轴电机各自独立) + * @param addr 电机地址 (1~4) + * @param state 使能状态 + */ +void actuator_motor_enable(uint8_t addr, bool state); + +/** + * @brief 单电机速度模式 (Y 轴地址 3/4 自动同步) + * @param addr 电机地址 (1~4) + * @param dir 方向, 0=CW, 非0=CCW + * @param vel 速度 0~5000 RPM + * @param acc 加速度 0~255 + */ +void actuator_motor_vel(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc); + +/** + * @brief 单电机位置模式 (Y 轴地址 3/4 自动同步) + * @param addr 电机地址 (1~4) + * @param dir 方向, 0=CW, 非0=CCW + * @param vel 速度 0~5000 RPM + * @param acc 加速度 0~255 + * @param clk 脉冲数 + * @param raF 相对/绝对, false=相对, true=绝对 + */ +void actuator_motor_pos(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc, + uint32_t clk, bool raF); + +/** + * @brief 单电机角度直控 (Y 轴地址 3/4 自动同步) + * @param addr 电机地址 (1~4) + * @param angle_deg 目标角度 (°), 内部换算为脉冲 (3200=360°) + * @param vel 速度 0~5000 RPM + * @param acc 加速度 0~255 + * @param raF 相对/绝对, false=相对, true=绝对 + */ +void actuator_motor_angle(uint8_t addr, float angle_deg, uint16_t vel, + uint8_t acc, bool raF); + +/** + * @brief 单电机直线距离控制 (X/Y 轴, Y 轴地址 3/4 自动同步) + * @param addr 电机地址 (2=X, 3/4=Y) + * @param distance_mm 直线移动距离 (mm), 丝杆导程 8mm/圈 + * @param vel 速度 0~5000 RPM + * @param acc 加速度 0~255 + * @param raF 相对/绝对, false=相对, true=绝对 + */ +void actuator_motor_mm(uint8_t addr, float distance_mm, uint16_t vel, + uint8_t acc, bool raF); + +/** + * @brief 单电机立即停止 (Y 轴地址 3/4 自动同步) + * @param addr 电机地址 (1~4) + */ +void actuator_motor_stop(uint8_t addr); + +#ifdef __cplusplus +} +#endif + +#endif /* ACTUATOR_H */ diff --git a/components/emm_v5/emm_v5.c b/components/emm_v5/emm_v5.c index 6f987ba..d07dac7 100644 --- a/components/emm_v5/emm_v5.c +++ b/components/emm_v5/emm_v5.c @@ -5,6 +5,7 @@ #include "emm_v5.h" #include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_timer.h" @@ -161,6 +162,18 @@ void Emm_V5_Pos_Control(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc, Emm_V5_Send(cmd, 13); } +void Emm_V5_Pos_Control_Angle(uint8_t addr, uint8_t dir, uint16_t vel, + uint8_t acc, float angle_deg, bool raF, bool snF) +{ + /* 负角度 → 取绝对值 + 翻转方向 */ + float abs_deg = fabsf(angle_deg); + uint32_t clk = EMM_V5_DEG_TO_CLK(abs_deg); + if (angle_deg < 0.0f) dir = (uint8_t)(!dir); + ESP_LOGD("EMM_V5", "角度→脉冲: %.1f° (%s) → %lu", + (double)angle_deg, dir ? "CCW" : "CW", (unsigned long)clk); + Emm_V5_Pos_Control(addr, dir, vel, acc, clk, raF, snF); +} + void Emm_V5_Stop_Now(uint8_t addr, bool snF) { uint8_t cmd[5] = {addr, 0xFE, 0x98, (uint8_t)snF, 0x6B}; diff --git a/components/emm_v5/emm_v5.h b/components/emm_v5/emm_v5.h index b19da66..16ccfee 100644 --- a/components/emm_v5/emm_v5.h +++ b/components/emm_v5/emm_v5.h @@ -30,6 +30,16 @@ extern "C" { /* 接收超时时间 (毫秒) */ #define EMM_V5_RX_TIMEOUT_MS 100 +/** + * Emm 固件角度 ↔ 脉冲 换算 + * 1.8° 步进电机 + 16 微步 → 3200 脉冲/圈 + * 每脉冲 = 1.8° / 16 = 0.1125° + */ +#define EMM_V5_PULSES_PER_REV 3200 +#define EMM_V5_DEG_TO_CLK(deg) ((uint32_t)((float)(deg) * \ + (float)EMM_V5_PULSES_PER_REV / 360.0f + 0.5f)) +#define EMM_V5_CLK_TO_DEG(clk) ((float)(clk) * 0.1125f) + /* ============================================================ * 系统参数枚举 (与 Arduino 版本一致) * ============================================================ */ @@ -120,6 +130,19 @@ void Emm_V5_Vel_Control(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc, bo void Emm_V5_Pos_Control(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc, uint32_t clk, bool raF, bool snF); +/** + * @brief 位置模式控制 (角度输入, °) + * @param addr 电机地址 + * @param dir 方向, 0=CW, 其他=CCW + * @param vel 速度 0~5000 RPM + * @param acc 加速度 0~255 (0=直接启动) + * @param angle_deg 目标角度 (°), 内部换算为脉冲 (3200=360°) + * @param raF 相对/绝对, false=相对, true=绝对 + * @param snF 多机同步标志, false=不启用, true=启用 + */ +void Emm_V5_Pos_Control_Angle(uint8_t addr, uint8_t dir, uint16_t vel, + uint8_t acc, float angle_deg, bool raF, bool snF); + /** * @brief 立即停止电机 (所有控制模式通用) * @param addr 电机地址 @@ -129,7 +152,8 @@ void Emm_V5_Stop_Now(uint8_t addr, bool snF); /** * @brief 触发多机同步开始运动 - * @param addr 电机地址 + * @param addr 电机地址; 协议约定使用广播地址 0 发送一次, + * 所有以 snF=true 入队的电机会同时启动 */ void Emm_V5_Synchronous_motion(uint8_t addr); diff --git a/components/web_ctrl/CMakeLists.txt b/components/web_ctrl/CMakeLists.txt index 2de0881..d82679a 100644 --- a/components/web_ctrl/CMakeLists.txt +++ b/components/web_ctrl/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register(SRCS "web_ctrl.c" INCLUDE_DIRS "." - REQUIRES emm_v5 magnet servo esp_http_server esp_wifi nvs_flash protocol) + REQUIRES emm_v5 magnet servo esp_http_server esp_wifi nvs_flash protocol actuator) diff --git a/components/web_ctrl/web_ctrl.c b/components/web_ctrl/web_ctrl.c index ad0dbd7..198db93 100644 --- a/components/web_ctrl/web_ctrl.c +++ b/components/web_ctrl/web_ctrl.c @@ -8,6 +8,7 @@ #include "magnet.h" #include "servo.h" #include "protocol.h" +#include "actuator.h" #include #include @@ -118,37 +119,64 @@ button:active{opacity:.7}
-

电机使能

-
+

轴控制 (XYZA)

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

电机使能

+
+
+ +

速度模式

-
+
RPM
- - + +

位置模式

-
+
RPM
- - + + +
+
+ +
+

角度直控 (°)

+
+
° (3200=360°)
+
RPM
+
+
+
+ +
@@ -174,6 +202,56 @@ button:active{opacity:.7}
+
+

位置清零 & 堵转解除

+
+
+ + +
+
+ +
+

读取参数

+
+
+ +
+
+ +
+

回零控制

+
+
+
+
RPM
+
ms
+
RPM
+
mA
+
ms
+
+
+
+ + +
+
+ + +
+
+
@@ -208,6 +286,41 @@ function api(cmd){ l.innerHTML='
请求失败
'+l.innerHTML }) } +function apiEach(ep,id,ext){ + var addrs=v(id).split('|'); + for(var i=0;i'; + h+=' (raw: '+d.val32+' / 0x'+d.val32.toString(16).toUpperCase()+')'; + } + if(d.val16!==undefined && d.angle===undefined){ + h+='
'+d.val16+' (16bit)'; + } + h+='
raw: '+d.hex+''; + document.getElementById('rp_out').innerHTML = h; + }).catch(function(e){ + document.getElementById('rp_out').innerHTML='读取失败' + }); +} fetch('/api/status').then(function(r){return r.json()}).then(function(d){ document.getElementById('wifi').innerHTML='WiFi: '+d.ssid; document.getElementById('ip').textContent='IP: '+d.ip @@ -336,38 +449,44 @@ static int get_query_int(httpd_req_t *req, const char *key, int def) 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; + /* 一次性发送, 由 ESP-IDF 内部处理 TCP 缓冲管理, + * 避免手动分块导致的 EAGAIN / ECONNRESET 偶发失败 */ + esp_err_t err = httpd_resp_send(req, INDEX_HTML, HTTPD_RESP_USE_STRLEN); + if (err != ESP_OK) { + /* 客户端断开是正常现象 (刷新页面/关闭标签), 仅警告即可 */ + ESP_LOGW(TAG, "发送 HTML 失败: %d (客户端可能已断开)", err); } - httpd_resp_send_chunk(req, NULL, 0); /* 结束分块 */ - return ESP_OK; + return err; } 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); + actuator_motor_enable((uint8_t)addr, (bool)state); char resp[64]; snprintf(resp, sizeof(resp), "电机 %d %s", addr, state ? "使能" : "关闭"); httpd_resp_set_type(req, "text/plain; charset=utf-8"); @@ -381,7 +500,7 @@ static esp_err_t api_velocity_handler(httpd_req_t *req) 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); + actuator_motor_vel((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc); char resp[64]; snprintf(resp, sizeof(resp), "速度模式: 地址%d %s %dRPM acc=%d", addr, dir ? "CCW" : "CW", vel, acc); @@ -398,8 +517,8 @@ static esp_err_t api_position_handler(httpd_req_t *req) 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); + actuator_motor_pos((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc, + (uint32_t)clk, (bool)raf); char resp[80]; snprintf(resp, sizeof(resp), "位置模式: 地址%d %s %dRPM %d脉冲 %s", addr, dir ? "CCW" : "CW", vel, clk, raf ? "绝对" : "相对"); @@ -411,7 +530,7 @@ static esp_err_t api_position_handler(httpd_req_t *req) 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); + actuator_motor_stop((uint8_t)addr); char resp[32]; snprintf(resp, sizeof(resp), "电机 %d 已停止", addr); httpd_resp_set_type(req, "text/plain; charset=utf-8"); @@ -455,9 +574,9 @@ static esp_err_t api_servo_handler(httpd_req_t *req) char val[16]; if (httpd_query_key_value(buf, "pos", val, sizeof(val)) == ESP_OK) { if (strcmp(val, "up") == 0) { - servo_set_up(); + actuator_servo_up(); } else { - servo_set_down(); + actuator_servo_down(); } char resp[32]; snprintf(resp, sizeof(resp), "舵机: %s", val); @@ -467,7 +586,7 @@ static esp_err_t api_servo_handler(httpd_req_t *req) } if (httpd_query_key_value(buf, "angle", val, sizeof(val)) == ESP_OK) { float angle = atof(val); - servo_set_angle(angle); + actuator_servo_set_angle(angle); char resp[32]; snprintf(resp, sizeof(resp), "舵机: %.1f°", (double)angle); httpd_resp_set_type(req, "text/plain; charset=utf-8"); @@ -479,6 +598,214 @@ static esp_err_t api_servo_handler(httpd_req_t *req) 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); + + /* 允许 0 值: 绝对模式下 0=回到原点 */ + 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 → 脉冲 (丝杆导程 8mm) ---- */ +static esp_err_t api_mm_handler(httpd_req_t *req) +{ + int addr = get_query_int(req, "addr", 2); /* 默认 X 轴 */ + 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; +} + +/* ============================================================ + * 底层调试 API (直接对接 Emm_V5, 不走 actuator) + * ============================================================ */ + +/* ---- 当前位置清零 ---- */ +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; +} + +/* ---- 读取系统参数 (按 Emm 固件手册 V1.0.5 帧格式解析) ---- */ +/* 帧格式: {addr, cmd, sign(00/01), data[4]BE, 0x6B} */ +/* 位置类(Emm): val32=0~65535→角度=val32*360/65536 */ +/* 速度类: val16=RPM */ +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); /* EmmSysParams_t */ + + 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)); + + /* 解析: 跳过 addr(1)+cmd(1)+sign(1), 取 data[4] 大端 */ + 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]); + /* Emm 固件: 0~65535 表示 0~360°, 仅低 16 位有效 */ + 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; +} + +/* ---- 角度直控: 输入 ° 自动换算为 Emm 脉冲 (3200脉冲=360°) ---- */ +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_origin_set_handler(httpd_req_t *req) +{ + int addr = get_query_int(req, "addr", 1); + int svf = get_query_int(req, "svf", 0); /* 0=不存储 1=存储 */ + 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", 0); + 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", 0); + 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) { @@ -509,13 +836,21 @@ void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch) cycle->cycle_id, ch == EDC_CH_I2C ? "I2C" : "UART", cycle->frag_count); - /* I2C / UART 冗余下发, 同一 cycle_id 只回一次 DONE */ + /* I2C / UART 冗余下发, 同一 cycle_id 只执行一次 */ if (cycle->cycle_id == s_last_done_id) { return; } s_last_done_id = cycle->cycle_id; - /* 回复 MPU: Cycle 已收到 (后续实际执行电机后应移到执行完毕处) */ + /* 通信控制链路暂时断开, 仅 Web 手控生效 */ +#if 0 /* TODO: 恢复通信控制时取消注释 */ + /* 执行 Cycle 运动 */ + if (actuator_execute_cycle(cycle) == 0) { + /* 回复 MPU: Cycle 执行完成 */ + edc_proto_send_done(cycle->cycle_id); + } +#endif + /* 通信断开期间仍回复 DONE 以维持协议握手 (但不执行运动) */ edc_proto_send_done(cycle->cycle_id); } @@ -602,7 +937,7 @@ 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 = 16; + cfg.max_uri_handlers = 26; cfg.max_open_sockets = 4; cfg.lru_purge_enable = true; @@ -672,6 +1007,67 @@ static void http_server_init(void) }; 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); + + /* ---- 底层调试 API (直接 Emm_V5) ---- */ + 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); + + 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); + + 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); + /* favicon.ico — 返回空, 避免 404 警告 */ httpd_uri_t uri_favicon = { .uri = "/favicon.ico", diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 45e44d6..5bc56dd 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register(SRCS "main.c" INCLUDE_DIRS "." - PRIV_REQUIRES emm_v5 magnet servo web_ctrl protocol rgb) + PRIV_REQUIRES emm_v5 magnet servo web_ctrl protocol rgb actuator) diff --git a/main/main.c b/main/main.c index ab5858b..a668c06 100644 --- a/main/main.c +++ b/main/main.c @@ -26,6 +26,7 @@ #include "web_ctrl.h" #include "protocol.h" #include "rgb.h" +#include "actuator.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" @@ -38,15 +39,12 @@ void app_main(void) rgb_init(GPIO_NUM_27); rgb_set(32, 32, 0); /* 黄色 (低亮), 表示启动中 */ - /* 初始化电机驱动 (UART1, TX=GPIO6, RX=GPIO7, LED=GPIO2) */ - Emm_V5_Init(UART_NUM_1, GPIO_NUM_6, GPIO_NUM_7); + /* 初始化执行器 (步进电机 + 舵机) */ + actuator_init(); /* 初始化电磁铁 (GPIO5) */ magnet_init(GPIO_NUM_5); - /* 初始化舵机 (GPIO4, 上限180° 下限0°) */ - servo_init(GPIO_NUM_4); - /* 初始化通讯协议 (I2C 从机 GPIO2/3 + UART0 GPIO11/12) */ edc_proto_init(NULL);