├── .cargo └── config ├── .gdbinit ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── README.md ├── ci ├── after_success.sh ├── install.sh └── script.sh ├── examples └── stm32.rs ├── gen-examples.sh ├── memory.x └── src ├── examples ├── _00_stm32.rs └── mod.rs ├── input.rs ├── lexer.rs ├── lib.rs ├── macros.rs ├── output.rs ├── tests.rs └── tokenizer.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.thumbv7m-none-eabi] 2 | runner = 'arm-none-eabi-gdb' 3 | rustflags = [ 4 | "-C", "link-arg=-Tlink.x", 5 | ] 6 | -------------------------------------------------------------------------------- /.gdbinit: -------------------------------------------------------------------------------- 1 | 2 | #target remote 192.168.99.2:3333 3 | target remote localhost:3333 4 | 5 | # monitor arm semihosting enable 6 | 7 | # # send captured ITM to the file itm.fifo 8 | # # (the microcontroller SWO pin must be connected to the programmer SWO pin) 9 | # # 8000000 must match the core clock frequency 10 | monitor tpiu config internal itm.fifo uart off 8000000 11 | #monitor tpiu config external uart off 8000000 2000000 12 | 13 | # # OR: make the microcontroller SWO pin output compatible with UART (8N1) 14 | # # 2000000 is the frequency of the SWO pin 15 | # monitor tpiu config external uart off 8000000 2000000 16 | 17 | # # enable ITM port 0 18 | monitor itm port 0 on 19 | 20 | load 21 | step 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | **/*.rs.bk 4 | rls 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | matrix: 4 | include: 5 | - env: TARGET=x86_64-unknown-linux-gnu 6 | rust: nightly 7 | - env: TARGET=thumbv7m-none-eabi 8 | rust: nightly 9 | addons: 10 | apt: 11 | packages: 12 | - gcc-arm-none-eabi 13 | 14 | before_install: set -e 15 | 16 | install: 17 | - cargo update 18 | - bash ci/install.sh 19 | 20 | script: 21 | - bash ci/script.sh 22 | 23 | after_script: set +e 24 | 25 | after_success: 26 | - bash ci/after_success.sh 27 | 28 | cache: cargo 29 | before_cache: 30 | # Travis can't cache files that are not readable by "others" 31 | - chmod -R a+r $HOME/.cargo 32 | 33 | branches: 34 | only: 35 | - auto 36 | - master 37 | - try 38 | 39 | notifications: 40 | email: 41 | on_success: never 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "light-cli" 3 | version = "0.1.0" 4 | authors = ["Rudi Horn "] 5 | keywords = ["embedded-hal-crate", "cli", "serial", "terminal"] 6 | categories = ["embedded", "no-std"] 7 | description = "simple heapless command line interface parser for embedded devices" 8 | repository = "https://github.com/rudihorn/light-cli" 9 | documentation = "https://rudihorn.github.io/light-cli/light_cli/index.html" 10 | readme = "README.md" 11 | license = "MIT OR Apache-2.0" 12 | 13 | [dependencies] 14 | nb = "0.1.1" 15 | embedded-hal = "0.2.2" 16 | heapless = "0.4.2" 17 | 18 | [target."thumbv7m-none-eabi".dev-dependencies] 19 | cortex-m = "0.5.8" 20 | cortex-m-rt = "0.6.7" 21 | panic-abort = "0.3.1" 22 | stm32f1xx-hal = { version = "0.2.1", features = ["stm32f103"] } 23 | 24 | [features] 25 | doc = [] 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `light-cli` 2 | 3 | A lightweight and heapless command line interface / command passing tool. Probably more useful for machine to machine communication. 4 | 5 | ## [Documentation](https://rudihorn.github.io/light-cli/light_cli/) 6 | 7 | ## [Example] 8 | 9 | The following definition allows for the commands: 10 | * `HELLO Name=`: Set the name to `` 11 | * `EHLO`: Return the name 12 | 13 | ``` 14 | lightcli!(cl_in, cl_out, cmd, key, val, [ 15 | "HELLO" => [ 16 | "Name" => name = String::from(val) 17 | ] => { writeln!(cl_out, "Name set").unwrap(); }; 18 | "EHLO" => [ 19 | ] => { writeln!(cl_out, "EHLO Name={}", name.as_str()).unwrap(); } 20 | ]); 21 | ``` 22 | 23 | A typical serial communication could look like: 24 | 25 | ``` 26 | >> EHLO 27 | << EHLO Name= 28 | >> HELLO Name=Johnson 29 | << Name set 30 | >> EHLO 31 | << EHLO Name=Johnson 32 | ``` 33 | 34 | It is recommended to use this in conjunction with the program [`rlwrap`](https://linux.die.net/man/1/rlwrap). 35 | 36 | [Complete Example](https://github.com/rudihorn/light-cli/tree/master/examples/) 37 | 38 | ## What works 39 | 40 | - Read key value style commands in the form: 41 | `COMMAND KEY=VALUE` 42 | - UTF-8 encoding. 43 | - Specify the heapless string length. 44 | - Partial command evaluation as data is received through the serial connection. 45 | 46 | ## TODO 47 | 48 | - [X] Writing to output 49 | - [ ] Improve UTF-8 error detection / code. 50 | - [ ] Any form of autocompletion / backspaces etc. 51 | 52 | ## License 53 | 54 | Licensed under either of 55 | 56 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 57 | http://www.apache.org/licenses/LICENSE-2.0) 58 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 59 | at your option. 60 | 61 | ### Contribution 62 | 63 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the 64 | work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 65 | additional terms or conditions. 66 | 67 | -------------------------------------------------------------------------------- /ci/after_success.sh: -------------------------------------------------------------------------------- 1 | set -euxo pipefail 2 | 3 | main() { 4 | if [ $TARGET = x86_64-unknown-linux-gnu ]; then 5 | return 6 | fi 7 | 8 | cargo doc --features doc --target $TARGET 9 | 10 | mkdir ghp-import 11 | 12 | curl -Ls https://github.com/davisp/ghp-import/archive/master.tar.gz | \ 13 | tar --strip-components 1 -C ghp-import -xz 14 | 15 | touch target/$TARGET/doc/.nojekyll 16 | ./ghp-import/ghp_import.py target/$TARGET/doc 17 | 18 | set +x 19 | git push -fq https://$GH_TOKEN@github.com/$TRAVIS_REPO_SLUG.git gh-pages && \ 20 | echo OK 21 | } 22 | 23 | if [ $TRAVIS_BRANCH = master ]; then 24 | main 25 | fi 26 | -------------------------------------------------------------------------------- /ci/install.sh: -------------------------------------------------------------------------------- 1 | set -euxo pipefail 2 | 3 | main() { 4 | if [ "$TARGET" != "x86_64-unknown-linux-gnu" ]; then 5 | rustup target add "$TARGET" 6 | fi 7 | 8 | return 9 | } 10 | 11 | main 12 | -------------------------------------------------------------------------------- /ci/script.sh: -------------------------------------------------------------------------------- 1 | set -euxo pipefail 2 | 3 | main() { 4 | cargo check --target $TARGET 5 | 6 | if [ "$TARGET" = "thumbv7m-none-eabi" ]; then 7 | cargo check --example stm32 --target $TARGET 8 | fi 9 | 10 | if [ "$TARGET" = "x86_64-unknown-linux-gnu" ]; then 11 | # the --tests is required to ignore the examples 12 | # which will not compile under x86 13 | cargo test --target $TARGET --tests 14 | fi 15 | } 16 | 17 | main 18 | -------------------------------------------------------------------------------- /examples/stm32.rs: -------------------------------------------------------------------------------- 1 | //! An example of how to use light-cli on an STM32F103 chip. 2 | //! 3 | //! To run call `cargo run --example stm32 --target thumbv7m-none-eabi --release`. 4 | //! 5 | //! A typical command line communication for the example could look like: 6 | //! ``` 7 | //! >> EHLO 8 | //! << EHLO Name= 9 | //! >> HELLO Name=Johnson 10 | //! << Name set 11 | //! >> EHLO 12 | //! << EHLO Name=Johnson 13 | //! ``` 14 | //! 15 | 16 | #![no_std] 17 | #![no_main] 18 | 19 | extern crate cortex_m; 20 | extern crate cortex_m_rt; 21 | extern crate panic_abort; 22 | extern crate embedded_hal as hal; 23 | extern crate stm32f1xx_hal as dev_hal; 24 | extern crate heapless; 25 | 26 | #[macro_use] 27 | extern crate light_cli; 28 | 29 | use core::fmt::Write; 30 | use dev_hal::serial::Serial; 31 | use dev_hal::prelude::*; 32 | use light_cli::{LightCliInput, LightCliOutput}; 33 | use heapless::consts::*; 34 | use heapless::String; 35 | 36 | use cortex_m_rt::entry; 37 | 38 | #[entry] 39 | fn main() -> ! { 40 | let dp = dev_hal::device::Peripherals::take().unwrap(); 41 | 42 | let mut flash = dp.FLASH.constrain(); 43 | let mut rcc = dp.RCC.constrain(); 44 | 45 | let clocks = rcc.cfgr.freeze(&mut flash.acr); 46 | 47 | let mut afio = dp.AFIO.constrain(&mut rcc.apb2); 48 | 49 | let mut gpiob = dp.GPIOB.split(&mut rcc.apb2); 50 | 51 | // USART1 52 | let tx = gpiob.pb6.into_alternate_push_pull(&mut gpiob.crl); 53 | let rx = gpiob.pb7; 54 | 55 | let serial = Serial::usart1( 56 | dp.USART1, 57 | (tx, rx), 58 | &mut afio.mapr, 59 | 9_600.bps(), 60 | clocks, 61 | &mut rcc.apb2, 62 | ); 63 | 64 | let (mut tx, mut rx) = serial.split(); 65 | 66 | let mut name : String = String::new(); 67 | 68 | let mut cl_in : LightCliInput = LightCliInput::new(); 69 | let mut cl_out = LightCliOutput::new(&mut tx); 70 | 71 | writeln!(cl_out, "Commands:").unwrap(); 72 | writeln!(cl_out, " - HELLO Name=: Set the name").unwrap(); 73 | writeln!(cl_out, " - EHLO: Print the name").unwrap(); 74 | 75 | loop { 76 | let _ = cl_out.flush(); 77 | let _ = cl_in.fill(&mut rx); 78 | 79 | lightcli!(cl_in, cl_out, cmd, key, val, [ 80 | "HELLO" => [ 81 | "Name" => name = String::from(val) 82 | ] => { writeln!(cl_out, "Name set").unwrap(); }; 83 | "EHLO" => [ 84 | ] => { writeln!(cl_out, "EHLO Name={}", name.as_str()).unwrap(); } 85 | ] 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /gen-examples.sh: -------------------------------------------------------------------------------- 1 | # Converts the examples in the `examples` directory into documentation in the 2 | # `examples` module (`src/examples/*.rs`) 3 | 4 | set -euxo pipefail 5 | 6 | main() { 7 | local examples=( 8 | stm32 9 | ) 10 | 11 | rm -rf src/examples 12 | 13 | mkdir src/examples 14 | 15 | cat >src/examples/mod.rs <<'EOF' 16 | //! Examples 17 | //! 18 | //! In order of increasing complexity 19 | // Auto-generated. Do not modify. 20 | EOF 21 | 22 | local i=0 out= 23 | for ex in ${examples[@]}; do 24 | name=_$(printf "%02d" $i)_${ex//-/_} 25 | out=src/examples/${name}.rs 26 | 27 | echo "pub mod $name;" >> src/examples/mod.rs 28 | 29 | grep '//!' examples/$ex.rs > $out 30 | echo '//!' >> $out 31 | echo '//! ```' >> $out 32 | grep -v '//!' examples/$ex.rs | ( 33 | IFS='' 34 | 35 | while read line; do 36 | echo "//! $line" >> $out; 37 | done 38 | ) 39 | echo '//! ```' >> $out 40 | echo '// Auto-generated. Do not modify.' >> $out 41 | 42 | 43 | chmod -x $out 44 | 45 | i=$(( i + 1 )) 46 | done 47 | 48 | chmod -x src/examples/mod.rs 49 | } 50 | 51 | main 52 | -------------------------------------------------------------------------------- /memory.x: -------------------------------------------------------------------------------- 1 | /* Linker script for the STM32F103C8T6 */ 2 | MEMORY 3 | { 4 | FLASH : ORIGIN = 0x08000000, LENGTH = 64K 5 | RAM : ORIGIN = 0x20000000, LENGTH = 20K 6 | } 7 | -------------------------------------------------------------------------------- /src/examples/_00_stm32.rs: -------------------------------------------------------------------------------- 1 | //! An example of how to use light-cli on an STM32F103 chip. 2 | //! 3 | //! To run call `cargo run --example stm32 --target thumbv7m-none-eabi --release`. 4 | //! 5 | //! A typical command line communication for the example could look like: 6 | //! ``` 7 | //! >> EHLO 8 | //! << EHLO Name= 9 | //! >> HELLO Name=Johnson 10 | //! << Name set 11 | //! >> EHLO 12 | //! << EHLO Name=Johnson 13 | //! ``` 14 | //! 15 | //! 16 | //! ``` 17 | //! 18 | //! #![no_std] 19 | //! 20 | //! extern crate cortex_m; 21 | //! extern crate panic_abort; 22 | //! extern crate embedded_hal as hal; 23 | //! extern crate stm32f103xx_hal as dev_hal; 24 | //! extern crate heapless; 25 | //! 26 | //! #[macro_use] 27 | //! extern crate light_cli; 28 | //! 29 | //! use core::fmt::Write; 30 | //! use dev_hal::serial::Serial; 31 | //! use dev_hal::prelude::*; 32 | //! use light_cli::{LightCliInput, LightCliOutput}; 33 | //! use heapless::consts::*; 34 | //! use heapless::String; 35 | //! 36 | //! fn main() { 37 | //! let dp = dev_hal::stm32f103xx::Peripherals::take().unwrap(); 38 | //! 39 | //! let mut flash = dp.FLASH.constrain(); 40 | //! let mut rcc = dp.RCC.constrain(); 41 | //! 42 | //! let clocks = rcc.cfgr.freeze(&mut flash.acr); 43 | //! 44 | //! let mut afio = dp.AFIO.constrain(&mut rcc.apb2); 45 | //! 46 | //! let mut gpiob = dp.GPIOB.split(&mut rcc.apb2); 47 | //! 48 | //! // USART1 49 | //! let tx = gpiob.pb6.into_alternate_push_pull(&mut gpiob.crl); 50 | //! let rx = gpiob.pb7; 51 | //! 52 | //! let serial = Serial::usart1( 53 | //! dp.USART1, 54 | //! (tx, rx), 55 | //! &mut afio.mapr, 56 | //! 9_600.bps(), 57 | //! clocks, 58 | //! &mut rcc.apb2, 59 | //! ); 60 | //! 61 | //! let (mut tx, mut rx) = serial.split(); 62 | //! 63 | //! let mut name : String = String::new(); 64 | //! 65 | //! let mut cl_in : LightCliInput = LightCliInput::new(); 66 | //! let mut cl_out = LightCliOutput::new(&mut tx); 67 | //! 68 | //! writeln!(cl_out, "Commands:").unwrap(); 69 | //! writeln!(cl_out, " - HELLO Name=: Set the name").unwrap(); 70 | //! writeln!(cl_out, " - EHLO: Print the name").unwrap(); 71 | //! 72 | //! loop { 73 | //! let _ = cl_out.flush(); 74 | //! let _ = cl_in.fill(&mut rx); 75 | //! 76 | //! lightcli!(cl_in, cl_out, cmd, key, val, [ 77 | //! "HELLO" => [ 78 | //! "Name" => name = String::from(val) 79 | //! ] => { writeln!(cl_out, "Name set").unwrap(); }; 80 | //! "EHLO" => [ 81 | //! ] => { writeln!(cl_out, "EHLO Name={}", name.as_str()).unwrap(); } 82 | //! ] 83 | //! ); 84 | //! } 85 | //! } 86 | //! ``` 87 | // Auto-generated. Do not modify. 88 | -------------------------------------------------------------------------------- /src/examples/mod.rs: -------------------------------------------------------------------------------- 1 | //! Examples 2 | //! 3 | //! In order of increasing complexity 4 | // Auto-generated. Do not modify. 5 | pub mod _00_stm32; 6 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use heapless; 2 | use nb; 3 | use tokenizer; 4 | 5 | use tokenizer::{Tokenizer}; 6 | use lexer::{Lexer, CallbackCommand}; 7 | use hal::serial::Read; 8 | 9 | pub struct LightCliInput where SLEN: heapless::ArrayLength { 10 | tokenizer: Tokenizer, 11 | lexer: Lexer 12 | } 13 | 14 | impl> LightCliInput { 15 | /// Create a new LightCLI instance. 16 | pub fn new() -> Self { 17 | Self { 18 | tokenizer: Tokenizer::new(), 19 | lexer: Lexer::new(), 20 | } 21 | } 22 | 23 | /// Try to parse as much data from the internal ring buffer as possible. 24 | /// 25 | /// # Arguments 26 | /// * `callback` - This is the callback that will receive all parsing events. 27 | /// 28 | /// # Remarks 29 | /// All commands are in the form "COMMAND KEY=VALUE". For every parsed key/value 30 | /// pair the callback will be triggered with the current command string, current 31 | /// key and the corresponding value. When a newline is read the callback is 32 | /// triggered with a command event. 33 | pub fn parse_data(&mut self, callback: CB) -> nb::Result<(), tokenizer::Error> 34 | where CB: FnMut(CallbackCommand) -> () { 35 | self.lexer.parse_data(&mut self.tokenizer, callback) 36 | } 37 | 38 | /// Copy as many available bytes from `ser` into the buffer as possible. 39 | /// 40 | /// # Arguments 41 | /// * `ser` - The serial interface to read from. 42 | /// 43 | /// # Remarks 44 | /// 45 | /// This will continue to try to read a byte from the serial device until the 46 | /// device returns `nb::Error::WouldBlock`. 47 | pub fn fill(&mut self, ser: &mut Read) -> nb::Result<(), E> { 48 | self.tokenizer.fill(ser) 49 | } 50 | } -------------------------------------------------------------------------------- /src/lexer.rs: -------------------------------------------------------------------------------- 1 | use nb; 2 | use heapless::{ArrayLength, String}; 3 | 4 | use tokenizer; 5 | use tokenizer::{Token, Tokenizer}; 6 | 7 | #[derive(Clone)] 8 | #[derive(PartialEq)] 9 | enum MachineState { 10 | NewCommand, 11 | NewCommandCR, 12 | Key, 13 | Value, 14 | } 15 | 16 | pub enum CallbackCommand<'a> { 17 | Attribute(&'a str, &'a str, &'a str), 18 | Command(&'a str), 19 | } 20 | 21 | pub struct Lexer where SLEN: ArrayLength { 22 | current_cmd: String, 23 | current_key: String, 24 | state: MachineState, 25 | } 26 | 27 | impl Lexer where SLEN: ArrayLength { 28 | pub fn new() -> Self { 29 | Self { 30 | current_cmd: String::new(), 31 | current_key: String::new(), 32 | state: MachineState::NewCommand, 33 | } 34 | } 35 | 36 | pub fn parse_data(&mut self, tokenizer: &mut Tokenizer, mut callback: CB) -> nb::Result<(), tokenizer::Error> 37 | where CB: FnMut(CallbackCommand) -> () { 38 | tokenizer.get_tokens(|token| { 39 | let new_state = match token { 40 | Token::NewLine => { 41 | if self.state != MachineState::NewCommandCR { 42 | callback(CallbackCommand::Command(self.current_cmd.as_str())); 43 | self.current_cmd.clear(); 44 | self.current_key.clear(); 45 | } 46 | MachineState::NewCommand 47 | }, 48 | Token::CarriageReturn => { 49 | callback(CallbackCommand::Command(self.current_cmd.as_str())); 50 | self.current_cmd.clear(); 51 | self.current_key.clear(); 52 | MachineState::NewCommandCR 53 | }, // ignore carriage returns 54 | Token::Value(s) => { 55 | match self.state { 56 | MachineState::NewCommandCR => { 57 | self.current_cmd = String::from(s); 58 | MachineState::Key 59 | }, 60 | MachineState::NewCommand => { 61 | self.current_cmd = String::from(s); 62 | MachineState::Key 63 | }, 64 | MachineState::Key => { 65 | self.current_key = String::from(s); 66 | MachineState::Value 67 | }, 68 | MachineState::Value => { 69 | callback(CallbackCommand::Attribute(self.current_cmd.as_str(), self.current_key.as_str(), s)); 70 | MachineState::Key 71 | }, 72 | } 73 | }, 74 | Token::Space => { 75 | match self.state { 76 | MachineState::Value => { 77 | callback(CallbackCommand::Attribute(self.current_cmd.as_str(), self.current_key.as_str(), "")); 78 | MachineState::Key 79 | }, 80 | MachineState::NewCommand => self.state.clone(), 81 | MachineState::NewCommandCR => self.state.clone(), 82 | MachineState::Key => self.state.clone() 83 | } 84 | }, 85 | Token::Equals => { 86 | self.state.clone() 87 | } 88 | }; 89 | 90 | self.state = new_state; 91 | }) 92 | } 93 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Simple heapless command line interface parser for embedded devices 2 | //! 3 | //! This crates makes use of a serial interface that implements the read trait 4 | //! of the [`embedded-hal`] crate. 5 | //! 6 | //! [`embedded-hal`]: https://crates.io/crates/embedded-hal 7 | //! 8 | //! # Usage 9 | //! 10 | //! First define an instance of the CLI by initializing a [`LightCliInput`] and a 11 | //! [`LightCliOutput`]. The output instance requires the serial write instance 12 | //! which should implement the embedded-hal [`Write`] trait: 13 | //! 14 | //! [`LightCliInput`]: struct.LightCliInput.html 15 | //! [`LightCliOutput`]: struct.LightCliOutput.html 16 | //! [`Write`]: ../embedded_hal/serial/trait.Write.html 17 | //! 18 | //! ``` 19 | //! let mut cl_in : LightCliInput = LightCliInput::new(); 20 | //! let mut cl_out = LightCliOutput::new(tx); 21 | //! ``` 22 | //! 23 | //! Periodically copy all contents of the serial device into the cli buffer by using 24 | //! the [`fill`] method, passing it the serial read instance `rx`, which implements 25 | //! the embedded-hal [`Read`] trait. In addition it is necessary to try to empty 26 | //! the output buffer, by calling the [`flush`] method on the console output instance: 27 | //! 28 | //! [`fill`]: struct.LightCliInput.html#method.fill 29 | //! [`flush`]: struct.LightCliOutput.html#method.flush 30 | //! [`Read`]: ../embedded_hal/serial/trait.Read.html 31 | //! 32 | //! ``` 33 | //! let _ = cl_in.fill(&mut rx); 34 | //! let _ = cl_out.flush(); 35 | //! ``` 36 | //! 37 | //! Periodically parse the data in the buffer using the [`lightcli!`] macro: 38 | //! 39 | //! [`lightcli!`]: macro.lightcli.html 40 | //! 41 | //! ``` 42 | //! let mut name : String = String;:new(); 43 | //! 44 | //! loop { 45 | //! /* fill, flush, etc. */ 46 | //! 47 | //! lightcli!(cl_in, cl_out, cmd, key, val, [ 48 | //! "HELLO" => [ 49 | //! "Name" => name = String::from(val) 50 | //! ] => { writeln!(cl_out, "Name set").unwrap(); }; 51 | //! "EHLO" => [ 52 | //! ] => { writeln!(cl_out, "EHLO Name={}", name.as_str()).unwrap(); } 53 | //! ]); 54 | //! } 55 | //! ``` 56 | //! 57 | //! A serial communication may then look like: 58 | //! 59 | //! ``` 60 | //! >> EHLO 61 | //! << EHLO Name= 62 | //! >> HELLO Name=Johnson 63 | //! << Name set 64 | //! >> EHLO 65 | //! << EHLO Name=Johnson 66 | //! ``` 67 | //! 68 | //! # Examples 69 | //! 70 | //! See the [examples] module. 71 | //! 72 | //! [examples]: examples/index.html 73 | 74 | #![no_std] 75 | pub extern crate embedded_hal as hal; 76 | pub extern crate nb; 77 | pub extern crate heapless; 78 | 79 | #[macro_use] 80 | mod macros; 81 | mod tokenizer; 82 | mod lexer; 83 | mod output; 84 | mod input; 85 | 86 | #[cfg(feature = "doc")] 87 | pub mod examples; 88 | 89 | #[cfg(test)] 90 | mod tests; 91 | 92 | pub use lexer::CallbackCommand; 93 | 94 | pub use output::LightCliOutput; 95 | pub use input::LightCliInput; 96 | 97 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | 2 | /// This macro allows for an easy way to define key value commands while 3 | /// still allowing to define custom error handlers. 4 | /// 5 | /// # Arguments 6 | /// * `$cli`: The [`LightCliInput`] instance to parse data from. 7 | /// * `$cmd`: The identifier to use to access the current command. 8 | /// * `$key`: The identifier to use to access the curernt key. 9 | /// * `$val`: The identifier to use to access the curernt value. 10 | /// * `$cmdv`: The name of the command. 11 | /// * `$keyv`: The key for command `$cmdv`. 12 | /// * `$action`: What to do with the value `$val` for the given command and key. 13 | /// * `$done`: What to do when the command is complete. 14 | /// * `$nomatch1`: What to do when the command value is not found 15 | /// while trying to find a key action. 16 | /// * `$nomatch2`: What to do when the key value is not found 17 | /// while trying to find a key action. 18 | /// * `$nomatch3`: What to do when the command value is not found 19 | /// while trying to execute a command. 20 | /// 21 | /// [`LightCliInput`]: struct.LightCliInput.html 22 | /// 23 | /// # Remarks 24 | /// For a simpler way to write a command see the macro [`lightcli!`]. 25 | /// This macro makes use of the underlying function [`parse_data`]. 26 | /// 27 | /// [`lightcli_adv!`]: macro.lightcli_adv.html 28 | /// [`parse_data`]: struct.LightCliInput.html#method.parse_data 29 | #[macro_export] 30 | macro_rules! lightcli_adv { 31 | ($cli:expr, $cmd:ident, $key:ident, $val:ident, [ 32 | $( 33 | $cmdv:pat => [ 34 | $( $keyv:pat => $action:expr ),* 35 | ] => $done:expr 36 | );* 37 | ], $nomatch1:expr, $nomatch2:expr, $nomatch3:expr) => { 38 | let _ = $cli.parse_data(|cbcmd| { 39 | match cbcmd { 40 | $crate::CallbackCommand::Attribute($cmd, $key, $val) => { 41 | match $cmd { 42 | $( 43 | $cmdv => { 44 | match $key { 45 | $( 46 | $keyv => { $action }, 47 | )* 48 | _ => $nomatch2, 49 | } 50 | } 51 | )* 52 | _ => $nomatch1, 53 | } 54 | }, 55 | $crate::CallbackCommand::Command($cmd) => { 56 | match $cmd { 57 | $( 58 | $cmdv => $done, 59 | )* 60 | _ => $nomatch3, 61 | } 62 | } 63 | } 64 | }); 65 | }; 66 | } 67 | 68 | 69 | /// This macro allows for an easy way to define key value commands. 70 | /// 71 | /// # Arguments 72 | /// * `$cli`: The [`LightCliInput`] instance to parse data from. 73 | /// * `$cl_out`: The [`LightCliOutput`] instance to write errors to. 74 | /// * `$cmd`: The identifier to use to access the current command. 75 | /// * `$key`: The identifier to use to access the curernt key. 76 | /// * `$val`: The identifier to use to access the curernt value. 77 | /// * `$cmdv`: The name of the command. 78 | /// * `$keyv`: The key for command `$cmdv`. 79 | /// * `$action`: What to do with the value `$val` for the given command and key. 80 | /// * `$done`: What to do when the command is complete. 81 | /// 82 | /// [`LightCliInput`]: struct.LightCliInput.html 83 | /// [`LightCliOutput`]: struct.LightCliOutput.html 84 | /// 85 | /// # Remarks 86 | /// For a command that doesn't use the output and allows for custom 87 | /// error handling see the macro [`lightcli_adv!`]. This macro makes use 88 | /// of the underlying function [`parse_data`]. 89 | /// 90 | /// [`lightcli_adv!`]: macro.lightcli_adv.html 91 | /// [`parse_data`]: struct.LightCliInput.html#method.parse_data 92 | #[macro_export] 93 | macro_rules! lightcli { 94 | ($cli:expr, $cl_out:expr, $cmd:ident, $key:ident, $val:ident, [ 95 | $( 96 | $cmdv:pat => [ 97 | $( $keyv:pat => $action:expr ),* 98 | ] => $done:expr 99 | );* 100 | ]) => { 101 | lightcli_adv!($cli, $cmd, $key, $val, [ 102 | $( 103 | $cmdv => [ 104 | $( $keyv => $action ),* 105 | ] => $done 106 | );* 107 | ], 108 | {}, 109 | {writeln!($cl_out, "Unknown key for command {}: {}", $cmd, $key).unwrap()}, 110 | {writeln!($cl_out, "Unknown command: {}", $cmd).unwrap()} 111 | ); 112 | }; 113 | } -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | use core; 2 | 3 | use nb; 4 | use hal::serial::Write; 5 | 6 | use heapless::consts::*; 7 | use heapless::spsc::Queue; 8 | 9 | pub struct LightCliOutput<'a, E: 'a> { 10 | rb: Queue, 11 | writer: &'a mut Write 12 | } 13 | 14 | impl<'a, E> core::fmt::Write for LightCliOutput<'a, E> { 15 | fn write_str(&mut self, s: &str) -> core::fmt::Result { 16 | for c in s.as_bytes() { 17 | loop { 18 | if self.rb.enqueue(c.clone()).is_ok() { 19 | break; 20 | } else { 21 | match self.flush() { 22 | Err(nb::Error::Other(_)) => return Err(core::fmt::Error), 23 | _ => () // otherwise either non blocking or ok, so try to repeat 24 | } 25 | } 26 | } 27 | } 28 | Ok(()) 29 | } 30 | } 31 | 32 | impl<'a, E> LightCliOutput<'a, E> { 33 | /// Creates a now buffered console output instance. 34 | /// 35 | /// # Arguments 36 | /// * `writer`: The serial output instance, implementing the [`Write`] interface. 37 | /// 38 | /// [`Write`]: ../embedded_hal/serial/trait.Write.html 39 | pub fn new(writer: &'a mut Write) -> Self { 40 | Self { 41 | rb: Queue::new(), 42 | writer: writer 43 | } 44 | } 45 | 46 | fn peek(&self) -> Option { 47 | match self.rb.iter().next() { 48 | None => None, 49 | Some(v) => Some(v.clone()) 50 | } 51 | } 52 | 53 | /// Tries to send as many characters as it can until the interface 54 | /// starts blocking or there are no charactors to submit. 55 | /// 56 | /// # Remarks 57 | /// 58 | /// If the function returns Ok, then the buffer has succesfully been flushed 59 | /// whereas the error `WouldBlock` indicates that it is not empty 60 | /// but would have blocked if it tried to submit the character. 61 | /// 62 | /// To completely empty the buffer, use `block!(cl_output.flush()).unwrap()`. 63 | pub fn flush(&mut self) -> nb::Result<(), E> { 64 | let mut co = self.peek(); 65 | 66 | loop { 67 | match co { 68 | None => return Ok(()), 69 | Some(c) => { 70 | let res = self.writer.write(c.clone()); 71 | match res { 72 | Err(nb::Error::WouldBlock) => return Err(nb::Error::WouldBlock), 73 | Err(nb::Error::Other(o)) => return Err(nb::Error::Other(o)), 74 | Ok(()) => { 75 | self.rb.dequeue().unwrap(); 76 | co = self.peek(); 77 | }, 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | 2 | use hal::serial::Read; 3 | use heapless::consts::*; 4 | use heapless::RingBuffer; 5 | use heapless::String; 6 | use nb; 7 | 8 | use LightCliInput; 9 | use CallbackCommand; 10 | 11 | pub struct SerialBufferDevice { 12 | pub rb: RingBuffer, 13 | } 14 | 15 | #[allow(dead_code)] 16 | #[derive(Debug)] 17 | pub enum Error { 18 | None 19 | } 20 | 21 | impl Read for SerialBufferDevice { 22 | type Error = Error; 23 | 24 | fn read(&mut self) -> nb::Result { 25 | match self.rb.dequeue(){ 26 | Some(a) => Ok(a), 27 | None => Err(nb::Error::WouldBlock) 28 | } 29 | } 30 | } 31 | 32 | impl SerialBufferDevice { 33 | pub fn write_str(&mut self, s: &str) { 34 | let s : String = String::from(s); 35 | self.write(s.as_bytes()); 36 | } 37 | 38 | #[allow(dead_code)] 39 | pub fn write_one(&mut self, b: u8) { 40 | self.rb.enqueue(b).unwrap() 41 | } 42 | 43 | pub fn write(&mut self, dat: &[u8]) { 44 | for b in dat { 45 | self.rb.enqueue(*b).unwrap() 46 | } 47 | } 48 | } 49 | 50 | #[test] 51 | pub fn test1() { 52 | let mut sb = SerialBufferDevice { 53 | rb: RingBuffer::new() 54 | }; 55 | let mut cli : LightCliInput = LightCliInput::new(); 56 | 57 | sb.write_str("HELLO TE❤ST=50\n"); 58 | cli.fill(&mut sb).unwrap(); 59 | 60 | let mut ran = false; 61 | let mut done = false; 62 | 63 | let _ = cli.parse_data(|cbcmd| { 64 | match cbcmd { 65 | CallbackCommand::Attribute(cmd, key, val) => { 66 | assert!(cmd == "HELLO", "cmd={}", cmd); 67 | assert!(key == "TE❤ST", "key={}", key); 68 | assert!(val == "50", "val={}", val); 69 | ran = true; 70 | }, 71 | CallbackCommand::Command(cmd) => { 72 | assert!(cmd == "HELLO", "cmd={}", cmd); 73 | assert!(!done); 74 | done = true; 75 | } 76 | } 77 | }); 78 | 79 | assert!(ran); 80 | assert!(done); 81 | } 82 | 83 | #[test] 84 | pub fn test_win() { 85 | let mut sb = SerialBufferDevice { 86 | rb: RingBuffer::new() 87 | }; 88 | let mut cli : LightCliInput = LightCliInput::new(); 89 | 90 | sb.write_str("HELLO TE❤ST=50\r\n"); 91 | cli.fill(&mut sb).unwrap(); 92 | 93 | let mut ran = false; 94 | let mut done = false; 95 | 96 | let _ = cli.parse_data(|cbcmd| { 97 | match cbcmd { 98 | CallbackCommand::Attribute(cmd, key, val) => { 99 | assert!(cmd == "HELLO", "cmd={}", cmd); 100 | assert!(key == "TE❤ST", "key={}", key); 101 | assert!(val == "50", "val={}", val); 102 | ran = true; 103 | }, 104 | CallbackCommand::Command(cmd) => { 105 | assert!(cmd == "HELLO", "cmd={}", cmd); 106 | assert!(!done); 107 | done = true; 108 | } 109 | } 110 | }); 111 | 112 | assert!(ran); 113 | assert!(done); 114 | } 115 | 116 | 117 | #[test] 118 | pub fn test_partial() { 119 | let mut sb = SerialBufferDevice { 120 | rb: RingBuffer::new() 121 | }; 122 | let mut cli : LightCliInput = LightCliInput::new(); 123 | 124 | // fill the buffer with some data 125 | sb.write_str("HELLO TE❤ST=50 Five=A"); 126 | cli.fill(&mut sb).unwrap(); 127 | 128 | let mut ran = false; 129 | let mut done = false; 130 | 131 | // should only parse first argument, second is enver called 132 | let _ = cli.parse_data(|cbcmd| { 133 | match cbcmd { 134 | CallbackCommand::Attribute(cmd, key, val) => { 135 | assert!(cmd == "HELLO", "cmd={}", cmd); 136 | assert!(key == "TE❤ST", "key={}", key); 137 | assert!(val == "50", "val={}", val); 138 | ran = true; 139 | }, 140 | CallbackCommand::Command(_cmd) => { 141 | assert!(false, "Command isn't finished."); 142 | } 143 | } 144 | }); 145 | 146 | assert!(ran); 147 | 148 | // finish command 149 | sb.write_str("\n"); 150 | cli.fill(&mut sb).unwrap(); 151 | 152 | ran = false; 153 | 154 | let _ = cli.parse_data(|cbcmd| { 155 | match cbcmd { 156 | CallbackCommand::Attribute(cmd, key, val) => { 157 | assert!(cmd == "HELLO", "cmd={}", cmd); 158 | assert!(key == "Five", "key={}", key); 159 | assert!(val == "A", "val={}", val); 160 | ran = true; 161 | }, 162 | CallbackCommand::Command(cmd) => { 163 | assert!(cmd == "HELLO", "cmd={}", cmd); 164 | done = true; 165 | } 166 | } 167 | }); 168 | 169 | assert!(ran); 170 | assert!(done); 171 | } 172 | 173 | #[test] 174 | pub fn test_macro() { 175 | 176 | let mut sb = SerialBufferDevice { 177 | rb: RingBuffer::new() 178 | }; 179 | let mut cli : LightCliInput = LightCliInput::new(); 180 | 181 | sb.write_str("HELLO Name=Foo\n"); 182 | cli.fill(&mut sb).unwrap(); 183 | 184 | let mut ran = false; 185 | let mut done = false; 186 | 187 | let mut name : String = String::new(); 188 | 189 | lightcli_adv!(cli, cmd, key, val, [ 190 | "HELLO" => [ 191 | "Name" => { 192 | ran = true; 193 | name = String::from(val) 194 | } 195 | ] => {assert!(ran); done = true}; 196 | "EHLO" => [ 197 | ] => assert!(name == "Foo", "name = {}", name.as_str()) 198 | ], 199 | assert!(false, "Unknown cmd {}", cmd), 200 | assert!(false, "Unknown key {} for cmd {}", key, cmd), 201 | assert!(false, "Unknown cmd done {}", cmd) 202 | ); 203 | 204 | assert!(ran); 205 | assert!(done); 206 | } 207 | -------------------------------------------------------------------------------- /src/tokenizer.rs: -------------------------------------------------------------------------------- 1 | use core; 2 | 3 | use nb; 4 | use heapless; 5 | 6 | use hal::serial::Read; 7 | use heapless::consts::*; 8 | use heapless::spsc::Queue; 9 | use heapless::String; 10 | 11 | #[derive(Debug)] 12 | pub enum Error{ 13 | InvalidUTF8, 14 | InvalidCount, 15 | Overflow 16 | } 17 | 18 | pub struct Tokenizer where SLEN: heapless::ArrayLength { 19 | rb: Queue, 20 | nextstr: String, 21 | } 22 | 23 | pub enum Token<'a> { 24 | NewLine, 25 | CarriageReturn, 26 | Equals, 27 | Space, 28 | Value(&'a str), 29 | } 30 | 31 | impl Tokenizer 32 | where SLEN: heapless::ArrayLength { 33 | pub fn new() -> Self { 34 | Self { 35 | rb: Queue::new(), 36 | nextstr: String::new(), 37 | } 38 | } 39 | 40 | fn get_amount(&mut self, count : usize) -> nb::Result { 41 | if count > 4 { 42 | Err(nb::Error::Other(Error::InvalidCount)) 43 | } 44 | else if self.rb.len() < count { 45 | Err(nb::Error::WouldBlock) 46 | } else { 47 | let mut v : u32 = 0; 48 | 49 | for _ in 0..count { 50 | let b = self.rb.dequeue().unwrap() as u32; 51 | 52 | if b >> 3 == 0b11110 { 53 | v = (v << 3) | (b & 0b111) 54 | } else if b >> 4 == 0b1110 { 55 | v = (v << 4) | (b & 0b1111) 56 | } else if b >> 5 == 0b110 { 57 | v = (v << 5) | (b & 0b11111) 58 | } else if b >> 6 == 0b10 { 59 | v = (v << 6) | (b & 0b111111) 60 | } else { 61 | v = b 62 | } 63 | } 64 | Ok(core::char::from_u32(v).unwrap()) 65 | } 66 | } 67 | 68 | fn get_char(&mut self) -> nb::Result { 69 | if self.rb.is_empty() { 70 | Err(nb::Error::WouldBlock) 71 | } else { 72 | let b = *self.rb.iter().next().unwrap(); 73 | 74 | if b >> 3 == 0b11110 as u8 { 75 | Ok(self.get_amount(4).unwrap()) 76 | } else if b >> 4 == 0b1110 as u8 { 77 | Ok(self.get_amount(3).unwrap()) 78 | } else if b >> 5 == 0b110 as u8 { 79 | Ok(self.get_amount(2).unwrap()) 80 | } else if b >> 6 == 0b10 as u8 { 81 | Err(nb::Error::Other(Error::InvalidUTF8)) 82 | } else { 83 | Ok(self.rb.dequeue().unwrap() as char) 84 | } 85 | } 86 | } 87 | 88 | pub fn get_tokens(&mut self, mut callback : CB) -> nb::Result<(), Error> 89 | where CB: FnMut(Token) -> () { 90 | 91 | 92 | loop { 93 | let c = match self.get_char() { 94 | Err(e) => { 95 | return Err(e) 96 | }, 97 | Ok(c) => c 98 | }; 99 | 100 | let send_val = |callback: &mut CB, s: &mut String| { 101 | if !s.is_empty() { 102 | callback(Token::Value(s)) 103 | } 104 | s.clear(); 105 | }; 106 | 107 | match c { 108 | ' ' => { 109 | send_val(&mut callback, &mut self.nextstr); 110 | callback(Token::Space) 111 | }, 112 | '=' => { 113 | send_val(&mut callback, &mut self.nextstr); 114 | callback(Token::Equals) 115 | }, 116 | '\r' => { 117 | send_val(&mut callback, &mut self.nextstr); 118 | callback(Token::CarriageReturn) 119 | } 120 | '\n' => { 121 | send_val(&mut callback, &mut self.nextstr); 122 | callback(Token::NewLine) 123 | }, 124 | _ => { 125 | match self.nextstr.push(c) { 126 | // if we aren't able to push a char onto the string 127 | // it probably means it is full 128 | Err(_) => return Err(nb::Error::Other(Error::Overflow)), 129 | _ => () 130 | } 131 | } 132 | }; 133 | 134 | } 135 | } 136 | 137 | pub fn fill(&mut self, ser: &mut Read) -> nb::Result<(), E> { 138 | loop { 139 | let r = ser.read(); 140 | match r { 141 | Err(nb::Error::WouldBlock) => return Ok(()), 142 | Err(e) => return Err(e), 143 | Ok(c) => self.rb.enqueue(c).unwrap() 144 | } 145 | } 146 | } 147 | } 148 | --------------------------------------------------------------------------------