1059 lines
42 KiB
C
1059 lines
42 KiB
C
/**
|
|
* @file web_ctrl.c
|
|
* @brief WiFi STA + HTTP 服务器 + 网页控制面板
|
|
*/
|
|
|
|
#include "web_ctrl.h"
|
|
#include "emm_v5.h"
|
|
#include "magnet.h"
|
|
#include "servo.h"
|
|
#include "protocol.h"
|
|
#include "actuator.h"
|
|
#include "rgb.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"
|
|
|
|
#ifdef EDC_DEBUG_WEB
|
|
|
|
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 green"></i>轴控制 (XYZA)</h2>
|
|
<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">
|
|
<button class="btn-go" onclick="axisMove()">移动到目标</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|2|3|4"></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 class="card">
|
|
<h2><i class="tag"></i>速度模式</h2>
|
|
<div class="row"><label>地址</label><input id="v_addr" type="text" value="1" placeholder="1|2|3|4"></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="apiEach('velocity','v_addr','&dir='+v('v_dir')+'&vel='+v('v_vel')+'&acc='+v('v_acc'))">启动</button>
|
|
<button class="btn-stop" onclick="apiEach('stop','v_addr','')">停止</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2><i class="tag"></i>位置模式</h2>
|
|
<div class="row"><label>地址</label><input id="p_addr" type="text" value="1" placeholder="1|2|3|4"></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="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="apiEach('stop','p_addr','')">停止</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2><i class="tag"></i>角度控制</h2>
|
|
<div class="row"><label>地址</label><input id="ag_addr" type="text" value="1" placeholder="1|2|3|4"></div>
|
|
<div class="row"><label>角度</label><input id="ag_angle" type="number" value="0" step="0.1"><span class="unit">°</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="0">相对运动</option><option value="1">绝对运动</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>
|
|
<button class="btn-off btn-block" onclick="apiEach('reset_pos','ag_addr','')">当前位置清零</button>
|
|
</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 class="card">
|
|
<h2><i class="tag"></i>读取参数</h2>
|
|
<div class="row"><label>地址</label><input id="rp_addr" type="text" value="1" placeholder="1"></div>
|
|
<div class="row"><label>参数</label><select id="rp_param"><option value="0">固件版本</option><option value="3">总线电压</option><option value="5">相电流</option><option value="7">编码器值</option><option value="8">目标位置</option><option value="9">实时转速</option><option value="10">实时位置</option><option value="11">位置误差</option><option value="13">状态标志</option><option value="16">回零状态</option></select></div>
|
|
<button class="btn-go btn-block" onclick="readParam()">读取</button>
|
|
<div id="rp_out" style="font-size:.82rem;margin-top:8px;word-break:break-all"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2><i class="tag green"></i>回零校准 (Origin)</h2>
|
|
<div class="row"><label>地址</label><input id="og_addr" type="text" value="2" placeholder="1|2|3|4"></div>
|
|
<div class="row"><label>模式</label><select id="og_omode"><option value="0">单圈就近</option><option value="1">单圈方向</option><option value="2" selected>多圈碰撞堵转</option><option value="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="1" 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="1" max="5000"><span class="unit">RPM</span></div>
|
|
<div class="row"><label>碰撞电流</label><input id="og_slma" type="number" value="500" min="1" max="5000"><span class="unit">mA</span></div>
|
|
<div class="row"><label>碰撞时间</label><input id="og_slms" type="number" value="100" min="1" max="5000"><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="apiEach('origin_set','og_addr','&svf='+v('og_svf'))">设零点</button>
|
|
<button class="btn-on" onclick="apiEach('origin_config','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="apiEach('origin_go','og_addr','&omode='+v('og_omode'))">触发回零</button>
|
|
<button class="btn-stop" onclick="apiEach('origin_stop','og_addr','')">中断回零</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="card" id="proto-card">
|
|
<h2><i class="tag amber"></i>通讯协议</h2>
|
|
<div class="row" style="font-size:.82rem">
|
|
<span>I2C: <b id="pi2c">-</b></span>
|
|
<span>UART: <b id="puart">-</b></span>
|
|
<span>通道: <b id="pch">-</b></span>
|
|
</div>
|
|
<div id="pcycle" style="font-size:.78rem;color:var(--sub);margin-top:6px">等待 Cycle...</div>
|
|
<div class="btn-row" style="margin-top:10px">
|
|
<button class="btn-go" onclick="var c=document.getElementById('pcid');api('proto/done?id='+(c?c.value:0))">Done</button>
|
|
<button class="btn-stop" onclick="api('proto/request')">Request</button>
|
|
</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
|
|
})
|
|
}
|
|
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');
|
|
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+'¶m='+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){
|
|
document.getElementById('wifi').innerHTML='<i class="dot"></i>WiFi: '+d.ssid;
|
|
document.getElementById('ip').textContent='IP: '+d.ip
|
|
})
|
|
/* 每秒刷新协议状态 */
|
|
function pollProto(){
|
|
fetch('/api/proto/status').then(function(r){return r.json()}).then(function(d){
|
|
document.getElementById('pi2c').textContent = d.i2c_ok + '/' + (d.i2c_ok+d.i2c_err);
|
|
document.getElementById('puart').textContent = d.uart_ok + '/' + (d.uart_ok+d.uart_err);
|
|
document.getElementById('pch').textContent = d.last_ch;
|
|
if(d.cycle){
|
|
document.getElementById('pcid').value = d.cycle.id;
|
|
var html = 'Cycle #'+d.cycle.id + ' ('+d.cycle.frags+' frags)<br>';
|
|
for(var i=0;i<d.cycle.data.length;i++){
|
|
var f=d.cycle.data[i];
|
|
html += 'F'+i+': ('+f.sx+','+f.sy+' '+f.sa+'°) → ('+f.tx+','+f.ty+' '+f.ta+'°)<br>';
|
|
}
|
|
document.getElementById('pcycle').innerHTML = html;
|
|
}
|
|
}).catch(function(){});
|
|
}
|
|
/* 隐藏的 cycle_id 存储 */
|
|
(function(){
|
|
var inp=document.createElement('input');
|
|
inp.type='hidden';inp.id='pcid';inp.value='0';
|
|
document.getElementById('proto-card').appendChild(inp);
|
|
})();
|
|
pollProto();setInterval(pollProto,1000);
|
|
</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;
|
|
}
|
|
|
|
static float get_query_float(httpd_req_t *req, const char *key, float def)
|
|
{
|
|
char buf[128];
|
|
size_t len = httpd_req_get_url_query_len(req) + 1;
|
|
if (len > sizeof(buf)) len = sizeof(buf);
|
|
if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) {
|
|
char val[16];
|
|
if (httpd_query_key_value(buf, key, val, sizeof(val)) == ESP_OK) {
|
|
return (float)atof(val);
|
|
}
|
|
}
|
|
return def;
|
|
}
|
|
|
|
/* ============================================================
|
|
* HTTP 处理器
|
|
* ============================================================ */
|
|
|
|
static esp_err_t root_handler(httpd_req_t *req)
|
|
{
|
|
const char *html = INDEX_HTML;
|
|
size_t total = strlen(html);
|
|
size_t offset = 0;
|
|
|
|
httpd_resp_set_type(req, "text/html; charset=utf-8");
|
|
httpd_resp_set_hdr(req, "Connection", "close");
|
|
|
|
while (offset < total) {
|
|
size_t chunk = total - offset;
|
|
if (chunk > 1024) chunk = 1024;
|
|
esp_err_t err = httpd_resp_send_chunk(req, html + offset, (int)chunk);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "发送 HTML 失败: %d", err);
|
|
return err;
|
|
}
|
|
offset += chunk;
|
|
}
|
|
httpd_resp_send_chunk(req, NULL, 0); /* 结束分块 */
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_enable_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
int state = get_query_int(req, "state", 0);
|
|
Emm_V5_En_Control((uint8_t)addr, (bool)state, false);
|
|
char resp[64];
|
|
snprintf(resp, sizeof(resp), "电机 %d %s", addr, state ? "使能" : "关闭");
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_velocity_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
int dir = get_query_int(req, "dir", 0);
|
|
int vel = get_query_int(req, "vel", 100);
|
|
int acc = get_query_int(req, "acc", 10);
|
|
Emm_V5_Vel_Control((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc, false);
|
|
char resp[64];
|
|
snprintf(resp, sizeof(resp), "速度模式: 地址%d %s %dRPM acc=%d",
|
|
addr, dir ? "CCW" : "CW", vel, acc);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_position_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
int dir = get_query_int(req, "dir", 0);
|
|
int vel = get_query_int(req, "vel", 100);
|
|
int acc = get_query_int(req, "acc", 10);
|
|
int clk = get_query_int(req, "clk", 2000);
|
|
int raf = get_query_int(req, "raf", 0);
|
|
Emm_V5_Pos_Control((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc,
|
|
(uint32_t)clk, (bool)raf, false);
|
|
char resp[80];
|
|
snprintf(resp, sizeof(resp), "位置模式: 地址%d %s %dRPM %d脉冲 %s",
|
|
addr, dir ? "CCW" : "CW", vel, clk, raf ? "绝对" : "相对");
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_stop_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
Emm_V5_Stop_Now((uint8_t)addr, false);
|
|
char resp[32];
|
|
snprintf(resp, sizeof(resp), "电机 %d 已停止", addr);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_magnet_handler(httpd_req_t *req)
|
|
{
|
|
int state = get_query_int(req, "state", 0);
|
|
magnet_set((bool)state);
|
|
char resp[32];
|
|
snprintf(resp, sizeof(resp), "电磁铁 %s", state ? "吸合" : "释放");
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_status_handler(httpd_req_t *req)
|
|
{
|
|
char json[128];
|
|
wifi_ap_record_t ap;
|
|
esp_err_t err = esp_wifi_sta_get_ap_info(&ap);
|
|
|
|
snprintf(json, sizeof(json),
|
|
"{\"ssid\":\"%s\",\"ip\":\"%s\"}",
|
|
(err == ESP_OK) ? (char *)ap.ssid : "N/A",
|
|
s_ip_str);
|
|
httpd_resp_set_type(req, "application/json");
|
|
httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_servo_handler(httpd_req_t *req)
|
|
{
|
|
char buf[128];
|
|
size_t len = httpd_req_get_url_query_len(req) + 1;
|
|
if (len > sizeof(buf)) len = sizeof(buf);
|
|
|
|
if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) {
|
|
char val[16];
|
|
if (httpd_query_key_value(buf, "pos", val, sizeof(val)) == ESP_OK) {
|
|
if (strcmp(val, "up") == 0) {
|
|
servo_set_up();
|
|
} else {
|
|
servo_set_down();
|
|
}
|
|
char resp[32];
|
|
snprintf(resp, sizeof(resp), "舵机: %s", val);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
if (httpd_query_key_value(buf, "angle", val, sizeof(val)) == ESP_OK) {
|
|
float angle = atof(val);
|
|
servo_set_angle(angle);
|
|
char resp[32];
|
|
snprintf(resp, sizeof(resp), "舵机: %.1f°", (double)angle);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
}
|
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 angle 或 pos 参数");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
/* ---- 轴联动: XYZA 同时驱动 (mm + °) ---- */
|
|
static esp_err_t api_axis_handler(httpd_req_t *req)
|
|
{
|
|
float x_mm = get_query_float(req, "x", 0.0f);
|
|
float y_mm = get_query_float(req, "y", 0.0f);
|
|
float z_deg = get_query_float(req, "zrot", 0.0f);
|
|
float s_ang = get_query_float(req, "servo", -1.0f);
|
|
int vel = get_query_int(req, "vel", ACTUATOR_DEFAULT_VEL);
|
|
int acc = get_query_int(req, "acc", ACTUATOR_DEFAULT_ACC);
|
|
|
|
actuator_motor_mm(ACTUATOR_ADDR_X, x_mm, (uint16_t)vel, (uint8_t)acc, true);
|
|
actuator_motor_mm(ACTUATOR_ADDR_Y1, y_mm, (uint16_t)vel, (uint8_t)acc, true);
|
|
actuator_motor_angle(ACTUATOR_ADDR_ANGLE, z_deg, (uint16_t)vel, (uint8_t)acc, true);
|
|
if (s_ang >= 0.0f)
|
|
actuator_servo_set_angle(s_ang);
|
|
|
|
char resp[128];
|
|
snprintf(resp, sizeof(resp),
|
|
"轴联动: X=%.1fmm Y=%.1fmm Z=%.1f° Servo=%.1f° vel=%d acc=%d",
|
|
(double)x_mm, (double)y_mm, (double)z_deg, (double)s_ang, vel, acc);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ---- 直线距离控制: mm → 脉冲 ---- */
|
|
static esp_err_t api_mm_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 2);
|
|
float mm = get_query_float(req, "mm", 0.0f);
|
|
int vel = get_query_int(req, "vel", ACTUATOR_DEFAULT_VEL);
|
|
int acc = get_query_int(req, "acc", ACTUATOR_DEFAULT_ACC);
|
|
int raf = get_query_int(req, "raf", 1);
|
|
|
|
actuator_motor_mm((uint8_t)addr, mm, (uint16_t)vel, (uint8_t)acc, (bool)raf);
|
|
|
|
uint32_t clk = ACTUATOR_MM_TO_PULSES(mm);
|
|
char resp[96];
|
|
snprintf(resp, sizeof(resp),
|
|
"直线模式: 地址%d %.2fmm → %lu脉冲 %s vel=%dRPM acc=%d",
|
|
addr, (double)mm, (unsigned long)clk, raf ? "绝对" : "相对", vel, acc);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ---- 角度直控: ° → 脉冲 ---- */
|
|
static esp_err_t api_angle_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
float angle = get_query_float(req, "angle", 0.0f);
|
|
int vel = get_query_int(req, "vel", ACTUATOR_DEFAULT_VEL);
|
|
int acc = get_query_int(req, "acc", ACTUATOR_DEFAULT_ACC);
|
|
int raf = get_query_int(req, "raf", 1);
|
|
|
|
actuator_motor_angle((uint8_t)addr, angle, (uint16_t)vel, (uint8_t)acc, (bool)raf);
|
|
|
|
uint32_t clk = EMM_V5_DEG_TO_CLK(angle);
|
|
char resp[96];
|
|
snprintf(resp, sizeof(resp),
|
|
"角度模式: 地址%d %.1f° → %lu脉冲 %s vel=%dRPM acc=%d",
|
|
addr, (double)angle, (unsigned long)clk, raf ? "绝对" : "相对", vel, acc);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ---- 当前位置清零 ---- */
|
|
static esp_err_t api_reset_pos_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
Emm_V5_Reset_CurPos_To_Zero((uint8_t)addr);
|
|
char resp[32];
|
|
snprintf(resp, sizeof(resp), "电机 %d 位置已清零", addr);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ---- 解除堵转保护 ---- */
|
|
static esp_err_t api_reset_clog_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
Emm_V5_Reset_Clog_Pro((uint8_t)addr);
|
|
char resp[32];
|
|
snprintf(resp, sizeof(resp), "电机 %d 堵转保护已解除", addr);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ---- 读取系统参数 ---- */
|
|
static esp_err_t api_read_param_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
int param = get_query_int(req, "param", 0);
|
|
|
|
Emm_V5_Read_Sys_Params((uint8_t)addr, (EmmSysParams_t)param);
|
|
|
|
uint8_t rx[128];
|
|
uint8_t len = Emm_V5_Receive_Data(rx, sizeof(rx));
|
|
|
|
uint32_t val32 = 0;
|
|
uint16_t val16 = 0;
|
|
int sign = 0;
|
|
float angle = 0.0f;
|
|
|
|
if (len >= 8) {
|
|
sign = rx[2];
|
|
val32 = ((uint32_t)rx[3] << 24) | ((uint32_t)rx[4] << 16) |
|
|
((uint32_t)rx[5] << 8) | ((uint32_t)rx[6]);
|
|
uint16_t pos16 = (uint16_t)(val32 & 0xFFFF);
|
|
angle = (sign ? -1.0f : 1.0f) * pos16 * 360.0f / 65536.0f;
|
|
}
|
|
if (len >= 6) {
|
|
val16 = ((uint16_t)rx[3] << 8) | ((uint16_t)rx[4]);
|
|
}
|
|
|
|
char json[512];
|
|
int off = snprintf(json, sizeof(json),
|
|
"{\"addr\":%d,\"param\":%d,\"len\":%d,\"sign\":%d"
|
|
",\"val32\":%lu,\"val16\":%u,\"angle\":%.2f",
|
|
addr, param, len, sign,
|
|
(unsigned long)val32, (unsigned)val16, (double)angle);
|
|
off += snprintf(json + off, sizeof(json) - off, ",\"hex\":\"");
|
|
for (int i = 0; i < len && off < (int)sizeof(json) - 4; i++) {
|
|
off += snprintf(json + off, sizeof(json) - off, "%02X", rx[i]);
|
|
}
|
|
off += snprintf(json + off, sizeof(json) - off, "\"}");
|
|
httpd_resp_set_type(req, "application/json");
|
|
httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ---- 回零: 设置零点 ---- */
|
|
static esp_err_t api_origin_set_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
int svf = get_query_int(req, "svf", 0);
|
|
Emm_V5_Origin_Set_O((uint8_t)addr, (bool)svf);
|
|
char resp[48];
|
|
snprintf(resp, sizeof(resp), "电机 %d 零点已设置 (svF=%d)", addr, svf);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ---- 回零: 配置参数 ---- */
|
|
static esp_err_t api_origin_config_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
int svf = get_query_int(req, "svf", 0);
|
|
int omode = get_query_int(req, "omode", 2); /* 默认: 多圈无限位碰撞 */
|
|
int odir = get_query_int(req, "odir", 0);
|
|
int ovel = get_query_int(req, "ovel", 200);
|
|
int otm = get_query_int(req, "otm", 10000);
|
|
int slvel = get_query_int(req, "slvel", 100);
|
|
int slma = get_query_int(req, "slma", 500);
|
|
int slms = get_query_int(req, "slms", 100);
|
|
int potf = get_query_int(req, "potf", 0);
|
|
|
|
Emm_V5_Origin_Modify_Params((uint8_t)addr, (bool)svf,
|
|
(uint8_t)omode, (uint8_t)odir, (uint16_t)ovel, (uint32_t)otm,
|
|
(uint16_t)slvel, (uint16_t)slma, (uint16_t)slms, (bool)potf);
|
|
char resp[128];
|
|
snprintf(resp, sizeof(resp),
|
|
"电机 %d 回零参数已配置 (mode=%d dir=%d vel=%d tm=%lu svF=%d potF=%d)",
|
|
addr, omode, odir, ovel, (unsigned long)otm, svf, potf);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ---- 回零: 触发回零 ---- */
|
|
static esp_err_t api_origin_go_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
int omode = get_query_int(req, "omode", 2); /* 默认: 多圈无限位碰撞 */
|
|
Emm_V5_Origin_Trigger_Return((uint8_t)addr, (uint8_t)omode, false);
|
|
char resp[48];
|
|
snprintf(resp, sizeof(resp), "电机 %d 回零已触发 (mode=%d)", addr, omode);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ---- 回零: 强制中断 ---- */
|
|
static esp_err_t api_origin_stop_handler(httpd_req_t *req)
|
|
{
|
|
int addr = get_query_int(req, "addr", 1);
|
|
Emm_V5_Origin_Interrupt((uint8_t)addr);
|
|
char resp[32];
|
|
snprintf(resp, sizeof(resp), "电机 %d 回零已中断", addr);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* favicon — 204 No Content, 避免浏览器反复请求 */
|
|
static esp_err_t favicon_handler(httpd_req_t *req)
|
|
{
|
|
httpd_resp_set_status(req, "204 No Content");
|
|
httpd_resp_send(req, NULL, 0);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ============================================================
|
|
* 协议状态 API (从 actuator 读取最近 Cycle 快照)
|
|
* ============================================================ */
|
|
|
|
static esp_err_t api_proto_status_handler(httpd_req_t *req)
|
|
{
|
|
edc_proto_stat_t stat;
|
|
edc_proto_get_stats(&stat);
|
|
|
|
char json[1024];
|
|
int off = snprintf(json, sizeof(json),
|
|
"{\"i2c_ok\":%lu,\"i2c_err\":%lu,\"uart_ok\":%lu,\"uart_err\":%lu,"
|
|
"\"last_ch\":\"%s\",\"done\":%lu,\"req\":%lu",
|
|
stat.i2c.cycles_ok, stat.i2c.cycles_err,
|
|
stat.uart.cycles_ok, stat.uart.cycles_err,
|
|
(stat.last_channel == EDC_CH_I2C) ? "I2C" :
|
|
(stat.last_channel == EDC_CH_UART) ? "UART" : "none",
|
|
stat.done_sent, stat.request_sent);
|
|
|
|
const edc_cycle_t *cycle = actuator_get_last_cycle();
|
|
if (cycle) {
|
|
off += snprintf(json + off, sizeof(json) - off,
|
|
",\"cycle\":{\"id\":%u,\"frags\":%u,\"data\":[",
|
|
cycle->cycle_id, cycle->frag_count);
|
|
for (int i = 0; i < cycle->frag_count; i++) {
|
|
const edc_fragment_t *f = &cycle->fragments[i];
|
|
off += snprintf(json + off, sizeof(json) - off,
|
|
"%s{\"sx\":%u,\"sy\":%u,\"sa\":%.1f,\"tx\":%u,\"ty\":%u,\"ta\":%.1f}",
|
|
(i > 0) ? "," : "",
|
|
f->source.x, f->source.y, f->source.angle_x10 / 10.0f,
|
|
f->target.x, f->target.y, f->target.angle_x10 / 10.0f);
|
|
}
|
|
off += snprintf(json + off, sizeof(json) - off, "]}");
|
|
}
|
|
off += snprintf(json + off, sizeof(json) - off, "}");
|
|
|
|
httpd_resp_set_type(req, "application/json");
|
|
httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_proto_request_handler(httpd_req_t *req)
|
|
{
|
|
edc_proto_send_request();
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, "已发送 CYCLE_REQUEST", HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_proto_done_handler(httpd_req_t *req)
|
|
{
|
|
edc_proto_stat_t stat;
|
|
edc_proto_get_stats(&stat);
|
|
/* 对最近收到的 cycle 发送 DONE */
|
|
if (stat.last_channel != EDC_CH_NONE) {
|
|
char buf[128];
|
|
size_t len = httpd_req_get_url_query_len(req) + 1;
|
|
if (len > sizeof(buf)) len = sizeof(buf);
|
|
uint16_t cid = 0;
|
|
if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) {
|
|
char val[16];
|
|
if (httpd_query_key_value(buf, "id", val, sizeof(val)) == ESP_OK) {
|
|
cid = (uint16_t)atoi(val);
|
|
}
|
|
}
|
|
edc_proto_send_done(cid);
|
|
char resp[64];
|
|
snprintf(resp, sizeof(resp), "已发送 CYCLE_DONE (id=%u)", cid);
|
|
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "尚无收到 Cycle");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
static void http_server_init(void)
|
|
{
|
|
httpd_handle_t server = NULL;
|
|
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
|
|
cfg.stack_size = 8192; /* 默认 4096 不够, 增大 */
|
|
cfg.max_uri_handlers = 30;
|
|
cfg.max_open_sockets = 4;
|
|
cfg.lru_purge_enable = true;
|
|
|
|
ESP_ERROR_CHECK(httpd_start(&server, &cfg));
|
|
|
|
httpd_uri_t uri_root = {
|
|
.uri = "/",
|
|
.method = HTTP_GET,
|
|
.handler = root_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_root);
|
|
|
|
httpd_uri_t uri_enable = {
|
|
.uri = "/api/enable",
|
|
.method = HTTP_GET,
|
|
.handler = api_enable_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_enable);
|
|
|
|
httpd_uri_t uri_velocity = {
|
|
.uri = "/api/velocity",
|
|
.method = HTTP_GET,
|
|
.handler = api_velocity_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_velocity);
|
|
|
|
httpd_uri_t uri_position = {
|
|
.uri = "/api/position",
|
|
.method = HTTP_GET,
|
|
.handler = api_position_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_position);
|
|
|
|
httpd_uri_t uri_stop = {
|
|
.uri = "/api/stop",
|
|
.method = HTTP_GET,
|
|
.handler = api_stop_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_stop);
|
|
|
|
httpd_uri_t uri_magnet = {
|
|
.uri = "/api/magnet",
|
|
.method = HTTP_GET,
|
|
.handler = api_magnet_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_magnet);
|
|
|
|
httpd_uri_t uri_status = {
|
|
.uri = "/api/status",
|
|
.method = HTTP_GET,
|
|
.handler = api_status_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_status);
|
|
|
|
httpd_uri_t uri_servo = {
|
|
.uri = "/api/servo",
|
|
.method = HTTP_GET,
|
|
.handler = api_servo_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_servo);
|
|
|
|
/* ---- 轴联动 + 直线/角度 ---- */
|
|
httpd_uri_t uri_axis = {
|
|
.uri = "/api/axis", .method = HTTP_GET,
|
|
.handler = api_axis_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_axis);
|
|
|
|
httpd_uri_t uri_mm = {
|
|
.uri = "/api/mm", .method = HTTP_GET,
|
|
.handler = api_mm_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_mm);
|
|
|
|
httpd_uri_t uri_angle = {
|
|
.uri = "/api/angle", .method = HTTP_GET,
|
|
.handler = api_angle_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_angle);
|
|
|
|
/* ---- 底层调试 API ---- */
|
|
httpd_uri_t uri_reset_pos = {
|
|
.uri = "/api/reset_pos", .method = HTTP_GET,
|
|
.handler = api_reset_pos_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_reset_pos);
|
|
|
|
httpd_uri_t uri_reset_clog = {
|
|
.uri = "/api/reset_clog", .method = HTTP_GET,
|
|
.handler = api_reset_clog_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_reset_clog);
|
|
|
|
httpd_uri_t uri_read_param = {
|
|
.uri = "/api/read_param", .method = HTTP_GET,
|
|
.handler = api_read_param_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_read_param);
|
|
|
|
/* favicon.ico — 返回空, 避免 404 警告 */
|
|
httpd_uri_t uri_favicon = {
|
|
.uri = "/favicon.ico",
|
|
.method = HTTP_GET,
|
|
.handler = favicon_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_favicon);
|
|
|
|
/* ---- 回零校准 API ---- */
|
|
httpd_uri_t uri_origin_set = {
|
|
.uri = "/api/origin_set", .method = HTTP_GET,
|
|
.handler = api_origin_set_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_origin_set);
|
|
|
|
httpd_uri_t uri_origin_config = {
|
|
.uri = "/api/origin_config", .method = HTTP_GET,
|
|
.handler = api_origin_config_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_origin_config);
|
|
|
|
httpd_uri_t uri_origin_go = {
|
|
.uri = "/api/origin_go", .method = HTTP_GET,
|
|
.handler = api_origin_go_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_origin_go);
|
|
|
|
httpd_uri_t uri_origin_stop = {
|
|
.uri = "/api/origin_stop", .method = HTTP_GET,
|
|
.handler = api_origin_stop_handler, .user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_origin_stop);
|
|
|
|
/* ---- 协议状态 API ---- */
|
|
httpd_uri_t uri_proto_status = {
|
|
.uri = "/api/proto/status",
|
|
.method = HTTP_GET,
|
|
.handler = api_proto_status_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_proto_status);
|
|
|
|
httpd_uri_t uri_proto_request = {
|
|
.uri = "/api/proto/request",
|
|
.method = HTTP_GET,
|
|
.handler = api_proto_request_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_proto_request);
|
|
|
|
httpd_uri_t uri_proto_done = {
|
|
.uri = "/api/proto/done",
|
|
.method = HTTP_GET,
|
|
.handler = api_proto_done_handler,
|
|
.user_ctx = NULL
|
|
};
|
|
httpd_register_uri_handler(server, &uri_proto_done);
|
|
|
|
ESP_LOGI(TAG, "HTTP 服务器已启动 (端口 %d)", cfg.server_port);
|
|
}
|
|
|
|
/* ============================================================
|
|
* 公开接口
|
|
* ============================================================ */
|
|
|
|
void web_ctrl_init(void)
|
|
{
|
|
wifi_init_sta();
|
|
http_server_init();
|
|
}
|
|
|
|
#endif /* EDC_DEBUG_WEB */
|