const UBX_MAX_PAYLOAD: usize = 8192;
const NMEA_MAX: usize = 120;
const BUF_MAX: usize = 64 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Frame {
Ubx {
class: u8,
id: u8,
payload: Vec<u8>,
},
Nmea(String),
Rtcm(Vec<u8>),
}
pub fn ubx_checksum(bytes: &[u8]) -> [u8; 2] {
let (mut a, mut b) = (0u8, 0u8);
for &x in bytes {
a = a.wrapping_add(x);
b = b.wrapping_add(a);
}
[a, b]
}
pub fn crc24q(bytes: &[u8]) -> u32 {
let mut crc: u32 = 0;
for &b in bytes {
crc ^= (b as u32) << 16;
for _ in 0..8 {
crc <<= 1;
if crc & 0x0100_0000 != 0 {
crc ^= 0x0186_4CFB;
}
}
}
crc & 0x00FF_FFFF
}
enum Step {
Emit(Frame, usize),
Skip,
Wait,
}
#[derive(Debug, Default)]
pub struct Splitter {
buf: Vec<u8>,
garbage: u64,
}
impl Splitter {
pub fn new() -> Self {
Self::default()
}
pub fn garbage(&self) -> u64 {
self.garbage
}
pub fn push(&mut self, bytes: &[u8]) -> Vec<Frame> {
self.buf.extend_from_slice(bytes);
let mut out = Vec::new();
let mut pos = 0;
while pos < self.buf.len() {
match scan(&self.buf[pos..]) {
Step::Emit(frame, used) => {
out.push(frame);
pos += used;
}
Step::Skip => {
let next = self.buf[pos + 1..]
.iter()
.position(|&b| matches!(b, 0xB5 | b'$' | 0xD3))
.map_or(self.buf.len(), |i| pos + 1 + i);
self.garbage += (next - pos) as u64;
pos = next;
}
Step::Wait => break,
}
}
self.buf.drain(..pos);
if self.buf.len() > BUF_MAX {
self.garbage += self.buf.len() as u64;
self.buf.clear();
}
out
}
}
fn scan(b: &[u8]) -> Step {
match b[0] {
0xB5 => scan_ubx(b),
b'$' => scan_nmea(b),
0xD3 => scan_rtcm(b),
_ => Step::Skip,
}
}
fn scan_ubx(b: &[u8]) -> Step {
if b.len() < 2 {
return Step::Wait;
}
if b[1] != 0x62 {
return Step::Skip;
}
if b.len() < 6 {
return Step::Wait;
}
let len = u16::from_le_bytes([b[4], b[5]]) as usize;
if len > UBX_MAX_PAYLOAD {
return Step::Skip;
}
let total = 8 + len;
if b.len() < total {
return Step::Wait;
}
if ubx_checksum(&b[2..6 + len]) != [b[6 + len], b[7 + len]] {
return Step::Skip;
}
Step::Emit(
Frame::Ubx {
class: b[2],
id: b[3],
payload: b[6..6 + len].to_vec(),
},
total,
)
}
fn scan_nmea(b: &[u8]) -> Step {
let window = &b[..b.len().min(NMEA_MAX)];
let Some(nl) = window.iter().position(|&c| c == b'\n') else {
return if b.len() < NMEA_MAX {
Step::Wait
} else {
Step::Skip
};
};
let line = &b[..nl];
let line = line.strip_suffix(b"\r").unwrap_or(line);
match nmea_valid(line) {
Some(s) => Step::Emit(Frame::Nmea(s), nl + 1),
None => Step::Skip,
}
}
fn nmea_valid(line: &[u8]) -> Option<String> {
if line.len() < 4 || line[0] != b'$' {
return None;
}
let star = line.len().checked_sub(3)?;
if line[star] != b'*' {
return None;
}
let want = u8::from_str_radix(std::str::from_utf8(&line[star + 1..]).ok()?, 16).ok()?;
let body = &line[1..star];
if !body.iter().all(|c| (0x20..0x7F).contains(c)) {
return None;
}
let got = body.iter().fold(0u8, |acc, &c| acc ^ c);
(got == want).then(|| String::from_utf8_lossy(line).into_owned())
}
fn scan_rtcm(b: &[u8]) -> Step {
if b.len() < 3 {
return Step::Wait;
}
if b[1] & 0xFC != 0 {
return Step::Skip;
}
let len = (((b[1] & 0x03) as usize) << 8) | b[2] as usize;
let total = 3 + len + 3;
if b.len() < total {
return Step::Wait;
}
let crc = crc24q(&b[..3 + len]);
let got = ((b[3 + len] as u32) << 16) | ((b[4 + len] as u32) << 8) | b[5 + len] as u32;
if crc != got {
return Step::Skip;
}
Step::Emit(Frame::Rtcm(b[..total].to_vec()), total)
}
pub fn ubx_frame(class: u8, id: u8, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + payload.len());
out.extend_from_slice(&[0xB5, 0x62, class, id]);
out.extend_from_slice(&(payload.len() as u16).to_le_bytes());
out.extend_from_slice(payload);
let ck = ubx_checksum(&out[2..]);
out.extend_from_slice(&ck);
out
}
pub fn rtcm_msg_type(frame: &[u8]) -> Option<u16> {
(frame.len() >= 5).then(|| ((frame[3] as u16) << 4) | (frame[4] as u16 >> 4))
}
#[cfg(test)]
mod tests;