磁铁、舵机、网页控制、通信协议
This commit is contained in:
1 parent
230ad1254a
commit
40ee976236
17 files changed
+1879
-32
No files matched your search
@@ -64,22 +64,122 @@ typedef enum {
|
|||||||
* @param rx_pin RX 引脚, 如 GPIO_NUM_7
|
* @param rx_pin RX 引脚, 如 GPIO_NUM_7
|
||||||
*/
|
*/
|
||||||
void Emm_V5_Init(uart_port_t port, int tx_pin, int rx_pin);
|
void Emm_V5_Init(uart_port_t port, int tx_pin, int rx_pin);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将当前位置清零
|
||||||
|
* @param addr 电机地址
|
||||||
|
*/
|
||||||
void Emm_V5_Reset_CurPos_To_Zero(uint8_t addr);
|
void Emm_V5_Reset_CurPos_To_Zero(uint8_t addr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解除堵转保护
|
||||||
|
* @param addr 电机地址
|
||||||
|
*/
|
||||||
void Emm_V5_Reset_Clog_Pro(uint8_t addr);
|
void Emm_V5_Reset_Clog_Pro(uint8_t addr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 读取系统参数
|
||||||
|
* @param addr 电机地址
|
||||||
|
* @param s 系统参数类型 (@see EmmSysParams_t)
|
||||||
|
*/
|
||||||
void Emm_V5_Read_Sys_Params(uint8_t addr, EmmSysParams_t s);
|
void Emm_V5_Read_Sys_Params(uint8_t addr, EmmSysParams_t s);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 修改开环/闭环控制模式
|
||||||
|
* @param addr 电机地址
|
||||||
|
* @param svF 是否存储, false=不存储, true=存储
|
||||||
|
* @param ctrl_mode 控制模式: 0=关闭脉冲输入, 1=开环, 2=闭环, 3=En复用为限位/到位输出
|
||||||
|
*/
|
||||||
void Emm_V5_Modify_Ctrl_Mode(uint8_t addr, bool svF, uint8_t ctrl_mode);
|
void Emm_V5_Modify_Ctrl_Mode(uint8_t addr, bool svF, uint8_t ctrl_mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使能信号控制
|
||||||
|
* @param addr 电机地址
|
||||||
|
* @param state 使能状态, true=使能, false=关闭
|
||||||
|
* @param snF 多机同步标志, false=不启用, true=启用
|
||||||
|
*/
|
||||||
void Emm_V5_En_Control(uint8_t addr, bool state, bool snF);
|
void Emm_V5_En_Control(uint8_t addr, bool state, bool snF);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 速度模式控制
|
||||||
|
* @param addr 电机地址
|
||||||
|
* @param dir 方向, 0=CW, 其他=CCW
|
||||||
|
* @param vel 速度 0~5000 RPM
|
||||||
|
* @param acc 加速度 0~255 (0=直接启动)
|
||||||
|
* @param snF 多机同步标志, false=不启用, true=启用
|
||||||
|
*/
|
||||||
void Emm_V5_Vel_Control(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc, bool snF);
|
void Emm_V5_Vel_Control(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc, bool snF);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 位置模式控制
|
||||||
|
* @param addr 电机地址
|
||||||
|
* @param dir 方向, 0=CW, 其他=CCW
|
||||||
|
* @param vel 速度 0~5000 RPM
|
||||||
|
* @param acc 加速度 0~255 (0=直接启动)
|
||||||
|
* @param clk 脉冲数 0~(2^32-1)
|
||||||
|
* @param raF 相位/绝对标志, false=相对运动, true=绝对运动
|
||||||
|
* @param snF 多机同步标志, false=不启用, true=启用
|
||||||
|
*/
|
||||||
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 snF 多机同步标志, false=不启用, true=启用
|
||||||
|
*/
|
||||||
void Emm_V5_Stop_Now(uint8_t addr, bool snF);
|
void Emm_V5_Stop_Now(uint8_t addr, bool snF);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 触发多机同步开始运动
|
||||||
|
* @param addr 电机地址
|
||||||
|
*/
|
||||||
void Emm_V5_Synchronous_motion(uint8_t addr);
|
void Emm_V5_Synchronous_motion(uint8_t addr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置单圈回零的零点位置
|
||||||
|
* @param addr 电机地址
|
||||||
|
* @param svF 是否存储, false=不存储, true=存储
|
||||||
|
*/
|
||||||
void Emm_V5_Origin_Set_O(uint8_t addr, bool svF);
|
void Emm_V5_Origin_Set_O(uint8_t addr, bool svF);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 修改回零参数
|
||||||
|
* @param addr 电机地址
|
||||||
|
* @param svF 是否存储, false=不存储, true=存储
|
||||||
|
* @param o_mode 回零模式: 0=单圈就近, 1=单圈方向, 2=多圈无限位碰撞, 3=多圈有限位开关
|
||||||
|
* @param o_dir 回零方向, 0=CW, 其他=CCW
|
||||||
|
* @param o_vel 回零速度 (RPM)
|
||||||
|
* @param o_tm 回零超时时间 (ms)
|
||||||
|
* @param sl_vel 碰撞检测转速 (RPM)
|
||||||
|
* @param sl_ma 碰撞检测电流 (mA)
|
||||||
|
* @param sl_ms 碰撞检测时间 (ms)
|
||||||
|
* @param potF 上电自动回零, false=不使能, true=使能
|
||||||
|
*/
|
||||||
void Emm_V5_Origin_Modify_Params(uint8_t addr, bool svF, uint8_t o_mode,
|
void Emm_V5_Origin_Modify_Params(uint8_t addr, bool svF, uint8_t o_mode,
|
||||||
uint8_t o_dir, uint16_t o_vel, uint32_t o_tm,
|
uint8_t o_dir, uint16_t o_vel, uint32_t o_tm,
|
||||||
uint16_t sl_vel, uint16_t sl_ma, uint16_t sl_ms, bool potF);
|
uint16_t sl_vel, uint16_t sl_ma, uint16_t sl_ms, bool potF);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 触发回零
|
||||||
|
* @param addr 电机地址
|
||||||
|
* @param o_mode 回零模式: 0=单圈就近, 1=单圈方向, 2=多圈无限位碰撞, 3=多圈有限位开关
|
||||||
|
* @param snF 多机同步标志, false=不启用, true=启用
|
||||||
|
*/
|
||||||
void Emm_V5_Origin_Trigger_Return(uint8_t addr, uint8_t o_mode, bool snF);
|
void Emm_V5_Origin_Trigger_Return(uint8_t addr, uint8_t o_mode, bool snF);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 强制中断并退出回零
|
||||||
|
* @param addr 电机地址
|
||||||
|
*/
|
||||||
void Emm_V5_Origin_Interrupt(uint8_t addr);
|
void Emm_V5_Origin_Interrupt(uint8_t addr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 接收电机返回数据 (100ms 超时判定帧结束)
|
||||||
|
* @param rxCmd 接收缓冲区
|
||||||
|
* @param maxLen 缓冲区最大长度
|
||||||
|
* @return 实际接收到的字节数
|
||||||
|
*/
|
||||||
uint8_t Emm_V5_Receive_Data(uint8_t *rxCmd, uint8_t maxLen);
|
uint8_t Emm_V5_Receive_Data(uint8_t *rxCmd, uint8_t maxLen);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
idf_component_register(SRCS "magnet.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES esp_driver_gpio driver)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* @file magnet.c
|
||||||
|
* @brief 单 GPIO 电磁铁控制实现
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "magnet.h"
|
||||||
|
|
||||||
|
static int s_gpio = -1; /* 未初始化 */
|
||||||
|
|
||||||
|
void magnet_init(int gpio)
|
||||||
|
{
|
||||||
|
s_gpio = gpio;
|
||||||
|
|
||||||
|
gpio_reset_pin(s_gpio);
|
||||||
|
gpio_set_direction(s_gpio, GPIO_MODE_OUTPUT);
|
||||||
|
gpio_set_level(s_gpio, 0); /* 默认关闭 */
|
||||||
|
}
|
||||||
|
|
||||||
|
void magnet_set(bool on)
|
||||||
|
{
|
||||||
|
if (s_gpio >= 0) {
|
||||||
|
gpio_set_level(s_gpio, on ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* @file magnet.h
|
||||||
|
* @brief 单 GPIO 电磁铁控制
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MAGNET_H
|
||||||
|
#define MAGNET_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化电磁铁 GPIO (推挽输出, 默认关闭)
|
||||||
|
* @param gpio GPIO 引脚号, 如 GPIO_NUM_5
|
||||||
|
*/
|
||||||
|
void magnet_init(int gpio);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置电磁铁状态
|
||||||
|
* @param on true=吸合, false=释放
|
||||||
|
*/
|
||||||
|
void magnet_set(bool on);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* MAGNET_H */
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
idf_component_register(SRCS "protocol.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES driver esp_driver_i2c esp_driver_uart esp_driver_gpio freertos)
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
# EDC 通讯协议
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本协议用于 MPU 与 MCU (ESP32-C5) 之间的数据交互,支持 **I2C** 与 **UART** 双通道冗余传输。
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 帧头 | `0xAA 0x55` |
|
||||||
|
| 字节序 | 大端 (Big-Endian) |
|
||||||
|
| CRC | CRC-16/MODBUS (多项式 `0x8005`) |
|
||||||
|
| I2C 模式 | ESP32 从机, 地址 `0x2A` |
|
||||||
|
| UART 波特率 | `115200`, 8N1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 数据模型
|
||||||
|
|
||||||
|
```
|
||||||
|
Point = { x: uint16, y: uint16, angle_x10: uint16 }
|
||||||
|
Fragment = { source: Point, target: Point }
|
||||||
|
Cycle = { cycle_id: uint16, frag_count: uint8 (2~4), fragments: Fragment[] }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Point**: 单个坐标点, `angle_x10 = 角度 × 10` (如 `450` = 45.0°)
|
||||||
|
- **Fragment**: 一次运动片段, 从 `source` 移动到 `target`
|
||||||
|
- **Cycle**: 一个执行周期, 包含 2~4 个 Fragment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 帧格式
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────┬────────┬────────┬────────┬───────────────────┬──────────────┐
|
||||||
|
│ SOF1 │ SOF2 │ CMD │ LEN │ DATA (N bytes) │ CRC16 (LE) │
|
||||||
|
│ 0xAA │ 0x55 │ 1B │ 1B │ variable │ 2B │
|
||||||
|
└────────┴────────┴────────┴────────┴───────────────────┴──────────────┘
|
||||||
|
◄──────────── CRC 计算范围 (CMD + LEN + DATA) ────────────►
|
||||||
|
```
|
||||||
|
|
||||||
|
- **SOF**: 帧头, 固定 `0xAA 0x55`
|
||||||
|
- **CMD**: 命令字 (见 §3)
|
||||||
|
- **LEN**: DATA 区字节数
|
||||||
|
- **DATA**: 有效载荷
|
||||||
|
- **CRC**: CRC-16/MODBUS, 低位在前
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 命令字
|
||||||
|
|
||||||
|
| 命令 | 值 | 方向 | 说明 |
|
||||||
|
|------|-----|------|------|
|
||||||
|
| `CYCLE_DATA` | `0x01` | MPU → MCU | 下发 Cycle 数据 |
|
||||||
|
| `CYCLE_DONE` | `0x02` | MCU → MPU | Cycle 执行完成 |
|
||||||
|
| `CYCLE_REQUEST` | `0x03` | MCU → MPU | 请求下一个 Cycle |
|
||||||
|
| `ACK` | `0x80` | 双向 | 通用应答 (预留) |
|
||||||
|
| `NACK` | `0xFF` | 双向 | 错误 (预留) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 帧类型详解
|
||||||
|
|
||||||
|
### 4.1 CYCLE_DATA (MPU → MCU)
|
||||||
|
|
||||||
|
```
|
||||||
|
SOF(2) + CMD=0x01 + LEN + DATA + CRC(2)
|
||||||
|
|
||||||
|
DATA:
|
||||||
|
┌──────────────┬────────────┬─────────────────────────┐
|
||||||
|
│ cycle_id │ frag_count │ fragments │
|
||||||
|
│ uint16 (BE) │ uint8 │ N × 12B │
|
||||||
|
│ 2B │ 1B │ 12~48B │
|
||||||
|
└──────────────┴────────────┴─────────────────────────┘
|
||||||
|
|
||||||
|
LEN = 3 + frag_count × 12
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fragment (12B)**:
|
||||||
|
```
|
||||||
|
┌──────────┬──────────┬───────────────┬──────────┬──────────┬───────────────┐
|
||||||
|
│ src.x │ src.y │ src.angle_x10 │ dst.x │ dst.y │ dst.angle_x10 │
|
||||||
|
│ uint16 │ uint16 │ uint16 │ uint16 │ uint16 │ uint16 │
|
||||||
|
│ 2B │ 2B │ 2B │ 2B │ 2B │ 2B │
|
||||||
|
└──────────┴──────────┴───────────────┴──────────┴──────────┴───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例** (2 个 Fragment, `cycle_id=1`):
|
||||||
|
|
||||||
|
```
|
||||||
|
AA 55 01 1B 00 01 02
|
||||||
|
00 64 00 C8 01 C2 01 2C 01 90 02 58 ← Fragment 0
|
||||||
|
00 96 01 2C 03 84 01 F4 02 BC 04 B0 ← Fragment 1
|
||||||
|
XX XX ← CRC16
|
||||||
|
```
|
||||||
|
|
||||||
|
解析:
|
||||||
|
| 字段 | 值 | 含义 |
|
||||||
|
|------|-----|------|
|
||||||
|
| cycle_id | `0x0001` = 1 | 第 1 个 Cycle |
|
||||||
|
| frag_count | `0x02` = 2 | 2 个 Fragment |
|
||||||
|
| F0 src | (100, 200), 45.0° | |
|
||||||
|
| F0 dst | (300, 400), 60.0° | |
|
||||||
|
| F1 src | (150, 300), 90.0° | |
|
||||||
|
| F1 dst | (500, 700), 120.0° | |
|
||||||
|
|
||||||
|
### 4.2 CYCLE_DONE (MCU → MPU)
|
||||||
|
|
||||||
|
```
|
||||||
|
SOF(2) + CMD=0x02 + LEN=2 + cycle_id(2B, BE) + CRC(2)
|
||||||
|
|
||||||
|
总长度: 8 字节
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例** (`cycle_id=1` 完成):
|
||||||
|
```
|
||||||
|
AA 55 02 02 00 01 XX XX
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 CYCLE_REQUEST (MCU → MPU)
|
||||||
|
|
||||||
|
```
|
||||||
|
SOF(2) + CMD=0x03 + LEN=0 + CRC(2)
|
||||||
|
|
||||||
|
总长度: 6 字节
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
AA 55 03 00 XX XX
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. CRC-16/MODBUS
|
||||||
|
|
||||||
|
| 参数 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 多项式 | `0x8005` (逆序 `0xA001`) |
|
||||||
|
| 初始值 | `0xFFFF` |
|
||||||
|
| 输入反射 | 是 |
|
||||||
|
| 输出反射 | 是 |
|
||||||
|
| 异或输出 | `0x0000` |
|
||||||
|
| 校验范围 | CMD + LEN + DATA |
|
||||||
|
|
||||||
|
### 参考实现 (C)
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint16_t edc_crc16(const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
uint16_t crc = 0xFFFF;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
crc ^= data[i];
|
||||||
|
for (int j = 0; j < 8; j++) {
|
||||||
|
if (crc & 0x0001)
|
||||||
|
crc = (crc >> 1) ^ 0xA001;
|
||||||
|
else
|
||||||
|
crc >>= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 传输通道
|
||||||
|
|
||||||
|
### 6.1 I2C (冗余接收)
|
||||||
|
|
||||||
|
- ESP32 作为 **I2C 从机**, 地址 `0x2A` (7-bit)
|
||||||
|
- MPU 作为 I2C 主机
|
||||||
|
- MPU 将完整帧直接 `Write` 到从机地址
|
||||||
|
- ESP32 通过 ISR 回调接收并 CRC 校验
|
||||||
|
- 默认引脚: SDA=GPIO2, SCL=GPIO3
|
||||||
|
|
||||||
|
### 6.2 UART (双向)
|
||||||
|
|
||||||
|
- **RX**: 接收 CYCLE_DATA (冗余备份)
|
||||||
|
- **TX**: 发送 CYCLE_DONE / CYCLE_REQUEST
|
||||||
|
- 默认引脚: TX=GPIO8, RX=GPIO9
|
||||||
|
- 波特率: 115200, 8N1
|
||||||
|
|
||||||
|
### 6.3 冗余策略
|
||||||
|
|
||||||
|
同一帧 CYCLE_DATA 同时通过 I2C 和 UART 发送, **任一通道先收到有效帧即采纳**, 通过 `edc_proto_stat_t.last_channel` 可判断来源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 交互流程
|
||||||
|
|
||||||
|
```
|
||||||
|
MPU MCU (ESP32-C5)
|
||||||
|
│ │
|
||||||
|
│──── CYCLE_DATA (I2C+UART) ────────►│ 下发 Cycle
|
||||||
|
│ │
|
||||||
|
│ │ 执行 fragments[0..N]
|
||||||
|
│ │
|
||||||
|
│◄──── CYCLE_DONE (UART) ───────────│ 上报完成
|
||||||
|
│ │
|
||||||
|
│──── CYCLE_DATA (I2C+UART) ────────►│ 下发下一个...
|
||||||
|
│ │
|
||||||
|
— 或 —
|
||||||
|
│ │
|
||||||
|
│ (超时无数据) │
|
||||||
|
│ │
|
||||||
|
│◄──── CYCLE_REQUEST (UART) ────────│ 主动请求
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. MCU 端 API
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "protocol.h"
|
||||||
|
|
||||||
|
// 初始化 (I2C 从机 + UART)
|
||||||
|
edc_proto_init(NULL);
|
||||||
|
|
||||||
|
// 方式一: 手动轮询
|
||||||
|
edc_cycle_t cycle;
|
||||||
|
if (edc_proto_poll(&cycle) > 0) {
|
||||||
|
// 处理 cycle.fragments[0..frag_count-1]
|
||||||
|
edc_proto_send_done(cycle.cycle_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式二: FreeRTOS 任务 + 回调
|
||||||
|
xTaskCreate(edc_proto_task, "proto", 4096, NULL, 5, NULL);
|
||||||
|
|
||||||
|
void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch) {
|
||||||
|
// ch: EDC_CH_I2C 或 EDC_CH_UART
|
||||||
|
// 执行完调用:
|
||||||
|
edc_proto_send_done(cycle->cycle_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时请求
|
||||||
|
edc_proto_send_request();
|
||||||
|
|
||||||
|
// 查看统计
|
||||||
|
edc_proto_stat_t stat;
|
||||||
|
edc_proto_get_stats(&stat);
|
||||||
|
// stat.i2c.cycles_ok, stat.uart.cycles_err, stat.done_sent ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 帧总长速查
|
||||||
|
|
||||||
|
| frag_count | DATA 长度 | 帧总长 (SOF+CMD+LEN+DATA+CRC) |
|
||||||
|
|------------|-----------|------|
|
||||||
|
| 2 | 3 + 24 = 27 | 2 + 1 + 1 + 27 + 2 = **33** |
|
||||||
|
| 3 | 3 + 36 = 39 | 2 + 1 + 1 + 39 + 2 = **45** |
|
||||||
|
| 4 | 3 + 48 = 51 | 2 + 1 + 1 + 51 + 2 = **57** |
|
||||||
|
| DONE | 2 | **8** |
|
||||||
|
| REQUEST | 0 | **6** |
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
/**
|
||||||
|
* @file protocol.c
|
||||||
|
* @brief EDC 通讯协议实现 — CRC16 / 组帧 / 解析 / I2C / UART
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "protocol.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
static const char *TAG = "EDC_PROTO";
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* CRC-16/MODBUS
|
||||||
|
*
|
||||||
|
* 多项式: 0x8005 (逆序 0xA001)
|
||||||
|
* 初始值: 0xFFFF
|
||||||
|
* 输入/输出: 反射
|
||||||
|
* 异或输出: 0x0000
|
||||||
|
* 校验范围: CMD + LEN + DATA
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
uint16_t edc_crc16(const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
uint16_t crc = 0xFFFF;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
crc ^= data[i];
|
||||||
|
for (int j = 0; j < 8; j++) {
|
||||||
|
if (crc & 0x0001) {
|
||||||
|
crc = (crc >> 1) ^ 0xA001;
|
||||||
|
} else {
|
||||||
|
crc >>= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 大端序 uint16_t 写入 / 读取
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static inline void write_u16_be(uint8_t *buf, uint16_t val)
|
||||||
|
{
|
||||||
|
buf[0] = (uint8_t)(val >> 8);
|
||||||
|
buf[1] = (uint8_t)(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint16_t read_u16_be(const uint8_t *buf)
|
||||||
|
{
|
||||||
|
return ((uint16_t)buf[0] << 8) | buf[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Cycle 组帧
|
||||||
|
*
|
||||||
|
* DATA 区: cycle_id(2B,BE) + frag_count(1B) + fragments(N×12B)
|
||||||
|
* Fragment: src.x(2B) src.y(2B) src.angle(2B) dst.x(2B) dst.y(2B) dst.angle(2B)
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
int edc_cycle_pack(const edc_cycle_t *cycle, uint8_t *out)
|
||||||
|
{
|
||||||
|
uint8_t count = cycle->frag_count;
|
||||||
|
if (count < EDC_FRAG_MIN) count = EDC_FRAG_MIN;
|
||||||
|
if (count > EDC_FRAG_MAX) count = EDC_FRAG_MAX;
|
||||||
|
|
||||||
|
uint8_t data_len = 3 + count * EDC_FRAG_BYTES; /* id(2) + cnt(1) + frags */
|
||||||
|
|
||||||
|
out[0] = EDC_PROTO_SOF1;
|
||||||
|
out[1] = EDC_PROTO_SOF2;
|
||||||
|
out[2] = EDC_CMD_CYCLE_DATA;
|
||||||
|
out[3] = data_len;
|
||||||
|
|
||||||
|
/* cycle_id (大端) */
|
||||||
|
uint8_t *p = &out[4];
|
||||||
|
write_u16_be(p, cycle->cycle_id);
|
||||||
|
p += 2;
|
||||||
|
|
||||||
|
/* frag_count */
|
||||||
|
*p++ = count;
|
||||||
|
|
||||||
|
/* fragments */
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
const edc_fragment_t *f = &cycle->fragments[i];
|
||||||
|
write_u16_be(p, f->source.x); p += 2;
|
||||||
|
write_u16_be(p, f->source.y); p += 2;
|
||||||
|
write_u16_be(p, f->source.angle_x10); p += 2;
|
||||||
|
write_u16_be(p, f->target.x); p += 2;
|
||||||
|
write_u16_be(p, f->target.y); p += 2;
|
||||||
|
write_u16_be(p, f->target.angle_x10); p += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CRC16 over [CMD, LEN, DATA] */
|
||||||
|
uint16_t crc = edc_crc16(&out[2], 2 + data_len);
|
||||||
|
*p++ = (uint8_t)(crc);
|
||||||
|
*p++ = (uint8_t)(crc >> 8);
|
||||||
|
|
||||||
|
return (int)(p - out); /* 2 + 1 + 1 + data_len + 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Cycle 解析 (逐字节)
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
edc_parse_result_t edc_cycle_parse_byte(edc_frame_ctx_t *ctx, uint8_t byte,
|
||||||
|
edc_cycle_t *cycle)
|
||||||
|
{
|
||||||
|
switch (ctx->state) {
|
||||||
|
case EDC_PARSE_OK:
|
||||||
|
ctx->idx = 0;
|
||||||
|
ctx->state = EDC_PARSE_WAITING;
|
||||||
|
/* fall through */
|
||||||
|
case EDC_PARSE_WAITING:
|
||||||
|
default:
|
||||||
|
if (byte == EDC_PROTO_SOF1) {
|
||||||
|
ctx->buf[0] = byte;
|
||||||
|
ctx->idx = 1;
|
||||||
|
}
|
||||||
|
return EDC_PARSE_WAITING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 收帧中 (idx >= 1) ---- */
|
||||||
|
|
||||||
|
if (ctx->idx == 1) {
|
||||||
|
if (byte != EDC_PROTO_SOF2) {
|
||||||
|
ctx->idx = 0;
|
||||||
|
if (byte == EDC_PROTO_SOF1) { ctx->buf[0] = byte; ctx->idx = 1; }
|
||||||
|
return EDC_PARSE_WAITING;
|
||||||
|
}
|
||||||
|
ctx->buf[1] = byte;
|
||||||
|
ctx->idx = 2;
|
||||||
|
return EDC_PARSE_WAITING;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->idx < EDC_PROTO_FRAME_MAX) {
|
||||||
|
ctx->buf[ctx->idx] = byte;
|
||||||
|
}
|
||||||
|
ctx->idx++;
|
||||||
|
|
||||||
|
/* 确定帧总长 */
|
||||||
|
if (ctx->idx == 4) {
|
||||||
|
uint8_t data_len = ctx->buf[3];
|
||||||
|
if (data_len < 3 || data_len > EDC_PROTO_FRAME_MAX - 6) {
|
||||||
|
ctx->idx = 0; ctx->state = EDC_PARSE_WAITING;
|
||||||
|
return EDC_PARSE_LEN_ERR;
|
||||||
|
}
|
||||||
|
ctx->len = 2 + 1 + 1 + data_len + 2; /* SOF+CMD+LEN+DATA+CRC */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收满一帧 */
|
||||||
|
if (ctx->idx >= ctx->len) {
|
||||||
|
ctx->idx = ctx->len;
|
||||||
|
|
||||||
|
/* CRC 校验 */
|
||||||
|
uint16_t crc_calc = edc_crc16(&ctx->buf[2], 2 + ctx->buf[3]);
|
||||||
|
uint16_t crc_recv = ctx->buf[ctx->len - 2] | ((uint16_t)ctx->buf[ctx->len - 1] << 8);
|
||||||
|
if (crc_calc != crc_recv) {
|
||||||
|
ctx->idx = 0; ctx->state = EDC_PARSE_WAITING;
|
||||||
|
return EDC_PARSE_CRC_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 仅处理 CYCLE_DATA 命令 */
|
||||||
|
uint8_t cmd = ctx->buf[2];
|
||||||
|
if (cmd != EDC_CMD_CYCLE_DATA) {
|
||||||
|
ctx->idx = 0; ctx->state = EDC_PARSE_WAITING;
|
||||||
|
return EDC_PARSE_CMD_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 解析 DATA 区 */
|
||||||
|
const uint8_t *d = &ctx->buf[4];
|
||||||
|
cycle->cycle_id = read_u16_be(d); d += 2;
|
||||||
|
cycle->frag_count = *d; d += 1;
|
||||||
|
|
||||||
|
if (cycle->frag_count < EDC_FRAG_MIN || cycle->frag_count > EDC_FRAG_MAX) {
|
||||||
|
ctx->idx = 0; ctx->state = EDC_PARSE_WAITING;
|
||||||
|
return EDC_PARSE_LEN_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < cycle->frag_count; i++) {
|
||||||
|
edc_fragment_t *f = &cycle->fragments[i];
|
||||||
|
f->source.x = read_u16_be(d); d += 2;
|
||||||
|
f->source.y = read_u16_be(d); d += 2;
|
||||||
|
f->source.angle_x10 = read_u16_be(d); d += 2;
|
||||||
|
f->target.x = read_u16_be(d); d += 2;
|
||||||
|
f->target.y = read_u16_be(d); d += 2;
|
||||||
|
f->target.angle_x10 = read_u16_be(d); d += 2;
|
||||||
|
}
|
||||||
|
cycle->timestamp_ms = 0;
|
||||||
|
|
||||||
|
ctx->state = EDC_PARSE_OK;
|
||||||
|
return EDC_PARSE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EDC_PARSE_WAITING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Cycle 解析 (一次性)
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
edc_parse_result_t edc_cycle_parse_buf(const uint8_t *data, size_t len,
|
||||||
|
edc_cycle_t *cycle)
|
||||||
|
{
|
||||||
|
edc_frame_ctx_t ctx = { .state = EDC_PARSE_WAITING };
|
||||||
|
edc_parse_result_t res;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
res = edc_cycle_parse_byte(&ctx, data[i], cycle);
|
||||||
|
if (res != EDC_PARSE_WAITING) return res;
|
||||||
|
}
|
||||||
|
return EDC_PARSE_WAITING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 统一收发器
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
/* ---- 共享最新 Cycle ---- */
|
||||||
|
static edc_cycle_t s_latest_cycle;
|
||||||
|
static volatile bool s_cycle_updated;
|
||||||
|
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||||
|
|
||||||
|
/* ---- 统计 ---- */
|
||||||
|
static edc_proto_stat_t s_stat;
|
||||||
|
|
||||||
|
/* ---- 通道使能 ---- */
|
||||||
|
static bool s_i2c_enabled;
|
||||||
|
static bool s_uart_enabled;
|
||||||
|
|
||||||
|
/* ---- I2C ---- */
|
||||||
|
static i2c_slave_dev_handle_t s_i2c_handle;
|
||||||
|
static edc_cycle_t s_i2c_rx_cycle;
|
||||||
|
static volatile bool s_i2c_rx_pending;
|
||||||
|
|
||||||
|
/* ---- UART ---- */
|
||||||
|
static uart_port_t s_uart_port;
|
||||||
|
static edc_frame_ctx_t s_uart_ctx;
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 内部: 存入共享 Cycle
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static void store_cycle(const edc_cycle_t *cycle, edc_channel_t ch)
|
||||||
|
{
|
||||||
|
portENTER_CRITICAL(&s_lock);
|
||||||
|
memcpy(&s_latest_cycle, cycle, sizeof(edc_cycle_t));
|
||||||
|
s_latest_cycle.timestamp_ms = xTaskGetTickCount() * portTICK_PERIOD_MS;
|
||||||
|
s_cycle_updated = true;
|
||||||
|
portEXIT_CRITICAL(&s_lock);
|
||||||
|
|
||||||
|
if (ch == EDC_CH_I2C) s_stat.i2c.cycles_ok++;
|
||||||
|
else if (ch == EDC_CH_UART) s_stat.uart.cycles_ok++;
|
||||||
|
s_stat.last_channel = ch;
|
||||||
|
s_stat.last_tick = xTaskGetTickCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* I2C 从机 接收
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static bool on_i2c_rx(i2c_slave_dev_handle_t handle,
|
||||||
|
const i2c_slave_rx_done_event_data_t *evt,
|
||||||
|
void *arg)
|
||||||
|
{
|
||||||
|
s_stat.i2c.bytes_total += evt->length;
|
||||||
|
|
||||||
|
edc_cycle_t cycle;
|
||||||
|
if (edc_cycle_parse_buf(evt->buffer, evt->length, &cycle) == EDC_PARSE_OK) {
|
||||||
|
memcpy(&s_i2c_rx_cycle, &cycle, sizeof(edc_cycle_t));
|
||||||
|
s_i2c_rx_cycle.timestamp_ms = xTaskGetTickCountFromISR() * portTICK_PERIOD_MS;
|
||||||
|
s_i2c_rx_pending = true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool i2c_poll_once(void)
|
||||||
|
{
|
||||||
|
if (!s_i2c_rx_pending) return false;
|
||||||
|
|
||||||
|
edc_cycle_t cycle;
|
||||||
|
portENTER_CRITICAL(&s_lock);
|
||||||
|
memcpy(&cycle, &s_i2c_rx_cycle, sizeof(edc_cycle_t));
|
||||||
|
s_i2c_rx_pending = false;
|
||||||
|
portEXIT_CRITICAL(&s_lock);
|
||||||
|
|
||||||
|
store_cycle(&cycle, EDC_CH_I2C);
|
||||||
|
ESP_LOGD(TAG, "I2C cycle OK (id=%u frags=%u)", cycle.cycle_id, cycle.frag_count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* UART 接收
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static bool uart_poll_once(void)
|
||||||
|
{
|
||||||
|
uint8_t byte;
|
||||||
|
bool got = false;
|
||||||
|
|
||||||
|
while (uart_read_bytes(s_uart_port, &byte, 1, pdMS_TO_TICKS(0)) > 0) {
|
||||||
|
s_stat.uart.bytes_total++;
|
||||||
|
|
||||||
|
edc_cycle_t cycle;
|
||||||
|
edc_parse_result_t res = edc_cycle_parse_byte(&s_uart_ctx, byte, &cycle);
|
||||||
|
if (res == EDC_PARSE_OK) {
|
||||||
|
store_cycle(&cycle, EDC_CH_UART);
|
||||||
|
got = true;
|
||||||
|
ESP_LOGD(TAG, "UART cycle OK (id=%u frags=%u)", cycle.cycle_id, cycle.frag_count);
|
||||||
|
} else if (res < 0) {
|
||||||
|
s_stat.uart.cycles_err++;
|
||||||
|
ESP_LOGW(TAG, "UART parse err: %d", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return got;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 内部: 通过 UART 发送短帧
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static void uart_send_frame(uint8_t cmd, const uint8_t *data, uint8_t data_len)
|
||||||
|
{
|
||||||
|
uint8_t buf[EDC_PROTO_FRAME_MAX];
|
||||||
|
buf[0] = EDC_PROTO_SOF1;
|
||||||
|
buf[1] = EDC_PROTO_SOF2;
|
||||||
|
buf[2] = cmd;
|
||||||
|
buf[3] = data_len;
|
||||||
|
if (data && data_len) memcpy(&buf[4], data, data_len);
|
||||||
|
|
||||||
|
uint16_t crc = edc_crc16(&buf[2], 2 + data_len);
|
||||||
|
buf[4 + data_len] = (uint8_t)(crc);
|
||||||
|
buf[4 + data_len + 1] = (uint8_t)(crc >> 8);
|
||||||
|
|
||||||
|
int total = 4 + data_len + 2;
|
||||||
|
uart_write_bytes(s_uart_port, (const char *)buf, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 初始化
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
void edc_proto_init(const edc_proto_config_t *cfg)
|
||||||
|
{
|
||||||
|
edc_proto_config_t dft = {
|
||||||
|
.i2c_enable = true,
|
||||||
|
.i2c_port = EDC_I2C_PORT_DEFAULT,
|
||||||
|
.i2c_addr = EDC_I2C_SLAVE_ADDR,
|
||||||
|
.i2c_sda = EDC_I2C_SDA_DEFAULT,
|
||||||
|
.i2c_scl = EDC_I2C_SCL_DEFAULT,
|
||||||
|
.uart_enable = true,
|
||||||
|
.uart_port = EDC_UART_PORT_DEFAULT,
|
||||||
|
.uart_baudrate = EDC_UART_BAUDRATE,
|
||||||
|
.uart_rx = EDC_UART_RX_DEFAULT,
|
||||||
|
.uart_tx = EDC_UART_TX_DEFAULT,
|
||||||
|
};
|
||||||
|
if (cfg) dft = *cfg;
|
||||||
|
|
||||||
|
s_i2c_enabled = dft.i2c_enable;
|
||||||
|
s_uart_enabled = dft.uart_enable;
|
||||||
|
memset(&s_stat, 0, sizeof(s_stat));
|
||||||
|
|
||||||
|
/* I2C */
|
||||||
|
if (s_i2c_enabled) {
|
||||||
|
i2c_slave_config_t i2c_cfg = {
|
||||||
|
.i2c_port = dft.i2c_port,
|
||||||
|
.sda_io_num = dft.i2c_sda,
|
||||||
|
.scl_io_num = dft.i2c_scl,
|
||||||
|
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||||
|
.send_buf_depth = 0,
|
||||||
|
.receive_buf_depth = EDC_I2C_BUF_SIZE,
|
||||||
|
.slave_addr = dft.i2c_addr,
|
||||||
|
.addr_bit_len = I2C_ADDR_BIT_LEN_7,
|
||||||
|
.intr_priority = 0,
|
||||||
|
.flags.enable_internal_pullup = true,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(i2c_new_slave_device(&i2c_cfg, &s_i2c_handle));
|
||||||
|
|
||||||
|
i2c_slave_event_callbacks_t cbs = {
|
||||||
|
.on_receive = on_i2c_rx,
|
||||||
|
.on_request = NULL,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(i2c_slave_register_event_callbacks(s_i2c_handle, &cbs, NULL));
|
||||||
|
ESP_LOGI(TAG, "I2C slave OK (0x%02X SDA=%d SCL=%d)",
|
||||||
|
dft.i2c_addr, dft.i2c_sda, dft.i2c_scl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* UART */
|
||||||
|
if (s_uart_enabled) {
|
||||||
|
s_uart_port = dft.uart_port;
|
||||||
|
memset(&s_uart_ctx, 0, sizeof(s_uart_ctx));
|
||||||
|
s_uart_ctx.state = EDC_PARSE_WAITING;
|
||||||
|
|
||||||
|
uart_config_t uart_cfg = {
|
||||||
|
.baud_rate = dft.uart_baudrate,
|
||||||
|
.data_bits = UART_DATA_8_BITS,
|
||||||
|
.parity = UART_PARITY_DISABLE,
|
||||||
|
.stop_bits = UART_STOP_BITS_1,
|
||||||
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||||
|
.source_clk = UART_SCLK_DEFAULT,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(uart_driver_install(dft.uart_port,
|
||||||
|
EDC_UART_BUF_SIZE, EDC_UART_BUF_SIZE,
|
||||||
|
0, NULL, 0));
|
||||||
|
ESP_ERROR_CHECK(uart_param_config(dft.uart_port, &uart_cfg));
|
||||||
|
ESP_ERROR_CHECK(uart_set_pin(dft.uart_port,
|
||||||
|
dft.uart_tx, dft.uart_rx,
|
||||||
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
|
||||||
|
ESP_LOGI(TAG, "UART OK (port=%d baud=%d RX=%d TX=%d)",
|
||||||
|
dft.uart_port, dft.uart_baudrate, dft.uart_rx, dft.uart_tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 轮询
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
int edc_proto_poll(edc_cycle_t *cycle)
|
||||||
|
{
|
||||||
|
bool got = false;
|
||||||
|
if (s_i2c_enabled) got |= i2c_poll_once();
|
||||||
|
if (s_uart_enabled) got |= uart_poll_once();
|
||||||
|
|
||||||
|
if (got) {
|
||||||
|
portENTER_CRITICAL(&s_lock);
|
||||||
|
memcpy(cycle, &s_latest_cycle, sizeof(edc_cycle_t));
|
||||||
|
s_cycle_updated = false;
|
||||||
|
portEXIT_CRITICAL(&s_lock);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 发送 DONE / REQUEST
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
void edc_proto_send_done(uint16_t cycle_id)
|
||||||
|
{
|
||||||
|
if (!s_uart_enabled) return;
|
||||||
|
uint8_t data[2];
|
||||||
|
write_u16_be(data, cycle_id);
|
||||||
|
uart_send_frame(EDC_CMD_CYCLE_DONE, data, 2);
|
||||||
|
s_stat.done_sent++;
|
||||||
|
ESP_LOGI(TAG, "DONE sent (cycle_id=%u)", cycle_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void edc_proto_send_request(void)
|
||||||
|
{
|
||||||
|
if (!s_uart_enabled) return;
|
||||||
|
uart_send_frame(EDC_CMD_CYCLE_REQUEST, NULL, 0);
|
||||||
|
s_stat.request_sent++;
|
||||||
|
ESP_LOGI(TAG, "REQUEST sent");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 统计
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
void edc_proto_get_stats(edc_proto_stat_t *stat)
|
||||||
|
{
|
||||||
|
memcpy(stat, &s_stat, sizeof(edc_proto_stat_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 弱回调
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
__attribute__((weak))
|
||||||
|
void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch)
|
||||||
|
{
|
||||||
|
(void)cycle; (void)ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* FreeRTOS 任务
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
void edc_proto_task(void *pvParam)
|
||||||
|
{
|
||||||
|
edc_cycle_t cycle;
|
||||||
|
while (1) {
|
||||||
|
if (edc_proto_poll(&cycle) > 0) {
|
||||||
|
edc_channel_t ch;
|
||||||
|
portENTER_CRITICAL(&s_lock);
|
||||||
|
ch = s_stat.last_channel;
|
||||||
|
portEXIT_CRITICAL(&s_lock);
|
||||||
|
edc_proto_on_cycle(&cycle, ch);
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* @file protocol.h
|
||||||
|
* @brief EDC 通讯协议 — I2C / UART 双通道, Cycle 收发, CRC16 校验
|
||||||
|
*
|
||||||
|
* 顶层概念:
|
||||||
|
* Point = { x, y, angle_x10 }
|
||||||
|
* Fragment = { source: Point, target: Point }
|
||||||
|
* Cycle = { cycle_id, frag_count (2~4), fragments[] }
|
||||||
|
*
|
||||||
|
* 帧格式 (大端序):
|
||||||
|
* ┌──────┬──────┬──────┬──────┬───────────────────┬──────────┐
|
||||||
|
* │ SOF1 │ SOF2 │ CMD │ LEN │ DATA (N bytes) │ CRC16 │
|
||||||
|
* │ 0xAA │ 0x55 │ 1B │ 1B │ variable │ 2B (LE) │
|
||||||
|
* └──────┴──────┴──────┴──────┴───────────────────┴──────────┘
|
||||||
|
* ◄──────── CRC 计算范围 (CMD + LEN + DATA) ─────────►
|
||||||
|
*
|
||||||
|
* Cycle 数据帧 DATA 区:
|
||||||
|
* cycle_id(2B, BE) + frag_count(1B, 2~4) + fragments(N × 12B)
|
||||||
|
*
|
||||||
|
* Fragment (12B, 大端):
|
||||||
|
* src.x(2B) src.y(2B) src.angle(2B) dst.x(2B) dst.y(2B) dst.angle(2B)
|
||||||
|
*
|
||||||
|
* 交互流程:
|
||||||
|
* MPU ──[CYCLE_DATA]──► MCU (I2C + UART 冗余下发)
|
||||||
|
* MCU ──[CYCLE_DONE]──► MPU (UART 上报完成, 含 cycle_id)
|
||||||
|
* MCU ──[CYCLE_REQ]───► MPU (UART 请求下一 Cycle)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef EDC_PROTOCOL_H
|
||||||
|
#define EDC_PROTOCOL_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
#include "driver/i2c_slave.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 协议常量
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
#define EDC_PROTO_SOF1 0xAA
|
||||||
|
#define EDC_PROTO_SOF2 0x55
|
||||||
|
|
||||||
|
#define EDC_PROTO_FRAME_MAX 80 /* 最大帧长: 2+1+1+3+4*12+2 = 57 < 80 */
|
||||||
|
#define EDC_PROTO_FRAME_MIN 6 /* SOF(2) + CMD(1) + LEN(1) + CRC(2) */
|
||||||
|
|
||||||
|
#define EDC_FRAG_MIN 2 /* 每 Cycle 最少 fragment 数 */
|
||||||
|
#define EDC_FRAG_MAX 4 /* 每 Cycle 最多 fragment 数 */
|
||||||
|
#define EDC_FRAG_BYTES 12 /* 单个 fragment 字节数 (3×uint16×2) */
|
||||||
|
|
||||||
|
/* ---- I2C 默认 ---- */
|
||||||
|
#define EDC_I2C_PORT_DEFAULT 0
|
||||||
|
#define EDC_I2C_SLAVE_ADDR 0x2A
|
||||||
|
#define EDC_I2C_SDA_DEFAULT GPIO_NUM_2
|
||||||
|
#define EDC_I2C_SCL_DEFAULT GPIO_NUM_3
|
||||||
|
#define EDC_I2C_BUF_SIZE 256
|
||||||
|
|
||||||
|
/* ---- UART 默认 ---- */
|
||||||
|
#define EDC_UART_PORT_DEFAULT UART_NUM_0
|
||||||
|
#define EDC_UART_BAUDRATE 115200
|
||||||
|
#define EDC_UART_RX_DEFAULT GPIO_NUM_9
|
||||||
|
#define EDC_UART_TX_DEFAULT GPIO_NUM_8
|
||||||
|
#define EDC_UART_BUF_SIZE 512
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 命令字
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
EDC_CMD_CYCLE_DATA = 0x01, /* MPU → MCU: 下发 Cycle 数据 */
|
||||||
|
EDC_CMD_CYCLE_DONE = 0x02, /* MCU → MPU: Cycle 执行完成 */
|
||||||
|
EDC_CMD_CYCLE_REQUEST = 0x03, /* MCU → MPU: 请求下一个 Cycle */
|
||||||
|
EDC_CMD_ACK = 0x80, /* 通用应答 */
|
||||||
|
EDC_CMD_NACK = 0xFF, /* 错误 */
|
||||||
|
} edc_cmd_t;
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 数据结构
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
/** 单个坐标点 (source 或 target) */
|
||||||
|
typedef struct {
|
||||||
|
uint16_t x;
|
||||||
|
uint16_t y;
|
||||||
|
uint16_t angle_x10; /* 角度 × 10, 如 450 = 45.0° */
|
||||||
|
} edc_point_t;
|
||||||
|
|
||||||
|
/** 一个运动片段: 从 source 移动到 target */
|
||||||
|
typedef struct {
|
||||||
|
edc_point_t source;
|
||||||
|
edc_point_t target;
|
||||||
|
} edc_fragment_t;
|
||||||
|
|
||||||
|
/** 一个 Cycle: 包含 2~4 个 fragment */
|
||||||
|
typedef struct {
|
||||||
|
uint16_t cycle_id;
|
||||||
|
uint8_t frag_count; /* 实际 fragment 数 (2~4) */
|
||||||
|
edc_fragment_t fragments[EDC_FRAG_MAX];
|
||||||
|
uint32_t timestamp_ms; /* 本地时间戳 (不随帧传输) */
|
||||||
|
} edc_cycle_t;
|
||||||
|
|
||||||
|
/** 帧来源通道 */
|
||||||
|
typedef enum {
|
||||||
|
EDC_CH_NONE = 0,
|
||||||
|
EDC_CH_I2C = 1,
|
||||||
|
EDC_CH_UART = 2,
|
||||||
|
} edc_channel_t;
|
||||||
|
|
||||||
|
/** 每通道统计 */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t cycles_ok;
|
||||||
|
uint32_t cycles_err;
|
||||||
|
uint32_t bytes_total;
|
||||||
|
} edc_channel_stat_t;
|
||||||
|
|
||||||
|
/** 统一收发统计 */
|
||||||
|
typedef struct {
|
||||||
|
edc_channel_stat_t i2c;
|
||||||
|
edc_channel_stat_t uart;
|
||||||
|
edc_channel_t last_channel;
|
||||||
|
uint32_t last_tick;
|
||||||
|
uint32_t done_sent; /* 已发送 DONE 次数 */
|
||||||
|
uint32_t request_sent; /* 已发送 REQUEST 次数 */
|
||||||
|
} edc_proto_stat_t;
|
||||||
|
|
||||||
|
/** 帧解析状态 */
|
||||||
|
typedef enum {
|
||||||
|
EDC_PARSE_OK = 0,
|
||||||
|
EDC_PARSE_WAITING = 1,
|
||||||
|
EDC_PARSE_CRC_ERR = -1,
|
||||||
|
EDC_PARSE_LEN_ERR = -2,
|
||||||
|
EDC_PARSE_SOF_ERR = -3,
|
||||||
|
EDC_PARSE_CMD_ERR = -4,
|
||||||
|
} edc_parse_result_t;
|
||||||
|
|
||||||
|
/** 帧解析上下文 (逐字节喂入) */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t buf[EDC_PROTO_FRAME_MAX];
|
||||||
|
uint8_t len;
|
||||||
|
uint8_t idx;
|
||||||
|
edc_parse_result_t state;
|
||||||
|
} edc_frame_ctx_t;
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 配置
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool i2c_enable;
|
||||||
|
int i2c_port;
|
||||||
|
uint16_t i2c_addr;
|
||||||
|
gpio_num_t i2c_sda;
|
||||||
|
gpio_num_t i2c_scl;
|
||||||
|
|
||||||
|
bool uart_enable;
|
||||||
|
uart_port_t uart_port;
|
||||||
|
int uart_baudrate;
|
||||||
|
gpio_num_t uart_rx;
|
||||||
|
gpio_num_t uart_tx;
|
||||||
|
} edc_proto_config_t;
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* CRC16 / 组帧 / 逐字节解析 (底层)
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
uint16_t edc_crc16(const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
/** 将 Cycle 序列化为协议字节流 (含帧头与 CRC) */
|
||||||
|
int edc_cycle_pack(const edc_cycle_t *cycle, uint8_t *out);
|
||||||
|
|
||||||
|
/** 逐字节解析器 (与旧版兼容, 内部使用 edc_cycle_parse_byte) */
|
||||||
|
edc_parse_result_t edc_cycle_parse_byte(edc_frame_ctx_t *ctx, uint8_t byte,
|
||||||
|
edc_cycle_t *cycle);
|
||||||
|
|
||||||
|
/** 从完整缓冲区一次性解析 Cycle */
|
||||||
|
edc_parse_result_t edc_cycle_parse_buf(const uint8_t *data, size_t len,
|
||||||
|
edc_cycle_t *cycle);
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* 统一收发器 API (I2C 从机 + UART 双向)
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化协议栈 (I2C 从机 + UART)
|
||||||
|
* @param cfg 传 NULL 使用默认值
|
||||||
|
*/
|
||||||
|
void edc_proto_init(const edc_proto_config_t *cfg);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 轮询双通道接收 + 返回最新 Cycle (非阻塞)
|
||||||
|
* @return >0: 有新 Cycle; 0: 无; <0: 错误
|
||||||
|
*/
|
||||||
|
int edc_proto_poll(edc_cycle_t *cycle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 发送 CYCLE_DONE (通过 UART)
|
||||||
|
* @param cycle_id 已完成的 Cycle ID
|
||||||
|
*/
|
||||||
|
void edc_proto_send_done(uint16_t cycle_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 发送 CYCLE_REQUEST (通过 UART)
|
||||||
|
*/
|
||||||
|
void edc_proto_send_request(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取统计信息
|
||||||
|
*/
|
||||||
|
void edc_proto_get_stats(edc_proto_stat_t *stat);
|
||||||
|
|
||||||
|
/* ---- FreeRTOS 任务 + 回调 ---- */
|
||||||
|
|
||||||
|
void edc_proto_task(void *pvParam);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cycle 回调 (弱符号, 用户实现)
|
||||||
|
* @param cycle 已校验的有效 Cycle
|
||||||
|
* @param ch 来源通道
|
||||||
|
*/
|
||||||
|
void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* EDC_PROTOCOL_H */
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
idf_component_register(SRCS "servo.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES esp_driver_ledc esp_driver_gpio driver)
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* @file servo.c
|
||||||
|
* @brief 180° PWM 舵机控制实现 (LEDC, 50Hz, 0.5~2.5ms)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "servo.h"
|
||||||
|
#include "driver/ledc.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
#define SERVO_FREQ_HZ 50
|
||||||
|
#define SERVO_TIMER LEDC_TIMER_0
|
||||||
|
#define SERVO_MODE LEDC_LOW_SPEED_MODE
|
||||||
|
#define SERVO_DUTY_RES LEDC_TIMER_16_BIT /* 0~65535 */
|
||||||
|
#define SERVO_MAX_DUTY 65535
|
||||||
|
|
||||||
|
/* 脉宽范围 (微秒) */
|
||||||
|
#define PULSE_MIN_US 500
|
||||||
|
#define PULSE_MAX_US 2500
|
||||||
|
#define PERIOD_US 20000 /* 50Hz = 20ms */
|
||||||
|
|
||||||
|
static const char *TAG = "SERVO";
|
||||||
|
static ledc_channel_t s_channel;
|
||||||
|
static float s_limit_up = 180.0f;
|
||||||
|
static float s_limit_down = 0.0f;
|
||||||
|
|
||||||
|
/* 角度 → 占空比 */
|
||||||
|
static uint32_t angle_to_duty(float angle)
|
||||||
|
{
|
||||||
|
if (angle < 0.0f) angle = 0.0f;
|
||||||
|
if (angle > 180.0f) angle = 180.0f;
|
||||||
|
|
||||||
|
float pulse_us = PULSE_MIN_US + (angle / 180.0f) * (PULSE_MAX_US - PULSE_MIN_US);
|
||||||
|
return (uint32_t)((pulse_us / PERIOD_US) * SERVO_MAX_DUTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void servo_init(int gpio)
|
||||||
|
{
|
||||||
|
/* 分配 LEDC 通道 */
|
||||||
|
static int chan_idx = 0;
|
||||||
|
s_channel = (chan_idx++ == 0) ? LEDC_CHANNEL_0 : LEDC_CHANNEL_1;
|
||||||
|
|
||||||
|
ledc_timer_config_t timer_cfg = {
|
||||||
|
.speed_mode = SERVO_MODE,
|
||||||
|
.duty_resolution = SERVO_DUTY_RES,
|
||||||
|
.timer_num = SERVO_TIMER,
|
||||||
|
.freq_hz = SERVO_FREQ_HZ,
|
||||||
|
.clk_cfg = LEDC_AUTO_CLK,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(ledc_timer_config(&timer_cfg));
|
||||||
|
|
||||||
|
ledc_channel_config_t chan_cfg = {
|
||||||
|
.gpio_num = gpio,
|
||||||
|
.speed_mode = SERVO_MODE,
|
||||||
|
.channel = s_channel,
|
||||||
|
.intr_type = LEDC_INTR_DISABLE,
|
||||||
|
.timer_sel = SERVO_TIMER,
|
||||||
|
.duty = angle_to_duty(0), /* 初始: 0° */
|
||||||
|
.hpoint = 0,
|
||||||
|
.flags.output_invert = 0,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(ledc_channel_config(&chan_cfg));
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "初始化完成 (GPIO%d, 50Hz, 通道%d)", gpio, s_channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void servo_set_angle(float angle)
|
||||||
|
{
|
||||||
|
uint32_t duty = angle_to_duty(angle);
|
||||||
|
ESP_ERROR_CHECK(ledc_set_duty(SERVO_MODE, s_channel, duty));
|
||||||
|
ESP_ERROR_CHECK(ledc_update_duty(SERVO_MODE, s_channel));
|
||||||
|
ESP_LOGD(TAG, "角度: %.1f° (duty=%lu)", angle, duty);
|
||||||
|
}
|
||||||
|
|
||||||
|
void servo_set_up(void)
|
||||||
|
{
|
||||||
|
servo_set_angle(s_limit_up);
|
||||||
|
}
|
||||||
|
|
||||||
|
void servo_set_down(void)
|
||||||
|
{
|
||||||
|
servo_set_angle(s_limit_down);
|
||||||
|
}
|
||||||
|
|
||||||
|
void servo_set_limits(float up, float down)
|
||||||
|
{
|
||||||
|
s_limit_up = up;
|
||||||
|
s_limit_down = down;
|
||||||
|
ESP_LOGI(TAG, "限位: 上=%.0f° 下=%.0f°", up, down);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* @file servo.h
|
||||||
|
* @brief 180° PWM 舵机控制 (LEDC)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SERVO_H
|
||||||
|
#define SERVO_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化舵机 (50Hz PWM)
|
||||||
|
* @param gpio PWM 输出引脚
|
||||||
|
*/
|
||||||
|
void servo_init(int gpio);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置舵机角度
|
||||||
|
* @param angle 角度 0.0 ~ 180.0
|
||||||
|
*/
|
||||||
|
void servo_set_angle(float angle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置舵机到上限位 (默认 180°)
|
||||||
|
*/
|
||||||
|
void servo_set_up(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置舵机到下限位 (默认 0°)
|
||||||
|
*/
|
||||||
|
void servo_set_down(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 自定义上下限位角度
|
||||||
|
* @param up 上限位角度 (默认 180°)
|
||||||
|
* @param down 下限位角度 (默认 0°)
|
||||||
|
*/
|
||||||
|
void servo_set_limits(float up, float down);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* SERVO_H */
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
idf_component_register(SRCS "web_ctrl.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES emm_v5 magnet servo esp_http_server esp_wifi nvs_flash)
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
/**
|
||||||
|
* @file web_ctrl.c
|
||||||
|
* @brief WiFi STA + HTTP 服务器 + 网页控制面板
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "web_ctrl.h"
|
||||||
|
#include "emm_v5.h"
|
||||||
|
#include "magnet.h"
|
||||||
|
#include "servo.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#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(<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>EDC 步进电机控制</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#f6f7f9;--card:#fff;--border:#e6e8ec;--text:#1f2329;--sub:#8a8f99;
|
||||||
|
--accent:#4f7cff;--accent-bg:#eef3ff;
|
||||||
|
--green:#34c38f;--green-bg:#e8f8f2;
|
||||||
|
--red:#f66;--red-bg:#feefef;
|
||||||
|
--amber:#f7b84b;--amber-bg:#fef6e8;
|
||||||
|
--radius:12px;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:system-ui,-apple-system,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased}
|
||||||
|
|
||||||
|
/* ---- 顶栏 ---- */
|
||||||
|
.header{background:var(--card);border-bottom:1px solid var(--border);padding:0 clamp(16px,4vw,32px);height:60px;display:flex;align-items:center;justify-content:space-between;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{font-size:1.05rem;font-weight:600;letter-spacing:.02em;display:flex;align-items:center;gap:10px}
|
||||||
|
.header h1::before{content:'';width:10px;height:10px;border-radius:3px;background:var(--accent)}
|
||||||
|
.status-bar{display:flex;gap:8px;font-size:.8rem;color:var(--sub)}
|
||||||
|
.status-bar span{display:flex;align-items:center;gap:6px;background:var(--bg);border:1px solid var(--border);padding:5px 12px;border-radius:999px;white-space:nowrap}
|
||||||
|
.dot{width:7px;height:7px;border-radius:50%;background:var(--green)}
|
||||||
|
|
||||||
|
/* ---- 主布局 ---- */
|
||||||
|
.container{width:100%;max-width:1200px;margin:0 auto;padding:clamp(14px,3vw,28px) clamp(16px,4vw,32px) 40px}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:14px}
|
||||||
|
|
||||||
|
/* ---- 卡片 ---- */
|
||||||
|
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:20px}
|
||||||
|
.card h2{font-size:.95rem;font-weight:600;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
|
||||||
|
.card h2 .tag{width:8px;height:8px;border-radius:2px;background:var(--accent)}
|
||||||
|
.card h2 .tag.green{background:var(--green)}
|
||||||
|
.card h2 .tag.amber{background:var(--amber)}
|
||||||
|
|
||||||
|
.row{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;margin-bottom:10px}
|
||||||
|
.row label{min-width:52px;font-size:.82rem;color:var(--sub);font-weight:500}
|
||||||
|
input,select{flex:1;min-width:80px;padding:8px 12px;border-radius:8px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:.88rem;transition:border-color .15s,background .15s}
|
||||||
|
input:focus,select:focus{border-color:var(--accent);background:var(--card);outline:none}
|
||||||
|
input[type=range]{flex:2;padding:0;border:none;background:transparent;accent-color:var(--accent)}
|
||||||
|
.unit{font-size:.78rem;color:var(--sub)}
|
||||||
|
.servo-val{color:var(--accent);font-weight:600;min-width:44px;text-align:center;font-variant-numeric:tabular-nums}
|
||||||
|
|
||||||
|
/* ---- 按钮 ---- */
|
||||||
|
.btn-row{display:flex;gap:8px;margin-top:14px}
|
||||||
|
button{flex:1;padding:10px 8px;border:none;border-radius:8px;font-size:.86rem;font-weight:600;cursor:pointer;transition:opacity .15s;white-space:nowrap}
|
||||||
|
button:hover{opacity:.85}
|
||||||
|
button:active{opacity:.7}
|
||||||
|
.btn-on{background:var(--green-bg);color:#0f9d6e}
|
||||||
|
.btn-off{background:var(--red-bg);color:#e04f4f}
|
||||||
|
.btn-go{background:var(--accent);color:#fff}
|
||||||
|
.btn-stop{background:var(--amber-bg);color:#d99413}
|
||||||
|
.btn-block{width:100%;margin-top:14px}
|
||||||
|
|
||||||
|
/* ---- 日志 ---- */
|
||||||
|
.log-card{margin-top:14px}
|
||||||
|
.log{font-family:'Cascadia Code','Fira Code',Consolas,monospace;font-size:.78rem;max-height:180px;overflow-y:auto;color:var(--sub);line-height:1.8}
|
||||||
|
.log .ok{color:var(--text)}
|
||||||
|
.log .err{color:#e04f4f}
|
||||||
|
|
||||||
|
@media(max-width:560px){
|
||||||
|
.header{height:auto;padding:12px 16px;flex-direction:column;align-items:flex-start;gap:8px}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>EDC 步进电机控制</h1>
|
||||||
|
<div class="status-bar">
|
||||||
|
<span id="wifi"><i class="dot"></i>WiFi: --</span>
|
||||||
|
<span id="ip">IP: --</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2><i class="tag"></i>电机使能</h2>
|
||||||
|
<div class="row"><label>地址</label><input id="en_addr" type="number" value="1" min="1" max="247"></div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn-on" onclick="api('enable?addr='+v('en_addr')+'&state=1')">使能</button>
|
||||||
|
<button class="btn-off" onclick="api('enable?addr='+v('en_addr')+'&state=0')">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<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><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_acc" type="number" value="10" min="0" max="255"></div>
|
||||||
|
<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-stop" onclick="api('stop?addr='+v('v_addr'))">停止</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<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><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_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><select id="p_raf"><option value="0">相对运动</option><option value="1">绝对运动</option></select></div>
|
||||||
|
<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-stop" onclick="api('stop?addr='+v('p_addr'))">停止</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2><i class="tag green"></i>舵机控制</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label>角度</label>
|
||||||
|
<input id="s_angle" type="range" value="90" min="0" max="180" step="1" oninput="document.getElementById('s_val').textContent=this.value+'°'">
|
||||||
|
<span class="servo-val" id="s_val">90°</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn-go btn-block" onclick="api('servo?angle='+v('s_angle'))">转到指定角度</button>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn-on" onclick="api('servo?pos=up')">上限</button>
|
||||||
|
<button class="btn-off" onclick="api('servo?pos=down')">下限</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2><i class="tag amber"></i>电磁铁</h2>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn-on" onclick="api('magnet?state=1')">吸合</button>
|
||||||
|
<button class="btn-off" onclick="api('magnet?state=0')">释放</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card log-card">
|
||||||
|
<h2><i class="tag green"></i>运行日志</h2>
|
||||||
|
<div class="log" id="log">等待指令...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function v(id){return document.getElementById(id).value}
|
||||||
|
function api(cmd){
|
||||||
|
fetch('/api/'+cmd).then(r=>r.text()).then(t=>{
|
||||||
|
var l=document.getElementById('log');
|
||||||
|
l.innerHTML='<div class="ok">'+t+'</div>'+l.innerHTML;
|
||||||
|
while(l.children.length>50)l.removeChild(l.lastChild)
|
||||||
|
}).catch(function(e){
|
||||||
|
var l=document.getElementById('log');
|
||||||
|
l.innerHTML='<div class="err">请求失败</div>'+l.innerHTML
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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('ip').textContent='IP: '+d.ip
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>)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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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);
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "HTTP 服务器已启动 (端口 %d)", cfg.server_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 公开接口
|
||||||
|
* ============================================================ */
|
||||||
|
|
||||||
|
void web_ctrl_init(void)
|
||||||
|
{
|
||||||
|
wifi_init_sta();
|
||||||
|
http_server_init();
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* @file web_ctrl.h
|
||||||
|
* @brief WiFi + HTTP 网页控制步进电机
|
||||||
|
*
|
||||||
|
* 连接 WiFi "ICE" (密码 12345678), 启动 HTTP 服务器,
|
||||||
|
* 通过网页控制 Emm_V5 步进电机。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef WEB_CTRL_H
|
||||||
|
#define WEB_CTRL_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化 WiFi (STA 模式) 并启动 HTTP 服务器
|
||||||
|
* @note 阻塞直到 WiFi 连接成功, 然后启动 HTTP 服务器
|
||||||
|
*/
|
||||||
|
void web_ctrl_init(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* WEB_CTRL_H */
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
idf_component_register(SRCS "main.c"
|
idf_component_register(SRCS "main.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
PRIV_REQUIRES emm_v5)
|
PRIV_REQUIRES emm_v5 magnet servo web_ctrl)
|
||||||
+18
-31
@@ -1,56 +1,43 @@
|
|||||||
/**
|
/**
|
||||||
* @file main.c
|
* @file main.c
|
||||||
* @brief Emm_V5.0 步进闭环电机驱动演示 (ESP-IDF)
|
* @brief EDC 步进电机网页控制 (ESP-IDF)
|
||||||
*
|
*
|
||||||
* 硬件接线:
|
* 硬件接线:
|
||||||
* ESP32 TX (GPIO6) -> 电机 RX
|
* ESP32 TX (GPIO6) -> 电机 RX
|
||||||
* ESP32 RX (GPIO7) -> 电机 TX
|
* ESP32 RX (GPIO7) -> 电机 TX
|
||||||
|
* ESP32 GPIO5 -> 电磁铁
|
||||||
|
* ESP32 GPIO4 -> 舵机
|
||||||
* ESP32 GND -> 电机 GND
|
* ESP32 GND -> 电机 GND
|
||||||
|
*
|
||||||
|
* WiFi: 连接 ICE (密码 12345678), 获取 IP 后用浏览器打开控制页面
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "emm_v5.h"
|
#include "emm_v5.h"
|
||||||
|
#include "magnet.h"
|
||||||
|
#include "servo.h"
|
||||||
|
#include "web_ctrl.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
|
||||||
void app_main(void)
|
void app_main(void)
|
||||||
{
|
{
|
||||||
uint8_t rx_buf[128] = {0};
|
/* 初始化电机驱动 (UART1, TX=GPIO6, RX=GPIO7, LED=GPIO2) */
|
||||||
uint8_t rx_len;
|
|
||||||
|
|
||||||
/* 初始化驱动 (UART1, TX=GPIO6, RX=GPIO7, LED=GPIO2) */
|
|
||||||
Emm_V5_Init(UART_NUM_1, GPIO_NUM_6, GPIO_NUM_7);
|
Emm_V5_Init(UART_NUM_1, GPIO_NUM_6, GPIO_NUM_7);
|
||||||
|
|
||||||
|
/* 初始化电磁铁 (GPIO5) */
|
||||||
|
magnet_init(GPIO_NUM_5);
|
||||||
|
|
||||||
|
/* 初始化舵机 (GPIO4, 上限180° 下限0°) */
|
||||||
|
servo_init(GPIO_NUM_4);
|
||||||
|
|
||||||
/* 上电延时 2 秒, 等待电机闭环初始化完毕 */
|
/* 上电延时 2 秒, 等待电机闭环初始化完毕 */
|
||||||
ESP_LOGI("MAIN", "等待电机初始化...");
|
ESP_LOGI("MAIN", "等待电机初始化...");
|
||||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||||
|
|
||||||
/* ---- 速度模式演示 ---- */
|
/* 启动 WiFi + HTTP 网页控制服务器 */
|
||||||
ESP_LOGI("MAIN", "发送速度控制命令: 地址1, CW, 1000RPM, 加速度10");
|
web_ctrl_init();
|
||||||
Emm_V5_Vel_Control(1, 0, 100, 10, false);
|
|
||||||
|
|
||||||
/* 接收电机返回的数据 */
|
ESP_LOGI("MAIN", "系统就绪, 等待网页指令...");
|
||||||
rx_len = Emm_V5_Receive_Data(rx_buf, sizeof(rx_buf));
|
|
||||||
|
|
||||||
/* 验证校验字节 (0x6B), 点亮/熄灭 LED */
|
|
||||||
if (rx_len > 0 && rx_buf[rx_len - 1] == 0x6B) {
|
|
||||||
gpio_set_level(EMM_V5_LED_GPIO, 1);
|
|
||||||
ESP_LOGI("MAIN", "通信正常 (校验通过, LED亮)");
|
|
||||||
} else {
|
|
||||||
gpio_set_level(EMM_V5_LED_GPIO, 0);
|
|
||||||
ESP_LOGW("MAIN", "通信异常 (校验失败, LED灭)");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 打印返回的原始数据 (调试用, 每个字节+1 避免与电机地址冲突) */
|
|
||||||
ESP_LOGI("MAIN", "返回数据长度: %d", rx_len);
|
|
||||||
if (rx_len > 0) {
|
|
||||||
printf("返回数据(+1偏移): ");
|
|
||||||
for (int i = 0; i < rx_len; i++) {
|
|
||||||
printf("%02X ", rx_buf[i] + 1);
|
|
||||||
}
|
|
||||||
printf("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI("MAIN", "演示完成, 程序挂起");
|
|
||||||
while (1) {
|
while (1) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
# 4MB Flash 自定义分区表
|
||||||
|
nvs, data, nvs, 0x9000, 0x6000,
|
||||||
|
phy_init, data, phy, 0xf000, 0x1000,
|
||||||
|
factory, app, factory, 0x10000, 0x3E0000,
|
||||||
|
Reference in new issue
Block a user