esp32-phone/src/states.rs
2026-06-04 19:04:01 +03:00

456 lines
14 KiB
Rust

use core::iter::repeat;
use alloc::{
borrow::ToOwned,
boxed::Box,
format,
string::{String, ToString},
};
use esp_hal::time::{Duration, Instant};
use crate::{
async_io::{ATPromise, KeypadButton, NumberInput, TextInput},
at_commands::{
ATCommand, ATError, ATInformationCommand, CheckPinCommand, CheckPinResult, EnterPinCommand,
EnterPinResult, ListSMSMessages, SMSFormat, SMSMessageStat, SelectSMSFormatCommand,
SendSMSCommand, SendSMSResponse, SetTECharsetCommand, SimpleATResponse, TECharset,
},
display::{Position, Rgb565},
font::{HorizontalAlignment, VerticalAlignment},
state::{ATCommandHelper, Menu, MenuItem, State, StateData, TextSettings},
};
#[derive(Debug, Clone)]
pub struct DotsMessage {
message: String,
dots: u8,
prev_dots: Instant,
}
impl Default for DotsMessage {
fn default() -> Self {
Self {
message: Default::default(),
dots: Default::default(),
prev_dots: Instant::now(),
}
}
}
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, ATError>) -> 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, ATError>) -> 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, ATError>) -> Box<dyn State>,
{
fn init(&mut self, data: &mut StateData) {
self.promise = Some(data.io.send_at_command(self.command.clone()).unwrap());
}
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
// Update dots
self.message.poll();
if let Some(promise) = &self.promise {
match promise.poll(&mut data.io) {
Some(response) => {
return Some((self.fun.clone())(response));
}
None => {}
}
return None;
}
None
}
fn draw(&self, data: &mut StateData) {
data.clear_screen(Rgb565::black().as_color());
data.draw_text(
self.message.render(),
Position::new(0, 0),
TextSettings::default(),
);
}
}
#[derive(Debug, Clone)]
pub struct InitATState {
ati: ATCommandHelper<ATInformationCommand>,
sms_charset: ATCommandHelper<SelectSMSFormatCommand>,
te_charset: ATCommandHelper<SetTECharsetCommand>,
message: DotsMessage,
}
impl Default for InitATState {
fn default() -> Self {
Self {
ati: ATCommandHelper::new(ATInformationCommand),
sms_charset: ATCommandHelper::new(SelectSMSFormatCommand(SMSFormat::TextMode)),
te_charset: ATCommandHelper::new(SetTECharsetCommand(TECharset::IRA)),
message: DotsMessage::default(),
}
}
}
impl State for InitATState {
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
// Update dots
self.message.poll();
if let Some(resp) = self.ati.poll(&mut data.io) {
match resp {
Ok(_) => {}
Err(err) => {
return Some(Box::new(TextState {
text: format!("Err: {:?}", err),
after: InitATState::default(),
}));
}
}
} else {
self.message.message = "Checking ATI".to_owned();
return None;
};
if let Some(resp) = self.sms_charset.poll(&mut data.io) {
match resp {
Ok(resp) => match resp {
SimpleATResponse::Ok => {}
SimpleATResponse::Error => {
return Some(Box::new(TextState {
text: "ERROR!".to_owned(),
after: InitATState::default(),
}));
}
},
Err(err) => {
return Some(Box::new(TextState {
text: format!("Err: {:?}", err),
after: InitATState::default(),
}));
}
}
} else {
self.message.message = "Selecting SMS\ncharset".to_owned();
return None;
};
if let Some(resp) = self.te_charset.poll(&mut data.io) {
match resp {
Ok(resp) => match resp {
SimpleATResponse::Ok => {}
SimpleATResponse::Error => {
return Some(Box::new(TextState {
text: "ERROR!".to_owned(),
after: InitATState::default(),
}));
}
},
Err(err) => {
return Some(Box::new(TextState {
text: format!("Err: {:?}", err),
after: InitATState::default(),
}));
}
}
} else {
self.message.message = "Selecting TE\ncharset".to_owned();
return None;
};
Some(Box::new(EnterPinState::default()))
}
fn draw(&self, data: &mut StateData) {
data.clear_screen(Rgb565::black().as_color());
data.draw_text(
self.message.render(),
Position::new(0, 0),
TextSettings::default(),
);
}
}
#[derive(Clone)]
pub struct TextState<T: State + Clone + 'static> {
text: String,
after: T,
}
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(self.after.clone()))
} else {
None
}
}
fn draw(&self, data: &mut StateData) {
data.clear_screen(Rgb565::black().as_color());
data.draw_text(&self.text, Position::new(0, 0), TextSettings::default());
data.draw_text(
"OK",
Position::new(120, 240),
TextSettings {
h_align: HorizontalAlignment::Center,
v_align: VerticalAlignment::BottomToTop,
..Default::default()
},
);
}
}
#[derive(Clone, Default)]
pub struct EnterPinState {
input: NumberInput,
helper: Option<ATCommandHelper<EnterPinCommand>>,
}
impl State for EnterPinState {
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
if let Some(helper) = &mut self.helper {
match helper.poll(&mut data.io) {
Some(response) => match response {
Ok(response) => match response {
EnterPinResult::Ok => {
return Some(Box::new(MainMenuState::default()));
}
EnterPinResult::Error => {
return Some(Box::new(TextState {
text: "ERROR!".to_owned(),
after: EnterPinState::default(),
}));
}
EnterPinResult::ErrorMessage(msg) => {
return Some(Box::new(TextState {
text: format!("Error:\n{}", msg),
after: EnterPinState::default(),
}));
}
},
Err(err) => {
return Some(Box::new(TextState {
text: format!("Error:\n{:?}", err),
after: EnterPinState::default(),
}));
}
},
None => return None,
}
}
self.input.poll(&mut data.io);
if data.io.keypad.get_presses(KeypadButton::KeypadA) > 0 {
self.helper = Some(ATCommandHelper::new(EnterPinCommand(
self.input.read().clone(),
)));
}
None
}
fn draw(&self, data: &mut StateData) {
data.clear_screen(Rgb565::black().as_color());
data.draw_text("PIN:", Position::new(0, 0), TextSettings::default());
data.draw_text(
format!("{}", self.input.read()),
Position::new(0, 30),
TextSettings::default(),
);
}
}
#[derive(Debug, Default, Clone)]
enum MainMenuItem {
#[default]
SendSMS,
ReadSMS,
}
impl MenuItem for MainMenuItem {
fn as_text(&self) -> String {
match self {
MainMenuItem::SendSMS => "Send SMS",
MainMenuItem::ReadSMS => "Read SMS",
}
.to_string()
}
}
#[derive(Debug, Clone)]
pub struct MainMenuState {
menu: Menu<MainMenuItem>,
}
impl Default for MainMenuState {
fn default() -> Self {
Self {
menu: Menu::default()
.with_item(MainMenuItem::SendSMS)
.with_item(MainMenuItem::ReadSMS),
}
}
}
impl State for MainMenuState {
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
if let Some(option) = self.menu.poll(&mut data.io) {
match option {
MainMenuItem::SendSMS => return Some(Box::new(PhoneNumberState::default())),
MainMenuItem::ReadSMS => {
return Some(Box::new(ATCommandState::with(
"Loading messages".to_owned(),
ListSMSMessages {
stat: SMSMessageStat::All,
},
|resp| match resp {
Ok(messages) => {
log::info!("{:?}", messages);
Box::new(TextState {
text: format!("Messages read"),
after: MainMenuState::default(),
})
}
Err(err) => Box::new(TextState {
text: format!("Error:\n{:?}", err),
after: MainMenuState::default(),
}),
},
)));
}
}
}
None
}
fn draw(&self, data: &mut StateData) {
data.clear_screen(Rgb565::black().as_color());
self.menu.draw(data);
}
}
#[derive(Clone, Default)]
pub struct PhoneNumberState {
input: NumberInput,
}
impl State for PhoneNumberState {
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
self.input.poll(&mut data.io);
if data.io.keypad.get_presses(KeypadButton::KeypadA) > 0 {
Some(Box::new(MessageState {
number: self.input.read().clone(),
input: Default::default(),
}))
} else {
None
}
}
fn draw(&self, data: &mut StateData) {
data.clear_screen(Rgb565::black().as_color());
data.draw_text("Phone num:", Position::new(0, 0), TextSettings::default());
data.draw_text(
format!("{}", self.input.read()),
Position::new(0, 30),
TextSettings::default(),
);
}
}
pub struct MessageState {
number: String,
input: TextInput,
}
impl State for MessageState {
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
if !self.input.poll(&mut data.io) {
if data.io.keypad.get_presses(KeypadButton::KeypadA) > 0 {
Some(Box::new(ATCommandState::with(
"Sending SMS..".to_owned(),
SendSMSCommand {
destination: self.number.clone(),
message: self.input.read().clone(),
},
|resp| match resp {
Ok(resp) => match resp {
SendSMSResponse::MessageRefrence(refrence) => Box::new(TextState {
text: format!("SMS sent\nRef: {}", refrence),
after: MainMenuState::default(),
}),
SendSMSResponse::Error => Box::new(TextState {
text: "ERROR!".to_owned(),
after: MainMenuState::default(),
}),
SendSMSResponse::ErrorMessage(msg) => Box::new(TextState {
text: format!("Error:\n{}", msg),
after: MainMenuState::default(),
}),
},
Err(err) => Box::new(TextState {
text: format!("Error:\n{:?}", err),
after: MainMenuState::default(),
}),
},
)))
} else {
None
}
} else {
None
}
}
fn draw(&self, data: &mut StateData) {
data.clear_screen(Rgb565::black().as_color());
data.draw_text("Message:", Position::new(0, 0), TextSettings::default());
data.draw_text(
format!("{}", self.input.read()),
Position::new(0, 30),
TextSettings::default(),
);
}
}