539 lines
16 KiB
Rust
539 lines
16 KiB
Rust
use core::iter::repeat;
|
|
|
|
use alloc::{
|
|
borrow::ToOwned,
|
|
boxed::Box,
|
|
format,
|
|
string::{String, ToString},
|
|
vec::Vec,
|
|
};
|
|
use esp_hal::time::{Duration, Instant};
|
|
|
|
use crate::{
|
|
async_io::{ATPromise, KeypadButton, NumberInput, TextInput},
|
|
at_commands::{
|
|
ATCommand, ATError, ATInformationCommand, CheckPinCommand, CheckPinResult, EnterPinCommand,
|
|
EnterPinResult, ListSMSMessages, ListSMSMessagesResponse, SMSFormat, SMSMessageListing,
|
|
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\nmessages".to_owned(),
|
|
ListSMSMessages {
|
|
stat: SMSMessageStat::All,
|
|
},
|
|
|resp| match resp {
|
|
Ok(response) => match response {
|
|
ListSMSMessagesResponse::Ok(messages) => {
|
|
Box::new(ListSMSMessagesState::from(messages))
|
|
}
|
|
ListSMSMessagesResponse::Error(err) => Box::new(TextState {
|
|
text: format!("Error:\n{:?}", err),
|
|
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 MessageListing {
|
|
from: String,
|
|
text: String,
|
|
}
|
|
|
|
impl MenuItem for MessageListing {
|
|
fn as_text(&self) -> String {
|
|
format!("{}: {}", self.from, self.text)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct ListSMSMessagesState {
|
|
menu: Menu<MessageListing>,
|
|
}
|
|
|
|
impl ListSMSMessagesState {
|
|
pub fn from(messages: Vec<SMSMessageListing>) -> ListSMSMessagesState {
|
|
let mut menu = Menu::default();
|
|
for message in &messages {
|
|
menu = menu.with_item(MessageListing {
|
|
from: message.from_addr.clone(),
|
|
text: message.text.clone(),
|
|
})
|
|
}
|
|
|
|
ListSMSMessagesState { menu }
|
|
}
|
|
}
|
|
|
|
impl State for ListSMSMessagesState {
|
|
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
|
|
if let Some(listing) = self.menu.poll(&mut data.io) {
|
|
return Some(Box::new(ReadMessageState {
|
|
message: listing.clone(),
|
|
}));
|
|
}
|
|
if data.io.keypad.get_presses(KeypadButton::KeypadB) > 0 {
|
|
Some(Box::new(MainMenuState::default()))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn draw(&self, data: &mut StateData) {
|
|
self.menu.draw(data);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct ReadMessageState {
|
|
message: MessageListing,
|
|
}
|
|
|
|
impl State for ReadMessageState {
|
|
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
|
|
if data.io.keypad.get_presses(KeypadButton::KeypadB) > 0 {
|
|
Some(Box::new(MainMenuState::default()))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn draw(&self, data: &mut StateData) {
|
|
data.clear_screen(Rgb565::black().as_color());
|
|
data.draw_text(
|
|
self.message.from.clone(),
|
|
Position::new(0, 0),
|
|
TextSettings::default(),
|
|
);
|
|
data.draw_text(
|
|
self.message.text.clone(),
|
|
Position::new(0, 30),
|
|
TextSettings::default(),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[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(),
|
|
);
|
|
}
|
|
}
|