├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENCE-APACHE ├── LICENCE-MIT ├── README.md └── src ├── builder.rs ├── formatter.rs ├── lib.rs ├── parser.rs └── tuple_concat.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rustfmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | 50 | clippy: 51 | name: Clippy 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | ## [0.5.5] - 2024-09-26 10 | 11 | - Add raw parameter to command builder. 12 | 13 | ## [0.5.4] - 2023-09-18 14 | - Added optional parser parameters: [diondokter/at-commands#9](https://github.com/diondokter/at-commands/pull/9) 15 | - Changed &str to AsRef<[u8]> in CommandBuilder methods: [diondokter/at-commands#10](https://github.com/diondokter/at-commands/pull/10) 16 | 17 | ## [0.5.3] - 2022-09-11 18 | - More arguments are now supported: [diondokter/at-commands#6](https://github.com/diondokter/at-commands/pull/6) 19 | - Small bugfix for parsing string parameters that have no characters after it in the input string: [diondokter/at-commands#6](https://github.com/diondokter/at-commands/pull/6) 20 | 21 | ## [0.5.2] - 2022-07-27 22 | - Added optional defmt feature 23 | 24 | ## [0.5.1] - 2022-03-02 25 | ### Changed 26 | - The parser now trims all spaces after each identifier and parameter function making it more robust 27 | 28 | ## [0.5.0] - 2021-05-24 (~~[0.4.1] - 2021-05-23~~) 29 | ### Added 30 | - Integer parsing now discards the leading `+` character if present: [diondokter/at-commands#5](https://github.com/diondokter/at-commands/pull/5) 31 | 32 | ### Changed 33 | - The parser is no longer behind a feature gate and is always enabled 34 | 35 | ## [0.4.0] - 2020-11-16 36 | ### Added 37 | - Optional and empty parameter support added: [diondokter/at-commands#2](https://github.com/diondokter/at-commands/pull/2) 38 | - Experimental parser [diondokter/at-commands#3](https://github.com/diondokter/at-commands/pull/3) 39 | 40 | ## [0.3.0] - 2020-08-14 41 | ### Changed 42 | - **Breaking**: Command is now terminated with `\r\n` instead of `\n`. 43 | 44 | ### Added 45 | - The function `finish_with` has been added so users can choose their own termination. 46 | 47 | ## [0.2.1] - 2020-05-21 48 | ### Changed 49 | - All formatting (fmt) has been removed. (This drastically reduces the amount of flash memory used) 50 | - Removed dependency on ArrayVec because it was no longer needed. 51 | 52 | ## [0.2.0] - 2020-05-20 53 | ### Added 54 | - Ability to not have an AT prefix in the command. 55 | 56 | ### Fixed 57 | - The arrayvec dependency still used the std. Now changed to not use default features. 58 | 59 | ## [0.1.1] - 2020-04-13 60 | ### Added 61 | - Setup required for publishing the crate. 62 | 63 | ## [0.1.0] - 2020-04-13 64 | ### Added 65 | - Initial `CommandBuilder` implementation. 66 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "at-commands" 3 | version = "0.5.5" 4 | authors = ["Dion Dokter "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | description = "AT Commands builder and parser for Rust #![no_std]" 8 | homepage = "https://github.com/diondokter/at-commands" 9 | repository = "https://github.com/diondokter/at-commands" 10 | readme = "README.md" 11 | keywords = ["no_std", "AT", "Command", "Builder"] 12 | 13 | [dependencies] 14 | defmt = { version = "0.3", optional = true } 15 | 16 | [features] 17 | defmt = ["dep:defmt"] 18 | -------------------------------------------------------------------------------- /LICENCE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENCE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dion Dokter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AT Commands builder and parser for Rust #![no_std] [![crates.io](https://img.shields.io/crates/v/at-commands.svg)](https://crates.io/crates/at-commands) [![Documentation](https://docs.rs/at-commands/badge.svg)](https://docs.rs/at-commands) 2 | 3 | This crate can be used to build and parse at command style messages efficiently. 4 | 5 | Help would be appreciated! Interested in new features, efficiency improvements and API improvements. 6 | 7 | ## Usage 8 | 9 | Builder: 10 | ```rust 11 | use at_commands::builder::CommandBuilder; 12 | 13 | let mut buffer = [0; 128]; 14 | 15 | // Make a query command 16 | let result = CommandBuilder::create_query(&mut buffer, true) 17 | .named("+MYQUERY") 18 | .finish() 19 | .unwrap(); 20 | 21 | // Buffer now contains "AT+MYQUERY?" 22 | // Copy or DMA the resulting slice to the device. 23 | 24 | // Make a set command 25 | let result = CommandBuilder::create_set(&mut buffer, false) 26 | .named("+MYSET") 27 | .with_int_parameter(42) 28 | .finish() 29 | .unwrap(); 30 | 31 | // Buffer now contains "+MYSET=42" 32 | // Copy or DMA the resulting slice to the device. 33 | ``` 34 | 35 | Parser: 36 | ```rust 37 | use at_commands::parser::CommandParser; 38 | 39 | let (x, y, z) = CommandParser::parse(b"+SYSGPIOREAD:654,\"true\",-65154\r\nOK\r\n") 40 | .expect_identifier(b"+SYSGPIOREAD:") 41 | .expect_int_parameter() 42 | .expect_string_parameter() 43 | .expect_int_parameter() 44 | .expect_identifier(b"\r\nOK\r\n") 45 | .finish() 46 | .unwrap(); 47 | 48 | // x = 654 49 | // y = "true" 50 | // z = -65154 51 | ``` 52 | 53 | 54 | ## License 55 | 56 | Licensed under either of 57 | 58 | * Apache License, Version 2.0 59 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 60 | * MIT license 61 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 62 | 63 | at your option. 64 | 65 | ## Contribution 66 | 67 | Unless you explicitly state otherwise, any contribution intentionally submitted 68 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 69 | dual licensed as above, without any additional terms or conditions. -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of the CommandBuilder 2 | 3 | /// # CommandBuilder 4 | /// A builder struct for AT Commands 5 | /// 6 | /// ## Summary 7 | /// This can be used to build: 8 | /// * A test command in the form `AT{name}=?` 9 | /// * A query command in the form `AT{name}?` 10 | /// * A set command in the form `AT{name}={param},{param},{param}` 11 | /// * An execute command in the form `AT{name}` 12 | /// 13 | /// ## Example 14 | /// ```rust 15 | /// use at_commands::builder::CommandBuilder; 16 | /// 17 | /// let mut buffer = [0; 128]; 18 | /// 19 | /// // Make a query command 20 | /// let result = CommandBuilder::create_query(&mut buffer, true) 21 | /// .named("+MYQUERY") 22 | /// .finish() 23 | /// .unwrap(); 24 | /// 25 | /// // Buffer now contains "AT+MYQUERY?" 26 | /// // Copy or DMA the resulting slice to the device. 27 | /// 28 | /// // Make a set command 29 | /// let result = CommandBuilder::create_set(&mut buffer, false) 30 | /// .named("+MYSET") 31 | /// .with_int_parameter(42) 32 | /// .finish() 33 | /// .unwrap(); 34 | /// 35 | /// // Buffer now contains "+MYSET=42" 36 | /// // Copy or DMA the resulting slice to the device. 37 | /// ``` 38 | pub struct CommandBuilder<'a, STAGE> { 39 | buffer: &'a mut [u8], 40 | index: usize, 41 | phantom: core::marker::PhantomData, 42 | } 43 | 44 | impl<'a> CommandBuilder<'a, Uninitialized> { 45 | /// Creates a builder for a test command. 46 | /// 47 | /// The given buffer is used to build the command in and must be big enough to contain it. 48 | pub fn create_test( 49 | buffer: &'a mut [u8], 50 | at_prefix: bool, 51 | ) -> CommandBuilder<'a, Initialized> { 52 | let mut builder = CommandBuilder::<'a, Initialized> { 53 | buffer, 54 | index: 0, 55 | phantom: Default::default(), 56 | }; 57 | 58 | if at_prefix { 59 | builder.try_append_data(b"AT"); 60 | } 61 | 62 | builder 63 | } 64 | 65 | /// Creates a builder for a query command. 66 | /// 67 | /// The given buffer is used to build the command in and must be big enough to contain it. 68 | pub fn create_query( 69 | buffer: &'a mut [u8], 70 | at_prefix: bool, 71 | ) -> CommandBuilder<'a, Initialized> { 72 | let mut builder = CommandBuilder::<'a, Initialized> { 73 | buffer, 74 | index: 0, 75 | phantom: Default::default(), 76 | }; 77 | 78 | if at_prefix { 79 | builder.try_append_data(b"AT"); 80 | } 81 | 82 | builder 83 | } 84 | 85 | /// Creates a builder for a set command. 86 | /// 87 | /// The given buffer is used to build the command in and must be big enough to contain it. 88 | pub fn create_set( 89 | buffer: &'a mut [u8], 90 | at_prefix: bool, 91 | ) -> CommandBuilder<'a, Initialized> { 92 | let mut builder = CommandBuilder::<'a, Initialized> { 93 | buffer, 94 | index: 0, 95 | phantom: Default::default(), 96 | }; 97 | 98 | if at_prefix { 99 | builder.try_append_data(b"AT"); 100 | } 101 | 102 | builder 103 | } 104 | 105 | /// Creates a builder for an test execute. 106 | /// 107 | /// The given buffer is used to build the command in and must be big enough to contain it. 108 | pub fn create_execute( 109 | buffer: &'a mut [u8], 110 | at_prefix: bool, 111 | ) -> CommandBuilder<'a, Initialized> { 112 | let mut builder = CommandBuilder::<'a, Initialized> { 113 | buffer, 114 | index: 0, 115 | phantom: Default::default(), 116 | }; 117 | 118 | if at_prefix { 119 | builder.try_append_data(b"AT"); 120 | } 121 | 122 | builder 123 | } 124 | } 125 | impl<'a, ANY> CommandBuilder<'a, ANY> { 126 | /// Tries to append data to the buffer. 127 | /// 128 | /// If it won't fit, it silently fails and won't copy the data. 129 | /// The index field is incremented no matter what. 130 | fn try_append_data(&mut self, data: &[u8]) { 131 | let data_length = data.len(); 132 | 133 | // Why not just use copy_from_slice? 134 | // That can give a panic and thus dumps a lot of fmt code in the binary. 135 | // The compiler can check every aspect of this and so the code will never panic. 136 | 137 | // Does the buffer have enough space left? 138 | if let Some(buffer_slice) = self.buffer.get_mut(self.index..(self.index + data_length)) { 139 | // Yes, zip the buffer with the data 140 | for (buffer, data) in buffer_slice.iter_mut().zip(data) { 141 | // Copy over the bytes. 142 | *buffer = *data; 143 | } 144 | } 145 | 146 | // Increment the index 147 | self.index += data_length; 148 | } 149 | } 150 | 151 | impl<'a, N: Nameable> CommandBuilder<'a, Initialized> { 152 | /// Set the name of the command. 153 | pub fn named>(mut self, name: T) -> CommandBuilder<'a, N> { 154 | self.try_append_data(name.as_ref()); 155 | self.try_append_data(N::NAME_SUFFIX); 156 | 157 | CommandBuilder::<'a, N> { 158 | buffer: self.buffer, 159 | index: self.index, 160 | phantom: Default::default(), 161 | } 162 | } 163 | } 164 | 165 | impl<'a> CommandBuilder<'a, Set> { 166 | /// Add an integer parameter. 167 | pub fn with_int_parameter>(mut self, value: INT) -> Self { 168 | let mut formatting_buffer = [0; crate::formatter::MAX_INT_DIGITS]; 169 | self.try_append_data(crate::formatter::write_int( 170 | &mut formatting_buffer, 171 | value.into(), 172 | )); 173 | self.try_append_data(b","); 174 | self 175 | } 176 | 177 | /// Add a string parameter 178 | pub fn with_string_parameter>(mut self, value: T) -> Self { 179 | self.try_append_data(b"\""); 180 | self.try_append_data(value.as_ref()); 181 | self.try_append_data(b"\""); 182 | self.try_append_data(b","); 183 | self 184 | } 185 | 186 | /// Add an optional integer parameter. 187 | pub fn with_optional_int_parameter>(self, value: Option) -> Self { 188 | match value { 189 | None => self.with_empty_parameter(), 190 | Some(value) => self.with_int_parameter(value), 191 | } 192 | } 193 | 194 | /// Add an optional string parameter. 195 | pub fn with_optional_string_parameter>(self, value: Option) -> Self { 196 | match value { 197 | None => self.with_empty_parameter(), 198 | Some(value) => self.with_string_parameter(value), 199 | } 200 | } 201 | 202 | /// Add a comma, representing an unset optional parameter. 203 | pub fn with_empty_parameter(mut self) -> Self { 204 | self.try_append_data(b","); 205 | self 206 | } 207 | 208 | /// Add an unformatted parameter 209 | pub fn with_raw_parameter>(mut self, value: T) -> Self { 210 | self.try_append_data(value.as_ref()); 211 | self.try_append_data(b","); 212 | self 213 | } 214 | } 215 | 216 | impl<'a, F: Finishable> CommandBuilder<'a, F> { 217 | /// Finishes the builder. 218 | /// 219 | /// When Ok, it returns a slice with the built command. 220 | /// The slice points to the same memory as the buffer, 221 | /// but is only as long as is required to contain the command. 222 | /// 223 | /// The command length is thus the length of the slice. 224 | /// 225 | /// If the buffer was not long enough, 226 | /// then an Err is returned with the size that was required for it to succeed. 227 | pub fn finish(self) -> Result<&'a [u8], usize> { 228 | self.finish_with(b"\r\n") 229 | } 230 | 231 | /// Finishes the builder. 232 | /// 233 | /// With the terminator variable, you can decide how to end the command. 234 | /// Normally this is `\r\n`. 235 | /// 236 | /// ```rust 237 | /// use at_commands::builder::CommandBuilder; 238 | /// 239 | /// let mut buffer = [0; 128]; 240 | /// 241 | /// // Make a query command 242 | /// let result = CommandBuilder::create_query(&mut buffer, true) 243 | /// .named("+MYQUERY") 244 | /// .finish_with(b"\0") 245 | /// .unwrap(); 246 | /// ``` 247 | /// 248 | /// When Ok, it returns a slice with the built command. 249 | /// The slice points to the same memory as the buffer, 250 | /// but is only as long as is required to contain the command. 251 | /// 252 | /// The command length is thus the length of the slice. 253 | /// 254 | /// If the buffer was not long enough, 255 | /// then an Err is returned with the size that was required for it to succeed. 256 | pub fn finish_with(mut self, terminator: &[u8]) -> Result<&'a [u8], usize> { 257 | // if last byte is a comma, decrement index to drop it 258 | if let Some(c) = self.buffer.get(self.index - 1) { 259 | if *c == b',' { 260 | self.index -= 1; 261 | } 262 | } 263 | self.try_append_data(terminator); 264 | 265 | if self.index > self.buffer.len() { 266 | Err(self.index) 267 | } else { 268 | Ok(&self.buffer[0..self.index]) 269 | } 270 | } 271 | } 272 | 273 | /// Marker struct for uninitialized builders. 274 | pub struct Uninitialized; 275 | /// Marker struct for initialized builders. 276 | /// The T type is the type the builder will be marked after it has been named. 277 | pub struct Initialized(core::marker::PhantomData); 278 | 279 | /// Marker struct for builders that produce a test command. 280 | pub struct Test; 281 | /// Marker struct for builders that produce a query command. 282 | pub struct Query; 283 | /// Marker struct for builders that produce a set command. 284 | pub struct Set; 285 | /// Marker struct for builders that produce a execute command. 286 | pub struct Execute; 287 | 288 | /// A trait that can be implemented for marker structs to indicate that the command is ready to be finished. 289 | pub trait Finishable {} 290 | impl Finishable for Test {} 291 | impl Finishable for Query {} 292 | impl Finishable for Set {} 293 | impl Finishable for Execute {} 294 | 295 | /// A trait that can be implemented for marker structs to indicate that the command is ready to be named. 296 | pub trait Nameable { 297 | /// The data that must be put after a name to comply with the type of command that is named. 298 | const NAME_SUFFIX: &'static [u8]; 299 | } 300 | impl Nameable for Test { 301 | const NAME_SUFFIX: &'static [u8] = b"=?"; 302 | } 303 | impl Nameable for Query { 304 | const NAME_SUFFIX: &'static [u8] = b"?"; 305 | } 306 | impl Nameable for Set { 307 | const NAME_SUFFIX: &'static [u8] = b"="; 308 | } 309 | impl Nameable for Execute { 310 | const NAME_SUFFIX: &'static [u8] = b""; 311 | } 312 | 313 | #[cfg(test)] 314 | mod tests { 315 | use super::*; 316 | 317 | #[test] 318 | fn test_command() { 319 | let mut buffer = [0; 128]; 320 | let value = CommandBuilder::create_test(&mut buffer, true) 321 | .named("+TEST") 322 | .finish() 323 | .unwrap(); 324 | 325 | assert_eq!(core::str::from_utf8(value).unwrap(), "AT+TEST=?\r\n"); 326 | } 327 | 328 | #[test] 329 | fn test_query() { 330 | let mut buffer = [0; 128]; 331 | let value = CommandBuilder::create_query(&mut buffer, true) 332 | .named("+QUERY") 333 | .finish() 334 | .unwrap(); 335 | 336 | assert_eq!(core::str::from_utf8(value).unwrap(), "AT+QUERY?\r\n"); 337 | } 338 | 339 | #[test] 340 | fn test_set() { 341 | let mut buffer = [0; 128]; 342 | let value = CommandBuilder::create_set(&mut buffer, true) 343 | .named("+SET") 344 | .with_int_parameter(12345) 345 | .with_string_parameter("my_string_param") 346 | .with_int_parameter(67) 347 | .with_int_parameter(89) 348 | .finish() 349 | .unwrap(); 350 | 351 | assert_eq!( 352 | core::str::from_utf8(value).unwrap(), 353 | "AT+SET=12345,\"my_string_param\",67,89\r\n" 354 | ); 355 | } 356 | 357 | #[test] 358 | fn test_execute() { 359 | let mut buffer = [0; 128]; 360 | let value = CommandBuilder::create_execute(&mut buffer, true) 361 | .named("+EXECUTE") 362 | .finish() 363 | .unwrap(); 364 | 365 | assert_eq!(core::str::from_utf8(value).unwrap(), "AT+EXECUTE\r\n"); 366 | } 367 | 368 | #[test] 369 | fn test_buffer_too_short() { 370 | let mut buffer = [0; 5]; 371 | assert!(CommandBuilder::create_execute(&mut buffer, true) 372 | .named("+BUFFERLENGTH") 373 | .finish() 374 | .is_err()); 375 | assert!(CommandBuilder::create_execute(&mut buffer, true) 376 | .named("+A") 377 | .finish() 378 | .is_err()); // too short by only one byte 379 | } 380 | 381 | #[test] 382 | fn test_buffer_exact_size() { 383 | let mut buffer = [0; 32]; 384 | let value = CommandBuilder::create_execute(&mut buffer[..8], true) 385 | .named("+GMR") 386 | .finish() 387 | .unwrap(); 388 | 389 | assert_eq!(core::str::from_utf8(value).unwrap(), "AT+GMR\r\n"); 390 | 391 | let value = CommandBuilder::create_set(&mut buffer[..19], true) 392 | .named("+CWRECONNCFG") 393 | .with_int_parameter(15) 394 | .finish() 395 | .unwrap(); 396 | 397 | assert_eq!( 398 | core::str::from_utf8(value).unwrap(), 399 | "AT+CWRECONNCFG=15\r\n" 400 | ); 401 | 402 | let value = CommandBuilder::create_query(&mut buffer[..14], true) 403 | .named("+UART_CUR") 404 | .finish() 405 | .unwrap(); 406 | 407 | assert_eq!(core::str::from_utf8(value).unwrap(), "AT+UART_CUR?\r\n"); 408 | } 409 | 410 | #[test] 411 | fn test_terminator() { 412 | let mut buffer = [0; 128]; 413 | let value = CommandBuilder::create_test(&mut buffer, true) 414 | .named("+TEST") 415 | .finish_with(b"\0") 416 | .unwrap(); 417 | 418 | assert_eq!(core::str::from_utf8(value).unwrap(), "AT+TEST=?\0"); 419 | } 420 | 421 | #[test] 422 | fn test_optional() { 423 | let mut buffer = [0; 128]; 424 | let value = CommandBuilder::create_set(&mut buffer, true) 425 | .named("+CCUG") 426 | .with_empty_parameter() 427 | .with_optional_int_parameter(Some(9)) 428 | .finish_with(b"\r") 429 | .unwrap(); 430 | // see https://www.multitech.com/documents/publications/manuals/s000453c.pdf 431 | // pages 8 and 85 for command and 150 for CR ending 432 | assert_eq!(core::str::from_utf8(value).unwrap(), "AT+CCUG=,9\r"); 433 | 434 | let value = CommandBuilder::create_set(&mut buffer, true) 435 | .named("+BLEGATTSSETATTR") 436 | .with_int_parameter(1) 437 | .with_int_parameter(1) 438 | .with_empty_parameter() 439 | .with_int_parameter(4) 440 | .finish() 441 | .unwrap(); 442 | // https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/BLE_AT_Commands.html#cmd-GSSETA 443 | assert_eq!( 444 | core::str::from_utf8(value).unwrap(), 445 | "AT+BLEGATTSSETATTR=1,1,,4\r\n" 446 | ); 447 | 448 | let value = CommandBuilder::create_set(&mut buffer, true) 449 | .named("+HTTPCLIENT") 450 | .with_int_parameter(2) 451 | .with_int_parameter(1) 452 | .with_optional_string_parameter(Some("http://localpc/ip")) 453 | .with_empty_parameter() 454 | .with_empty_parameter() 455 | .with_int_parameter(1) 456 | .finish() 457 | .unwrap(); 458 | 459 | assert_eq!( 460 | core::str::from_utf8(value).unwrap(), 461 | "AT+HTTPCLIENT=2,1,\"http://localpc/ip\",,,1\r\n" 462 | ); 463 | } 464 | 465 | #[test] 466 | fn test_raw_parameter() { 467 | let mut buffer = [0; 128]; 468 | let value = CommandBuilder::create_set(&mut buffer, true) 469 | .named("+CPIN") 470 | .with_raw_parameter(b"1234") 471 | .with_optional_int_parameter(Some(9)) 472 | .finish_with(b"\r") 473 | .unwrap(); 474 | assert_eq!(core::str::from_utf8(value).unwrap(), "AT+CPIN=1234,9\r"); 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /src/formatter.rs: -------------------------------------------------------------------------------- 1 | //! This module contains some helper functions to avoid having to call into the expensive fmt code. 2 | 3 | pub const MAX_INT_DIGITS: usize = 11; 4 | 5 | /// Writes ascii bytes to the buffer to represent the given int value. 6 | /// 7 | /// Returns the slice of the buffer that was written to. 8 | /// It can be used as a value or to determine the length of the formatting. 9 | /// 10 | /// Panics if the buffer is less than [MAX_INT_DIGITS] long. 11 | pub fn write_int(buffer: &mut [u8], mut value: i32) -> &mut [u8] { 12 | // Check in debug mode if the buffer is long enough. 13 | // We don't do this in release to have less overhead. 14 | debug_assert!(buffer.len() >= MAX_INT_DIGITS); 15 | 16 | let mut buffer_index = 0; 17 | let is_negative = value.is_negative(); 18 | 19 | // We want a negative value because that can hold every absolute value. 20 | if !is_negative { 21 | value = -value; 22 | } 23 | 24 | // Special case for 0 25 | if value == 0 { 26 | buffer[buffer_index] = b'0'; 27 | buffer_index += 1; 28 | } 29 | 30 | // Write the smallest digit to the buffer. 31 | // This will put it in there in reverse. 32 | while value != 0 { 33 | // The value is negative, so invert the smallest digit, offset it with the 0 character 34 | // and put it in the buffer. 35 | buffer[buffer_index] = b'0' + -(value % 10) as u8; 36 | buffer_index += 1; 37 | // Divide the value to get rid of the smallest digit. 38 | value /= 10; 39 | } 40 | 41 | if is_negative { 42 | // Don't forget to put the minus sign there. 43 | buffer[buffer_index] = b'-'; 44 | buffer_index += 1; 45 | } 46 | 47 | // We built the buffer in reverse, so now we've got to undo that. 48 | buffer[0..buffer_index].reverse(); 49 | 50 | &mut buffer[0..buffer_index] 51 | } 52 | 53 | /// Parses an int 54 | pub fn parse_int(mut buffer: &[u8]) -> Option { 55 | if buffer.is_empty() || buffer.len() > MAX_INT_DIGITS { 56 | return None; 57 | } 58 | 59 | let is_negative = buffer[0] == b'-'; 60 | 61 | if is_negative { 62 | buffer = &buffer[1..]; 63 | } 64 | 65 | let mut value = 0; 66 | for char in buffer.iter() { 67 | if *char < b'0' || *char > b'9' { 68 | return None; 69 | } else { 70 | value *= 10; 71 | value -= (*char - b'0') as i32; 72 | } 73 | } 74 | 75 | if is_negative { 76 | Some(value) 77 | } else { 78 | Some(-value) 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | 86 | #[test] 87 | fn test_write_int() { 88 | let mut buffer = [0; 128]; 89 | 90 | assert_eq!(write_int(&mut buffer, 0), b"0"); 91 | assert_eq!(write_int(&mut buffer, -1), b"-1"); 92 | assert_eq!(write_int(&mut buffer, 1), b"1"); 93 | assert_eq!(write_int(&mut buffer, -42), b"-42"); 94 | assert_eq!(write_int(&mut buffer, 42), b"42"); 95 | assert_eq!(write_int(&mut buffer, -2147483648), b"-2147483648"); 96 | assert_eq!(write_int(&mut buffer, 2147483647), b"2147483647"); 97 | } 98 | 99 | #[test] 100 | fn test_parse_int() { 101 | assert_eq!(parse_int(b"0"), Some(0)); 102 | assert_eq!(parse_int(b"-1"), Some(-1)); 103 | assert_eq!(parse_int(b"1"), Some(1)); 104 | assert_eq!(parse_int(b"-42"), Some(-42)); 105 | assert_eq!(parse_int(b"42"), Some(42)); 106 | assert_eq!(parse_int(b"-2147483648"), Some(-2147483648)); 107 | assert_eq!(parse_int(b"2147483647"), Some(2147483647)); 108 | 109 | assert_eq!(parse_int(b""), None); 110 | assert_eq!(parse_int(b"abc"), None); 111 | assert_eq!(parse_int(b"-b"), None); 112 | assert_eq!(parse_int(b"123456a"), None); 113 | assert_eq!(parse_int(b"z12354"), None); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Crate for building and parsing AT Commands 2 | 3 | #![cfg_attr(not(test), no_std)] 4 | #![deny(missing_docs)] 5 | 6 | pub mod builder; 7 | pub(crate) mod formatter; 8 | pub mod parser; 9 | pub(crate) mod tuple_concat; 10 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | //! Module that defines the at command parser 2 | 3 | use crate::tuple_concat::TupleConcat; 4 | 5 | /// ``` 6 | /// use at_commands::parser::CommandParser; 7 | /// let (x, y, z) = CommandParser::parse(b"+SYSGPIOREAD:654,\"true\",-65154\r\nOK\r\n") 8 | /// .expect_identifier(b"+SYSGPIOREAD:") 9 | /// .expect_int_parameter() 10 | /// .expect_string_parameter() 11 | /// .expect_int_parameter() 12 | /// .expect_identifier(b"\r\nOK\r\n") 13 | /// .finish() 14 | /// .unwrap(); 15 | /// 16 | /// assert_eq!(x, 654); 17 | /// assert_eq!(y, "true"); 18 | /// assert_eq!(z, -65154); 19 | /// 20 | /// let (w,) = CommandParser::parse(b"+STATUS: READY\r\nOK\r\n") 21 | /// .expect_identifier(b"+STATUS: ") 22 | /// .expect_raw_string() 23 | /// .expect_identifier(b"\r\nOK\r\n") 24 | /// .finish() 25 | /// .unwrap(); 26 | /// 27 | /// assert_eq!(w, "READY"); 28 | /// ``` 29 | #[must_use] 30 | pub struct CommandParser<'a, D> { 31 | buffer: &'a [u8], 32 | buffer_index: usize, 33 | data_valid: bool, 34 | data: D, 35 | } 36 | 37 | impl<'a> CommandParser<'a, ()> { 38 | /// Start parsing the command 39 | pub fn parse(buffer: &'a [u8]) -> CommandParser<'a, ()> { 40 | CommandParser { 41 | buffer, 42 | buffer_index: 0, 43 | data_valid: true, 44 | data: (), 45 | } 46 | } 47 | } 48 | impl<'a, D> CommandParser<'a, D> { 49 | /// Tries reading an identifier 50 | pub fn expect_identifier(mut self, identifier: &[u8]) -> Self { 51 | // If we're already not valid, then quit 52 | if !self.data_valid { 53 | return self; 54 | } 55 | 56 | if self.buffer[self.buffer_index..].len() < identifier.len() { 57 | self.data_valid = false; 58 | return self; 59 | } 60 | 61 | // Zip together the identifier and the buffer data. If all bytes are the same, the data is valid. 62 | self.data_valid = self.buffer[self.buffer_index..] 63 | .iter() 64 | .zip(identifier) 65 | .all(|(buffer, id)| *buffer == *id); 66 | // Advance the index 67 | self.buffer_index += identifier.len(); 68 | 69 | self.trim_space() 70 | } 71 | 72 | /// Tries reading an optional identifier. 73 | pub fn expect_optional_identifier(mut self, identifier: &[u8]) -> Self { 74 | // If we're already not valid, then quit 75 | if !self.data_valid { 76 | return self; 77 | } 78 | 79 | // empty identifier is always valid 80 | if self.buffer[self.buffer_index..].is_empty() { 81 | return self; 82 | } 83 | 84 | if self.buffer[self.buffer_index..].len() < identifier.len() { 85 | self.data_valid = false; 86 | return self; 87 | } 88 | 89 | // Zip together the identifier and the buffer data. If all bytes are the same, the data is valid. 90 | self.data_valid = self.buffer[self.buffer_index..] 91 | .iter() 92 | .zip(identifier) 93 | .all(|(buffer, id)| *buffer == *id); 94 | // Advance the index 95 | self.buffer_index += identifier.len(); 96 | 97 | self.trim_space() 98 | } 99 | 100 | /// Moves the internal buffer index over the next bit of space characters, if any 101 | fn trim_space(mut self) -> Self { 102 | // If we're already not valid, then quit 103 | if !self.data_valid { 104 | return self; 105 | } 106 | 107 | while let Some(c) = self.buffer.get(self.buffer_index) { 108 | if *c == b' ' { 109 | self.buffer_index += 1; 110 | } else { 111 | break; 112 | } 113 | } 114 | 115 | self 116 | } 117 | 118 | /// Finds the index of the character after the int parameter or the end of the data. 119 | fn find_end_of_int_parameter(&self) -> usize { 120 | self.buffer_index 121 | + self 122 | .buffer 123 | .get(self.buffer_index..) 124 | .map(|buffer| { 125 | buffer 126 | .iter() 127 | .take_while(|byte| { 128 | byte.is_ascii_digit() || **byte == b'-' || **byte == b'+' 129 | }) 130 | .count() 131 | }) 132 | .unwrap_or(self.buffer.len()) 133 | } 134 | 135 | /// Finds the index of the character after the string parameter or the end of the data. 136 | fn find_end_of_string_parameter(&self) -> usize { 137 | let mut counted_quotes = 0; 138 | 139 | self.buffer_index 140 | + self 141 | .buffer 142 | .get(self.buffer_index..) 143 | .map(|buffer| { 144 | buffer 145 | .iter() 146 | .take_while(|byte| { 147 | counted_quotes += (**byte == b'"') as u8; 148 | counted_quotes < 2 149 | }) 150 | .count() 151 | + 1 152 | }) 153 | .unwrap_or(self.buffer.len()) 154 | } 155 | 156 | /// Finds the index of the control character after the non-quoted string or the end of the data. 157 | fn find_end_of_raw_string(&self) -> usize { 158 | self.buffer_index 159 | + self 160 | .buffer 161 | .get(self.buffer_index..) 162 | .map(|buffer| { 163 | buffer 164 | .iter() 165 | .take_while(|byte| !(**byte as char).is_ascii_control()) 166 | .count() 167 | + 1 168 | }) 169 | .unwrap_or(self.buffer.len()) 170 | } 171 | 172 | fn parse_int_parameter(&self) -> (usize, bool, Option) { 173 | let mut new_buffer_index = self.buffer_index; 174 | // Get the end index of the current parameter. 175 | let parameter_end = self.find_end_of_int_parameter(); 176 | // Get the bytes in which the int should reside. 177 | let int_slice = match self.buffer.get(self.buffer_index..parameter_end) { 178 | None => { 179 | return (new_buffer_index, false, None); 180 | } 181 | Some(int_slice) => int_slice, 182 | }; 183 | if int_slice.is_empty() { 184 | // We probably hit the end of the buffer. 185 | // The parameter is empty but as it is optional not invalid 186 | // Advance the index to the character after the parameter separator (comma) if it's there. 187 | new_buffer_index = 188 | parameter_end + (self.buffer.get(parameter_end) == Some(&b',')) as usize; 189 | return (new_buffer_index, true, None); 190 | } 191 | 192 | // Skip the leading '+' 193 | let int_slice = if int_slice[0] == b'+' { 194 | &int_slice[1..] 195 | } else { 196 | int_slice 197 | }; 198 | 199 | // Parse the int 200 | let parsed_int = crate::formatter::parse_int(int_slice); 201 | 202 | // Advance the index to the character after the parameter separator (comma) if it's there. 203 | new_buffer_index = parameter_end + (self.buffer.get(parameter_end) == Some(&b',')) as usize; 204 | // If we've found an int, then the data may be valid and we allow the closure to set the result ok data. 205 | if let Some(parameter_value) = parsed_int { 206 | (new_buffer_index, true, Some(parameter_value)) 207 | } else { 208 | (new_buffer_index, false, None) 209 | } 210 | } 211 | 212 | fn parse_string_parameter(&self) -> (usize, bool, Option<&'a str>) { 213 | let mut new_buffer_index = self.buffer_index; 214 | // Get the end index of the current parameter. 215 | let parameter_end = self.find_end_of_string_parameter(); 216 | if parameter_end > self.buffer.len() { 217 | // We hit the end of the buffer. 218 | // The parameter is empty but as it is optional not invalid 219 | return (new_buffer_index, true, None); 220 | } 221 | // Get the bytes in which the string should reside. 222 | let string_slice = &self.buffer[(new_buffer_index + 1)..(parameter_end - 1)]; 223 | 224 | let has_comma_after_parameter = if let Some(next_char) = self.buffer.get(parameter_end) { 225 | *next_char == b',' 226 | } else { 227 | false 228 | }; 229 | 230 | // Advance the index to the character after the parameter separator. 231 | new_buffer_index = parameter_end + has_comma_after_parameter as usize; 232 | // If we've found a valid string, then the data may be valid and we allow the closure to set the result ok data. 233 | if let Ok(parameter_value) = core::str::from_utf8(string_slice) { 234 | (new_buffer_index, true, Some(parameter_value)) 235 | } else { 236 | (new_buffer_index, false, None) 237 | } 238 | } 239 | 240 | fn parse_raw_string_parameter(&self) -> (usize, bool, Option<&'a str>) { 241 | let mut new_buffer_index = self.buffer_index; 242 | // Get the end index of the current string. 243 | let end = self.find_end_of_raw_string(); 244 | // Get the bytes in which the string should reside. 245 | let string_slice = &self.buffer[new_buffer_index..(end - 1)]; 246 | 247 | // Advance the index to the character after the string. 248 | new_buffer_index = end - 1usize; 249 | 250 | // If we've found a valid string, then the data may be valid and we allow the closure to set the result ok data. 251 | if let Ok(parameter_value) = core::str::from_utf8(string_slice) { 252 | (new_buffer_index, true, Some(parameter_value)) 253 | } else { 254 | (new_buffer_index, false, None) 255 | } 256 | } 257 | 258 | /// Finish parsing the command and get the results 259 | pub fn finish(self) -> Result { 260 | if self.data_valid { 261 | Ok(self.data) 262 | } else { 263 | Err(ParseError(self.buffer_index)) 264 | } 265 | } 266 | } 267 | 268 | impl<'a, D: TupleConcat> CommandParser<'a, D> { 269 | /// Tries reading an int parameter 270 | pub fn expect_int_parameter(self) -> CommandParser<'a, D::Out> { 271 | // If we're already not valid, then quit 272 | if !self.data_valid { 273 | return CommandParser { 274 | buffer: self.buffer, 275 | buffer_index: self.buffer_index, 276 | data_valid: self.data_valid, 277 | data: self.data.tup_cat(0), 278 | }; 279 | } 280 | 281 | let (buffer_index, data_valid, data) = self.parse_int_parameter(); 282 | if let Some(parameter_value) = data { 283 | return CommandParser { 284 | buffer: self.buffer, 285 | buffer_index, 286 | data_valid, 287 | data: self.data.tup_cat(parameter_value), 288 | } 289 | .trim_space(); 290 | } else { 291 | return CommandParser { 292 | buffer: self.buffer, 293 | buffer_index, 294 | data_valid: false, 295 | data: self.data.tup_cat(0), 296 | } 297 | .trim_space(); 298 | } 299 | } 300 | } 301 | 302 | impl<'a, D: TupleConcat<&'a str>> CommandParser<'a, D> { 303 | /// Tries reading a string parameter 304 | pub fn expect_string_parameter(self) -> CommandParser<'a, D::Out> { 305 | // If we're already not valid, then quit 306 | if !self.data_valid { 307 | return CommandParser { 308 | buffer: self.buffer, 309 | buffer_index: self.buffer_index, 310 | data_valid: self.data_valid, 311 | data: self.data.tup_cat(""), 312 | }; 313 | } 314 | 315 | let (buffer_index, data_valid, data) = self.parse_string_parameter(); 316 | if let Some(parameter_value) = data { 317 | return CommandParser { 318 | buffer: self.buffer, 319 | buffer_index, 320 | data_valid, 321 | data: self.data.tup_cat(parameter_value), 322 | } 323 | .trim_space(); 324 | } else { 325 | return CommandParser { 326 | buffer: self.buffer, 327 | buffer_index, 328 | data_valid: false, 329 | data: self.data.tup_cat(""), 330 | } 331 | .trim_space(); 332 | } 333 | } 334 | 335 | /// Tries reading a non-parameter, non-quoted string 336 | pub fn expect_raw_string(self) -> CommandParser<'a, D::Out> { 337 | // If we're already not valid, then quit 338 | if !self.data_valid { 339 | return CommandParser { 340 | buffer: self.buffer, 341 | buffer_index: self.buffer_index, 342 | data_valid: self.data_valid, 343 | data: self.data.tup_cat(""), 344 | }; 345 | } 346 | 347 | let (buffer_index, data_valid, data) = self.parse_raw_string_parameter(); 348 | if let Some(parameter_value) = data { 349 | return CommandParser { 350 | buffer: self.buffer, 351 | buffer_index, 352 | data_valid, 353 | data: self.data.tup_cat(parameter_value), 354 | } 355 | .trim_space(); 356 | } else { 357 | return CommandParser { 358 | buffer: self.buffer, 359 | buffer_index, 360 | data_valid: false, 361 | data: self.data.tup_cat(""), 362 | } 363 | .trim_space(); 364 | } 365 | } 366 | } 367 | 368 | // 369 | // Optional parameters 370 | // 371 | 372 | impl<'a, D: TupleConcat>> CommandParser<'a, D> { 373 | /// Tries reading an int parameter 374 | pub fn expect_optional_int_parameter(self) -> CommandParser<'a, D::Out> { 375 | // If we're already not valid, then quit 376 | if !self.data_valid { 377 | return CommandParser { 378 | buffer: self.buffer, 379 | buffer_index: self.buffer_index, 380 | data_valid: self.data_valid, 381 | data: self.data.tup_cat(None), 382 | }; 383 | } 384 | 385 | let (buffer_index, data_valid, data) = self.parse_int_parameter(); 386 | return CommandParser { 387 | buffer: self.buffer, 388 | buffer_index, 389 | data_valid, 390 | data: self.data.tup_cat(data), 391 | } 392 | .trim_space(); 393 | } 394 | } 395 | 396 | impl<'a, D: TupleConcat>> CommandParser<'a, D> { 397 | /// Tries reading a string parameter 398 | pub fn expect_optional_string_parameter(self) -> CommandParser<'a, D::Out> { 399 | // If we're already not valid, then quit 400 | if !self.data_valid { 401 | return CommandParser { 402 | buffer: self.buffer, 403 | buffer_index: self.buffer_index, 404 | data_valid: self.data_valid, 405 | data: self.data.tup_cat(None), 406 | }; 407 | } 408 | 409 | let (buffer_index, data_valid, data) = self.parse_string_parameter(); 410 | return CommandParser { 411 | buffer: self.buffer, 412 | buffer_index, 413 | data_valid, 414 | data: self.data.tup_cat(data), 415 | } 416 | .trim_space(); 417 | } 418 | 419 | /// Tries reading a non-parameter, non-quoted string 420 | pub fn expect_optional_raw_string(self) -> CommandParser<'a, D::Out> { 421 | // If we're already not valid, then quit 422 | if !self.data_valid { 423 | return CommandParser { 424 | buffer: self.buffer, 425 | buffer_index: self.buffer_index, 426 | data_valid: self.data_valid, 427 | data: self.data.tup_cat(None), 428 | }; 429 | } 430 | 431 | let (buffer_index, data_valid, data) = self.parse_raw_string_parameter(); 432 | return CommandParser { 433 | buffer: self.buffer, 434 | buffer_index, 435 | data_valid, 436 | data: self.data.tup_cat(data), 437 | } 438 | .trim_space(); 439 | } 440 | } 441 | 442 | /// Error type for parsing 443 | /// 444 | /// The number is the index of up to where it was correctly parsed 445 | #[derive(Debug, Clone, PartialEq)] 446 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 447 | pub struct ParseError(usize); 448 | 449 | #[cfg(test)] 450 | mod tests { 451 | use super::*; 452 | 453 | #[test] 454 | fn test_ok() { 455 | let (x, y, z) = CommandParser::parse(b"+SYSGPIOREAD:654,\"true\",-65154\r\nOK\r\n") 456 | .expect_identifier(b"+SYSGPIOREAD:") 457 | .expect_int_parameter() 458 | .expect_string_parameter() 459 | .expect_int_parameter() 460 | .expect_identifier(b"\r\nOK\r\n") 461 | .finish() 462 | .unwrap(); 463 | 464 | assert_eq!(x, 654); 465 | assert_eq!(y, "true"); 466 | assert_eq!(z, -65154); 467 | } 468 | 469 | #[test] 470 | fn test_positive_int_param() { 471 | let (x,) = CommandParser::parse(b"OK+RP:+20dBm\r\n") 472 | .expect_identifier(b"OK+RP:") 473 | .expect_int_parameter() 474 | .expect_identifier(b"dBm\r\n") 475 | .finish() 476 | .unwrap(); 477 | 478 | assert_eq!(x, 20); 479 | } 480 | 481 | #[test] 482 | fn test_whitespace() { 483 | let (x, y, z) = CommandParser::parse(b"+SYSGPIOREAD: 654, \"true\", -65154 \r\nOK\r\n") 484 | .expect_identifier(b"+SYSGPIOREAD:") 485 | .expect_int_parameter() 486 | .expect_string_parameter() 487 | .expect_int_parameter() 488 | .expect_identifier(b"\r\nOK\r\n") 489 | .finish() 490 | .unwrap(); 491 | 492 | assert_eq!(x, 654); 493 | assert_eq!(y, "true"); 494 | assert_eq!(z, -65154); 495 | } 496 | 497 | #[test] 498 | fn string_param_at_end() { 499 | let (x, y) = CommandParser::parse(br#"+SYSGPIOREAD: 42, "param at end""#) 500 | .expect_identifier(b"+SYSGPIOREAD:") 501 | .expect_int_parameter() 502 | .expect_string_parameter() 503 | .finish() 504 | .unwrap(); 505 | 506 | assert_eq!(x, 42); 507 | assert_eq!(y, "param at end"); 508 | } 509 | 510 | #[test] 511 | fn test_optional_int_parameter_all_present() { 512 | let (x, y, z) = CommandParser::parse(b"+SYSGPIOREAD:654,\"true\",-65154\r\nOK\r\n") 513 | .expect_identifier(b"+SYSGPIOREAD:") 514 | .expect_optional_int_parameter() 515 | .expect_optional_string_parameter() 516 | .expect_optional_int_parameter() 517 | .expect_identifier(b"\r\nOK\r\n") 518 | .finish() 519 | .unwrap(); 520 | 521 | assert_eq!(x, Some(654)); 522 | assert_eq!(y, Some("true")); 523 | assert_eq!(z, Some(-65154)); 524 | } 525 | 526 | #[test] 527 | fn test_optional_int_parameter_middle_not_present() { 528 | let (x, y, z) = CommandParser::parse(b"+SYSGPIOREAD:,\"true\"\r\nOK\r\n") 529 | .expect_identifier(b"+SYSGPIOREAD:") 530 | .expect_optional_int_parameter() 531 | .expect_optional_string_parameter() 532 | .expect_optional_int_parameter() 533 | .expect_identifier(b"\r\nOK\r\n") 534 | .finish() 535 | .unwrap(); 536 | 537 | assert_eq!(x, None); 538 | assert_eq!(y, Some("true")); 539 | assert_eq!(z, None); 540 | } 541 | 542 | #[test] 543 | fn test_optional_int_parameter_end_not_present() { 544 | let (x, y, z) = CommandParser::parse(b"+SYSGPIOREAD:654,\"true\",\r\nOK\r\n") 545 | .expect_identifier(b"+SYSGPIOREAD:") 546 | .expect_optional_int_parameter() 547 | .expect_optional_string_parameter() 548 | .expect_optional_int_parameter() 549 | .expect_optional_identifier(b"\r\nOK\r\n") 550 | .finish() 551 | .unwrap(); 552 | 553 | assert_eq!(x, Some(654)); 554 | assert_eq!(y, Some("true")); 555 | assert_eq!(z, None); 556 | } 557 | 558 | #[test] 559 | fn test_optional_identifier() { 560 | let r = CommandParser::parse(b"+SYSGPIOREAD:,\"true\"\r\nK\r\n") 561 | .expect_identifier(b"+SYSGPIOREAD:") 562 | .expect_optional_int_parameter() 563 | .expect_optional_string_parameter() 564 | .expect_optional_int_parameter() 565 | .expect_optional_identifier(b"\r\nOK\r\n") 566 | .finish(); 567 | 568 | assert_eq!(r, Err(ParseError(20))); 569 | 570 | let (x, y, z) = CommandParser::parse(b"+SYSGPIOREAD:,\"true\"\r\nOK\r\n") 571 | .expect_identifier(b"+SYSGPIOREAD:") 572 | .expect_optional_int_parameter() 573 | .expect_optional_string_parameter() 574 | .expect_optional_int_parameter() 575 | .expect_optional_identifier(b"\r\nOK\r\n") 576 | .finish() 577 | .unwrap(); 578 | 579 | assert_eq!(x, None); 580 | assert_eq!(y, Some("true")); 581 | assert_eq!(z, None); 582 | 583 | let (x, y, z) = CommandParser::parse(b"+SYSGPIOREAD:,\"true\"") 584 | .expect_identifier(b"+SYSGPIOREAD:") 585 | .expect_optional_int_parameter() 586 | .expect_optional_string_parameter() 587 | .expect_optional_int_parameter() 588 | .expect_optional_identifier(b"\r\nOK\r\n") 589 | .finish() 590 | .unwrap(); 591 | 592 | assert_eq!(x, None); 593 | assert_eq!(y, Some("true")); 594 | assert_eq!(z, None); 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /src/tuple_concat.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_attributes)] 2 | 3 | pub trait TupleConcat { 4 | type Out; 5 | fn tup_cat(self, c: C) -> Self::Out; 6 | } 7 | 8 | impl TupleConcat for () { 9 | type Out = (C,); 10 | fn tup_cat(self, c: C) -> Self::Out { 11 | (c,) 12 | } 13 | } 14 | 15 | impl TupleConcat for (T0,) { 16 | type Out = (T0, C); 17 | fn tup_cat(self, c: C) -> Self::Out { 18 | (self.0, c) 19 | } 20 | } 21 | 22 | impl TupleConcat for (T0, T1) { 23 | type Out = (T0, T1, C); 24 | fn tup_cat(self, c: C) -> Self::Out { 25 | (self.0, self.1, c) 26 | } 27 | } 28 | 29 | impl TupleConcat for (T0, T1, T2) { 30 | type Out = (T0, T1, T2, C); 31 | fn tup_cat(self, c: C) -> Self::Out { 32 | (self.0, self.1, self.2, c) 33 | } 34 | } 35 | 36 | impl TupleConcat for (T0, T1, T2, T3) { 37 | type Out = (T0, T1, T2, T3, C); 38 | fn tup_cat(self, c: C) -> Self::Out { 39 | (self.0, self.1, self.2, self.3, c) 40 | } 41 | } 42 | 43 | impl TupleConcat for (T0, T1, T2, T3, T4) { 44 | type Out = (T0, T1, T2, T3, T4, C); 45 | fn tup_cat(self, c: C) -> Self::Out { 46 | (self.0, self.1, self.2, self.3, self.4, c) 47 | } 48 | } 49 | 50 | impl TupleConcat for (T0, T1, T2, T3, T4, T5) { 51 | type Out = (T0, T1, T2, T3, T4, T5, C); 52 | fn tup_cat(self, c: C) -> Self::Out { 53 | (self.0, self.1, self.2, self.3, self.4, self.5, c) 54 | } 55 | } 56 | 57 | impl TupleConcat for (T0, T1, T2, T3, T4, T5, T6) { 58 | type Out = (T0, T1, T2, T3, T4, T5, T6, C); 59 | fn tup_cat(self, c: C) -> Self::Out { 60 | (self.0, self.1, self.2, self.3, self.4, self.5, self.6, c) 61 | } 62 | } 63 | 64 | impl TupleConcat for (T0, T1, T2, T3, T4, T5, T6, T7) { 65 | type Out = (T0, T1, T2, T3, T4, T5, T6, T7, C); 66 | fn tup_cat(self, c: C) -> Self::Out { 67 | ( 68 | self.0, self.1, self.2, self.3, self.4, self.5, self.6, self.7, c, 69 | ) 70 | } 71 | } 72 | 73 | impl TupleConcat 74 | for (T0, T1, T2, T3, T4, T5, T6, T7, T8) 75 | { 76 | type Out = (T0, T1, T2, T3, T4, T5, T6, T7, T8, C); 77 | fn tup_cat(self, c: C) -> Self::Out { 78 | ( 79 | self.0, self.1, self.2, self.3, self.4, self.5, self.6, self.7, self.8, c, 80 | ) 81 | } 82 | } 83 | 84 | impl TupleConcat 85 | for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9) 86 | { 87 | type Out = (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, C); 88 | fn tup_cat(self, c: C) -> Self::Out { 89 | ( 90 | self.0, self.1, self.2, self.3, self.4, self.5, self.6, self.7, self.8, self.9, c, 91 | ) 92 | } 93 | } 94 | 95 | impl TupleConcat 96 | for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) 97 | { 98 | type Out = (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, C); 99 | fn tup_cat(self, c: C) -> Self::Out { 100 | ( 101 | self.0, self.1, self.2, self.3, self.4, self.5, self.6, self.7, self.8, self.9, 102 | self.10, c, 103 | ) 104 | } 105 | } 106 | 107 | impl TupleConcat 108 | for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) 109 | { 110 | type Out = (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, C); 111 | fn tup_cat(self, c: C) -> Self::Out { 112 | ( 113 | self.0, self.1, self.2, self.3, self.4, self.5, self.6, self.7, self.8, self.9, 114 | self.10, self.11, c, 115 | ) 116 | } 117 | } 118 | 119 | impl TupleConcat 120 | for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) 121 | { 122 | type Out = (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, C); 123 | fn tup_cat(self, c: C) -> Self::Out { 124 | ( 125 | self.0, self.1, self.2, self.3, self.4, self.5, self.6, self.7, self.8, self.9, 126 | self.10, self.11, self.12, c, 127 | ) 128 | } 129 | } 130 | 131 | impl TupleConcat 132 | for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13) 133 | { 134 | type Out = ( 135 | T0, 136 | T1, 137 | T2, 138 | T3, 139 | T4, 140 | T5, 141 | T6, 142 | T7, 143 | T8, 144 | T9, 145 | T10, 146 | T11, 147 | T12, 148 | T13, 149 | C, 150 | ); 151 | fn tup_cat(self, c: C) -> Self::Out { 152 | ( 153 | self.0, self.1, self.2, self.3, self.4, self.5, self.6, self.7, self.8, self.9, 154 | self.10, self.11, self.12, self.13, c, 155 | ) 156 | } 157 | } 158 | 159 | impl TupleConcat 160 | for ( 161 | T0, 162 | T1, 163 | T2, 164 | T3, 165 | T4, 166 | T5, 167 | T6, 168 | T7, 169 | T8, 170 | T9, 171 | T10, 172 | T11, 173 | T12, 174 | T13, 175 | T14, 176 | ) 177 | { 178 | type Out = ( 179 | T0, 180 | T1, 181 | T2, 182 | T3, 183 | T4, 184 | T5, 185 | T6, 186 | T7, 187 | T8, 188 | T9, 189 | T10, 190 | T11, 191 | T12, 192 | T13, 193 | T14, 194 | C, 195 | ); 196 | fn tup_cat(self, c: C) -> Self::Out { 197 | ( 198 | self.0, self.1, self.2, self.3, self.4, self.5, self.6, self.7, self.8, self.9, 199 | self.10, self.11, self.12, self.13, self.14, c, 200 | ) 201 | } 202 | } 203 | 204 | impl TupleConcat 205 | for ( 206 | T0, 207 | T1, 208 | T2, 209 | T3, 210 | T4, 211 | T5, 212 | T6, 213 | T7, 214 | T8, 215 | T9, 216 | T10, 217 | T11, 218 | T12, 219 | T13, 220 | T14, 221 | T15, 222 | ) 223 | { 224 | type Out = ( 225 | T0, 226 | T1, 227 | T2, 228 | T3, 229 | T4, 230 | T5, 231 | T6, 232 | T7, 233 | T8, 234 | T9, 235 | T10, 236 | T11, 237 | T12, 238 | T13, 239 | T14, 240 | T15, 241 | C, 242 | ); 243 | fn tup_cat(self, c: C) -> Self::Out { 244 | ( 245 | self.0, self.1, self.2, self.3, self.4, self.5, self.6, self.7, self.8, self.9, 246 | self.10, self.11, self.12, self.13, self.14, self.15, c, 247 | ) 248 | } 249 | } 250 | --------------------------------------------------------------------------------