From b38014686b00fc533265a850ab6086227a96f55a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 3 May 2024 16:16:24 +0200 Subject: [PATCH] Added rotctl protocol parser --- .gitignore | 1 + Cargo.lock | 32 ++++++ Cargo.toml | 7 ++ src/main.rs | 5 + src/rotctl.rs | 278 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 323 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/rotctl.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..551b0ca --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,32 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "radomctld" +version = "0.1.0" +dependencies = [ + "nom", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4a2ed71 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "radomctld" +version = "0.1.0" +edition = "2021" + +[dependencies] +nom = "7.1.3" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..baf00d4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +mod rotctl; + +fn main() { + println!("Hello, world!"); +} diff --git a/src/rotctl.rs b/src/rotctl.rs new file mode 100644 index 0000000..d895e5d --- /dev/null +++ b/src/rotctl.rs @@ -0,0 +1,278 @@ +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::{alphanumeric1, i8, multispace1, space1}, + combinator::{all_consuming, map, rest}, + error::{context, convert_error, VerboseError}, + number::complete::float, + sequence::{preceded, separated_pair, terminated}, + IResult, Parser, +}; + +#[derive(PartialEq, Debug)] +pub enum Command { + Exit, + SetPos(f32, f32), + GetPos, + Move(Direction, i8), + Stop, + Park, + SetConf(String, String), + Reset, + GetInfo, + DumpState, + DumpCaps, + SendCmd(String), +} + +#[derive(PartialEq, Debug)] +pub enum Direction { + UP, + DOWN, + CW, + CCW, +} + +fn exit(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + all_consuming(map(alt((tag("q"), tag("Q"))), |_| Command::Exit)).parse(input) +} + +fn float_pair(input: &str) -> IResult<&str, (f32, f32), VerboseError<&str>> { + context("float_pair", separated_pair(float, multispace1, float)).parse(input) +} + +fn set_pos(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + context( + "set_pos", + all_consuming(map( + preceded( + terminated(alt((tag("set_pos"), tag("P"))), multispace1), + float_pair, + ), + |pair: (f32, f32)| Command::SetPos(pair.0, pair.1), + )), + ) + .parse(input) +} + +fn get_pos(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + all_consuming(map(alt((tag("get_pos"), tag("p"))), |_| Command::GetPos)).parse(input) +} + +fn direction(input: &str) -> IResult<&str, Direction, VerboseError<&str>> { + context( + "direction", + alt(( + map(alt((tag("UP"), tag("2"))), |_| Direction::UP), + map(alt((tag("DOWN"), tag("4"))), |_| Direction::DOWN), + map(alt((tag("CCW"), tag("LEFT"), tag("8"))), |_| Direction::CCW), + map(alt((tag("CW"), tag("RIGHT"), tag("16"))), |_| Direction::CW), + )), + ) + .parse(input) +} + +fn move_parameters(input: &str) -> IResult<&str, (Direction, i8), VerboseError<&str>> { + context( + "move_parameters", + separated_pair(direction, multispace1, i8), + ) + .parse(input) +} + +fn move_command(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + context( + "move", + all_consuming(map( + preceded( + terminated(alt((tag("move"), tag("M"))), multispace1), + move_parameters, + ), + |pair: (Direction, i8)| Command::Move(pair.0, pair.1), + )), + ) + .parse(input) +} + +fn stop(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + all_consuming(map(alt((tag("stop"), tag("S"))), |_| Command::Stop)).parse(input) +} + +fn park(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + all_consuming(map(alt((tag("park"), tag("K"))), |_| Command::Park)).parse(input) +} + +fn string_pair(input: &str) -> IResult<&str, (&str, &str), VerboseError<&str>> { + context( + "string_pair", + separated_pair(alphanumeric1, multispace1, alphanumeric1), + ) + .parse(input) +} + +fn set_conf(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + context( + "set_comf", + all_consuming(map( + preceded( + terminated(alt((tag("set_conf"), tag("C"))), multispace1), + string_pair, + ), + |pair: (&str, &str)| Command::SetConf(pair.0.to_owned(), pair.1.to_owned()), + )), + ) + .parse(input) +} + +fn reset(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + all_consuming(map(alt((tag("reset"), tag("R"))), |_| Command::Reset)).parse(input) +} + +fn get_info(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + all_consuming(map(alt((tag("get_info"), tag("_"))), |_| Command::GetInfo)).parse(input) +} + +fn dump_state(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + all_consuming(map(tag("dump_state"), |_| Command::DumpState)).parse(input) +} + +fn dump_caps(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + all_consuming(map(alt((tag("dump_caps"), tag("1"))), |_| { + Command::DumpCaps + })) + .parse(input) +} + +fn send_cmd(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + context( + "set_comf", + all_consuming(map( + preceded(terminated(alt((tag("send_cmd"), tag("w"))), space1), rest), + |rest: &str| Command::SendCmd(rest.to_owned()), + )), + ) + .parse(input) +} + +fn command(input: &str) -> IResult<&str, Command, VerboseError<&str>> { + context( + "command", + alt(( + exit, + set_pos, + get_pos, + move_command, + stop, + park, + set_conf, + reset, + get_info, + dump_state, + dump_caps, + send_cmd, + )), + ) + .parse(input) +} + +#[cfg(test)] +mod tests { + use super::*; + use nom::Err as NomErr; + + fn assert_command(input: &str, expected: Command) { + let result = command(input); + match result { + Ok(("", cmd)) => assert_eq!(cmd, expected), + Err(err) => match err { + NomErr::Incomplete(_) => panic!("Command was ncomplete"), + NomErr::Error(err) | NomErr::Failure(err) => { + panic!("{}", convert_error(input, err)) + } + }, + _ => {} + } + } + + #[test] + fn test_exit() { + assert_command("Q", Command::Exit); + assert_command("q", Command::Exit); + } + + #[test] + fn test_set_pos() { + assert_command("set_pos 180.0 10.0", Command::SetPos(180.0, 10.0)); + assert_command("P 180.0 10.0", Command::SetPos(180.0, 10.0)); + } + + #[test] + fn test_get_pos() { + assert_command("get_pos", Command::GetPos); + assert_command("p", Command::GetPos); + } + + #[test] + fn test_move() { + assert_command("M UP 100", Command::Move(Direction::UP, 100)); + assert_command("M DOWN -1", Command::Move(Direction::DOWN, -1)); + assert_command("M CCW 42", Command::Move(Direction::CCW, 42)); + assert_command("M LEFT 42", Command::Move(Direction::CCW, 42)); + assert_command("M CW 42", Command::Move(Direction::CW, 42)); + assert_command("M RIGHT 42", Command::Move(Direction::CW, 42)); + assert_command("move UP 100", Command::Move(Direction::UP, 100)); + } + + #[test] + fn test_stop() { + assert_command("stop", Command::Stop); + assert_command("S", Command::Stop); + } + + #[test] + fn test_park() { + assert_command("park", Command::Park); + assert_command("K", Command::Park); + } + + #[test] + fn test_reset() { + assert_command("reset", Command::Reset); + assert_command("R", Command::Reset); + } + + #[test] + fn test_get_info() { + assert_command("get_info", Command::GetInfo); + assert_command("_", Command::GetInfo); + } + + #[test] + fn test_dump_state() { + assert_command("dump_state", Command::DumpState); + } + + #[test] + fn test_dump_caps() { + assert_command("dump_caps", Command::DumpCaps); + assert_command("1", Command::DumpCaps); + } + + #[test] + fn test_set_conf() { + assert_command( + "set_conf foo bar", + Command::SetConf("foo".to_owned(), "bar".to_owned()), + ); + assert_command( + "C foo bar", + Command::SetConf("foo".to_owned(), "bar".to_owned()), + ); + } + + #[test] + fn test_send_cmd() { + assert_command("send_cmd foo bar", Command::SendCmd("foo bar".to_owned())); + assert_command("w foo bar", Command::SendCmd("foo bar".to_owned())); + } +}