Add additional ways to manage AT Commands

This commit is contained in:
Sofia 2026-06-01 19:45:36 +03:00
parent c1241798fe
commit dde828d63d
4 changed files with 240 additions and 91 deletions

View File

@ -135,7 +135,7 @@ impl Default for AsyncIO {
unsafe impl Send for AsyncIO {}
unsafe impl Sync for AsyncIO {}
#[derive(Default, Clone)]
#[derive(Default, Clone, Debug)]
pub struct ATPromise<T: ATCommand> {
_data: PhantomData<T>,
}

View File

@ -264,7 +264,7 @@ pub trait ATResponse {
fn is_error(&self) -> bool;
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum SimpleATResponse {
Ok,
Error,
@ -326,7 +326,7 @@ impl ATCommand for ATInformationCommand {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum EnterPinResult {
Ok,
Error,
@ -364,7 +364,8 @@ impl ATCommand for EnterPinCommand {
Err(ATParseError::EOF)
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum CheckPinResult {
Status(String),
Error,
@ -437,7 +438,7 @@ impl ATCommand for SelectSMSFormatCommand {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum CheckSMSFormatResult {
Mode(String),
Error,
@ -480,7 +481,7 @@ impl ATCommand for CheckSMSFormatCommand {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum TECharacterSets {
Charsets(Vec<String>),
}
@ -557,7 +558,7 @@ impl ATCommand for SetTECharsetCommand {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum SendSMSResponse {
MessageRefrence(String),
Error,

View File

@ -2,7 +2,8 @@ use alloc::{boxed::Box, string::String, vec::Vec};
use esp_hal::{Blocking, delay::Delay};
use crate::{
async_io::AsyncIO,
async_io::{ATPromise, AsyncIO},
at_commands::{ATCommand, ATParseError},
display::{Color, Display, Position, Rgb565},
font::{FontRenderer, HorizontalAlignment, VerticalAlignment},
};
@ -112,3 +113,47 @@ impl<'a> StateManager<'a> {
}
}
}
#[derive(Debug, Clone)]
pub struct ATCommandHelper<T: ATCommand> {
command: Option<T>,
promise: Option<ATPromise<T>>,
response: Option<Result<T::Response, ATParseError>>,
}
impl<T: ATCommand + 'static> ATCommandHelper<T>
where
T::Response: Clone,
{
pub fn new(cmd: T) -> ATCommandHelper<T> {
ATCommandHelper {
command: Some(cmd),
promise: None,
response: None,
}
}
pub fn poll(&mut self, io: &mut AsyncIO) -> Option<Result<T::Response, ATParseError>> {
if let Some(resp) = self.response.clone() {
return Some(resp);
} else {
if let Some(promise) = &mut self.promise {
match promise.poll(io) {
Some(response) => match response {
Some(response) => {
self.response = Some(response);
self.response.clone()
}
None => None,
},
None => None,
}
} else if let Some(cmd) = self.command.take() {
self.promise = Some(io.send_at_command(cmd).unwrap());
None
} else {
panic!();
}
}
}
}

View File

@ -11,46 +11,76 @@ use esp_hal::time::{Duration, Instant};
use crate::{
async_io::{ATPromise, KeypadButton},
at_commands::{
ATCommand, ATInformationCommand, ATResponse, CheckPinCommand, EnterPinCommand, SMSFormat,
SelectSMSFormatCommand, SendSMSCommand, SetTECharsetCommand, TECharset,
ATCommand, ATInformationCommand, ATParseError, ATResponse, CheckPinCommand, CheckPinResult,
EnterPinCommand, EnterPinResult, SMSFormat, SelectSMSFormatCommand, SendSMSCommand,
SendSMSResponse, SetTECharsetCommand, SimpleATResponse, TECharset,
},
display::{Position, Rgb565},
font::{HorizontalAlignment, VerticalAlignment},
state::{State, StateData, TextSettings},
state::{ATCommandHelper, State, StateData, TextSettings},
};
#[derive(Clone)]
pub struct ATCommandState<T: State, TErr: State, Cmd: ATCommand> {
#[derive(Debug)]
pub struct DotsMessage {
message: String,
command: Cmd,
dots: u8,
prev_dots: Instant,
promise: Option<ATPromise<Cmd>>,
after_state: T,
err_state: TErr,
}
impl<T: State + Clone, TErr: State + Clone, Cmd: ATCommand> ATCommandState<T, TErr, Cmd> {
pub fn with(
message: String,
command: Cmd,
after: T,
err: TErr,
) -> ATCommandState<T, TErr, Cmd> {
ATCommandState {
message,
command,
dots: 0,
impl Default for DotsMessage {
fn default() -> Self {
Self {
message: Default::default(),
dots: Default::default(),
prev_dots: Instant::now(),
promise: None,
after_state: after,
err_state: err,
}
}
}
impl<T: State + Clone + 'static, TErr: State + Clone + 'static, Cmd: ATCommand + Clone + 'static>
State for ATCommandState<T, TErr, Cmd>
impl DotsMessage {
pub fn poll(&mut self) {
if self.prev_dots.elapsed() > Duration::from_millis(200) {
self.dots = (self.dots + 1) % 3;
self.prev_dots = Instant::now();
}
}
pub fn render(&self) -> String {
let dots = repeat(".").take(self.dots as usize).collect::<String>();
format!("{}{}", self.message, dots)
}
}
pub struct ATCommandState<Cmd: ATCommand, F>
where
F: FnOnce(Result<Cmd::Response, ATParseError>) -> Box<dyn State>,
{
message: DotsMessage,
command: Cmd,
promise: Option<ATPromise<Cmd>>,
fun: F,
}
impl<Cmd: ATCommand, F> ATCommandState<Cmd, F>
where
F: FnOnce(Result<Cmd::Response, ATParseError>) -> Box<dyn State>,
{
pub fn with(message: String, command: Cmd, after: F) -> ATCommandState<Cmd, F> {
ATCommandState {
message: DotsMessage {
message,
..DotsMessage::default()
},
command,
promise: None,
fun: after,
}
}
}
impl<Cmd: ATCommand + Clone + 'static, F: Clone> State for ATCommandState<Cmd, F>
where
F: FnOnce(Result<Cmd::Response, ATParseError>) -> Box<dyn State>,
{
fn init(&mut self, data: &mut StateData) {
self.promise = Some(data.io.send_at_command(self.command.clone()).unwrap());
@ -58,23 +88,13 @@ impl<T: State + Clone + 'static, TErr: State + Clone + 'static, Cmd: ATCommand +
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
// Update dots
if self.prev_dots.elapsed() > Duration::from_millis(200) {
self.dots = (self.dots + 1) % 3;
self.prev_dots = Instant::now();
}
self.message.poll();
if let Some(promise) = &self.promise {
match promise.poll(&mut data.io) {
Some(response) => match response {
Some(response) => {
match response {
Ok(response) => match response.is_error() {
true => return Some(Box::new(self.err_state.clone())),
false => log::info!("Response: {:?}", response),
},
Err(_) => return Some(Box::new(self.err_state.clone())),
}
self.promise = None;
return Some((self.fun.clone())(response));
}
None => {}
},
@ -83,14 +103,13 @@ impl<T: State + Clone + 'static, TErr: State + Clone + 'static, Cmd: ATCommand +
return None;
}
return Some(Box::new(self.after_state.clone()));
None
}
fn draw(&self, data: &mut StateData) {
data.clear_screen(Rgb565::black().as_color());
let dots = repeat(".").take(self.dots as usize).collect::<String>();
data.draw_text(
format!("{}{}", self.message, dots),
self.message.render(),
Position::new(0, 0),
TextSettings::default(),
);
@ -98,62 +117,137 @@ impl<T: State + Clone + 'static, TErr: State + Clone + 'static, Cmd: ATCommand +
}
#[derive(Debug, Clone)]
pub struct InitATState;
pub struct InitATState {
ati: ATCommandHelper<ATInformationCommand>,
enter_pin: ATCommandHelper<EnterPinCommand>,
check_pin: ATCommandHelper<CheckPinCommand>,
sms_charset: ATCommandHelper<SelectSMSFormatCommand>,
te_charset: ATCommandHelper<SetTECharsetCommand>,
message: String,
}
impl Default for InitATState {
fn default() -> Self {
Self {}
Self {
ati: ATCommandHelper::new(ATInformationCommand),
enter_pin: ATCommandHelper::new(EnterPinCommand("1234".to_owned())),
check_pin: ATCommandHelper::new(CheckPinCommand),
sms_charset: ATCommandHelper::new(SelectSMSFormatCommand(SMSFormat::TextMode)),
te_charset: ATCommandHelper::new(SetTECharsetCommand(TECharset::IRA)),
message: "Initializing".to_owned(),
}
}
}
impl State for InitATState {
fn update(&mut self, _: &mut StateData) -> Option<Box<dyn State>> {
let state = ATCommandState::with(
"Checking info".to_owned(),
ATInformationCommand,
ATCommandState::with(
"Entering PIN".to_owned(),
EnterPinCommand("1234".to_owned()),
ATCommandState::with(
"Checking PIN".to_owned(),
CheckPinCommand,
ATCommandState::with(
"Selecting SMS\ncharset".to_owned(),
SelectSMSFormatCommand(SMSFormat::TextMode),
ATCommandState::with(
"Setting\nTE charset".to_owned(),
SetTECharsetCommand(TECharset::IRA),
TextState {
text: "All done!".to_owned(),
},
InitATState::default(),
),
InitATState::default(),
),
InitATState::default(),
),
InitATState::default(),
),
InitATState::default(),
);
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
if let Some(resp) = self.ati.poll(&mut data.io) {
resp.unwrap()
} else {
self.message = "Checking ATI".to_owned();
return None;
};
Some(Box::new(state))
if let Some(resp) = self.enter_pin.poll(&mut data.io) {
match resp.unwrap() {
EnterPinResult::Ok => {}
EnterPinResult::Error => {
return Some(Box::new(TextState {
text: "ERROR!".to_owned(),
after: InitATState::default(),
}));
}
EnterPinResult::ErrorMessage(msg) => {
return Some(Box::new(TextState {
text: format!("Error:\n{}", msg),
after: InitATState::default(),
}));
}
}
} else {
self.message = "Entering PIN".to_owned();
return None;
};
if let Some(resp) = self.check_pin.poll(&mut data.io) {
match resp.unwrap() {
CheckPinResult::Status(status) => {
log::info!("Status: {}", status)
}
CheckPinResult::Error => {
return Some(Box::new(TextState {
text: "ERROR!".to_owned(),
after: InitATState::default(),
}));
}
CheckPinResult::ErrorMessage(msg) => {
return Some(Box::new(TextState {
text: format!("Error:\n{}", msg),
after: InitATState::default(),
}));
}
}
} else {
self.message = "Checking PIN".to_owned();
return None;
};
if let Some(resp) = self.sms_charset.poll(&mut data.io) {
match resp.unwrap() {
SimpleATResponse::Ok => {}
SimpleATResponse::Error => {
return Some(Box::new(TextState {
text: "ERROR!".to_owned(),
after: InitATState::default(),
}));
}
}
} else {
self.message = "Selecting SMS\ncharset".to_owned();
return None;
};
if let Some(resp) = self.te_charset.poll(&mut data.io) {
match resp.unwrap() {
SimpleATResponse::Ok => {}
SimpleATResponse::Error => {
return Some(Box::new(TextState {
text: "ERROR!".to_owned(),
after: InitATState::default(),
}));
}
}
} else {
self.message = "Selecting TE\ncharset".to_owned();
return None;
};
Some(Box::new(TextState {
text: "All done!".to_owned(),
after: PhoneNumberState::default(),
}))
}
fn draw(&self, _: &mut StateData) {}
fn draw(&self, data: &mut StateData) {
data.clear_screen(Rgb565::black().as_color());
data.draw_text(
format!("{}", self.message),
Position::new(0, 0),
TextSettings::default(),
);
}
}
#[derive(Clone)]
pub struct TextState {
pub struct TextState<T: State + Clone + 'static> {
text: String,
after: T,
}
impl State for TextState {
impl<T: State + Clone> State for TextState<T> {
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
if data.io.keypad.get_presses(KeypadButton::KeypadA) > 0 {
Some(Box::new(PhoneNumberState {
written: String::new(),
}))
Some(Box::new(self.after.clone()))
} else {
None
}
@ -174,6 +268,7 @@ impl State for TextState {
}
}
#[derive(Clone, Default)]
pub struct PhoneNumberState {
written: String,
}
@ -256,11 +351,19 @@ impl State for MessageState {
destination: self.number.clone(),
message: self.written.clone(),
},
TextState {
text: "SMS Sent!".to_owned(),
},
TextState {
text: "Error!".to_owned(),
|resp| match resp.unwrap() {
SendSMSResponse::MessageRefrence(refrence) => Box::new(TextState {
text: format!("SMS sent\nRef: {}", refrence),
after: PhoneNumberState::default(),
}),
SendSMSResponse::Error => Box::new(TextState {
text: "ERROR!".to_owned(),
after: PhoneNumberState::default(),
}),
SendSMSResponse::ErrorMessage(msg) => Box::new(TextState {
text: format!("Error:\n{}", msg),
after: PhoneNumberState::default(),
}),
},
)))
} else {