actuator应用层基本完成

This commit is contained in:
kirl committed 2026-07-31 17:51:37 +08:00
1 parent 32a3a87ef2
commit a226b15413
9 files changed
+1081 -44

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
idf_component_register(SRCS "actuator.c"
INCLUDE_DIRS "."
REQUIRES emm_v5 servo protocol driver freertos)
+364
View File
@@ -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 <math.h>
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);
}
}
+239
View File
@@ -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 <stdint.h>
#include <stdbool.h>
#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 */
+13
View File
@@ -5,6 +5,7 @@
#include "emm_v5.h" #include "emm_v5.h"
#include <string.h> #include <string.h>
#include <math.h>
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "esp_timer.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); 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) void Emm_V5_Stop_Now(uint8_t addr, bool snF)
{ {
uint8_t cmd[5] = {addr, 0xFE, 0x98, (uint8_t)snF, 0x6B}; uint8_t cmd[5] = {addr, 0xFE, 0x98, (uint8_t)snF, 0x6B};
+25 -1
View File
@@ -30,6 +30,16 @@ extern "C" {
/* 接收超时时间 (毫秒) */ /* 接收超时时间 (毫秒) */
#define EMM_V5_RX_TIMEOUT_MS 100 #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 版本一致) * 系统参数枚举 (与 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, void Emm_V5_Pos_Control(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc,
uint32_t clk, bool raF, bool snF); 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 立即停止电机 (所有控制模式通用) * @brief 立即停止电机 (所有控制模式通用)
* @param addr 电机地址 * @param addr 电机地址
@@ -129,7 +152,8 @@ void Emm_V5_Stop_Now(uint8_t addr, bool snF);
/** /**
* @brief 触发多机同步开始运动 * @brief 触发多机同步开始运动
* @param addr 电机地址 * @param addr 电机地址; 协议约定使用广播地址 0 发送一次,
* 所有以 snF=true 入队的电机会同时启动
*/ */
void Emm_V5_Synchronous_motion(uint8_t addr); void Emm_V5_Synchronous_motion(uint8_t addr);
+1 -1
View File
@@ -1,3 +1,3 @@
idf_component_register(SRCS "web_ctrl.c" idf_component_register(SRCS "web_ctrl.c"
INCLUDE_DIRS "." 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)
+432 -36
View File
@@ -8,6 +8,7 @@
#include "magnet.h" #include "magnet.h"
#include "servo.h" #include "servo.h"
#include "protocol.h" #include "protocol.h"
#include "actuator.h"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
@@ -118,37 +119,64 @@ button:active{opacity:.7}
<div class="grid"> <div class="grid">
<div class="card"> <div class="card">
<h2><i class="tag"></i>电机使能</h2> <h2><i class="tag green"></i>轴控制 (XYZA)</h2>
<div class="row"><label>地址</label><input id="en_addr" type="number" value="1" min="1" max="247"></div> <div class="row"><label>X mm</label><input id="ax_x" type="number" value="0" min="0" max="210" step="0.1"><span class="unit">→电机2 (CW) 0~210</span></div>
<div class="row"><label>Y mm</label><input id="ax_y" type="number" value="0" min="0" max="335" step="0.1"><span class="unit">→电机3+4 (CCW) 0~335</span></div>
<div class="row"><label>Z °</label><input id="ax_zrot" type="number" value="0" step="0.1"><span class="unit">→电机1 (±: CW/CCW)</span></div>
<div class="row"><label>舵机</label><input id="ax_servo" type="number" value="90" min="0" max="180" step="0.5"><span class="unit">°</span></div>
<div class="row"><label>速度</label><input id="ax_vel" type="number" value="500" min="0" max="5000"><span class="unit">RPM</span></div>
<div class="row"><label>加速度</label><input id="ax_acc" type="number" value="50" min="0" max="255"></div>
<div class="btn-row"> <div class="btn-row">
<button class="btn-on" onclick="api('enable?addr='+v('en_addr')+'&state=1')">使能</button> <button class="btn-go" onclick="axisMove()">移动到目标</button>
<button class="btn-off" onclick="api('enable?addr='+v('en_addr')+'&state=0')">关闭</button> <button class="btn-stop" onclick="api('stop?addr=1');api('stop?addr=2');api('stop?addr=3');api('stop?addr=4')">急停全部</button>
</div>
</div>
<div class="card">
<h2><i class="tag"></i>电机使能</h2>
<div class="row"><label>地址</label><input id="en_addr" type="text" value="1" placeholder="1 或 1|2"></div>
<div class="btn-row">
<button class="btn-on" onclick="apiEach('enable','en_addr','&state=1')">使能</button>
<button class="btn-off" onclick="apiEach('enable','en_addr','&state=0')">关闭</button>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<h2><i class="tag"></i>速度模式</h2> <h2><i class="tag"></i>速度模式</h2>
<div class="row"><label>地址</label><input id="v_addr" type="number" value="1" min="1" max="247"></div> <div class="row"><label>地址</label><input id="v_addr" type="text" value="1" placeholder="1 或 1|2"></div>
<div class="row"><label>方向</label><select id="v_dir"><option value="0">CW 正转</option><option value="1">CCW 反转</option></select></div> <div class="row"><label>方向</label><select id="v_dir"><option value="0">CW 正转</option><option value="1">CCW 反转</option></select></div>
<div class="row"><label>速度</label><input id="v_vel" type="number" value="100" min="0" max="5000"><span class="unit">RPM</span></div> <div class="row"><label>速度</label><input id="v_vel" type="number" value="100" min="0" max="5000"><span class="unit">RPM</span></div>
<div class="row"><label>加速度</label><input id="v_acc" type="number" value="10" min="0" max="255"></div> <div class="row"><label>加速度</label><input id="v_acc" type="number" value="10" min="0" max="255"></div>
<div class="btn-row"> <div class="btn-row">
<button class="btn-go" onclick="api('velocity?addr='+v('v_addr')+'&dir='+v('v_dir')+'&vel='+v('v_vel')+'&acc='+v('v_acc'))">启动</button> <button class="btn-go" onclick="apiEach('velocity','v_addr','&dir='+v('v_dir')+'&vel='+v('v_vel')+'&acc='+v('v_acc'))">启动</button>
<button class="btn-stop" onclick="api('stop?addr='+v('v_addr'))">停止</button> <button class="btn-stop" onclick="apiEach('stop','v_addr','')">停止</button>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<h2><i class="tag"></i>位置模式</h2> <h2><i class="tag"></i>位置模式</h2>
<div class="row"><label>地址</label><input id="p_addr" type="number" value="1" min="1" max="247"></div> <div class="row"><label>地址</label><input id="p_addr" type="text" value="1" placeholder="1 或 1|2"></div>
<div class="row"><label>方向</label><select id="p_dir"><option value="0">CW 正转</option><option value="1">CCW 反转</option></select></div> <div class="row"><label>方向</label><select id="p_dir"><option value="0">CW 正转</option><option value="1">CCW 反转</option></select></div>
<div class="row"><label>速度</label><input id="p_vel" type="number" value="100" min="0" max="5000"><span class="unit">RPM</span></div> <div class="row"><label>速度</label><input id="p_vel" type="number" value="100" min="0" max="5000"><span class="unit">RPM</span></div>
<div class="row"><label>加速度</label><input id="p_acc" type="number" value="10" min="0" max="255"></div> <div class="row"><label>加速度</label><input id="p_acc" type="number" value="10" min="0" max="255"></div>
<div class="row"><label>脉冲数</label><input id="p_clk" type="number" value="2000" min="0"></div> <div class="row"><label>脉冲数</label><input id="p_clk" type="number" value="2000" min="0"></div>
<div class="row"><label>模式</label><select id="p_raf"><option value="0">相对运动</option><option value="1">绝对运动</option></select></div> <div class="row"><label>模式</label><select id="p_raf"><option value="0">相对运动</option><option value="1">绝对运动</option></select></div>
<div class="btn-row"> <div class="btn-row">
<button class="btn-go" onclick="api('position?addr='+v('p_addr')+'&dir='+v('p_dir')+'&vel='+v('p_vel')+'&acc='+v('p_acc')+'&clk='+v('p_clk')+'&raf='+v('p_raf'))">启动</button> <button class="btn-go" onclick="apiEach('position','p_addr','&dir='+v('p_dir')+'&vel='+v('p_vel')+'&acc='+v('p_acc')+'&clk='+v('p_clk')+'&raf='+v('p_raf'))">启动</button>
<button class="btn-stop" onclick="api('stop?addr='+v('p_addr'))">停止</button> <button class="btn-stop" onclick="apiEach('stop','p_addr','')">停止</button>
</div>
</div>
<div class="card">
<h2><i class="tag green"></i>角度直控 (°)</h2>
<div class="row"><label>地址</label><input id="ag_addr" type="text" value="1" placeholder="1 或 1|2"></div>
<div class="row"><label>角度</label><input id="ag_angle" type="number" value="90" min="-3600" max="3600" step="0.1"><span class="unit">° (3200=360°)</span></div>
<div class="row"><label>速度</label><input id="ag_vel" type="number" value="500" min="0" max="5000"><span class="unit">RPM</span></div>
<div class="row"><label>加速度</label><input id="ag_acc" type="number" value="50" min="0" max="255"></div>
<div class="row"><label>模式</label><select id="ag_raf"><option value="1">绝对运动</option><option value="0">相对运动</option></select></div>
<div class="btn-row">
<button class="btn-go" onclick="apiEach('angle','ag_addr','&angle='+v('ag_angle')+'&vel='+v('ag_vel')+'&acc='+v('ag_acc')+'&raf='+v('ag_raf'))">转到角度</button>
<button class="btn-stop" onclick="apiEach('stop','ag_addr','')">停止</button>
</div> </div>
</div> </div>
@@ -174,6 +202,56 @@ button:active{opacity:.7}
</div> </div>
</div> </div>
<div class="card">
<h2><i class="tag amber"></i>位置清零 & 堵转解除</h2>
<div class="row"><label>地址</label><input id="rz_addr" type="number" value="1" min="1" max="247"></div>
<div class="btn-row">
<button class="btn-go" onclick="api('reset_pos?addr='+v('rz_addr'))">位置清零</button>
<button class="btn-stop" onclick="api('reset_clog?addr='+v('rz_addr'))">解除堵转</button>
</div>
</div>
<div class="card">
<h2><i class="tag"></i>读取参数</h2>
<div class="row"><label>地址</label><input id="rp_addr" type="number" value="1" min="1" max="247"></div>
<div class="row"><label>参数</label><select id="rp_param">
<option value="0">0-固件版本</option><option value="1">1-相电阻/电感</option>
<option value="2">2-PID参数</option><option value="3">3-总线电压</option>
<option value="5">5-相电流</option><option value="7">7-编码器值</option>
<option value="8">8-目标位置</option><option value="9">9-实时转速</option>
<option value="10" selected>10-实时位置</option><option value="11">11-位置误差</option>
<option value="13">13-状态标志位</option><option value="14">14-驱动参数</option>
<option value="15">15-系统状态</option><option value="16">16-回零状态</option>
</select></div>
<button class="btn-go btn-block" onclick="readParam()">读取</button>
<div id="rp_out" style="font-size:.75rem;color:var(--sub);margin-top:8px;word-break:break-all"></div>
</div>
<div class="card">
<h2><i class="tag amber"></i>回零控制</h2>
<div class="row"><label>地址</label><input id="og_addr" type="number" value="1" min="1" max="247"></div>
<div class="row"><label>模式</label><select id="og_omode">
<option value="0">0-单圈就近</option><option value="1">1-单圈方向</option>
<option value="2">2-多圈无限位</option><option value="3">3-多圈有限位</option>
</select></div>
<div class="row"><label>方向</label><select id="og_odir"><option value="0">CW</option><option value="1">CCW</option></select></div>
<div class="row"><label>速度</label><input id="og_ovel" type="number" value="200" min="0" max="5000"><span class="unit">RPM</span></div>
<div class="row"><label>超时</label><input id="og_otm" type="number" value="10000" min="0"><span class="unit">ms</span></div>
<div class="row"><label>碰检速</label><input id="og_slvel" type="number" value="100" min="0" max="5000"><span class="unit">RPM</span></div>
<div class="row"><label>碰检流</label><input id="og_slma" type="number" value="500" min="0"><span class="unit">mA</span></div>
<div class="row"><label>碰检时</label><input id="og_slms" type="number" value="100" min="0"><span class="unit">ms</span></div>
<div class="row"><label>存储</label><select id="og_svf"><option value="0">不存储</option><option value="1">存储</option></select></div>
<div class="row"><label>上电回零</label><select id="og_potf"><option value="0">不使能</option><option value="1">使能</option></select></div>
<div class="btn-row">
<button class="btn-go" onclick="api('origin_set?addr='+v('og_addr')+'&svf='+v('og_svf'))">设零点</button>
<button class="btn-on" onclick="api('origin_config?addr='+v('og_addr')+'&svf='+v('og_svf')+'&omode='+v('og_omode')+'&odir='+v('og_odir')+'&ovel='+v('og_ovel')+'&otm='+v('og_otm')+'&slvel='+v('og_slvel')+'&slma='+v('og_slma')+'&slms='+v('og_slms')+'&potf='+v('og_potf'))">配置参数</button>
</div>
<div class="btn-row">
<button class="btn-go" onclick="api('origin_go?addr='+v('og_addr')+'&omode='+v('og_omode'))">触发回零</button>
<button class="btn-stop" onclick="api('origin_stop?addr='+v('og_addr'))">中断回零</button>
</div>
</div>
</div> </div>
<div class="card" id="proto-card"> <div class="card" id="proto-card">
@@ -208,6 +286,41 @@ function api(cmd){
l.innerHTML='<div class="err">请求失败</div>'+l.innerHTML l.innerHTML='<div class="err">请求失败</div>'+l.innerHTML
}) })
} }
function apiEach(ep,id,ext){
var addrs=v(id).split('|');
for(var i=0;i<addrs.length;i++){
api(ep+'?addr='+addrs[i].trim()+ext)
}
}
function axisMove(){
var x=v('ax_x'),y=v('ax_y'),z=v('ax_zrot'),s=v('ax_servo');
var vel=v('ax_vel'),acc=v('ax_acc');
/* X: CW(dir=0) | Y: CCW(dir=1) 由 actuator_motor_mm 自动处理 | Z: CW(dir=0) */
/* 允许 0 值: 绝对模式下 0=回到原点 */
api('mm?addr=2&mm='+(x||'0')+'&vel='+vel+'&acc='+acc+'&raf=1');
api('mm?addr=3&mm='+(y||'0')+'&vel='+vel+'&acc='+acc+'&raf=1');
api('angle?addr=1&angle='+(z||'0')+'&vel='+vel+'&acc='+acc+'&raf=1');
api('servo?angle='+(s||'90'));
}
function readParam(){
var addr=v('rp_addr'),param=v('rp_param');
fetch('/api/read_param?addr='+addr+'&param='+param)
.then(r=>r.json()).then(d=>{
var h='addr='+d.addr+' param='+d.param+' sign='+d.sign;
if(d.angle!==undefined){
var sg=d.sign?'-':'';
h+='<br><b style="color:var(--accent);font-size:1.2rem">'+sg+d.angle.toFixed(2)+'°</b>';
h+=' <span style="font-size:.7rem">(raw: '+d.val32+' / 0x'+d.val32.toString(16).toUpperCase()+')</span>';
}
if(d.val16!==undefined && d.angle===undefined){
h+='<br><b style="color:var(--accent)">'+d.val16+'</b> <span style="font-size:.7rem">(16bit)</span>';
}
h+='<br><span style="font-size:.6rem;opacity:.5">raw: '+d.hex+'</span>';
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){ fetch('/api/status').then(function(r){return r.json()}).then(function(d){
document.getElementById('wifi').innerHTML='<i class="dot"></i>WiFi: '+d.ssid; document.getElementById('wifi').innerHTML='<i class="dot"></i>WiFi: '+d.ssid;
document.getElementById('ip').textContent='IP: '+d.ip 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; 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 处理器 * HTTP 处理器
* ============================================================ */ * ============================================================ */
static esp_err_t root_handler(httpd_req_t *req) 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_type(req, "text/html; charset=utf-8");
httpd_resp_set_hdr(req, "Connection", "close"); httpd_resp_set_hdr(req, "Connection", "close");
while (offset < total) { /* 一次性发送, 由 ESP-IDF 内部处理 TCP 缓冲管理,
size_t chunk = total - offset; * 避免手动分块导致的 EAGAIN / ECONNRESET 偶发失败 */
if (chunk > 1024) chunk = 1024; esp_err_t err = httpd_resp_send(req, INDEX_HTML, HTTPD_RESP_USE_STRLEN);
esp_err_t err = httpd_resp_send_chunk(req, html + offset, (int)chunk); if (err != ESP_OK) {
if (err != ESP_OK) { /* 客户端断开是正常现象 (刷新页面/关闭标签), 仅警告即可 */
ESP_LOGE(TAG, "发送 HTML 失败: %d", err); ESP_LOGW(TAG, "发送 HTML 失败: %d (客户端可能已断开)", err);
return err;
}
offset += chunk;
} }
httpd_resp_send_chunk(req, NULL, 0); /* 结束分块 */ return err;
return ESP_OK;
} }
static esp_err_t api_enable_handler(httpd_req_t *req) static esp_err_t api_enable_handler(httpd_req_t *req)
{ {
int addr = get_query_int(req, "addr", 1); int addr = get_query_int(req, "addr", 1);
int state = get_query_int(req, "state", 0); 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]; char resp[64];
snprintf(resp, sizeof(resp), "电机 %d %s", addr, state ? "使能" : "关闭"); snprintf(resp, sizeof(resp), "电机 %d %s", addr, state ? "使能" : "关闭");
httpd_resp_set_type(req, "text/plain; charset=utf-8"); 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 dir = get_query_int(req, "dir", 0);
int vel = get_query_int(req, "vel", 100); int vel = get_query_int(req, "vel", 100);
int acc = get_query_int(req, "acc", 10); 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]; char resp[64];
snprintf(resp, sizeof(resp), "速度模式: 地址%d %s %dRPM acc=%d", snprintf(resp, sizeof(resp), "速度模式: 地址%d %s %dRPM acc=%d",
addr, dir ? "CCW" : "CW", vel, acc); 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 acc = get_query_int(req, "acc", 10);
int clk = get_query_int(req, "clk", 2000); int clk = get_query_int(req, "clk", 2000);
int raf = get_query_int(req, "raf", 0); int raf = get_query_int(req, "raf", 0);
Emm_V5_Pos_Control((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc, actuator_motor_pos((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc,
(uint32_t)clk, (bool)raf, false); (uint32_t)clk, (bool)raf);
char resp[80]; char resp[80];
snprintf(resp, sizeof(resp), "位置模式: 地址%d %s %dRPM %d脉冲 %s", snprintf(resp, sizeof(resp), "位置模式: 地址%d %s %dRPM %d脉冲 %s",
addr, dir ? "CCW" : "CW", vel, clk, raf ? "绝对" : "相对"); 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) static esp_err_t api_stop_handler(httpd_req_t *req)
{ {
int addr = get_query_int(req, "addr", 1); 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]; char resp[32];
snprintf(resp, sizeof(resp), "电机 %d 已停止", addr); snprintf(resp, sizeof(resp), "电机 %d 已停止", addr);
httpd_resp_set_type(req, "text/plain; charset=utf-8"); 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]; char val[16];
if (httpd_query_key_value(buf, "pos", val, sizeof(val)) == ESP_OK) { if (httpd_query_key_value(buf, "pos", val, sizeof(val)) == ESP_OK) {
if (strcmp(val, "up") == 0) { if (strcmp(val, "up") == 0) {
servo_set_up(); actuator_servo_up();
} else { } else {
servo_set_down(); actuator_servo_down();
} }
char resp[32]; char resp[32];
snprintf(resp, sizeof(resp), "舵机: %s", val); 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) { if (httpd_query_key_value(buf, "angle", val, sizeof(val)) == ESP_OK) {
float angle = atof(val); float angle = atof(val);
servo_set_angle(angle); actuator_servo_set_angle(angle);
char resp[32]; char resp[32];
snprintf(resp, sizeof(resp), "舵机: %.1f°", (double)angle); snprintf(resp, sizeof(resp), "舵机: %.1f°", (double)angle);
httpd_resp_set_type(req, "text/plain; charset=utf-8"); 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; 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, 避免浏览器反复请求 */ /* favicon — 204 No Content, 避免浏览器反复请求 */
static esp_err_t favicon_handler(httpd_req_t *req) 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->cycle_id, ch == EDC_CH_I2C ? "I2C" : "UART",
cycle->frag_count); cycle->frag_count);
/* I2C / UART 冗余下发, 同一 cycle_id 只回一次 DONE */ /* I2C / UART 冗余下发, 同一 cycle_id 只执行一次 */
if (cycle->cycle_id == s_last_done_id) { if (cycle->cycle_id == s_last_done_id) {
return; return;
} }
s_last_done_id = cycle->cycle_id; 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); edc_proto_send_done(cycle->cycle_id);
} }
@@ -602,7 +937,7 @@ static void http_server_init(void)
httpd_handle_t server = NULL; httpd_handle_t server = NULL;
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
cfg.stack_size = 8192; /* 默认 4096 不够, 增大 */ cfg.stack_size = 8192; /* 默认 4096 不够, 增大 */
cfg.max_uri_handlers = 16; cfg.max_uri_handlers = 26;
cfg.max_open_sockets = 4; cfg.max_open_sockets = 4;
cfg.lru_purge_enable = true; cfg.lru_purge_enable = true;
@@ -672,6 +1007,67 @@ static void http_server_init(void)
}; };
httpd_register_uri_handler(server, &uri_servo); 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 警告 */ /* favicon.ico — 返回空, 避免 404 警告 */
httpd_uri_t uri_favicon = { httpd_uri_t uri_favicon = {
.uri = "/favicon.ico", .uri = "/favicon.ico",
+1 -1
View File
@@ -1,3 +1,3 @@
idf_component_register(SRCS "main.c" idf_component_register(SRCS "main.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
PRIV_REQUIRES emm_v5 magnet servo web_ctrl protocol rgb) PRIV_REQUIRES emm_v5 magnet servo web_ctrl protocol rgb actuator)
+3 -5
View File
@@ -26,6 +26,7 @@
#include "web_ctrl.h" #include "web_ctrl.h"
#include "protocol.h" #include "protocol.h"
#include "rgb.h" #include "rgb.h"
#include "actuator.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "esp_log.h" #include "esp_log.h"
@@ -38,15 +39,12 @@ void app_main(void)
rgb_init(GPIO_NUM_27); rgb_init(GPIO_NUM_27);
rgb_set(32, 32, 0); /* 黄色 (低亮), 表示启动中 */ 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) */ /* 初始化电磁铁 (GPIO5) */
magnet_init(GPIO_NUM_5); magnet_init(GPIO_NUM_5);
/* 初始化舵机 (GPIO4, 上限180° 下限0°) */
servo_init(GPIO_NUM_4);
/* 初始化通讯协议 (I2C 从机 GPIO2/3 + UART0 GPIO11/12) */ /* 初始化通讯协议 (I2C 从机 GPIO2/3 + UART0 GPIO11/12) */
edc_proto_init(NULL); edc_proto_init(NULL);