use std::{io, time::Duration};
use bytemuck::{Pod, Zeroable, bytes_of_mut, from_bytes};
use tracing::{Level, error, span, trace};
use crate::util::BytesExt;
use super::{
NgPayload,
common::{self, Command, MixShim, NgShim, SZ_ARGS, SZ_DATA},
};
pub type ResponseOld = common::FrameOld;
pub type ResponseMix = ResponseNgFrame<MixShim>;
pub type ResponseNg = ResponseNgFrame<NgShim>;
#[derive(Debug, Copy, Clone)]
pub struct ResponseNgFrame<D> {
pub cmd: Command,
pub status: i8,
pub reason: i8,
pub shim: D,
}
pub(super) const SZ_BUF: usize = if SZ_OLD_BUF > SZ_NG_BUF {
SZ_OLD_BUF
} else {
SZ_NG_BUF
};
#[derive(Debug, Clone)]
pub enum Response {
Old(ResponseOld),
Mix(ResponseMix),
Ng(ResponseNg),
}
impl Response {
#[must_use]
pub fn cmd(&self) -> Command {
match self {
Response::Old(frame_old) => frame_old.cmd,
Response::Mix(frame_mix) => frame_mix.cmd,
Response::Ng(frame_ng) => frame_ng.cmd,
}
}
#[must_use]
pub fn payload(&self) -> &[u8] {
match self {
Response::Old(frame_old) => &frame_old.payload,
Response::Mix(frame_mix) => frame_mix.shim.payload(),
Response::Ng(frame_ng) => frame_ng.shim.payload(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IO error")]
Io(#[from] std::io::Error),
#[error("serialport error")]
Serialport(#[from] serialport::Error),
#[error("payload was too large: {0} bytes. Max: {SZ_DATA}")]
PayloadTooLarge(usize),
#[error("payload was too small for MIX frame: {0} bytes. Min: {SZ_ARGS}")]
MixPayloadTooSmall(usize),
#[error(
"invalid CRC checksum. Actual: 0x{actual:02X}, expected: 0x{expected:02X} (or fallback: 0x{NG_RESPONSE_CRC_FALLBACK:02X})"
)]
InvalidCrc {
expected: u16,
actual: u16,
},
#[error("an OLD frame contained a u64 command outside the bounds supported by u16: {0}")]
CommandOutOfBounds(u64),
#[error("response was not received before timeout ({}+{} ms)", original.as_millis(), extensions.as_millis())]
Timeout {
original: Duration,
extensions: Duration,
},
}
pub(super) fn read_from(mut r: impl io::Read, msg_b: &mut [u8; SZ_BUF]) -> Result<Response, Error> {
r.read_exact(&mut msg_b[..SZ_NG_HEADER])?;
let head: &NgHeader = from_bytes(&msg_b[..SZ_NG_HEADER]);
let response = if head.magic == NG_RESPONSE_MAGIC {
let _span = span!(Level::TRACE, "NG/MIX packet");
let length_ng = common::NgLength::from_bits(head.length_ng);
let payload_len = length_ng.length() as usize;
if payload_len > SZ_DATA {
return Err(Error::PayloadTooLarge(payload_len));
}
let checksum_len = SZ_NG_HEADER + payload_len;
let frame_len = checksum_len + SZ_NG_FOOTER;
r.read_exact(&mut msg_b[SZ_NG_HEADER..frame_len])?;
let foot: &NgFooter = from_bytes(&msg_b[checksum_len..frame_len]);
let crc_expected = common::CRC.checksum(&msg_b[..checksum_len]);
let crc_got = foot.crc; if crc_got != crc_expected && crc_got != NG_RESPONSE_CRC_FALLBACK {
error!(
"bad packet CRC! expected: 0x{crc_expected:04X} or fallback 0x{NG_RESPONSE_CRC_FALLBACK:04X}, got: 0x{crc_got:04X}",
);
return Err(Error::InvalidCrc {
expected: crc_expected,
actual: foot.crc,
});
}
let head: &NgHeader = from_bytes(&msg_b[..SZ_NG_HEADER]);
if length_ng.ng() {
let frame = ResponseNgFrame {
cmd: head.cmd,
status: head.status,
reason: head.reason,
shim: NgShim {
len: payload_len,
buf: msg_b[SZ_NG_HEADER..checksum_len].to_buffer(),
},
};
Response::Ng(frame)
} else {
if payload_len < SZ_ARGS {
return Err(Error::MixPayloadTooSmall(payload_len));
}
let mut args = [0u64; 3];
bytes_of_mut(&mut args).copy_from_slice(&msg_b[SZ_NG_HEADER..][..SZ_ARGS]);
let frame = ResponseNgFrame {
cmd: head.cmd,
status: head.status,
reason: head.reason,
shim: MixShim {
args,
len: checksum_len - (SZ_NG_HEADER + SZ_ARGS),
buf: msg_b[SZ_NG_HEADER + SZ_ARGS..checksum_len].to_buffer(),
},
};
Response::Mix(frame)
}
} else {
const SZ_FRAME_OLD: usize = size_of::<FrameOldPacked>();
let _span = span!(Level::TRACE, "OLD packet");
trace!("reading packet remainder...");
r.read_exact(&mut msg_b[SZ_NG_HEADER..SZ_FRAME_OLD])?;
let old: &FrameOldPacked = from_bytes(&msg_b[..SZ_FRAME_OLD]);
let cmd = u16::try_from(old.cmd)
.map(Command::from)
.map_err(|_| Error::CommandOutOfBounds(old.cmd))?;
let frame = common::FrameOld {
cmd,
args: old.arg,
payload: old.data.to_buffer(),
};
Response::Old(frame)
};
Ok(response)
}
const NG_RESPONSE_MAGIC: u32 = 0x62_33_4d_50;
const NG_RESPONSE_CRC_FALLBACK: u16 = 0x33_62;
const SZ_NG_HEADER: usize = size_of::<NgHeader>();
const SZ_NG_FOOTER: usize = size_of::<NgFooter>();
const SZ_NG_BUF: usize = SZ_NG_HEADER + SZ_DATA + SZ_NG_FOOTER;
const SZ_OLD_BUF: usize = size_of::<FrameOldPacked>();
crate::mod_const! {
pub errors: i8, "Common error codes";
SUCCESS = 0,
ERFTRANS = -7,
}
#[derive(Debug, Copy, Clone, Zeroable, Pod)]
#[repr(C, packed)]
struct NgHeader {
magic: u32,
length_ng: u16,
status: i8,
reason: i8,
cmd: Command,
}
#[derive(Debug, Copy, Clone, Zeroable, Pod)]
#[repr(C, packed)]
struct NgFooter {
crc: u16,
}
#[derive(Debug, Copy, Clone, Zeroable, Pod)]
#[repr(C, packed)]
struct FrameOldPacked {
cmd: u64,
arg: [u64; 3],
data: [u8; SZ_DATA],
}