//! 上报通道:把 [`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::() 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 { // 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::() 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::() 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) }; } }