Files
simba-rmic-2026/armor/armor-firmware/src/net.rs
T
Horldsence a30eb3200a 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。
2026-10-14 19:43:14 +08:00

143 lines
5.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 上报通道:把 [`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) };
}
}