diff --git a/examples/version.rs b/examples/version.rs index d240ed8..2739a41 100644 --- a/examples/version.rs +++ b/examples/version.rs @@ -1,24 +1,24 @@ -use arctictis::{ - Command, Scanner, - command::{GetFirmwareVersion, GetModelInfo}, -}; +use arctictis::{Command, Scanner, bc125at::GetFirmwareVersion}; #[tokio::main] async fn main() -> Result<(), Box> { let mut scanner = Scanner::open()?; println!("{scanner:#?}"); print_response(&mut scanner, GetFirmwareVersion).await?; - print_response(&mut scanner, GetModelInfo).await?; Ok(()) } -async fn print_response(scanner: &mut Scanner, cmd: T) -> Result<(), Box> +async fn print_response( + scanner: &mut Scanner, + cmd: Cmd, +) -> Result<(), Box> where - T: Command, + Cmd: Command<'static> + 'static, + Cmd::Response: std::fmt::Debug, { - let name = cmd.to_string(); - let r = scanner.command(cmd.clone()).await?; + let name = String::from_utf8_lossy(Cmd::TEXT); + let r = scanner.command(cmd).await?; println!("{name} => {r:#?}"); Ok(()) } diff --git a/src/codec.rs b/src/codec.rs new file mode 100644 index 0000000..f37fcbd --- /dev/null +++ b/src/codec.rs @@ -0,0 +1,112 @@ +use bytes::BufMut; +use tokio_util::codec::{AnyDelimiterCodec, AnyDelimiterCodecError, Decoder, Encoder}; + +use crate::command::{Command, ParamSet, Response}; + +#[derive(Clone, Debug)] +pub struct Codec { + decoder: AnyDelimiterCodec, +} + +impl Codec { + pub fn new() -> Self { + Self { + decoder: AnyDelimiterCodec::new(b"\r".to_vec(), b"\r".to_vec()), + } + } +} + +impl<'p, Cmd> Encoder for Codec +where + Cmd: Command<'p>, +{ + type Error = std::io::Error; + + fn encode( + &mut self, + item: Cmd, + dst: &mut tokio_util::bytes::BytesMut, + ) -> Result<(), Self::Error> { + let params = item.param_set(); + let est_len = Cmd::TEXT.len() + params.count() + params.size() + 1; + dst.reserve(est_len); + + dst.extend_from_slice(Cmd::TEXT); + + for param in item.param_set() { + dst.put_u8(b','); + param.write_bytes(dst); + } + + dst.put_u8(b'\r'); + + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ResponseError { + #[error("response is for wrong command")] + WrongCommand, + + #[error(transparent)] + InvalidFields(#[from] E), +} + +pub struct RawResponse { + cmd: Vec, + raw_values: Vec, +} + +impl RawResponse { + pub fn parse<'p, Cmd>( + &self, + ) -> Result::Error>> + where + Cmd: Command<'p>, + { + if self.cmd != Cmd::TEXT { + return Err(ResponseError::WrongCommand); + } + + let response = Cmd::Response::parse_from_values(self.raw_values.split(|b| *b == b','))?; + + Ok(response) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum DecoderError { + #[error("malformed response")] + Malformed, + + #[error(transparent)] + DelimiterError(#[from] AnyDelimiterCodecError), + + #[error(transparent)] + Io(#[from] std::io::Error), +} + +impl Decoder for Codec { + type Item = RawResponse; + type Error = DecoderError; + + fn decode( + &mut self, + src: &mut tokio_util::bytes::BytesMut, + ) -> Result, Self::Error> { + let Some(output) = self.decoder.decode(src)? else { + return Ok(None); + }; + + let mut fields = output.split(|b| *b == b','); + let Some(cmd) = fields.next() else { + return Err(DecoderError::Malformed); + }; + + Ok(Some(RawResponse { + cmd: cmd.to_owned(), + raw_values: output[4..].to_owned(), + })) + } +} diff --git a/src/codec/command.rs b/src/codec/command.rs deleted file mode 100644 index c5ba47c..0000000 --- a/src/codec/command.rs +++ /dev/null @@ -1,73 +0,0 @@ -#[macro_use] -mod macros; - -mod private { - pub trait Sealed {} -} - -pub trait Command: - private::Sealed + Send + Sync + Clone + std::fmt::Display + std::fmt::Debug -{ - fn as_bytes(&self) -> &'static [u8]; - - fn params(&self) -> impl IntoIterator { - [] - } -} - -pub trait Param: private::Sealed + Send + Sync + std::fmt::Debug { - fn write_bytes(&self, dst: &mut tokio_util::bytes::BytesMut); -} - -impl private::Sealed for u8 {} -impl Param for u8 { - fn write_bytes(&self, dst: &mut tokio_util::bytes::BytesMut) { - let mut buff = itoa::Buffer::new(); - dst.extend_from_slice(buff.format(*self).as_bytes()); - } -} - -gen_command!(b"PRG", EnterProgramMode); -gen_command!(b"EPG", ExitProgramMode); -gen_command!(b"MDL", GetModelInfo); -gen_command!(b"VER", GetFirmwareVersion); - -gen_command!(b"BLT", GetBacklight); -gen_command!(b"BLT", SetBacklight(Backlight)); -gen_param!(pub enum Backlight { - AlwaysOn => b"AO", - AlwaysOff => b"AF", - Keypress => b"KY", - Squelch => b"SQ", - KeySquelch => b"KS", -}); - -gen_command!(b"BSV", GetBatteryInfo); -gen_command!(b"BSV", SetBatteryInfo(BatteryChargeTime)); -gen_param!(pub range BatteryChargeTime(1..=16)); - -gen_command!(b"CLR", ClearAllMemory); - -gen_command!(b"BPL", GetBandPlan); -gen_command!(b"BPL", SetBandPlan(BandPlan)); -gen_param!(pub enum BandPlan { - Usa => b"0", - Canada => b"1" -}); - -gen_command!(b"KBP", GetKeyBeep); -gen_command!( - b"KBP", - SetKeyBeep { - beep_level: BeepLevel, - key_lock_status: KeyLockStatus - } -); -gen_param!(pub enum BeepLevel { - Auto => b"0", - Off => b"99", -}); -gen_param!(pub enum KeyLockStatus { - Off => b"0", - On => b"1", -}); diff --git a/src/codec/command/macros.rs b/src/codec/command/macros.rs deleted file mode 100644 index 8a937aa..0000000 --- a/src/codec/command/macros.rs +++ /dev/null @@ -1,91 +0,0 @@ -// #[macro_export] -// #[doc(hidden)] -macro_rules! gen_command { - ($cmd:literal, $name:ident) => { - #[derive(Clone, Debug)] - pub struct $name; - - gen_command!(@traits $cmd, $name); - - impl Command for $name { - fn as_bytes(&self) -> &'static [u8] { $cmd } - } - }; - ($cmd:literal, $name: ident ( $param_ty:ty )) => { - #[derive(Clone, Debug)] - pub struct $name(pub $param_ty); - - gen_command!(@traits $cmd, $name); - - impl Command for $name { - fn as_bytes(&self) -> &'static [u8] { $cmd } - fn params(&self) -> impl IntoIterator { - [&self.0 as &dyn Param] - } - } - }; - ($cmd:literal, $name: ident { $($param:ident: $param_ty:ty),+ }) => { - #[derive(Clone, Debug)] - pub struct $name { $(pub $param: $param_ty),+ } - - gen_command!(@traits $cmd, $name); - - impl Command for $name { - fn as_bytes(&self) -> &'static [u8] { $cmd } - fn params(&self) -> impl IntoIterator { - [$(&self.$param as &dyn Param),+] - } - } - }; - (@traits $cmd:literal, $name:ident) => { - impl private::Sealed for $name {} - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - String::from_utf8_lossy($cmd).fmt(f) - } - } - }; -} - -// #[macro_export] -// #[doc(hidden)] -macro_rules! gen_param { - (pub enum $name:ident { $($variant:ident => $val:literal),+ $(,)? }) => { - #[derive(Clone, Debug)] - pub enum $name { - $($variant),+ - } - - gen_param!(@traits $name); - - impl Param for $name { - fn write_bytes(&self, dst: &mut tokio_util::bytes::BytesMut) { - let s = match self { - $(Self::$variant => $val.as_slice()),+ - }; - dst.extend_from_slice(s); - } - } - }; - (pub range $name:ident ($range:expr)) => { - #[derive(Clone, Debug)] - pub struct $name(u8); - - gen_param!(@traits $name); - - impl $name { - pub fn new(value: u8) -> Self { - assert!($range.contains(&value)); - Self(value) - } - } - impl Param for $name { - fn write_bytes(&self, dst: &mut tokio_util::bytes::BytesMut) { - self.0.write_bytes(dst); - } - } - }; - (@traits $name:ident) => { - impl private::Sealed for $name {} - }; -} diff --git a/src/codec/mod.rs b/src/codec/mod.rs deleted file mode 100644 index a4b1c84..0000000 --- a/src/codec/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -pub mod command; - -use bytes::BufMut; -use tokio_util::codec::{AnyDelimiterCodec, Decoder, Encoder}; - -use command::Command; - -#[derive(Clone, Debug)] -pub struct Codec { - // encoder: AnyDelimiterCodec, - decoder: AnyDelimiterCodec, -} - -impl Codec { - pub fn new() -> Self { - let decoder = AnyDelimiterCodec::new(b"\r".to_vec(), b"\r".to_vec()); - Self { - // encoder: codec.clone(), - decoder, - } - } -} - -impl Encoder for Codec -where - T: Command, -{ - type Error = std::io::Error; - - fn encode( - &mut self, - item: T, - dst: &mut tokio_util::bytes::BytesMut, - ) -> Result<(), Self::Error> { - dst.extend_from_slice(item.as_bytes()); - - for param in item.params() { - dst.put_u8(b','); - param.write_bytes(dst); - } - - dst.put_u8(b'\r'); - - Ok(()) - } -} - -impl Decoder for Codec { - type Item = String; - type Error = ::Error; - - fn decode( - &mut self, - src: &mut tokio_util::bytes::BytesMut, - ) -> Result, Self::Error> { - let output = self.decoder.decode(src)?; - let s = output.map(|out| String::from_utf8_lossy(out.as_ref()).into_owned()); - Ok(s) - } -} diff --git a/src/command/bc125at.rs b/src/command/bc125at.rs new file mode 100644 index 0000000..c3a9915 --- /dev/null +++ b/src/command/bc125at.rs @@ -0,0 +1,58 @@ +use std::str::Utf8Error; + +use crate::command::{Command, NoParams, OkResponse, Response}; + +#[derive(Debug)] +pub struct EnterProgramMode; + +impl Command<'static> for EnterProgramMode { + const TEXT: &'static [u8] = b"EPG"; + type Params = NoParams; + type Response = OkResponse; + + fn param_set(&self) -> Self::Params { + NoParams + } +} + +#[derive(Debug, thiserror::Error)] +pub enum FirmwareVersionError { + #[error("invalid UTF-8 bytes")] + Utf8Error(#[from] Utf8Error), + #[error("expected one response field")] + WrongNumberOfFields, +} + +#[derive(Debug)] +pub struct FirmwareVersion(pub String); + +impl Response for FirmwareVersion { + type Error = FirmwareVersionError; + fn parse_from_values<'f>( + mut raw_values: impl Iterator, + ) -> Result { + let bytes = raw_values + .next() + .ok_or(FirmwareVersionError::WrongNumberOfFields)?; + + let version = str::from_utf8(bytes)?; + + if raw_values.next().is_some() { + return Err(FirmwareVersionError::WrongNumberOfFields); + } + + Ok(Self(version.to_string())) + } +} + +pub struct GetFirmwareVersion; + +impl Command<'static> for GetFirmwareVersion { + const TEXT: &'static [u8] = b"VER"; + type Params = NoParams; + type Response = FirmwareVersion; + + fn param_set(&self) -> Self::Params { + NoParams + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs new file mode 100644 index 0000000..4b4b7d1 --- /dev/null +++ b/src/command/mod.rs @@ -0,0 +1,33 @@ +pub mod bc125at; +mod no_params; +mod ok_response; + +use bytes::BytesMut; + +pub(crate) use no_params::NoParams; +pub(crate) use ok_response::OkResponse; + +pub trait Command<'p> { + const TEXT: &'static [u8]; + type Params: ParamSet<'p>; + type Response: Response; + + fn param_set(&self) -> Self::Params; +} + +pub trait ParamSet<'p>: IntoIterator { + fn count(&self) -> usize; + fn size(&self) -> usize; +} + +pub trait Param { + fn write_bytes(&self, dst: &mut BytesMut); +} + +pub trait Response: Sized { + type Error: std::error::Error; + + fn parse_from_values<'f>( + raw_values: impl Iterator, + ) -> Result; +} diff --git a/src/command/no_params.rs b/src/command/no_params.rs new file mode 100644 index 0000000..8f3dd45 --- /dev/null +++ b/src/command/no_params.rs @@ -0,0 +1,22 @@ +use crate::command::{Param, ParamSet}; + +pub struct NoParams; + +impl IntoIterator for NoParams { + type Item = &'static dyn Param; + type IntoIter = std::iter::Empty; + + fn into_iter(self) -> Self::IntoIter { + std::iter::empty() + } +} + +impl ParamSet<'static> for NoParams { + fn count(&self) -> usize { + 0 + } + + fn size(&self) -> usize { + 0 + } +} diff --git a/src/command/ok_response.rs b/src/command/ok_response.rs new file mode 100644 index 0000000..944b23f --- /dev/null +++ b/src/command/ok_response.rs @@ -0,0 +1,35 @@ +use crate::command::Response; + +#[derive(Debug, thiserror::Error)] +pub enum OkResponseError { + #[error("expected `OK`, got `{0}`")] + UnexpectedValue(String), + #[error("expected one response field")] + WrongNumberOfFields, +} + +pub struct OkResponse; + +impl Response for OkResponse { + type Error = OkResponseError; + + fn parse_from_values<'f>( + mut raw_values: impl Iterator, + ) -> Result { + let val = raw_values + .next() + .ok_or(OkResponseError::WrongNumberOfFields)?; + + if val != b"OK" { + return Err(OkResponseError::UnexpectedValue( + String::from_utf8_lossy(val).to_string(), + )); + } + + if raw_values.next().is_some() { + return Err(OkResponseError::WrongNumberOfFields); + } + + Ok(Self) + } +} diff --git a/src/command2.rs b/src/command2.rs deleted file mode 100644 index bc5969f..0000000 --- a/src/command2.rs +++ /dev/null @@ -1,205 +0,0 @@ -#![expect(dead_code)] - -use std::str::Utf8Error; - -use bytes::{BufMut, BytesMut}; - -pub trait Command<'p> { - const TEXT: &'static [u8]; - // type Params: IntoIterator; - type Params: ParamSet<'p>; - type Response: Response; - - fn param_set(&self) -> Self::Params; -} - -pub trait ParamSet<'p>: IntoIterator { - fn count(&self) -> usize; - fn size(&self) -> usize; -} - -pub trait Param { - fn write_bytes(&self, dst: &mut BytesMut); -} - -struct NoParams; - -impl IntoIterator for NoParams { - type Item = &'static dyn Param; - type IntoIter = std::iter::Empty; - - fn into_iter(self) -> Self::IntoIter { - std::iter::empty() - } -} - -impl ParamSet<'static> for NoParams { - fn count(&self) -> usize { - 0 - } - - fn size(&self) -> usize { - 0 - } -} - -pub trait Response: Sized { - type Error: std::error::Error; - - fn parse_from_values<'f>( - raw_values: impl Iterator, - ) -> Result; -} - -#[derive(Debug, thiserror::Error)] -pub enum OkResponseError { - #[error("expected `OK`, got `{0}`")] - UnexpectedValue(String), - #[error("expected one response field")] - WrongNumberOfFields, -} - -pub struct OkResponse; - -impl Response for OkResponse { - type Error = OkResponseError; - - fn parse_from_values<'f>( - mut raw_values: impl Iterator, - ) -> Result { - let val = raw_values - .next() - .ok_or(OkResponseError::WrongNumberOfFields)?; - - if val != b"OK" { - return Err(OkResponseError::UnexpectedValue( - String::from_utf8_lossy(val).to_string(), - )); - } - - if raw_values.next().is_some() { - return Err(OkResponseError::WrongNumberOfFields); - } - - Ok(Self) - } -} - -struct EnterProgramMode; - -impl Command<'static> for EnterProgramMode { - const TEXT: &'static [u8] = b"EPG"; - type Params = NoParams; - type Response = OkResponse; - - fn param_set(&self) -> Self::Params { - NoParams - } -} - -#[derive(Debug, thiserror::Error)] -pub enum FirmwareVersionError { - #[error("invalid UTF-8 bytes")] - Utf8Error(#[from] Utf8Error), - #[error("expected one response field")] - WrongNumberOfFields, -} - -struct FirmwareVersion(String); - -impl Response for FirmwareVersion { - type Error = FirmwareVersionError; - fn parse_from_values<'f>( - mut raw_values: impl Iterator, - ) -> Result { - let bytes = raw_values - .next() - .ok_or(FirmwareVersionError::WrongNumberOfFields)?; - - let version = str::from_utf8(bytes)?; - - if raw_values.next().is_some() { - return Err(FirmwareVersionError::WrongNumberOfFields); - } - - Ok(Self(version.to_string())) - } -} - -struct GetFirmwareVersion; - -impl Command<'static> for GetFirmwareVersion { - const TEXT: &'static [u8] = b"VER"; - type Params = NoParams; - type Response = FirmwareVersion; - - fn param_set(&self) -> Self::Params { - NoParams - } -} - -fn send_command<'a, Cmd: Command<'a>>(dst: &mut BytesMut, item: &Cmd) { - let params = item.param_set(); - let est_len = Cmd::TEXT.len() + params.count() + params.size() + 1; - dst.reserve(est_len); - - dst.extend_from_slice(Cmd::TEXT); - - for param in item.param_set() { - dst.put_u8(b','); - param.write_bytes(dst); - } - - dst.put_u8(b'\r'); -} - -#[derive(Debug, thiserror::Error)] -enum ResponseError { - #[error("malformed response")] - Malformed, - #[error("response is for wrong command")] - WrongCommand, - #[error(transparent)] - InvalidFields(#[from] E), -} - -fn parse_response<'a, 'b, Cmd: Command<'a>>( - response: &'b [u8], -) -> Result::Error>> { - let mut fields = response.split(|b| *b == b','); - - let Some(cmd) = fields.next() else { - return Err(ResponseError::Malformed); - }; - - if cmd != Cmd::TEXT { - return Err(ResponseError::WrongCommand); - } - - let response = Cmd::Response::parse_from_values(fields)?; - - Ok(response) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn sending_works() { - let mut buf = BytesMut::new(); - let cmd = GetFirmwareVersion; - send_command(&mut buf, &cmd); - - let buf = buf.freeze(); - assert_eq!(buf.as_ref(), b"VER\r"); - } - - #[test] - fn receiving_works() { - let response = b"VER,FOOBAR"; - - #[allow(clippy::unwrap_used)] - parse_response::(response).unwrap(); - } -} diff --git a/src/lib.rs b/src/lib.rs index a359162..0fa1f0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,8 @@ mod codec; mod scanner; -mod command2; +mod command; -pub use crate::{ - codec::command::{self, Command}, - scanner::{Scanner, ScannerError}, -}; +pub use crate::command::bc125at; +pub use crate::command::{Command, Param, ParamSet, Response}; +pub use crate::scanner::{Scanner, ScannerError}; diff --git a/src/scanner.rs b/src/scanner.rs index 26c42a6..6366357 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -2,9 +2,12 @@ use std::time::Duration; use futures_util::{SinkExt, StreamExt}; use tokio_serial::{SerialPortBuilderExt, SerialPortType, SerialStream}; -use tokio_util::codec::{AnyDelimiterCodecError, Framed}; +use tokio_util::codec::Framed; -use crate::{codec::Codec, command::Command}; +use crate::{ + codec::{Codec, DecoderError, ResponseError}, + command::{Command, Response}, +}; const VENDOR_ID: u16 = 0x1965; const PRODUCT_ID: u16 = 0x0017; @@ -17,9 +20,6 @@ pub enum ScannerError { #[error("scanner not found")] ScannerNotFound, - #[error(transparent)] - Codec(#[from] AnyDelimiterCodecError), - #[error(transparent)] Io(#[from] std::io::Error), @@ -27,6 +27,18 @@ pub enum ScannerError { Serial(#[from] tokio_serial::Error), } +#[derive(Debug, thiserror::Error)] +pub enum EncodingError { + #[error(transparent)] + Scanner(#[from] ScannerError), + + #[error(transparent)] + Decoder(#[from] DecoderError), + + #[error(transparent)] + ResponseParserError(#[from] ResponseError), +} + #[derive(Debug)] pub struct Scanner(Framed); @@ -52,17 +64,16 @@ impl Scanner { Ok(Self(framed)) } - pub async fn response(&mut self) -> Result { - let r = self.0.next().await.ok_or(ScannerError::PortClosed)??; - Ok(r) - } - - pub async fn command(&mut self, cmd: T) -> Result + pub async fn command<'p, Cmd>( + &mut self, + cmd: Cmd, + ) -> Result::Error>> where - T: Command, + Cmd: Command<'p>, { - self.0.send(cmd).await?; - let r = self.0.next().await.ok_or(ScannerError::PortClosed)??; - Ok(r) + self.0.send(cmd).await.map_err(ScannerError::from)?; + let raw_response = self.0.next().await.ok_or(ScannerError::PortClosed)??; + let response = raw_response.parse::()?; + Ok(response) } }