armor: ESP32-C3 装甲板固件(一主三从)+ 烧录脚本,之前一直没提交
这一块代码本来就写好了、根 README 里也按"已实现"描述,但一直没有进仓库, 只存在于工作区里。同一个提交里带上 `.env.example` 的两个新变量(ARMOR_TEAM、 ARMOR_THRESHOLD)——它们的说明文字就是为这套固件写的。 内容: - armor-protocol:规则层(血量、每板独立 50ms 受击冷却、受击后只有那一路灯带全黑、 按键 FSM、上报编码)。纯计算,主机上 22 个测试,已跑通。 - armor-firmware:四路压电片 ADC 连续采样、四路 WS2812(C3 只有一个 RMT 通道可用, 靠 rmt_tx_switch_gpio 轮流驱动)、NVS、BOOT 键、WiFi + UDP 广播。 - scripts:build_armor.sh / flash_armor.sh / armor_probe.py(不开界面单看广播)。 armor/ 是独立 workspace(目标 RISC-V 的 ESP32-C3),与外层 Xtensa 的图传固件互不相干, 各自 .cargo/config.toml 就是这条边界。构建产物由根 .gitignore 的 `/armor/target/` 覆盖, 本次没有把任何 target/ 或 build/ 下的东西加进来。 顺带把 `.DS_Store` 加进 .gitignore。
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
//! 上报通道:把 [`ArmorHealth`](armor_protocol::ArmorHealth) 广播给本项目裁判终端。
|
||||
//!
|
||||
//! # 为什么直接写 lwIP 的 socket
|
||||
//!
|
||||
//! 与同仓库的图传固件(`src/udp.rs`)一样:`std::net` 在 lwIP 上的行为要靠一层
|
||||
//! VFS 转接,而这里只需要"建一个 UDP socket + 广播"这一件事,直接用 IDF 原生的
|
||||
//! `lwip_*` 就能与图传板跑在同一条被验证过的代码路径上。
|
||||
//!
|
||||
//! # 为什么不 bind
|
||||
//!
|
||||
//! 装甲板只发不收:血量是板子算出来的,裁判端只显示。所以 socket 不绑定端口,
|
||||
//! 由 lwIP 选一个临时端口即可 —— 绑定 5601 反而会和裁判端、以及同网段的另一块
|
||||
//! 装甲板抢同一个端口。裁判端监听 5601,板子往 5601 发,方向是单向的。
|
||||
//!
|
||||
//! # 广播而不是单播
|
||||
//!
|
||||
//! 裁判终端的地址是 DHCP 变的(图传板的 Hello 广播就是为了绕开这件事)。装甲板
|
||||
//! 沿用同一个约定:不知道也不关心裁判端在哪,反正同一个局域网里它会收到。
|
||||
|
||||
use core::ffi::c_void;
|
||||
use std::net::SocketAddrV4;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use esp_idf_svc::sys as ffi;
|
||||
use log::{info, warn};
|
||||
|
||||
use armor_protocol::{ARMOR_PORT, BROADCAST_ADDR};
|
||||
|
||||
/// 把 Rust 的 socket 地址转成 lwIP 期望的 C 结构。
|
||||
///
|
||||
/// 逐字段构造而不是 `transmute`:`SocketAddrV4` 是紧凑的 6 字节(端口在前、主机
|
||||
/// 字节序),C 的 `sockaddr_in` 是 16 字节且地址是网络字节序 —— 指针强转会发到一个
|
||||
/// 乱地址,而且不会报错。
|
||||
fn to_sockaddr(addr: SocketAddrV4) -> ffi::sockaddr_in {
|
||||
let octets = addr.ip().octets();
|
||||
|
||||
ffi::sockaddr_in {
|
||||
sin_len: core::mem::size_of::<ffi::sockaddr_in>() as u8,
|
||||
sin_family: ffi::AF_INET as ffi::sa_family_t,
|
||||
// 用 lwIP 的 htons,字节序不必在 Rust 侧推理。
|
||||
sin_port: unsafe { ffi::lwip_htons(addr.port()) },
|
||||
sin_addr: ffi::in_addr {
|
||||
s_addr: u32::from_le_bytes(octets),
|
||||
},
|
||||
sin_zero: [0; 8],
|
||||
}
|
||||
}
|
||||
|
||||
/// 本机 id:由 WiFi MAC 派生(FNV-1a),重启不变,且避开 0 —— 0 留给裁判终端。
|
||||
pub fn local_id() -> u32 {
|
||||
let mut mac = [0u8; 6];
|
||||
|
||||
// SAFETY: `mac` 有 6 字节空间,函数会把 6 字节写满。
|
||||
let rc = unsafe { ffi::esp_read_mac(mac.as_mut_ptr(), ffi::esp_mac_type_t_ESP_MAC_WIFI_STA) };
|
||||
|
||||
if rc != ffi::ESP_OK {
|
||||
warn!("could not read the WiFi MAC ({:#x}); using a fixed id", rc);
|
||||
return 0xF1F0_0001;
|
||||
}
|
||||
|
||||
let id = armor_protocol::id_from_mac(&mac);
|
||||
info!("this board's id is 0x{id:08X}, from MAC {mac:02X?}");
|
||||
id
|
||||
}
|
||||
|
||||
/// 广播上报器。
|
||||
pub struct Reporter {
|
||||
socket: i32,
|
||||
}
|
||||
|
||||
impl Reporter {
|
||||
/// 建一个开了广播许可的 UDP socket。
|
||||
pub fn new() -> Result<Self> {
|
||||
// SAFETY: 建 socket 不涉及 Rust 内存。
|
||||
let socket = unsafe { ffi::lwip_socket(ffi::AF_INET as i32, ffi::SOCK_DGRAM as i32, 0) };
|
||||
if socket < 0 {
|
||||
// 用 std 的 errno 读取:`__errno` 不是每个目标的绑定里都有,而 lwIP 的
|
||||
// socket 错误码就是同一个 errno。
|
||||
return Err(anyhow!(
|
||||
"could not create UDP socket: {}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
|
||||
// 目标地址是 255.255.255.255,不开这个选项 sendto 会返回 EACCES。默认就是
|
||||
// 关的,而且失败不会自己冒出来 —— 只会表现为"裁判端一直显示未接入"。
|
||||
let enable: i32 = 1;
|
||||
// SAFETY: optval 指向栈上的 i32,optlen 与它一致。
|
||||
let rc = unsafe {
|
||||
ffi::lwip_setsockopt(
|
||||
socket,
|
||||
ffi::SOL_SOCKET as i32,
|
||||
ffi::SO_BROADCAST as i32,
|
||||
&enable as *const i32 as *const c_void,
|
||||
core::mem::size_of::<i32>() as u32,
|
||||
)
|
||||
};
|
||||
if rc != 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
// SAFETY: 关闭本函数刚建的 socket。
|
||||
unsafe { ffi::lwip_close(socket) };
|
||||
return Err(anyhow!("could not enable SO_BROADCAST: {err}"));
|
||||
}
|
||||
|
||||
info!("reporting to {BROADCAST_ADDR}:{ARMOR_PORT} over UDP");
|
||||
|
||||
Ok(Self { socket })
|
||||
}
|
||||
|
||||
/// 把一个数据报广播到裁判终端。
|
||||
pub fn broadcast(&self, payload: &[u8]) -> Result<()> {
|
||||
let addr = to_sockaddr(SocketAddrV4::new(BROADCAST_ADDR, ARMOR_PORT));
|
||||
|
||||
// SAFETY: 缓冲区与地址在调用期间都有效;sendto 复制数据而不是持有指针。
|
||||
let rc = unsafe {
|
||||
ffi::lwip_sendto(
|
||||
self.socket,
|
||||
payload.as_ptr() as *const c_void,
|
||||
payload.len(),
|
||||
ffi::MSG_DONTWAIT as i32,
|
||||
&addr as *const ffi::sockaddr_in as *const ffi::sockaddr,
|
||||
core::mem::size_of::<ffi::sockaddr_in>() as ffi::socklen_t,
|
||||
)
|
||||
};
|
||||
|
||||
if rc < 0 {
|
||||
return Err(anyhow!(
|
||||
"broadcast failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Reporter {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: 关闭本结构体自己建的 socket。
|
||||
unsafe { ffi::lwip_close(self.socket) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user