commit fa60f228221b2757ac3d94b24526154a8e41b8dc Author: aOK Date: Mon Jul 22 17:16:42 2024 +0300 first commit diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..3fcc369 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,14 @@ +[target.xtensa-esp32-none-elf] +runner = "espflash flash -M -b 2000000 --partition-table partitions.csv -p /dev/cu.usbserial-1420" + + +[env] +ESP_LOGLEVEL = "INFO" + +[build] +rustflags = ["-C", "link-arg=-nostartfiles", "-Dclippy::clone_on_ref_ptr"] + +target = "xtensa-esp32-none-elf" + +[unstable] +build-std = ["alloc", "core"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e166f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb +*.lock +src/main.rs.txt diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..979d509 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.check.allTargets": false, +} diff --git a/A9G GPRS Operation.png b/A9G GPRS Operation.png new file mode 100644 index 0000000..119e2b6 Binary files /dev/null and b/A9G GPRS Operation.png differ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f15c728 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,108 @@ +[package] +name = "ppp-gsm" +version = "0.1.0" +authors = ["aok"] +edition = "2021" +license = "MIT OR Apache-2.0" + +[lib] +name = "ppp_gsm" + +[dependencies] +esp-backtrace = { version = "0.12.0", features = [ + "esp32", + "exception-handler", + "panic-handler", + "println", +] } +hal = { package = "esp-hal", version = "0.19.0", features = [ + # "embassy", + "async", + "esp32", + # "embassy-time-timg0", + # "embassy-executor-thread", +] } +esp-hal-embassy = { version = "0.2.0", features = ["esp32", "executors"] } +embassy-time = "0.3.1" +embassy-sync = "0.6.0" +embedded-io = { version = "0.6.1" } +embedded-io-async = "0.6.1" +# embedded-io = "0.6.1" +esp-println = { version = "0.9.1", features = ["esp32", "log"] } +log = { version = "0.4.21" } +heapless = "0.8.0" +esp-alloc = { version = "0.4.0" } +static_cell = { version = "2.1.0", features = ["nightly"] } +embassy-executor = { version = "0.5.0", features = [ + "nightly", + "integrated-timers", +] } +bytemuck = { version = "1.16.1", features = ["derive"] } + +embedded-storage = "0.3.1" +esp-storage = { version = "0.3.0", features = ["esp32"] } +# embassy-net = { version = "0.4.0", features=[ "std", "log", "medium-ethernet", "medium-ip", "tcp", "udp", "dns", "dhcpv4", "proto-ipv6"] } +rand_core = { version = "0.6.3", default-features = false } +# rand_chacha = { version = "0.3.1", default-features = false } +rand = { version = "0.8.5", default-features = false, features = ["small_rng"] } + +embassy-net = { version = "0.4.0", features = [ + "log", + "medium-ethernet", + "medium-ip", + "tcp", + "udp", + "dns", + "dhcpv4", + "proto-ipv6", +] } +embassy-net-ppp = { version = "0.1.0", features = ["log"] } +# ppproto = "0.1.2" +# smoltcp = { version = "0.11.0", default-features = false, features = [ +# "proto-igmp", +# "proto-ipv4", +# "socket-tcp", +# "socket-icmp", +# "socket-udp", +# "medium-ethernet", +# "proto-dhcpv4", +# "socket-raw", +# "socket-dhcpv4", +# ] } +picoserve = "0.11.1" +embedded-tls = { version = "0.17.0", default-features = false } +rust-mqtt = { version = "0.3.0", default-features = false } +# futures = { version = "0.3.30", default-features = false, features = ["alloc"] } +reqwless = { version = "0.12.1", features = ["alloc", "log"] } +embassy-futures = "0.1.1" +# embedded-tls = "0.17.0" + +# rust-mqtt = "0.3.0" +# rust-mqtt = { git = "https://github.com/obabec/rust-mqtt", rev = "483138c", default-features = false } + +# defmt-or-log = "0.2.1" + +# defmt-or-log = { version = "0.2.1", features = ["defmt"] } +# defmt = "0.3.8" +# defmt-rtt = "0.4.1" + +[dev-dependencies] +embassy-time = { version = "0.3.1", features = ["std", "generic-queue"] } +critical-section = { version = "1.1", features = ["std"] } +tokio = { version = "1", features = ["macros", "rt"] } +static_cell = { version = "2.0.0" } + + +[profile.dev] +# Rust debug is too slow. +# For debug builds always builds with some optimization +opt-level = "s" + +[profile.release] +codegen-units = 1 # LLVM can perform better optimizations using a single thread +debug = 2 +debug-assertions = false +incremental = false +lto = 'fat' +opt-level = 2 +overflow-checks = false diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..d2c040e --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..3d3dcba --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright [year] [fullname] + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5efe9c9 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rustc-link-arg-bins=-Tlinkall.x"); +} diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..426219e --- /dev/null +++ b/partitions.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x4000, +otadata, data, ota, 0xd000, 0x2000, +phy_init, data, phy, 0xf000, 0x1000, +ota_0, app, ota_0, 0x10000, 0x180000, +ota_1, app, ota_1, 0x190000, 0x180000, diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..a2f5ab5 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "esp" diff --git a/src/gps.rs b/src/gps.rs new file mode 100644 index 0000000..5fa5902 --- /dev/null +++ b/src/gps.rs @@ -0,0 +1,122 @@ +#![no_std] + +use core::str; + +fn get_gps_location(serial: &mut impl Serial) -> Result<(&[u8], &[u8]), ()> { + // Request GPS information + serial.write(b"AT+GPSRD=1\r\n").map_err(|_| ())?; + + // Wait for GPS information to be available + delay(2000); // You'll need to implement a suitable delay for your platform + + let mut response = [0u8; 128]; // Adjust buffer size as needed + let mut bytes_read = 0; + + while let Some(byte) = serial.read().map_err(|_| ())? { + if bytes_read < response.len() { + response[bytes_read] = byte; + bytes_read += 1; + } else { + // Handle buffer overflow if necessary + break; + } + } + + // Extract latitude and longitude from the response + let response_slice = &response[..bytes_read]; + if let Some(index) = find_subsequence(response_slice, b"GPSRD: ") { + let data = &response_slice[index + 7..]; + if let Some(comma_index) = data.iter().position(|&b| b == b',') { + let latitude = &data[..comma_index]; + let longitude = &data[comma_index + 1..]; + if let Some(end_index) = longitude.iter().position(|&b| b == b',') { + let longitude = &longitude[..end_index]; + Ok((latitude, longitude)) + } else { + Err(()) + } + } else { + Err(()) + } + } else { + Err(()) + } +} + +// Placeholder trait for serial communication +trait Serial { + fn write(&mut self, data: &[u8]) -> Result<(), ()>; + fn read(&mut self) -> Result, ()>; +} + +// Placeholder for delay function +fn delay(_ms: u32) { + // Implement a suitable delay for your platform +} + +// Helper function to find a subsequence in a byte slice +fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option { + haystack + .windows(needle.len()) + .position(|window| window == needle) +} + +use bytes::{BufMut, BytesMut}; +use core::fmt::Write; // For writing to strings // For byte buffer handling + +// Mock implementation of serial communication and delay +struct MySerial; + +impl MySerial { + fn available(&self) -> usize { + // Implementation to get number of bytes available in the serial buffer + unimplemented!() + } + + fn read(&self) -> u8 { + // Implementation to read a byte from the serial buffer + unimplemented!() + } +} + +fn delay(_ms: u32) { + // Implementation of delay + unimplemented!() +} + +fn send_at_command(command: &[u8]) { + // Implementation to send command to the GPS module + unimplemented!() +} + +fn get_gps_location(my_serial: &MySerial) -> Option<(BytesMut, BytesMut)> { + // Request GPS information + send_at_command(b"AT+GPSRD=1\r\n"); + // Wait for GPS information to be available + delay(2000); + + let mut response = BytesMut::with_capacity(128); // Pre-allocate buffer + + // Read available bytes from the serial + while my_serial.available() > 0 { + let byte = my_serial.read(); + response.put_u8(byte); // Store the byte in the buffer + } + + // Parse the response for latitude and longitude + if let Some(index) = response.windows(7).position(|w| w == b"GPSRD: ") { + let trimmed_response = &response[index + 8..]; // Skip "GPSRD: " + + if let Some(comma_index) = trimmed_response.iter().position(|&b| b == b',') { + let latitude = trimmed_response[..comma_index].to_vec().into(); + let remaining = &trimmed_response[comma_index + 1..]; + + if let Some(next_comma_index) = remaining.iter().position(|&b| b == b',') { + let longitude = remaining[..next_comma_index].to_vec().into(); + return Some((latitude, longitude)); + } + } + } + + None // Return None if parsing fails +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..623c71a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,85 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![deny(clippy::clone_on_ref_ptr)] + +// #[macro_use] +extern crate alloc; +pub mod run; +pub mod utils; + +// use embassy_executor::Spawner; +// use embassy_net::tcp::TcpSocket; +// use embassy_net::{Stack, StackResources}; +// use embassy_serial::{AsyncDevice, Config}; +// use embassy_time::{Duration, Timer}; +// use embedded_hal::digital::v2::OutputPin; +// use std::net::Ipv4Addr; + +// #[embassy_executor::main] +// async fn main(_spawner: Spawner) { +// // Configure the serial port connected to the ESP32 +// let config = Config::default(); +// let mut serial = AsyncDevice::new(peripherals.UART0, config).unwrap(); + +// // Configure the network stack +// static mut RX_BUFFER: [u8; 1024] = [0; 1024]; +// static mut TX_BUFFER: [u8; 1024] = [0; 1024]; +// let stack = &mut Stack::new( +// StackResources::new() +// .tx_buffer(TX_BUFFER) +// .rx_buffer(RX_BUFFER), +// ); + +// // Power on the modem (if necessary) +// // ... + +// // Initialize the PPP connection +// let mut ppp = embassy_net_ppp::Ppp::new(&mut serial, stack); + +// // Attach to the GPRS network +// ppp.write_at("AT+CGATT=1\r").await.unwrap(); +// ppp.wait_for_ok().await.unwrap(); + +// // Set the PDP context +// ppp.write_at("AT+CGDCONT=1,\"IP\",\"cmnet\"\r") +// .await +// .unwrap(); +// ppp.wait_for_ok().await.unwrap(); +// // AT+CGDCONT=1,"IP","internet" +// // Activate the PDP context +// ppp.write_at("AT+CGACT=1,1\r").await.unwrap(); +// ppp.wait_for_ok().await.unwrap(); + +// // Start PPP translation +// ppp.write_at("ATD*99***1#\r").await.unwrap(); +// ppp.wait_for("CONNECT").await.unwrap(); + +// // Wait for the connection to be established +// ppp.wait_for("+++").await.unwrap(); +// Timer::after(Duration::from_secs(3)).await; + +// // Configure the network interface +// let config = embassy_net::Config::dhcpv4(Default::default()); +// stack.configure(config).unwrap(); + +// // Wait for the network to be configured +// stack.wait_for_link().await; + +// // Get the assigned IP address +// let ip = stack.get_ip_address().unwrap(); +// println!("IP address: {}", ip); + +// // Create a TCP socket +// let mut socket = TcpSocket::new(stack, &mut [0; 1024]); +// socket.bind(Ipv4Addr::UNSPECIFIED, 8080).unwrap(); +// socket.listen(1).unwrap(); + +// loop { +// // Accept a connection +// let (_remote_addr, mut connection) = socket.accept().await.unwrap(); + +// // Handle the connection +// // ... +// } +// } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7a46c44 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,317 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![deny(clippy::clone_on_ref_ptr)] + +use alloc::vec::Vec; +use embassy_executor::Spawner; +use embassy_net::{Config, ConfigV4, Ipv4Address, Ipv4Cidr, Stack, StackResources}; +use embassy_net_ppp::Runner; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex, signal::Signal}; +use embassy_time::{with_timeout, Duration, Timer}; +use esp_backtrace as _; +// use hal::uart::ClockSource; +use hal::{ + clock::{ClockControl, Clocks}, + delay::Delay, + gpio::{GpioPin, Io}, + peripherals::{Peripherals, UART0, UART1}, + prelude::*, + system::SystemControl, + timer::{timg::TimerGroup, ErasedTimer, OneShotTimer}, + uart::{ + config::{Config as UartConfig, DataBits, Parity, StopBits}, + ClockSource, Uart, + }, +}; +use log::{error, info, warn}; +use ppp_gsm::{ + run::modem::{ + module::{SharedUart1, UartWrapper}, + parse::parse::{ModemResponse, ToSend}, + }, + singleton, + utils::generate_random_seed, +}; +use static_cell::StaticCell; +extern crate alloc; +use core::mem::MaybeUninit; + +#[global_allocator] +static ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty(); + +fn init_heap() { + const HEAP_SIZE: usize = 32 * 1024; + static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit(); + + unsafe { + ALLOCATOR.init(HEAP.as_mut_ptr() as *mut u8, HEAP_SIZE); + } +} + +pub const READ_BUF_SIZE: usize = 64; +// When you are okay with using a nightly compiler it's better to use https://docs.rs/static_cell/2.1.0/static_cell/macro.make_static.html +macro_rules! mk_static { + ($t:ty,$val:expr) => {{ + static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); + #[deny(unused_attributes)] + let x = STATIC_CELL.uninit().write(($val)); + x + }}; +} + +#[main] +async fn main(spawner: Spawner) { + let peripherals = Peripherals::take(); + let system = SystemControl::new(peripherals.SYSTEM); + + // let clocks = ClockControl::max(system.clock_control).freeze(); + let clocks = singleton!(ClockControl::max(system.clock_control).freeze(), Clocks<'_>); + + let timg0 = TimerGroup::new(peripherals.TIMG0, &clocks, None); + let timer0 = OneShotTimer::new(timg0.timer0.into()); + let timers = [timer0]; + let timers = mk_static!([OneShotTimer; 1], timers); + esp_hal_embassy::init(&clocks, timers); + + let delay = Delay::new(&clocks); + init_heap(); + + esp_println::logger::init_logger_from_env(); + + // Init network device + static STATE: StaticCell> = StaticCell::new(); + let state = STATE.init(embassy_net_ppp::State::<4, 4>::new()); + let (device, runner) = embassy_net_ppp::new(state); + + // Generate random seed + let seed = generate_random_seed(); + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static RESOURCES: StaticCell> = StaticCell::new(); + let stack = &*STACK.init(Stack::new( + device, + Config::default(), // don't configure IP yet + RESOURCES.init(StackResources::<3>::new()), + seed, + )); + let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); + + static SIGNAL: StaticCell)>> = StaticCell::new(); + let signal = &*SIGNAL.init(Signal::new()); + + static MODEM_MUTEX: StaticCell>> = StaticCell::new(); + let mut modem = UartWrapper::new( + peripherals.UART0, + peripherals.UART1, + io.pins.gpio14, + io.pins.gpio15, + signal, + &clocks, + ); + let modem = &*MODEM_MUTEX.init(Mutex::new(modem)); + + // spawner.spawn(uart_reader(modem)).unwrap(); + match spawner.spawn(uart_reader(modem)) { + Ok(_) => info!("uart_reader task spawned successfully"), + Err(e) => error!("Failed to spawn uart_reader task: {:?}", e), + } + // spawner.spawn(ppp_task(stack, runner, modem)).unwrap(); + match spawner.spawn(ppp_task(stack, runner, modem)) { + Ok(_) => info!("ppp_task task spawned successfully"), + Err(e) => error!("Failed to spawn ppp_task task: {:?}", e), + } + { + let mut modem_guard = modem.lock().await; + match modem_guard.init_with_retry().await { + Ok(_) => info!("init completed successfully"), + Err(e) => { + error!("init failure: {:?}", e); + } + }; + } + loop { + log::info!("Hello world!"); + delay.delay(500.millis()); + } +} +use embedded_io_async::Read; + +#[embassy_executor::task] +async fn ppp_task( + stack: &'static Stack>, + mut runner: Runner<'static>, + // mut modem: UartWrapper<'static>, + modem: &'static Mutex>, +) -> ! { + let config = embassy_net_ppp::Config { + username: b"myuser", + password: b"mypass", + }; + Timer::after(Duration::from_millis(10)).await; // Adjust delay if needed + + let mut modem_guard = modem.lock().await; + + runner + .run(&mut *modem_guard, config, |ipv4| { + let Some(addr) = ipv4.address else { + warn!("PPP did not provide an IP address."); + return; + }; + // let mut dns_servers = Vec::new(); + let mut dns_servers: heapless::Vec = heapless::Vec::new(); + + for s in ipv4.dns_servers.iter().flatten() { + let _ = dns_servers.push(Ipv4Address::from_bytes(&s.0)); + } + let config = ConfigV4::Static(embassy_net::StaticConfigV4 { + address: Ipv4Cidr::new(Ipv4Address::from_bytes(&addr.0), 0), + gateway: None, + dns_servers, + }); + stack.set_config_v4(config); + }) + .await + .unwrap(); + unreachable!() +} +#[embassy_executor::task] +// async fn uart_reader(reader: &'static Mutex>) -> ! { +async fn uart_reader(modem: &'static Mutex>) -> ! { + const MAX_BUFFER_SIZE: usize = 10 * READ_BUF_SIZE + 16; + let mut rbuf: [u8; MAX_BUFFER_SIZE] = [0u8; MAX_BUFFER_SIZE]; + let mut offset = 0; + + info!("uart_reader task started"); + + loop { + info!("reader after loop"); + { + let mut modem_guard = modem.lock().await; + + info!("rdr after loop"); + // Log UART configuration + // info!("UART1 configuration: {:?}", modem_guard.uart1.get_config()); + // Perform a direct write-read test to ensure UART is working + use embedded_io_async::Write; + let test_data = b"AT\r\n"; + modem_guard.write(test_data).await.unwrap(); + info!("Sent test data: {:?}", test_data); + + info!("rdr before read()"); + match modem_guard.read(&mut rbuf[offset..]).await { + Ok(len) => { + info!("rdr_guard after read()"); + if len > 0 { + offset += len; + info!("Received {} bytes: {:?}", len, &rbuf[..offset]); + + modem_guard.signal.signal((offset, rbuf[..offset].to_vec())); + offset = 0; // Reset the offset after signaling + } else { + info!("Received empty data (len = 0)"); + } + } + Err(e) => { + error!("[uart_reader] RX Error: {:?}", e); + } + } + } + + // Add a small delay to avoid busy looping too fast + Timer::after(Duration::from_millis(1)).await; // Adjust delay if needed + } +} + +trait NewTrait<'a> { + pub async fn send_at( + &mut self, + tosend: Option<&[u8]>, + msg: Option<&str>, + _timeout_ms: Option, + ); + fn new( + // uart: &'a mut Uart<'static, UART1, Async> + _uart0_per: UART0, + uart1_per: UART1, + tx_pin: GpioPin<14>, + rx_pin: GpioPin<15>, + signal: &'static Signal)>, + clocks: &Clocks<'static>, + ) -> Self; +} + +impl<'a> NewTrait<'a> for UartWrapper<'a> { + fn new( + // uart: &'a mut Uart<'static, UART1, Async> + _uart0_per: UART0, + uart1_per: UART1, + tx_pin: GpioPin<14>, + rx_pin: GpioPin<15>, + signal: &'static Signal)>, + clocks: &Clocks<'static>, + ) -> Self { + // let uart1 = { + let (tx, rx) = { + let config = UartConfig { + baudrate: 115200, + data_bits: DataBits::DataBits8, + parity: Parity::ParityNone, + stop_bits: StopBits::STOP1, + clock_source: ClockSource::Apb, + rx_fifo_full_threshold: 1, + rx_timeout: Some(3), + }; + + config.rx_fifo_full_threshold(1); + let uart = + Uart::new_async_with_config(uart1_per, config, &clocks, tx_pin, rx_pin).unwrap(); + uart.split() + }; + + let modem_res = ModemResponse::new(); + let tosend = ToSend::new(); + + Self { + uart1: SharedUart1 { tx, rx }, + signal, + buffer: [0; 256], + read_pos: 0, + write_pos: 0, + modem_res, + tosend, + } + } + async fn send_at( + &mut self, + tosend: Option<&[u8]>, + msg: Option<&str>, + _timeout_ms: Option, + ) { + let msg_str = msg.unwrap_or("default"); + let tosend_f = tosend.unwrap_or(AT_CMD); + + info!( + "\n\x1b[32m[{msg_str}]\x1b[0m writing command: {:?}", + tosend_f + ); + + // Set the command to be sent + self.tosend.set_to_sent(tosend_f); + // Get the command after setting it + let cmd = self.tosend.get_to_sent().to_vec(); // Clone the command to a local variable + + if cmd.is_empty() { + info!("EMPTY: Command to send to modem is empty"); + return; + } + use embedded_io::Write; + // Now we can safely call self.write without borrowing self.tosend + if let Err(e) = self.write(&cmd).await { + error!("UART1 write failure: {:?}", e); + } else { + info!("UART1 wrote [{:?}] successfully", cmd); + } + } +} diff --git a/src/run/mod.rs b/src/run/mod.rs new file mode 100644 index 0000000..d6f851f --- /dev/null +++ b/src/run/mod.rs @@ -0,0 +1 @@ +pub mod modem; diff --git a/src/run/modem/apn.rs b/src/run/modem/apn.rs new file mode 100644 index 0000000..0a63b2f --- /dev/null +++ b/src/run/modem/apn.rs @@ -0,0 +1,82 @@ +// use super::{ +// errors::{Error, Result}, +// module::{FoundResponse, Modem}, +// parse::parse::{ModemResponse, Status}, +// }; + +use super::{ + error::{Error, Result}, + module::FoundResponse, +}; + +#[derive(Clone)] +pub struct Apn<'a> { + pub apn: &'a str, + pub username: &'a str, + pub password: &'a str, +} + +impl<'a> Apn<'a> { + pub const fn new(apn: &'a str) -> Self { + Self { + apn, + username: "", + password: "", + } + } + + pub fn get_apn_field(&self) -> &'a str { + self.apn + } +} + +impl<'a> From<&'a str> for Apn<'a> { + fn from(value: &'a str) -> Self { + Apn::new(value) + } +} + +impl UartWrapper { + pub async fn get_apn(&mut self) -> Result { + self.tosend.set_to_sent(b"AT+CSTT?\r\n"); + self.send_at(None, Some("get_apn"), None).await; + + let response = self.wait_response(None, None, None).await?; + match response.1 { + FoundResponse::M1 => Ok(self.modem_res.parse_apn(&response.0.response.data[..])?), + _ => Err(Error::Custom("error getting apn")), + } + } + pub async fn set_apn( + &mut self, + provider_name: &str, + userid: &str, + password: &str, + ) -> Result { + self.tosend.set_to_sent( + alloc::format!( + "AT+CSTT=\"{}\",\"{}\",\"{}\"\r\n", + provider_name, + userid, + password + ) + .as_bytes(), + ); + + self.send_at(None, Some("set_apn"), None).await; + + let response = self.wait_response(None, None, None).await?; + match response.1 { + FoundResponse::M1 => Ok(response.0.response.status), + _ => Err(Error::Custom("error setting apn")), + } + } +} + +impl ModemResponse { + pub(crate) fn parse_apn(&self, apn_response: &[u8]) -> Result { + // +CSTT: \"CMNET\", \"\", \"\"" + let apn = Apn::new("internet"); + Ok(apn) + } +} diff --git a/src/run/modem/coms/gprs/mod.rs b/src/run/modem/coms/gprs/mod.rs new file mode 100644 index 0000000..9565735 --- /dev/null +++ b/src/run/modem/coms/gprs/mod.rs @@ -0,0 +1,435 @@ +use log::{error, info}; + +use crate::run::modem::{ + errors::{Error, Result}, + module::{FoundResponse, RegistrationStatus, UartWrapper}, + parse::parse::{ModemResponse, PdpState, Status}, +}; + +#[derive(Debug, PartialEq)] +#[repr(u8)] +pub enum CIPSTATUS { + IPINITIAL, + IPSTART, + IPCONFIG, + IPIND, + IPGPRSACT, + IPSTATUS, + TCPCONNECTING, + IPCLOSE, + CONNECTOK, +} +impl<'a> UartWrapper<'a> { + /* + AT+CGATT=1 ;Attach to the GPRS network, can also use parameter 0 to detach. + OK ;Response, attach successful + + AT+CGDCONT=? ;Input test command for help information. + +CGDCONT: (1..7), (IP,IPV6,PPP),(0..3),(0..4) OK ;Response, show the helpful information. + + // AT+CGDCONT=1,"IP","internet" + AT+CGDCONT=1, "IP", "cmnet" ;Before active, use this command to set PDP context. + OK ;Response. Set context OK. + + AT+CGACT=1,1 ;Active command is used to active the specified PDP context. + OK ;Response, active successful. + + ATD*99***1# ;This command is to start PPP translation. + CONNECT ;Response, when get this, the module has been set to data state. + PPP data should be transferred after this response and anything input is treated as data. + + +++ ;This command is to change the status to online data state. + Notice that before input this command, you need to wait for a + three seconds’ break, and it should also be followed by 3 seconds’ + break, otherwise “+++” will be treated as data. + + ATH ;Use this command to return COMMAND state + ok Response + */ + + pub async fn set_modem_gprs(&mut self) -> Result<(CIPSTATUS, Status)> { + // Attempt to check network registration, handle potential errors + self.check_network_registration().await.map_err(|e| { + info!("Failed to check network registration: {:?}", e); + Error::Custom("Network registration failed") + })?; + + // Check attached network status + match self.check_attached_network().await { + Ok(s) => match s { + PdpState::Activated => { + info!("Already attached to the network"); + let pdpstatus = self.set_pdp_parameter().await.map_err(|e| { + error!("Failed to set PDP parameter: {:?}", e); + Error::Custom("Setting PDP parameter failed") + })?; + match pdpstatus { + Status::OK => { + info!("PDP set"); + let pdpactivationstatus = + self.activating_pdp_context().await.map_err(|e| { + error!("Failed to activate PDP context: {:?}", e); + Error::Custom("Activating PDP context failed") + })?; + match pdpactivationstatus { + Status::OK => { + info!("PDP context activated"); + let cipstatus = self.check_ip_status().await.map_err(|e| { + error!("Failed to check IP status: {:?}", e); + Error::Custom("Checking IP status failed") + })?; + Ok((cipstatus, pdpactivationstatus)) + } + Status::ModemError(e) => { + error!("Modem responded with an error: {:?}", e); + Err(Error::Custom("Modem error during PDP context activation")) + } + _ => Err(Error::Custom("Unexpected status")), + } + } + Status::ModemError(e) => { + error!("Modem responded with an error: {:?}", e); + Err(Error::Custom("Modem error during PDP parameter setting")) + } + _ => Err(Error::Custom("Unexpected status")), + } + } + PdpState::Deactivated => { + let status = self.attach_network().await.map_err(|e| { + error!("Failed to attach network: {:?}", e); + Error::Custom("Attaching to network failed") + })?; + match status { + Status::OK => { + info!("Attached to the network"); + let pdpstatus = self.set_pdp_parameter().await.map_err(|e| { + error!("Failed to set PDP parameter: {:?}", e); + Error::Custom("Setting PDP parameter failed") + })?; + match pdpstatus { + Status::OK => { + info!("PDP set"); + let pdpactivationstatus = + self.activating_pdp_context().await.map_err(|e| { + error!("Failed to activate PDP context: {:?}", e); + Error::Custom("Activating PDP context failed") + })?; + match pdpactivationstatus { + Status::OK => { + info!("PDP context activated"); + let cipstatus = + self.check_ip_status().await.map_err(|e| { + error!("Failed to check IP status: {:?}", e); + Error::Custom("Checking IP status failed") + })?; + Ok((cipstatus, pdpactivationstatus)) + } + Status::ModemError(e) => { + error!("Modem responded with an error: {:?}", e); + Err(Error::Custom( + "Modem error during PDP context activation", + )) + } + // Status::CONNECT => {} // + _ => Err(Error::Custom("Unexpected status")), + } + } + Status::ModemError(e) => { + info!("Modem responded with an error: {:?}", e); + Err(Error::Custom("Modem error during PDP parameter setting")) + } + _ => Err(Error::Custom("Unexpected status")), + } + } + Status::ModemError(e) => { + error!("Modem responded with an error: {:?}", e); + Err(Error::Custom("Modem error during network attachment")) + } + _ => Err(Error::Custom("Unexpected status")), + } + } + }, + Err(e) => { + error!("Failed to check attached network: {:?}", e); + Err(Error::Custom("Checking attached network failed")) + } + } + } + + pub async fn check_ip_status(&mut self) -> Result { + // AT+CIPSTATUS + // self.tosend.set_to_sent(b"AT+CIPSTATUS\r\n"); + // self.send_at(None, Some("check_ip_status"), None).await; + // let response = self.wait_response(None, None, None).await?; + + let response = { + let cmd = b"AT+CIPSTATUS\r\n"; + self.send_at(Some(cmd), Some("check_ip_status"), None).await; + let res_prefix = b"AT+CIPSTATUS\r\n+CIPSTATUS:\r\nSTATE:"; + let res_suffix = b"\r\n\r\nOK\r\n"; + self.need(Some(res_prefix), Some(res_suffix)).await + }; + + match response.1 { + FoundResponse::M1 => Ok(self + .modem_res + .parse_ip_status(&response.0.response.data[..])?), + _ => Err(Error::Custom("Failed to check IP status")), + } + } + /// Returns the attach network of this [`Modem`]. + /// Attach to the GPRS network, can also use parameter 0 to detach. + /// Activate GPRS - "AT+CGATT=1" + pub async fn attach_network(&mut self) -> Result { + // self.tosend.set_to_sent(b"AT+CGATT=1\r\n"); + // self.send_at(None, Some("attach_network"), None).await; + // let response = self.wait_response(None, None, None).await?; + + let response = { + let cmd = b"AT+CGATT=1\r\n"; + self.send_at(Some(cmd), Some("attach_network"), None).await; + let res_prefix = b"AT+CGATT=1\r\n"; + let res_suffix = b"\r\n\r\nOK\r\n"; + self.need(Some(res_prefix), Some(res_suffix)).await + }; + match response.1 { + FoundResponse::M1 => { + let status = response.0.response.status; + Ok(status) + } + _ => Err(Error::Custom("failed to attach to network")), + } + } + pub async fn start_ppp(&mut self) -> Result { + // self.tosend.set_to_sent(b"AT+CGATT=1\r\n"); + // self.send_at(None, Some("attach_network"), None).await; + // let response = self.wait_response(None, None, None).await?; + + let response = { + let cmd = b"ATD*99***1#\r\n"; + self.send_at(Some(cmd), Some("start_ppp"), None).await; + let res_prefix = b"ATD*99***1#\r\n"; + let res_suffix = b"\r\n\r\nCONNECT\r\n"; + self.need(Some(res_prefix), Some(res_suffix)).await + }; + match response.1 { + FoundResponse::M1 => { + let status = response.0.response.status; + Ok(status) + } + _ => Err(Error::Custom("failed to attach to network")), + } + } + /// Command: AT+CGDCONT=? + /// Response: +CGDCONT: (1..7), (IP,IPV6,PPP),(0..3),(0..4) + /// Returns the read pdp context of this [`Modem`]. + /// Input test command for help information. + pub async fn read_pdp_context(&mut self) -> Result { + // self.tosend.set_to_sent(b"AT+CGDCONT=?\r\n"); + // self.send_at(None, Some("read_pdp_context"), None).await; + // let response = self.wait_response(None, None, None).await?; + + let response = { + let cmd = b"AT+CGDCONT=?\r\n"; + self.send_at(Some(cmd), Some("read_pdp_context"), None) + .await; + let res_prefix = b"AT+CGDCONT=?\r\n\r\n+CGDCONT: "; + let res_suffix = b"\r\n\r\nOK\r\n"; + self.need(Some(res_prefix), Some(res_suffix)).await + }; + match response.1 { + FoundResponse::M1 => { + let status = response.0.response.status; + Ok(status) + } + _ => Err(Error::Custom("failed to read pdp context")), + } + } + pub async fn set_pdp_parameter(&mut self) -> Result { + // "AT+CGDCONT=1,\"IP\",\"CMNET\"" + // "AT+CGDCONT=1,\"IP\",\"internet\"" + // AT+CGDCONT=? + // self.tosend + // .set_to_sent(b"AT+CGDCONT=1,\"IP\",\"CMNET\"\r\n"); + // self.send_at(None, Some("set_pdp_parameter"), None).await; + // let response = self.wait_response(None, None, None).await?; + + let response = { + let cmd = b"AT+CGDCONT=1,\"IP\",\"CMNET\"\r\n"; + self.send_at(Some(cmd), Some("set_pdp_parameter"), None) + .await; + let res_prefix = b"AT+CGDCONT=1,\"IP\",\"CMNET\"\r\n"; + let res_suffix = b"\r\n\r\nOK\r\n"; + self.need(Some(res_prefix), Some(res_suffix)).await + }; + match response.1 { + FoundResponse::M1 => { + let status = response.0.response.status; + Ok(status) + } + _ => Err(Error::Custom("failed to set pdp parameter")), + } + } + pub async fn activating_pdp_context(&mut self) -> Result { + // Activate PDP context - "AT+CGACT=1,1" + // self.tosend.set_to_sent(b"AT+CGACT=1,1\r\n"); + // self.send_at(None, Some("activating_pdp_context"), None) + // .await; + // let response = self.wait_response(None, None, None).await?; + + let response = { + let cmd = b"AT+CGACT=1,1\r\n"; + self.send_at(Some(cmd), Some("activating_pdp_context"), None) + .await; + let res_prefix = b"AT+CGACT=1,1\r\n"; + let res_suffix = b"\r\n\r\nOK\r\n"; + self.need(Some(res_prefix), Some(res_suffix)).await + }; + match response.1 { + FoundResponse::M1 => { + let status = response.0.response.status; + Ok(status) + } + _ => Err(Error::Custom("Failed to activate PDP context")), + } + } + pub async fn check_attached_network(&mut self) -> Result { + // get_pdp_context_states + // Activate GPRS - "AT+CGACT=?" + // self.tosend.set_to_sent(b"AT+CGACT=?\r\n"); + // self.send_at(None, Some("check_attached_network"), None) + // .await; + // let response = self.wait_response(None, None, None).await?; + + let response = { + let cmd = b"AT+CGACT=?\r\n"; + self.send_at(Some(cmd), Some("check_attached_network"), None) + .await; + let res_prefix = b"AT+CGACT=?\r\n\r\n+CGACT: "; + let res_suffix = b"\r\n\r\nOK\r\n"; + self.need(Some(res_prefix), Some(res_suffix)).await + }; + + match response.1 { + FoundResponse::M1 => Ok(self + .modem_res + .get_pdp_context_states(&response.0.response.data[..])?), + _ => Err(Error::Custom("Failed to activate GPRS")), + } + } + /// Returns the get gprs network registration status of this [`Modem`]. + /// GPRS network registration status + pub async fn get_gprs_network_registration_status( + &mut self, + ) -> Result<(RegistrationStatus, Status)> { + // let response = { + // // self.send_at("+CGREG?", None).await?; + // self.tosend.set_to_sent(b"AT+CGREG?\r\n"); + // self.send_at(None, Some("get_gprs_network_registration_status"), None) + // .await; + // let response = self.wait_response(None, None, None).await?; + // }; + let response = { + let cmd = b"AT+CGREG?"; + self.send_at(Some(cmd), Some("gprs_network"), None).await; + let res_prefix = b"AT+CGREG?\r\n\r\n+CGREG: "; + let res_suffix = b"\r\n\r\nOK\r\n"; + self.need(Some(res_prefix), Some(res_suffix)).await + }; + match response.1 { + FoundResponse::M1 => Ok(self + .modem_res + .parse_registration_status(&response.0.response.data[..])?), + // FoundResponse::M1 => Ok(&response.0), + _ => Err(Error::Custom("error getting registration status")), + } + } +} + +impl ModemResponse { + const fn cipstatus_from_repr(d: &[u8]) -> Option { + match d { + b"IP INITIAL" => Some(CIPSTATUS::IPINITIAL), + b"CONNECT OK" => Some(CIPSTATUS::CONNECTOK), + b"IP START" => Some(CIPSTATUS::IPSTART), + b"IP CONFIG" => Some(CIPSTATUS::IPCONFIG), + b"IP IND" => Some(CIPSTATUS::IPIND), + b"IP GPRSACT" => Some(CIPSTATUS::IPGPRSACT), + b"TCP CONNECTING" => Some(CIPSTATUS::TCPCONNECTING), + b"IP CLOSE" => Some(CIPSTATUS::IPCLOSE), + _ => None, + } + } + pub(crate) fn parse_ip_status(&self, cipstatus_response: &[u8]) -> Result { + // "AT+CIPSTATUS\r\n+CIPSTATUS:\r\nSTATE:IP INITIAL \r\n\r\nOK\r\n" + // "+CIPSTATUS:\r\nSTATE:IP INITIAL " + let prefix = b"+CIPSTATUS:\r\nSTATE:"; + + // Check if the response starts with the expected prefix + if !cipstatus_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse ip status")); + } + + // Remove the prefix by skipping its length + let status_content = &cipstatus_response[prefix.len()..]; + + // const IPINITIAL: &[u8] = b"IP INITIAL"; + // const CONNECTOK: &[u8] = b"CONNECT OK"; + // const CONNECTOK: &[u8] = b"IP CLOSE"; + + match Self::cipstatus_from_repr(status_content) { + Some(cip) => Ok(cip), + None => Err(Error::Custom("unknown cipstatus")), + } + } + + pub(crate) fn parse_ip_status_old(&self, cipstatus_response: &[u8]) -> Result { + // "AT+CIPSTATUS\r\n+CIPSTATUS:\r\nSTATE:IP INITIAL \r\n\r\nOK\r\n" + // "+CIPSTATUS:\r\nSTATE:IP INITIAL " + let prefix = b"+CIPSTATUS:\r\nSTATE:"; + + // Check if the response starts with the expected prefix + if !cipstatus_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse ip status")); + } + + // Remove the prefix by skipping its length + let status_content = &cipstatus_response[prefix.len()..]; + + // Find the position of the `:IP` substring + match status_content + .windows(b":IP".len()) + .position(|window| window == b":IP") + { + Some(ip_start) => { + // Check if a number exists before the `:IP` substring + if status_content[..ip_start] + .iter() + .rposition(|&b| b.is_ascii_digit()) + .is_none() + { + return Err(Error::NoNumberExist); + } + // Extract the number before the `:IP` substring + match status_content[..ip_start] + .iter() + .rposition(|&b| b.is_ascii_digit()) + { + Some(number_start) => { + let num_str = + core::str::from_utf8(&status_content[number_start..number_start + 1]) + .map_err(|_| Error::ParseError)?; + let number: u8 = num_str.parse().map_err(|_| Error::InvalidNumber)?; + if number <= 7 { + return Ok(number); + } else { + return Err(Error::InvalidNumber); + } + } + None => return Err(Error::NoNumberExist), + } + } + None => return Err(Error::NoNumberExist), + } + } +} diff --git a/src/run/modem/coms/mod.rs b/src/run/modem/coms/mod.rs new file mode 100644 index 0000000..682908f --- /dev/null +++ b/src/run/modem/coms/mod.rs @@ -0,0 +1 @@ +pub mod gprs; diff --git a/src/run/modem/errors.rs b/src/run/modem/errors.rs new file mode 100644 index 0000000..9e61138 --- /dev/null +++ b/src/run/modem/errors.rs @@ -0,0 +1,14 @@ +#[derive(Debug)] +pub enum Error { + Custom(&'static str), + ParseError, + BorrowError, + InvalidState, + InvalidNumber, + NoNumberExist, + InitTimeout, + Timeout, + UartError(), +} + +pub type Result = core::result::Result; diff --git a/src/run/modem/impls.rs b/src/run/modem/impls.rs new file mode 100644 index 0000000..2c31810 --- /dev/null +++ b/src/run/modem/impls.rs @@ -0,0 +1,44 @@ +use embedded_io_async::{BufRead, ErrorType, Read, Write}; + +use super::module::UartWrapper; + +impl<'a> ErrorType for UartWrapper<'a> { + type Error = hal::uart::Error; +} + +impl<'a> Read for UartWrapper<'a> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let available = self.write_pos - self.read_pos; + let to_copy = available.min(buf.len()); + buf[..to_copy].copy_from_slice(&self.buffer[self.read_pos..self.read_pos + to_copy]); + self.read_pos += to_copy; + + if to_copy == 0 { + // Buffer is empty, read directly from UART + self.uart1.rx.read(buf).await + } else { + Ok(to_copy) + } + } +} + +impl<'a> Write for UartWrapper<'a> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.uart1.tx.write(buf).await + } +} + +impl<'a> BufRead for UartWrapper<'a> { + async fn fill_buf(&mut self) -> Result<&[u8], Self::Error> { + if self.read_pos == self.write_pos { + // Buffer is empty, fill it + self.read_pos = 0; + self.write_pos = self.uart1.rx.read(&mut self.buffer).await?; + } + Ok(&self.buffer[self.read_pos..self.write_pos]) + } + + fn consume(&mut self, amt: usize) { + self.read_pos = (self.read_pos + amt).min(self.write_pos); + } +} diff --git a/src/run/modem/mod.rs b/src/run/modem/mod.rs new file mode 100644 index 0000000..d42617f --- /dev/null +++ b/src/run/modem/mod.rs @@ -0,0 +1,6 @@ +// pub mod apn; +pub mod coms; +pub mod errors; +pub mod impls; +pub mod module; +pub mod parse; diff --git a/src/run/modem/module.rs b/src/run/modem/module.rs new file mode 100644 index 0000000..149fa3c --- /dev/null +++ b/src/run/modem/module.rs @@ -0,0 +1,300 @@ +use crate::singleton; + +use super::{ + errors::{Error, Result}, + parse::parse::{ModemResponse, Status, ToSend}, +}; +use alloc::{ + // rc::Rc, + string::ToString, + vec::Vec, +}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; +use embassy_time::{with_timeout, Duration, Instant, Timer}; +use embedded_io_async::Write; +use hal::{ + clock::Clocks, + gpio::GpioPin, + peripherals::{UART0, UART1}, + uart::{ + config::{Config, DataBits, Parity, StopBits}, + ClockSource, Uart, UartRx, UartTx, + }, + Async, +}; +use log::{error, info, warn}; +// uart::{ +// config::{Config as UartConfig, DataBits, Parity, StopBits}, +// ClockSource, TxRxPins, Uart, UartTx, +// }, +// use static_cell::StaticCell; +pub struct SharedUart1<'a> { + pub tx: UartTx<'a, UART1, Async>, + pub rx: UartRx<'a, UART1, Async>, +} + +pub struct UartWrapper<'a> { + pub uart1: SharedUart1<'a>, + // pub uart1: Uart<'a, UART1, Async>, + pub signal: &'a Signal)>, + pub buffer: [u8; 256], // Adjust size as needed + pub read_pos: usize, + pub write_pos: usize, + pub modem_res: ModemResponse, + pub tosend: ToSend, +} +impl<'a> UartWrapper<'a> { + pub async fn init_with_retry(&mut self) -> Result<()> { + for attempt in 1..=3 { + info!("Init attempt {}", attempt); + match with_timeout(Duration::from_secs(10), self.init(None)).await { + Ok(result) => return result, + Err(_) => warn!("Init attempt {} timed out", attempt), + } + } + Err(Error::InitTimeout) + } + // async fn wait_for_response(&mut self, timeout: Duration) -> Result> { + // let mut buffer = Vec::new(); + // let deadline = Timer::after(timeout); + + // // let mut modem_guard = modem.lock().await; + + // loop { + // match embassy_futures::select::select(self.uart1.read(), deadline).await { + // embassy_futures::select::Either::First(Ok(byte)) => { + // buffer.push(byte); + // if buffer.ends_with(b"\r\nOK\r\n") { + // return Ok(buffer); + // } + // } + // embassy_futures::select::Either::First(Err(e)) => return Err(Error::UartError(e)), + // embassy_futures::select::Either::Second(_) => return Err(Error::Timeout), + // } + // } + // } + pub async fn init(&mut self, timeout_ms: Option) -> Result<()> { + let timeout = timeout_ms.unwrap_or(30000); + let start_time = Instant::now(); + loop { + info!("Modem: init function!"); + // Send a simple AT command + let (_, found_response) = { + let cmd = b"AT\r\n"; + self.send_at(Some(cmd), Some("init"), None).await; + let isok_res_prefix = b"AT\r\n\r\nOK\r\n"; + self.need(Some(isok_res_prefix), None).await + }; + if start_time.elapsed().as_millis() > timeout { + info!("timeout initializing modem"); + } + match found_response { + FoundResponse::M1 => break, + _ => Timer::after(Duration::from_millis(1000)).await, + }; + } + esp_println::println!("\nsuccessfully established communication with the modem"); + Ok(()) + } + /// Check that the cellular module is alive. + /// + /// See if the cellular module is responding at the AT interface by poking + /// it with "AT" up to `attempts` times, waiting 1 second for an "OK" + /// response each time + pub async fn is_alive(&mut self, attempts: u8) -> Result { + for i in 0..attempts { + let (res, found_response) = { + let cmd = b"AT\r\n"; + self.send_at( + Some(cmd), + Some(("is_alive: ".to_string() + &i.to_string()).as_str()), + None, + ) + .await; + let isok_res_prefix = b"AT\r\n\r\nOK\r\n"; + self.need(Some(isok_res_prefix), None).await + }; + match found_response { + FoundResponse::M1 => { + return Ok(res.response.status); + } + FoundResponse::M2 => { + return Ok(res.response.status); + } + _ => { + continue; + } + } + } + + Err(Error::Custom("Modem not responding in is_alive")) + } + pub async fn check_network_registration(&mut self) -> Result { + let response = { + let cmd = b"AT+CREG?\r\n"; + self.send_at(Some(cmd), Some("check_network_registration"), None) + .await; + let res_prefix = b"AT+CREG?\r\n\r\n+CREG: "; + let res_suffix = b"\r\n\r\nOK\r\n"; + self.need(Some(res_prefix), Some(res_suffix)).await + }; + match response.1 { + FoundResponse::M1 => Ok(self + .modem_res + .parse_network_registaration(&response.0.response.data[..])?), + _ => Err(Error::Custom("error getting subscriber number")), + } + } + + pub async fn need( + &mut self, + gps_prefixparam: Option<&[u8]>, + gps_suffixparam: Option<&[u8]>, + ) -> (ModemResponse, FoundResponse) { + let gps_prefix = gps_prefixparam.unwrap_or(AT_CMD); + let gps_suffix = gps_suffixparam.unwrap_or(AT_CMD); + let mut modem_response = ModemResponse::new(); + let mut found_response = FoundResponse::None; + let mut received_bytes: Vec = Vec::new(); + + loop { + let (_len, line_bytes) = self.signal.wait().await; + received_bytes.extend_from_slice(&line_bytes); + self.signal.reset(); + info!("looping……"); + + if gps_prefixparam.is_some() && gps_suffixparam.is_some() { + if let Some((prefix_pos, suffix_pos)) = + find_prefix_suffix(&received_bytes, gps_prefix, gps_suffix) + { + let extracted = &received_bytes[prefix_pos..suffix_pos + gps_suffix.len()]; + modem_response = self + .modem_res + .parse0(&extracted, Some(self.tosend.get_to_sent())); + found_response = FoundResponse::M1; + break; + } + } else if let Some(pos) = find_sequence(&received_bytes, gps_prefix) { + let extracted = &received_bytes[pos..pos + gps_prefix.len()]; + modem_response = self + .modem_res + .parse0(&extracted, Some(self.tosend.get_to_sent())); + found_response = FoundResponse::M1; + break; + } + + if find_sequence(&received_bytes, b"ERROR").is_some() { + modem_response = self + .modem_res + .parse0(&received_bytes, Some(self.tosend.get_to_sent())); + found_response = FoundResponse::M2; + break; + } + } + (modem_response, found_response) + } +} +const AT_CMD: &[u8] = b"AT\r\n"; +// const AT_OK_RESPONSE: &[u8] = b"AT\r\n\r\nOK\r\n"; +// const AT_CREG: &[u8] = b"AT+CREG?\r\n"; +// const CREG_RESPONSE_PREFIX: &[u8] = b"AT+CREG?\r\n\r\n+CREG: "; +// const RESPONSE_SUFFIX: &[u8] = b"\r\n\r\nOK\r\n"; + +fn find_prefix_suffix(buffer: &[u8], prefix: &[u8], suffix: &[u8]) -> Option<(usize, usize)> { + if let Some(prefix_pos) = buffer + .windows(prefix.len()) + .position(|window| window == prefix) + { + let after_prefix_pos = prefix_pos + prefix.len(); + if let Some(suffix_pos) = buffer[after_prefix_pos..] + .windows(suffix.len()) + .position(|window| window == suffix) + { + return Some((prefix_pos, after_prefix_pos + suffix_pos)); + } + } + None +} + +fn find_sequence(buffer: &[u8], sequence: &[u8]) -> Option { + buffer + .windows(sequence.len()) + .position(|window| window == sequence) +} +#[derive(Debug)] +pub enum FoundResponse { + None, + M1, + M2, +} +#[derive(Debug, PartialEq)] +#[repr(u8)] +pub enum RegistrationStatus { + NotRegistered = 0, + OkHome = 1, + Searching = 2, + Denied = 3, + Unknown = 4, + OkRoaming = 5, +} +#[derive(Debug, PartialEq)] +#[repr(u8)] +pub enum PreferredMode { + Automatic = 2, + CDMAOnly = 9, + EVDOOnly = 10, + GSMOnly = 13, + WCDMAOnly = 14, + GSMPlusWCDMAOnly = 19, + CDMAPlusEVDOOnly = 22, + LTEOnly = 37, + GSMPlusWCDMAPlusLTEOnly = 39, + AnyButLTE = 48, + GSMPlusLTEOnly = 51, + WCDMAPlusLTEOnly = 54, + TDCDMAOnly = 59, + GSMPlusTDSCDMAOnly = 60, + GSMPlusWCDMAPlusTDSCDMAOnly = 63, + CDMAPlusEVDOPlusGSMPlusWCDMAPlusTDSCDMAOnly = 67, +} +#[derive(Debug, PartialEq)] +#[repr(u16)] +pub enum ModemErrors { + OPERATIONNOTALLOWED = 3, + EXENOTSUPPORT = 49, + EXEFAIL = 50, + PARAMINVALID = 53, + INVALIDCOMMANDLINE = 58, + OPERNOTSUPP = 303, + INVALIDTXTPARAM = 305, + INVALIDMEMINDEX = 321, + UNKNOWNERROR = 500, +} + +#[derive(Debug, PartialEq)] +pub enum SIMStatus { + UNKNOWN, + READY, + SIMPIN, + SIMPUK, + PHSIMPIN, + SIMPIN2, + SIMPUK2, + PHNETPIN, +} + +pub struct SignalQuality { + pub rssi: u8, + pub ber: u8, +} + +impl SignalQuality { + pub fn rssi(&self) -> Option { + match self.rssi { + 0 => Some(-115), + 1 => Some(-111), + 2..=32 => Some(-110 + 2 * (self.rssi - 2) as i8), + _ => None, + } + } +} diff --git a/src/run/modem/parse/mod.rs b/src/run/modem/parse/mod.rs new file mode 100644 index 0000000..ea86848 --- /dev/null +++ b/src/run/modem/parse/mod.rs @@ -0,0 +1 @@ +pub mod parse; diff --git a/src/run/modem/parse/parse.rs b/src/run/modem/parse/parse.rs new file mode 100644 index 0000000..c0f20f7 --- /dev/null +++ b/src/run/modem/parse/parse.rs @@ -0,0 +1,1012 @@ +use core::fmt; + +use alloc::vec::Vec; +use log::info; + +use crate::run::modem::{ + // apn::Apn, + errors::{Error, Result}, + module::{ModemErrors, PreferredMode, RegistrationStatus, SIMStatus, SignalQuality}, +}; + +pub struct ToSend { + pub to_send: Vec, + pub len: usize, + pub timeout_ms: Option, +} + +impl ToSend { + pub fn new() -> Self { + Self { + to_send: Vec::new(), + len: 0, + timeout_ms: Some(0), + } + } + pub fn at_tosend(&mut self, _cmd: &[u8]) -> Self { + let cmd_to_send = self.get_to_sent(); + Self { + to_send: cmd_to_send.to_vec(), + len: cmd_to_send.len(), + timeout_ms: Some(10000), + } + } + pub fn empty_tosend(&mut self) -> Self { + Self { + to_send: Vec::new(), + len: 0, + timeout_ms: Some(0), + } + } + pub fn at_terminal_cmd(&mut self, cmd: &[u8]) -> Self { + // let mut extended_cmd = cmd.to_vec(); + // extended_cmd.push(10); + let cleaned_cmd = clean_array::remove_chars_and_add_newline(&cmd[..]); + + Self { + to_send: cleaned_cmd, + len: cmd.len() + 1, + timeout_ms: Some(10000), + } + } + + pub fn clear(&mut self) { + self.to_send = Vec::new(); + self.len = 0; + } + pub fn get_to_sent(&mut self) -> &[u8] { + let at = [65, 84]; + if self.to_send[..] + .windows(at.len()) + .any(|window| window == at) + { + &self.to_send[..] + // found_response = FoundResponse::M1; + } else if self.to_send[..] + .windows([26].len()) + .any(|window| window == [26]) + { + &self.to_send[..] + // found_response = FoundResponse::M1; + } else { + esp_println::println!("\nAT sent does not have the PREFIX [AT]"); + &self.to_send[..] + } + } + pub fn set_to_sent(&mut self, cmd: &[u8]) { + let cleaned_cmd = clean_array::remove_chars_and_add_newline(&cmd[..]); + self.to_send = cleaned_cmd; + } + pub fn cmd_t(&mut self, cmd: &[u8]) -> Vec { + let cleaned_cmd = clean_array::remove_chars_and_add_newline(&cmd[..]); + cleaned_cmd + } + pub fn get_timeout(&mut self) -> Option { + self.timeout_ms + } +} + +pub enum Status { + OK, + CONNECT, + ModemError(Option), +} + +pub enum PdpState { + Deactivated = 0, + Activated = 1, +} + +pub struct ResponseData { + pub data: Vec, + pub status: Status, + // error: Option<[u8; 8]>, +} + +impl ResponseData { + pub fn new() -> Self { + ResponseData { + data: Vec::new(), + status: Status::ModemError(None), + } + } + + pub fn set_data(&mut self, bytes: &[u8]) { + // let length = bytes.len(); + // self.data[..length].copy_from_slice(&bytes[..length]); + self.data.extend_from_slice(&bytes[..]); + } + + pub fn set_status(&mut self, bytes: &[u8]) { + if bytes.contains(&79) && bytes.contains(&75) { + self.status = Status::OK; + } else { + } + } + pub fn set_error_from_modem(&mut self, e: ModemErrors) { + self.status = Status::ModemError(Some(e)); + } +} + +impl core::fmt::Debug for ResponseData { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let cleaned = clean_array::remove_invalid_utf8(&self.data[..]); + let data_str = core::str::from_utf8(&cleaned).unwrap_or(""); + // let status_str = core::str::from_utf8(&self.status).unwrap_or(""); + + f.debug_struct("ResponseData") + .field("data", &data_str) + .field("status", &self.status) + .finish() + // f.debug_struct("ResponseData") + // .field("data", &self.data) + // .field("status", &self.status) + // .field("error", &error_str) + // .finish() + } +} + +pub struct ModemResponse { + pub bytesresponse: Vec, + pub command_sent: Vec, + pub response: ResponseData, +} + +pub struct DateTime { + pub datetime: Vec, + pub date: Vec, + pub time: Vec, +} +impl ModemResponse { + pub fn new() -> Self { + ModemResponse { + bytesresponse: Vec::new(), + command_sent: Vec::new(), + response: ResponseData::new(), + } + } + + pub fn set_command_sent(&mut self, command: &[u8]) { + // let length = command.len(); + // self.command_sent[..length].copy_from_slice(&command[..length]); + self.command_sent.extend_from_slice(&command[..]); + } + pub fn set_bytes_response(&mut self, command: &[u8]) { + self.bytesresponse.extend_from_slice(&command[..]); + } + pub fn clear(&mut self) { + self.command_sent = Vec::new(); + self.response = ResponseData { + data: Vec::new(), + status: Status::ModemError(None), + } + } + + pub fn parse0(&mut self, received_bytes: &[u8], separator: Option<&[u8]>) -> Self { + // if received_bytes + // .windows([26].len()) + // .any(|window| window == [26]) + // && self.bytesresponse.is_empty() + // { + // self.set_bytes_response(received_bytes); + // } + if self.bytesresponse.is_empty() { + if received_bytes + .windows([26].len()) + .any(|window| window == [26]) + && self.bytesresponse.is_empty() + { + self.set_bytes_response(received_bytes); + } + } else { + self.bytesresponse.clear() + } + let mut modem_response = ModemResponse::new(); + + let (nested_chunks, _final_chunk) = split_nested(&received_bytes, separator); + // if received_bytes + // .windows([26].len()) + // .any(|window| window == [26]) + // { + // esp_println::println!("\nHere, nested_chunks: {:?}", nested_chunks.len()); + // esp_println::println!("\nHere, nested_chunks: {:?}", nested_chunks); + // } + for (i, v) in nested_chunks.iter().enumerate() { + if nested_chunks.len() == 4 + && received_bytes + .windows([26].len()) + .any(|window| window == [26]) + { + match i { + 0 => modem_response.set_command_sent(v), + 1 => { + // debug_println!("Here: {:?}", v); + modem_response.set_command_sent(v); + modem_response.bytesresponse.clear(); + } + 2 => modem_response.response.set_data(v), + 3 => modem_response.response.set_status(v), + _ => { + esp_println::println!("\nOther element at index {}: {:?}", i, v); + // Perform some action for other elements + } + } + } else if nested_chunks.len() == 3 { + match i { + 0 => modem_response.set_command_sent(v), + 1 => { + // debug_println!("Here: {:?}", v); + modem_response.response.set_data(v) + } + 2 => modem_response.response.set_status(v), + _ => { + esp_println::println!("\nOther element at index {}: {:?}", i, v); + // Perform some action for other elements + } + } + } else if nested_chunks.len() == 2 { + //OK or CONNECT without data + match i { + 0 => modem_response.set_command_sent(v), + 1 => { + let okcrlf = [79, 75, 13, 10]; + let ee = [69, 82, 82, 79, 82]; + // let ctrlz = [26]; + if v.windows(okcrlf.len()).any(|window| window == okcrlf) { + // debug_println!("Here: {:?}", v); + modem_response.response.set_status(v) + } else if v.windows(ee.len()).any(|window| window == ee) { + // debug_println!("ModemError: {:?}", v); + match self.parse_error(v) { + Ok(e) => { + // Successfully parsed the error + // Use `e` as needed + esp_println::println!("\nParsed error: {:?}", e); + modem_response.response.set_error_from_modem(e) + } + Err(_) => { + // Handle the error case, possibly log it or take corrective action + esp_println::println!("\nFailed to parse error"); + } + } + } else if v.windows([26].len()).any(|window| window == [26]) { + // debug_println!("Here: {:?}", v); + modem_response.response.set_data(v) + } else { + info!("OK or Error not found{:?}", v); + } + } + _ => { + info!("Other element at index {}: {:?}", i, v); + } + } + } else if nested_chunks.len() == 1 { + match i { + 0 => { + let okcrlf = [79, 75, 13, 10]; + let ee = [69, 82, 82, 79, 82]; + // debug_println!("Here: {:?}", v); + if v.windows(ee.len()).any(|window| window == ee) { + match self.parse_error(v) { + Ok(e) => { + esp_println::println!("\nParsed error: {:?}", e); + modem_response.response.set_error_from_modem(e) + } + Err(_) => { + esp_println::println!("\nFailed to parse error"); + } + } + } else { + modem_response.response.set_data(v) + } + } + 1 => modem_response.response.set_status(v), + _ => { + info!("Other element at index {}: {:?}", i, v); + } + } + // debug_println!("NOT COVERED"); + } else { + info!("NOT COVERED"); + } + } + modem_response + } + + const fn modem_error_from_repr(d: u16) -> Option { + match d { + 3 => Some(ModemErrors::OPERATIONNOTALLOWED), + 49 => Some(ModemErrors::EXENOTSUPPORT), + 50 => Some(ModemErrors::EXEFAIL), + 53 => Some(ModemErrors::PARAMINVALID), + 58 => Some(ModemErrors::INVALIDCOMMANDLINE), + 303 => Some(ModemErrors::OPERNOTSUPP), + 305 => Some(ModemErrors::INVALIDTXTPARAM), + 321 => Some(ModemErrors::INVALIDMEMINDEX), + 500 => Some(ModemErrors::UNKNOWNERROR), + _ => None, + } + } + + pub fn parse_error(&mut self, error_response: &[u8]) -> Result { + // Convert the prefix into a byte array + // Define the possible prefixes + let cme_prefix = b"+CME ERROR: "; + let cms_prefix = b"+CMS ERROR: "; + + let data = if error_response.starts_with(cme_prefix) { + // If the response starts with the CME prefix, strip it + &error_response[cme_prefix.len()..] + } else if error_response.starts_with(cms_prefix) { + // If the response starts with the CMS prefix, strip it + &error_response[cms_prefix.len()..] + } else { + // If the response doesn't start with any known prefix, return an error + return Err(Error::Custom("no matches in parse error")); + }; + + // Parse the raw_rssi part as a u8 + let number = core::str::from_utf8(data) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + // Convert the modem_error_from_repr to a ModemErrors enum + match Self::modem_error_from_repr(number) { + Some(rs) => Ok(rs), + None => Err(Error::Custom("unknown code")), + } + } + const fn registration_status_from_repr(d: u8) -> Option { + match d { + 0 => Some(RegistrationStatus::NotRegistered), + 1 => Some(RegistrationStatus::OkHome), + 2 => Some(RegistrationStatus::Searching), + 3 => Some(RegistrationStatus::Denied), + 4 => Some(RegistrationStatus::Unknown), + 5 => Some(RegistrationStatus::OkRoaming), + _ => None, + } + } + const fn preferred_mode_from_repr(d: &[u8]) -> Option { + match d { + b"2" => Some(PreferredMode::Automatic), + b"9" => Some(PreferredMode::CDMAOnly), + b"10" => Some(PreferredMode::EVDOOnly), + b"13" => Some(PreferredMode::GSMOnly), + b"14" => Some(PreferredMode::WCDMAOnly), + b"19" => Some(PreferredMode::GSMPlusWCDMAOnly), + b"22" => Some(PreferredMode::CDMAPlusEVDOOnly), + b"37" => Some(PreferredMode::LTEOnly), + b"39" => Some(PreferredMode::GSMPlusWCDMAPlusLTEOnly), + b"48" => Some(PreferredMode::AnyButLTE), + b"51" => Some(PreferredMode::GSMPlusLTEOnly), + b"54" => Some(PreferredMode::WCDMAPlusLTEOnly), + b"59" => Some(PreferredMode::TDCDMAOnly), + b"60" => Some(PreferredMode::GSMPlusTDSCDMAOnly), + b"63" => Some(PreferredMode::GSMPlusWCDMAPlusTDSCDMAOnly), + b"67" => Some(PreferredMode::CDMAPlusEVDOPlusGSMPlusWCDMAPlusTDSCDMAOnly), + _ => None, + } + } + const fn sim_status_from_repr(d: &[u8]) -> Option { + match d { + b"UNKNOWN" => Some(SIMStatus::UNKNOWN), + b"READY" => Some(SIMStatus::READY), + b"SIMPIN" => Some(SIMStatus::SIMPIN), + b"SIMPUK" => Some(SIMStatus::SIMPUK), + b"PHSIMPIN" => Some(SIMStatus::PHSIMPIN), + b"SIMPIN2" => Some(SIMStatus::SIMPIN2), + b"SIMPUK2" => Some(SIMStatus::SIMPUK2), + b"PHNETPIN" => Some(SIMStatus::PHNETPIN), + _ => None, + } + } + + pub fn parse_realtime_clock(&self, cclk_response: &[u8]) -> Result { + // Modem Response String +CCLK: “24/07/01,19:29:31+03” + let prefix = b"+CCLK:"; + // Check if the response starts with the expected prefix + if !cclk_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse realtime clock")); + } + // Remove the prefix by skipping its length + let datetime = &cclk_response[prefix.len()..].to_vec(); + + // Find the first comma in the remaining data + let first_comma_idx = datetime + .iter() + .position(|&x| x == b',') + .ok_or(Error::Custom("invalid format"))?; + + // Split the remaining data at the first comma position + let (date, second_part) = datetime.split_at(first_comma_idx); + + // Remove the first comma from the second part + let time = &second_part[1..].to_vec(); + + Ok(DateTime { + datetime: datetime.to_vec(), + date: date.to_vec(), + time: time.to_vec(), + }) + } + + pub fn parse_operator_selection(&self, cops_response: &[u8]) -> Result> { + // Modem Response String +COPS: 0,2,”63902” + + let prefix = b"+COPS: "; + // Check if the response starts with the expected prefix + if !cops_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse operator selection")); + } + // Remove the prefix by skipping its length + let data = &cops_response[prefix.len()..]; + + // Find the second comma in the data + let second_comma_idx = data + .iter() + .enumerate() + .filter(|(_, &x)| x == b',') + .nth(1) + .map(|(idx, _)| idx) + .ok_or(Error::Custom("second comma not found"))?; + + // Split the remaining data at the first comma position + let (_mode_format, raw_operator) = data.split_at(second_comma_idx); + + // Remove the first comma from the second part + let operator = &raw_operator[1..]; + Ok(operator.to_vec()) + } + + pub fn parse_signal_quality(&self, csq_response: &[u8]) -> Result { + // Modem Response String +CSQ: 20,99 + // + let prefix = b"+CSQ: "; + // Check if the response starts with the expected prefix + if !csq_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse signal quality")); + } + // Remove the prefix by skipping its length + let data = &csq_response[prefix.len()..]; + + // Find the first comma in the remaining data + let first_comma_idx = data + .iter() + .position(|&x| x == b',') + .ok_or(Error::Custom("invalid format"))?; + + // Split the remaining data at the first comma position + let (raw_rssi, raw_ber) = data.split_at(first_comma_idx); + + // Remove the first comma from the second part + let flber = &raw_ber[1..]; + + // Parse the raw_rssi part as a u8 + let rssi = core::str::from_utf8(raw_rssi) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + // Parse the second part as a u8 + let ber = core::str::from_utf8(flber) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + + Ok(SignalQuality { rssi, ber }) + } + pub fn parse_sim_status(&self, cpin_response: &[u8]) -> Result { + // Modem Response String +CPIN:READY + let prefix = b"+CPIN:"; + // Check if the response starts with the expected prefix + if !cpin_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse sim status")); + } + // Remove the prefix by skipping its length + let data = &cpin_response[prefix.len()..]; + + // // Parse the first part as a u8 + // let sim_status = core::str::from_utf8(data) + // .map_err(|_| Error::ParseError)? + // .trim() + // .parse::<&str>() + // .map_err(|_| Error::ParseError)?; + // Convert the network_reg_status to a RegistrationStatus enum + match Self::sim_status_from_repr(data) { + Some(rs) => Ok(rs), + None => Err(Error::Custom("unknown code")), + } + } + pub fn parse_subscriber_number(&self, cnum_response: &[u8]) -> Result> { + // Modem Response String +CNUM: “11”,”+254114904097”,145 + let prefix = b"+CNUM: "; + // Check if the response starts with the expected prefix + if !cnum_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse subscriber number")); + } + // Remove the prefix by skipping its length + let data = &cnum_response[prefix.len()..]; + + // Find the second comma in the data + let second_comma_idx = data + .iter() + .enumerate() + .filter(|(_, &x)| x == b',') + .nth(1) + .map(|(idx, _)| idx) + .ok_or(Error::Custom("second comma not found"))?; + + // Split the remaining data at the first comma position + let (alpha_number, _ty_pe) = data.split_at(second_comma_idx); + + // Find the first comma in the alpha_number data + let first_comma_idx = alpha_number + .iter() + .position(|&x| x == b',') + .ok_or(Error::Custom("invalid format"))?; + + // Split the remaining data at the first comma position + let (_alpha, number) = data.split_at(first_comma_idx); + Ok(number.to_vec()) + } + + pub fn parse_preferred_mode(&self, cpol_response: &[u8]) -> Result { + // Modem Response String +CPOL: (1-99),2 + let prefix = b"+CPOL:"; + + // debug_println!("parse_registration_status: {:?}", cgreg_response); + + // Check if the response starts with the expected prefix + if !cpol_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse preferred mode")); + } + + // Remove the prefix by skipping its length + let data = &cpol_response[prefix.len()..]; + + // Find the first comma in the remaining data + let first_comma_idx = data + .iter() + .position(|&x| x == b',') + .ok_or(Error::Custom("invalid format"))?; + + // Split the remaining data at the first comma position + let (_first_part, second_part) = data.split_at(first_comma_idx); + + // Remove the first comma from the second part + let second_part = &second_part[1..]; + + match Self::preferred_mode_from_repr(second_part) { + Some(rs) => Ok(rs), + None => Err(Error::Custom("unknown code")), + } + } + + pub(crate) fn parse_activate_gprs_respose(&self, cgatt_response: &[u8]) -> Result { + // Modem Response String +CGATT:1 + // + let prefix = b"+CGATT: "; + // Check if the response starts with the expected prefix + if !cgatt_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse activate gprs respose")); + } + // Remove the prefix by skipping its length + let data = &cgatt_response[prefix.len()..]; + + // Parse the raw_rssi part as a u8 + let number = core::str::from_utf8(data) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + Ok(number) + } + + pub(crate) fn parse_mqtt_subscriber(&self, mqqt_publish_response: &[u8]) -> Result { + // Modem Response String +MQTTPUBLISH:1,app, 10,1234567890 + // + let prefix = b"+MQTTPUBLISH:"; + // Check if the response starts with the expected prefix + if !mqqt_publish_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse mqtt subscriber")); + } + // Remove the prefix by skipping its length + let data = &mqqt_publish_response[prefix.len()..]; + + // Find the first comma in the remaining data + let first_comma_idx = data + .iter() + .position(|&x| x == b',') + .ok_or(Error::Custom("invalid format"))?; + + // Split the remaining data at the first comma position + let (first_part, _second_part) = data.split_at(first_comma_idx); + + // Parse the raw_rssi part as a u8 + let number = core::str::from_utf8(first_part) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + Ok(number) + } + + pub(crate) fn parse_mqtt_publish(&self, mqqt_publish_response: &[u8]) -> Result { + // Modem Response String +MQTTPUBLISH: 1,test, 6,1234563 + // + let prefix = b"+MQTTPUBLISH: "; + // Check if the response starts with the expected prefix + if !mqqt_publish_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse mqtt publish")); + } + // Remove the prefix by skipping its length + let data = &mqqt_publish_response[prefix.len()..]; + + // Find the first comma in the remaining data + let first_comma_idx = data + .iter() + .position(|&x| x == b',') + .ok_or(Error::Custom("invalid format"))?; + + // Split the remaining data at the first comma position + let (first_part, _second_part) = data.split_at(first_comma_idx); + + // Parse the raw_rssi part as a u8 + let number = core::str::from_utf8(first_part) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + Ok(number) + } + pub fn parse_registration_status( + &mut self, + cgreg_response: &[u8], + ) -> Result<(RegistrationStatus, Status)> { + // Convert the prefix into a byte array + // +CGREG: 2,1,"10DC","0D2B" + let prefix = b"+CGREG:"; + + // debug_println!("parse_registration_status: {:?}", cgreg_response); + + // Check if the response starts with the expected prefix + if !cgreg_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse registration status")); + } + + // Remove the prefix by skipping its length + let data = &cgreg_response[prefix.len()..]; + + // Find the first comma in the remaining data + let first_comma_idx = data + .iter() + .position(|&x| x == b',') + .ok_or(Error::Custom("invalid format"))?; + + // Split the remaining data at the first comma position + let (first_part, second_part) = data.split_at(first_comma_idx); + + // Remove the first comma from the second part + let second_part = &second_part[1..]; + + // Parse the first part as a u8 + let _network_reg_mode = core::str::from_utf8(first_part) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + + // Parse the second part as a u8 + let network_reg_status = core::str::from_utf8(second_part) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + + // Convert the network_reg_status to a RegistrationStatus enum + match Self::registration_status_from_repr(network_reg_status) { + Some(rs) => Ok((rs, Status::OK)), + None => Err(Error::Custom("unknown code")), + } + } + + pub(crate) fn parse_network_registaration(&self, network_status_response: &[u8]) -> Result { + // +CREG: 1,1 + let prefix = b"+CREG: "; + // Check if the response starts with the expected prefix + if !network_status_response.starts_with(prefix) { + return Err(Error::Custom("no matches in parse network registaration")); + } + // Remove the prefix by skipping its length + let data = &network_status_response[prefix.len()..]; + + // Find the first comma in the remaining data + let first_comma_idx = data + .iter() + .position(|&x| x == b',') + .ok_or(Error::Custom("invalid format"))?; + + // Split the remaining data at the first comma position + let (first_part, second_part) = data.split_at(first_comma_idx); + // Remove the first comma from the second part + let part2 = &second_part[1..]; + + // Parse the raw_rssi part as a u8 + let n = core::str::from_utf8(first_part) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + // Parse the second part as a u8 + let _stat = core::str::from_utf8(part2) + .map_err(|_| Error::ParseError)? + .trim() + .parse::() + .map_err(|_| Error::ParseError)?; + Ok(n) + } + + pub(crate) fn get_pdp_context_states(&self, response: &[u8]) -> Result { + // Response +CGACT: (0,1) + let prefix = b"+CGACT: "; + + // Ensure the response starts with the expected prefix + if response.starts_with(prefix) { + // Find the position of the opening bracket '(' after the prefix + if let Some(start) = response.iter().position(|&b| b == b'(') { + // Find the position of the closing bracket ')' after the opening bracket + if let Some(end) = response.iter().position(|&b| b == b')') { + // Ensure the closing bracket comes after the opening bracket + if end > start { + // Extract the bytes within the brackets + let inside_brackets = &response[start + 1..end]; + // Extract the first digit and parse it + if let Some(&digit) = inside_brackets.iter().find(|&&b| b.is_ascii_digit()) + { + match digit { + b'0' => return Ok(PdpState::Deactivated), + b'1' => return Ok(PdpState::Activated), + _ => return Err(Error::InvalidState), + } + } + } + } + } + } + + Err(Error::ParseError) + } +} + +pub fn split_nested(bytes: &[u8], separator: Option<&[u8]>) -> (Vec>, Vec) { + let mut result = Vec::new(); + let mut result_final = Vec::new(); + result_final.push(b'<'); + let mut start = 0; + let delimiter = [13, 10, 13, 10]; + + // const SEPARATOR: &[u8] = b"AT+CIPSTATUS\r\n"; + // while let Some(end) = bytes[start..] + // .windows(delimiter.len()) + // .position(|window| window == delimiter) + // { + // result.push(bytes[start..start + end].to_vec()); + // start += end + delimiter.len(); + // } + + loop { + match bytes[start..] + .windows(delimiter.len()) + .position(|window| window == delimiter) + { + Some(end) => { + if let Some(index) = bytes[start..start + end] + .windows(separator.unwrap().len()) + .position(|window| window == separator.unwrap()) + { + let (part1, part2) = + bytes[start..start + end].split_at(index + separator.unwrap().len()); + // (part1, part2) + result.push(part1.to_vec()); + result.push(part2.to_vec()); + start += end + delimiter.len(); + } else { + // (response, &[]) + result.push(bytes[start..start + end].to_vec()); + start += end + delimiter.len(); + } + } + None => break, + } + } + + // Push the remaining part after the last delimiter + if start < bytes.len() { + result.push(bytes[start..].to_vec()); + // if bytes.len() > 1 { + // result.push(bytes[..delimiter[..2].len()].to_vec()); + // } else + } + // debug_println!("nested_chunks: {:?}", result); + + // result + // Add <> at the end of the array and use * to join the nested array + for slice in &result { + if !result_final.is_empty() { + result_final.push(b'*'); + } + result_final.extend_from_slice(&slice[..]); + } + result_final.push(b'>'); + + (result, result_final) +} +#[allow(unused)] +fn join_and_wrap(nested_slice: &[&[u8]]) -> Vec { + let mut result = Vec::new(); + result.push(b'<'); + + for slice in nested_slice { + if !result.is_empty() { + result.push(b'*'); + } + result.extend_from_slice(slice); + } + + result.push(b'>'); + result +} + +pub fn replace_crlf_with_lf(bytes: &[u8]) -> Vec { + let mut result = Vec::new(); + let mut i = 0; + + while i < bytes.len() { + if i < bytes.len() - 1 && bytes[i] == 13 && bytes[i + 1] == 10 { + result.push(10); // Append LF + i += 2; // Skip CR and LF + } else { + result.push(bytes[i]); + i += 1; + } + } + + result +} + +impl core::fmt::Debug for ModemResponse { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let cleaned = clean_array::remove_invalid_utf8(&self.command_sent[..]); + + let command_sent_str = core::str::from_utf8(&cleaned).unwrap_or(""); + f.debug_struct("ModemResponse") + .field("command_sent", &command_sent_str) + .field("response", &self.response) + .finish() + } +} + +impl fmt::Debug for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // let command_sent_str = + // core::str::from_utf8(&self.command_sent).unwrap_or(""); + match self { + Status::OK => write!(f, "OK"), + Status::CONNECT => write!(f, "CONNECT"), + Status::ModemError(Some(e)) => { + // let cleaned = clean_array::remove_invalid_utf8(&bytes[..]); + // let error_bytes = core::str::from_utf8(&cleaned).unwrap_or(""); + // write!(f, "ModemError({:?})", error_bytes) + write!(f, "ModemError({:?})", e) + } + Status::ModemError(None) => write!(f, "ModemError(None)"), + } + } +} + +pub mod clean_array { + use alloc::vec::Vec; + const KEY_LEFTSHIFT: u8 = 42; + const KEY_RIGHTSHIFT: u8 = 54; + // const CHAR_LESS_THAN: u8 = b'<'; // ASCII value for '<' + // const CHAR_GREATER_THAN: u8 = b'>'; // ASCII value for '>' + const CHAR_LESS_THAN: u8 = 60; // ASCII value for '<' + const CHAR_GREATER_THAN: u8 = 62; // ASCII value for '>' + const CR: u8 = 13; // Carriage Return + const LF: u8 = 10; // Line Feed + + pub fn remove_invalid_utf8(input: &[u8]) -> Vec { + let mut output = Vec::with_capacity(input.len()); + let mut i = 0; + while i < input.len() { + if input[i] <= 127 { + // ASCII character + output.push(input[i]); + i += 1; + } else if i + 1 < input.len() && is_valid_utf8_start(input[i]) { + // Potential start of a multi-byte sequence + let char_len = utf8_char_length(input[i]); + if i + char_len <= input.len() && is_valid_utf8_sequence(&input[i..i + char_len]) { + // Valid multi-byte sequence + output.extend_from_slice(&input[i..i + char_len]); + i += char_len; + } else { + // Invalid sequence, skip this byte + i += 1; + } + } else { + // Invalid byte, skip it + i += 1; + } + } + output + } + + // A function to remove shift keys + pub fn remove_chars_and_add_newline(input: &[u8]) -> Vec { + let mut output: Vec = input + .iter() + .filter(|&&byte| { + byte != KEY_LEFTSHIFT + && byte != KEY_RIGHTSHIFT + && byte != CHAR_LESS_THAN + && byte != CHAR_GREATER_THAN + }) + .cloned() + .collect(); + + // Check if [13, 10] already exists at the end + if !output.ends_with(&[CR, LF]) { + // If [13, 10] doesn't exist at the end, check for [13] + if output.ends_with(&[CR]) { + // If it ends with [13], just add [10] + output.push(LF); + } else { + // If it doesn't end with [13], add both [13, 10] + output.extend_from_slice(&[CR, LF]); + } + } + + output + } + + // Function to combine remove_shift_keys and remove_invalid_utf8 operations if needed + pub fn clean_input(input: &[u8]) -> Vec { + let without_shift = remove_chars_and_add_newline(input); + remove_invalid_utf8(&without_shift) + } + + fn is_valid_utf8_start(byte: u8) -> bool { + byte & 0b1100_0000 == 0b1100_0000 + } + + fn utf8_char_length(byte: u8) -> usize { + if byte & 0b1000_0000 == 0 { + 1 + } else if byte & 0b1110_0000 == 0b1100_0000 { + 2 + } else if byte & 0b1111_0000 == 0b1110_0000 { + 3 + } else if byte & 0b1111_1000 == 0b1111_0000 { + 4 + } else { + 1 + } // Invalid start byte, treat as 1-byte char + } + + fn is_valid_utf8_sequence(seq: &[u8]) -> bool { + match seq.len() { + 2 => seq[1] & 0b1100_0000 == 0b1000_0000, + 3 => seq[1] & 0b1100_0000 == 0b1000_0000 && seq[2] & 0b1100_0000 == 0b1000_0000, + 4 => { + seq[1] & 0b1100_0000 == 0b1000_0000 + && seq[2] & 0b1100_0000 == 0b1000_0000 + && seq[3] & 0b1100_0000 == 0b1000_0000 + } + _ => false, + } + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..4142610 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,41 @@ +extern crate rand; +extern crate rand_core; // or your preferred panic handler + +use rand::rngs::SmallRng; +use rand_core::RngCore; +use rand_core::SeedableRng; + +#[macro_export] +macro_rules! singleton { + ($val:expr, $T:ty) => {{ + static STATIC_CELL: ::static_cell::StaticCell<$T> = ::static_cell::StaticCell::new(); + STATIC_CELL.init($val) + }}; +} + +// Dummy entropy source for demonstration purposes +struct DummyEntropySource; + +impl DummyEntropySource { + fn new() -> Self { + DummyEntropySource + } + + fn get_entropy(&self) -> [u8; 16] { + // In a real `no_std` environment, you need to replace this with a proper entropy source. + [0x42; 16] // Just an example, replace with real entropy + } +} + +pub fn generate_random_seed() -> u64 { + let entropy_source = DummyEntropySource::new(); + let seed = entropy_source.get_entropy(); + + let mut rng = SmallRng::from_seed(seed); + + // Generate random seed + let mut seed_bytes = [0u8; 8]; + rng.fill_bytes(&mut seed_bytes); + + u64::from_le_bytes(seed_bytes) +}