2026-10-19 08:09:48 +00:00
|
|
|
|
use std::io::{BufRead, BufReader, Read, Write};
|
|
|
|
|
|
use std::net::TcpStream;
|
|
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
|
|
use std::sync::mpsc::{self, Receiver, Sender, RecvTimeoutError};
|
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
use std::thread::{self, JoinHandle};
|
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
|
|
use std::f32::consts::PI;
|
|
|
|
|
|
|
|
|
|
|
|
pub const ESP32_IP: &str = "192.168.4.1";
|
|
|
|
|
|
pub const ESP32_PORT: u16 = 3333;
|
|
|
|
|
|
pub const NUM_SERVOS: usize = 8;
|
|
|
|
|
|
|
|
|
|
|
|
const TELEMETRY_MAGIC: [u8; 2] = [0xA5, 0x5A];
|
|
|
|
|
|
const TELEMETRY_FRAME_LEN: usize = 16;
|
|
|
|
|
|
|
|
|
|
|
|
/// 从 ESP32 收到的一条消息(可包含 ASCII 文本,也可包含 36 维 IMU 数据)
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct Esp32Message {
|
|
|
|
|
|
text: String,
|
|
|
|
|
|
telemetry: Option<[f32; 17]>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Esp32Message {
|
|
|
|
|
|
pub fn new_text(text: impl Into<String>) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
text: text.into(),
|
|
|
|
|
|
telemetry: None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn new_telemetry(data: [f32; 17]) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
text: "IMU".to_string(),
|
|
|
|
|
|
telemetry: Some(data),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 是否是数据推送(二进制 IMU 帧)
|
|
|
|
|
|
pub fn is_data(&self) -> bool { self.telemetry.is_some() }
|
|
|
|
|
|
|
|
|
|
|
|
/// 是否是应答,比如 "ESP32: OK ..."
|
|
|
|
|
|
pub fn is_reply(&self) -> bool {
|
|
|
|
|
|
self.telemetry.is_none() && self.text.starts_with("ESP32:")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn text(&self) -> &str { &self.text }
|
|
|
|
|
|
|
|
|
|
|
|
/// 返回 36 维 f32 数组;其余位置按 0.0 填充。
|
|
|
|
|
|
pub fn telemetry(&self) -> Option<[f32; 17]> { self.telemetry }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 双向连接 ====================
|
|
|
|
|
|
|
|
|
|
|
|
pub struct Link {
|
|
|
|
|
|
writer: TcpStream,
|
|
|
|
|
|
rx: Receiver<Esp32Message>,
|
|
|
|
|
|
stop: Arc<AtomicBool>,
|
|
|
|
|
|
reader_job: Option<JoinHandle<()>>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Link {
|
|
|
|
|
|
/// 建立连接并启动后台接收线程
|
|
|
|
|
|
pub fn open(ip: &str, port: u16) -> std::io::Result<Self> {
|
|
|
|
|
|
let addr = format!("{}:{}", ip, port);
|
|
|
|
|
|
println!("[*] 连接 {} ...", addr);
|
|
|
|
|
|
println!("请连接上esp32的wifi热点后再运行本程序【ESP32_AP】");
|
|
|
|
|
|
let stream = TcpStream::connect(&addr)?;
|
|
|
|
|
|
stream.set_nodelay(true)?;
|
|
|
|
|
|
stream.set_read_timeout(Some(Duration::from_millis(200)))?;
|
|
|
|
|
|
println!("[+] 已连接");
|
|
|
|
|
|
|
|
|
|
|
|
let reader_stream = stream.try_clone()?;
|
|
|
|
|
|
let (tx, rx): (Sender<Esp32Message>, Receiver<Esp32Message>) = mpsc::channel();
|
|
|
|
|
|
let stop = Arc::new(AtomicBool::new(false));
|
|
|
|
|
|
|
|
|
|
|
|
let reader_job = spawn_reader(reader_stream, tx, Arc::clone(&stop));
|
|
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
|
writer: stream,
|
|
|
|
|
|
rx,
|
|
|
|
|
|
stop,
|
|
|
|
|
|
reader_job: Some(reader_job),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn open_default() -> std::io::Result<Self> {
|
|
|
|
|
|
Self::open(ESP32_IP, ESP32_PORT)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 发送 ----------
|
|
|
|
|
|
|
|
|
|
|
|
/// 发送弧度列表
|
|
|
|
|
|
pub fn send_angles(&mut self, rads: &[f32]) -> std::io::Result<()> {
|
|
|
|
|
|
let n = rads.len().min(NUM_SERVOS);
|
|
|
|
|
|
let payload = rads[..n]
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|r| format!("{:.4}", r.clamp(0.0, PI)))
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join(",");
|
|
|
|
|
|
self.send_line(&format!("RAD:{}", payload))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 发送角度列表
|
|
|
|
|
|
pub fn send_degrees(&mut self, degs: &[f32]) -> std::io::Result<()> {
|
|
|
|
|
|
let n = degs.len().min(NUM_SERVOS);
|
|
|
|
|
|
let payload = degs[..n]
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|d| format!("{:.2}", d.clamp(0.0, 180.0)))
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join(",");
|
|
|
|
|
|
self.send_line(&format!("DEG:{}", payload))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 发送任意一行(自动补 '\n')
|
|
|
|
|
|
pub fn send_line(&mut self, line: &str) -> std::io::Result<()> {
|
|
|
|
|
|
self.writer.write_all(line.as_bytes())?;
|
|
|
|
|
|
self.writer.write_all(b"\n")?;
|
|
|
|
|
|
self.writer.flush()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 接收 ----------
|
|
|
|
|
|
|
|
|
|
|
|
/// 阻塞等待一条消息(最多等 timeout)
|
|
|
|
|
|
pub fn recv_timeout(&self, timeout: Duration) -> Option<Esp32Message> {
|
|
|
|
|
|
match self.rx.recv_timeout(timeout) {
|
|
|
|
|
|
Ok(m) => Some(m),
|
|
|
|
|
|
Err(RecvTimeoutError::Timeout) => None,
|
|
|
|
|
|
Err(RecvTimeoutError::Disconnected) => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 非阻塞:立刻返回当前队列里有没有消息
|
|
|
|
|
|
pub fn try_recv(&self) -> Option<Esp32Message> {
|
|
|
|
|
|
self.rx.try_recv().ok()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Drop for Link {
|
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
|
self.stop.store(true, Ordering::SeqCst);
|
|
|
|
|
|
let _ = self.writer.shutdown(std::net::Shutdown::Both);
|
|
|
|
|
|
if let Some(job) = self.reader_job.take() {
|
|
|
|
|
|
let _ = job.join();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 后台接收线程 ====================
|
|
|
|
|
|
|
|
|
|
|
|
fn spawn_reader(
|
|
|
|
|
|
stream: TcpStream,
|
|
|
|
|
|
tx: Sender<Esp32Message>,
|
|
|
|
|
|
stop: Arc<AtomicBool>,
|
|
|
|
|
|
) -> JoinHandle<()> {
|
|
|
|
|
|
thread::spawn(move || {
|
|
|
|
|
|
let mut stream = stream;
|
|
|
|
|
|
let mut pending = Vec::new();
|
|
|
|
|
|
let mut buffer = [0u8; 1024];
|
|
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
|
if stop.load(Ordering::SeqCst) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
match stream.read(&mut buffer) {
|
|
|
|
|
|
Ok(0) => break,
|
|
|
|
|
|
Ok(n) => {
|
|
|
|
|
|
pending.extend_from_slice(&buffer[..n]);
|
|
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
|
if pending.len() >= TELEMETRY_FRAME_LEN
|
|
|
|
|
|
&& pending[..2] == TELEMETRY_MAGIC
|
|
|
|
|
|
{
|
|
|
|
|
|
let frame = &pending[..TELEMETRY_FRAME_LEN];
|
|
|
|
|
|
if let Some(data) = parse_telemetry_frame(frame) {
|
|
|
|
|
|
if tx.send(Esp32Message::new_telemetry(data)).is_err() {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
pending.drain(..TELEMETRY_FRAME_LEN);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CRC 错误,丢掉首字节继续找下一帧
|
|
|
|
|
|
pending.drain(..1);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(newline_pos) = pending.iter().position(|&b| b == b'\n') {
|
|
|
|
|
|
let line_bytes = &pending[..newline_pos];
|
|
|
|
|
|
// 只处理纯 ASCII 文本; 非法 UTF-8 也不阻断连接
|
|
|
|
|
|
let line = String::from_utf8_lossy(line_bytes).trim().to_string();
|
|
|
|
|
|
if !line.is_empty() {
|
|
|
|
|
|
if tx.send(Esp32Message::new_text(line)).is_err() {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
pending.drain(..=newline_pos);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 还没凑齐完整帧/整行,等下一批数据
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if pending.len() > 65536 {
|
|
|
|
|
|
pending.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(_) => break,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
println!("[*] 接收线程退出");
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn parse_telemetry_frame(frame: &[u8]) -> Option<[f32; 17]> {
|
|
|
|
|
|
// Frame layout (from ESP32):
|
|
|
|
|
|
// 0..1 magic 0xA5 0x5A
|
|
|
|
|
|
// 2..3 gravity.x (int16 big-endian) = projection * 10000
|
|
|
|
|
|
// 4..5 gravity.y
|
|
|
|
|
|
// 6..7 gravity.z
|
|
|
|
|
|
// 8..9 gyro_x encoded (int16 BE) = rad/s * 1000
|
|
|
|
|
|
// 10..11 gyro_y
|
|
|
|
|
|
// 12..13 gyro_z
|
|
|
|
|
|
// 14..15 CRC16 over bytes 0..13 (big-endian)
|
|
|
|
|
|
if frame.len() != TELEMETRY_FRAME_LEN {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
if frame[..2] != TELEMETRY_MAGIC {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let expected_crc = u16::from_be_bytes([frame[14], frame[15]]);
|
|
|
|
|
|
let actual_crc = calculate_crc16_ccitt(&frame[..14]);
|
|
|
|
|
|
if actual_crc != expected_crc {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Decode gravity projections (stored as projection * 10000)
|
|
|
|
|
|
let gx_proj = i16::from_be_bytes([frame[2], frame[3]]) as f32 / 10000.0;
|
|
|
|
|
|
let gy_proj = i16::from_be_bytes([frame[4], frame[5]]) as f32 / 10000.0;
|
|
|
|
|
|
let gz_proj = i16::from_be_bytes([frame[6], frame[7]]) as f32 / 10000.0;
|
|
|
|
|
|
|
|
|
|
|
|
// Decode base angular velocities (stored as rad/s * 1000)
|
|
|
|
|
|
let avx = i16::from_be_bytes([frame[8], frame[9]]) as f32 / 1000.0;
|
|
|
|
|
|
let avy = i16::from_be_bytes([frame[10], frame[11]]) as f32 / 1000.0;
|
|
|
|
|
|
let avz = i16::from_be_bytes([frame[12], frame[13]]) as f32 / 1000.0;
|
|
|
|
|
|
|
|
|
|
|
|
let mut values = [0.0f32; 17];
|
|
|
|
|
|
// fill first 6 entries: gravity x,y,z then angular vel x,y,z
|
|
|
|
|
|
values[0] = gx_proj;
|
|
|
|
|
|
values[1] = gy_proj;
|
|
|
|
|
|
values[2] = gz_proj;
|
|
|
|
|
|
values[3] = avx;
|
|
|
|
|
|
values[4] = avy;
|
|
|
|
|
|
values[5] = avz;
|
|
|
|
|
|
|
|
|
|
|
|
Some(values)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn calculate_crc16_ccitt(data: &[u8]) -> u16 {
|
|
|
|
|
|
let mut crc = 0xFFFFu16;
|
|
|
|
|
|
for &byte in data {
|
|
|
|
|
|
crc ^= (byte as u16) << 8;
|
|
|
|
|
|
for _ in 0..8 {
|
|
|
|
|
|
if crc & 0x8000 != 0 {
|
|
|
|
|
|
crc = ((crc << 1) ^ 0x1021) & 0xFFFF;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
crc = (crc << 1) & 0xFFFF;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
crc
|
|
|
|
|
|
}
|
|
|
|
|
|
|