Compare commits

..

No commits in common. "46a86f05b3d42d0da65642590f0503c8c2827ebf" and "da2eb1baadba3aad7f3098ae7943cd9cd466deda" have entirely different histories.

8 changed files with 173 additions and 855 deletions

View File

@ -1,32 +0,0 @@
## Phone-like device using esp32
This is a hobby project of mine where I try to recreate a simple phone-like
device using an ESP32-board using Rust and some additional hardware listed
below:
- Base board: https://www.electrokit.com/en/adafruit-esp32-feather-v2
- LCD Screen: https://www.electrokit.com/en/1.3lcd-monterad-pa-kort
- 4G module: https://www.electrokit.com/en/4g-modul-monterad-pa-kort-clipper-lte-4g
- 16-button keypad: https://www.electrokit.com/en/keypad-16-buttons-4x4-matrix
- 16-pin IO expander: https://www.electrokit.com/en/16-output-i/o-expander-breakout-sx1509
Drivers for each of these peripherals are written manually.
- Driver for the LCD screen is found at [display.rs](./src/display.rs)
- Interfaces via SPI
- Driver for the 4G module is found at [at_commands.rs](./src/at_commands.rs)
- Interfaces via UART
- Driver for the IO expander is found at [sx1509.rs](./src/sx1509.rs)
- Interfaces via I2C
This project additionally uses [esp-hal](github.com/esp-rs/esp-hal) as the sole
abstraction layer between the board and the code itself. Also
[rusttype](https://gitlab.redox-os.org/redox-os/rusttype) is used for rendering
TTF-fonts, which is used to render text with the bundled OpenSans-font.
This project also utilizes [esp32 RTOS](https://docs.rs/crate/esp-rtos/latest)
to run two threads on the two seperate cores on the device. One core processes
outward-facing state control and rendering, while the other processes I/O, such
as keypad presses and communication with the 4G-module.
## Demonstration
<video controls src="./demo.mp4" alt="demonstration">

BIN
demo.mp4

Binary file not shown.

View File

@ -1,6 +1,5 @@
use core::{cell::RefCell, char, marker::PhantomData};
use alloc::vec;
use alloc::{
boxed::Box,
collections::{btree_map::BTreeMap, btree_set::BTreeSet},
@ -9,8 +8,9 @@ use alloc::{
vec::Vec,
};
use critical_section::Mutex;
use esp_hal::delay::Delay;
use crate::at_commands::{ATCommand, ATError, ATResponseParser, SimpleATCommand};
use crate::at_commands::{ATCommand, ATParseError, ATResponseParser, SimpleATCommand};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum KeypadButton {
@ -54,19 +54,16 @@ impl Keypad {
critical_section::with(|cs| {
let mut was_pressed = self.was_pressed.borrow_ref_mut(cs);
let mut is_pressed = self.is_pressed.borrow_ref_mut(cs);
let mut num_presses = self.presses.borrow_ref(cs).clone();
let mut num_presses = self.presses.borrow_ref_mut(cs);
*was_pressed = is_pressed.clone();
*is_pressed = presses;
for button in is_pressed.iter() {
if !was_pressed.contains(button) {
let num = num_presses.get(button).copied().unwrap_or(0);
log::info!("{:?}", *button);
num_presses.insert(*button, num + 1);
}
}
*self.presses.borrow_ref_mut(cs) = num_presses;
});
}
@ -106,11 +103,22 @@ impl Keypad {
}
}
#[derive(Debug, Clone)]
pub enum ConstructedATCommand {
/// Single-part command
Single(String),
/// Two-part AT-command
AddInfo(String, String),
}
unsafe impl Send for ConstructedATCommand {}
unsafe impl Sync for ConstructedATCommand {}
#[derive(Clone)]
pub struct AsyncIO {
at_command: Rc<Mutex<RefCell<Option<Box<dyn SimpleATCommand>>>>>,
handling_at_command: Rc<Mutex<RefCell<bool>>>,
at_response: Rc<Mutex<RefCell<Option<Result<Vec<String>, ATError>>>>>,
at_response: Rc<Mutex<RefCell<Option<Vec<String>>>>>,
pub keypad: Keypad,
}
@ -134,19 +142,16 @@ pub struct ATPromise<T: ATCommand> {
}
impl<T: ATCommand> ATPromise<T> {
pub fn poll(&self, io: &mut AsyncIO) -> Option<Result<T::Response, ATError>> {
pub fn poll(&self, io: &mut AsyncIO) -> Option<Option<Result<T::Response, ATParseError>>> {
match io.poll_at_response() {
Some(response) => {
log::info!("Response: {:?}", response);
match response {
Ok(response) => {
let mut parser = ATResponseParser::from(response);
Some(T::parse_response(&mut parser))
}
Err(err) => Some(Err(err)),
Some(response) => match response {
Some(response) => {
let mut parser = ATResponseParser::from(response);
Some(Some(T::parse_response(&mut parser)))
}
}
None => None,
None => Some(None),
},
None => Some(None),
}
}
}
@ -176,7 +181,7 @@ impl AsyncIO {
})
}
pub unsafe fn set_at_response(&self, response_str: Result<Vec<String>, ATError>) {
pub unsafe fn set_at_response(&self, response_str: Vec<String>) {
critical_section::with(|cs| {
*self.handling_at_command.borrow_ref_mut(cs) = false;
let mut command = self.at_command.borrow_ref_mut(cs);
@ -186,15 +191,15 @@ impl AsyncIO {
})
}
fn poll_at_response(&self) -> Option<Result<Vec<String>, ATError>> {
fn poll_at_response(&self) -> Option<Option<Vec<String>>> {
critical_section::with(|cs| {
let command = self.at_command.borrow_ref(cs);
let mut response = self.at_response.borrow_ref_mut(cs);
if command.is_some() {
return None;
if command.is_some() || *self.handling_at_command.borrow_ref(cs) {
return Some(None);
}
if let Some(resp) = response.take() {
return Some(resp);
return Some(Some(resp));
}
None
})
@ -235,104 +240,3 @@ impl NumberInput {
&self.written
}
}
impl KeypadButton {
pub fn get_chars(&self) -> Option<Vec<char>> {
match self {
KeypadButton::Keypad1 => Some(vec![' ', '1']),
KeypadButton::Keypad2 => Some(vec!['a', 'b', 'c', '2']),
KeypadButton::Keypad3 => Some(vec!['d', 'e', 'f', '3']),
KeypadButton::Keypad4 => Some(vec!['g', 'h', 'i', '4']),
KeypadButton::Keypad5 => Some(vec!['j', 'k', 'l', '5']),
KeypadButton::Keypad6 => Some(vec!['m', 'n', 'o', '6']),
KeypadButton::Keypad7 => Some(vec!['p', 'q', 'r', 's', '7']),
KeypadButton::Keypad8 => Some(vec!['t', 'u', 'v', '8']),
KeypadButton::Keypad9 => Some(vec!['w', 'x', 'y', 'z', '9']),
KeypadButton::Keypad0 => Some(vec![' ', '0']),
KeypadButton::KeypadStar => Some(vec!['*']),
KeypadButton::KeypadHash => Some(vec!['#']),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct LastPressed {
key: KeypadButton,
idx: usize,
}
impl LastPressed {
pub fn increment(&mut self) {
let chars = self.key.get_chars();
if let Some(chars) = chars {
self.idx = (self.idx + 1) % chars.len()
}
}
pub fn get_char(&self) -> char {
self.key.get_chars().unwrap()[self.idx]
}
}
static TEXT_KEYS: [KeypadButton; 13] = [
KeypadButton::Keypad0,
KeypadButton::Keypad1,
KeypadButton::Keypad2,
KeypadButton::Keypad3,
KeypadButton::Keypad4,
KeypadButton::Keypad5,
KeypadButton::Keypad6,
KeypadButton::Keypad7,
KeypadButton::Keypad8,
KeypadButton::Keypad9,
KeypadButton::Keypad0,
KeypadButton::KeypadStar,
KeypadButton::KeypadHash,
];
#[derive(Debug, Default, Clone)]
pub struct TextInput {
written: String,
last_pressed: Option<LastPressed>,
}
impl TextInput {
pub fn poll(&mut self, io: &mut AsyncIO) -> bool {
let mut wrote = false;
for button in io.keypad.just_pressed_buttons() {
if let Some(mut last_pressed) = self.last_pressed.take() {
if last_pressed.key == button {
last_pressed.increment();
self.last_pressed = Some(last_pressed);
wrote = true;
} else if TEXT_KEYS.contains(&button) {
self.written += &last_pressed.get_char().to_string();
self.last_pressed = Some(LastPressed {
key: button,
idx: 0,
});
wrote = true;
} else if button == KeypadButton::KeypadA {
self.written += &last_pressed.get_char().to_string();
wrote = true;
}
} else if TEXT_KEYS.contains(&button) {
self.last_pressed = Some(LastPressed {
key: button,
idx: 0,
});
wrote = true;
}
}
wrote
}
pub fn read(&self) -> String {
let mut written = self.written.clone();
if let Some(last_pressed) = &self.last_pressed {
written += &last_pressed.get_char().to_string();
}
written
}
}

View File

@ -2,9 +2,10 @@ use alloc::string::{String, ToString};
use alloc::vec::Vec;
use alloc::{borrow::ToOwned, format};
use core::fmt::Write;
use esp_hal::time::{Duration, Instant};
use esp_hal::{Blocking, delay::Delay, gpio::Output, uart::Uart};
use crate::async_io::ConstructedATCommand;
static RESPONSES: [&'static str; 3] = ["OK", "ERROR", "DOWNLOAD"];
pub struct ATCommands<'a, 'd> {
@ -55,19 +56,15 @@ impl<'a, 'd> ATCommands<'a, 'd> {
self.flush_rx();
}
pub fn raw_command<T: SimpleATCommand>(
&mut self,
cmd: &T,
command: String,
timeout: Duration,
) -> Result<Vec<String>, ATError> {
pub fn raw_command<T: SimpleATCommand>(&mut self, cmd: &T, command: String) -> Vec<String> {
self.uart.flush().unwrap();
self.flush_rx();
let command = command.clone() + "\r";
self.uart.write_str(&command).unwrap();
self.uart.flush().unwrap();
self.delay.delay_millis(500);
log::info!("Wrote command {}", command);
self.read_response(cmd, command, timeout)
self.read_response(cmd, command)
}
pub fn raw_two_part_command<T: SimpleATCommand>(
@ -75,12 +72,12 @@ impl<'a, 'd> ATCommands<'a, 'd> {
cmd: &T,
command: String,
additional: String,
timeout: Duration,
) -> Result<Vec<String>, ATError> {
) -> Vec<String> {
self.uart.flush().unwrap();
self.flush_rx();
self.uart.write_str(&(command.clone() + "\r")).unwrap();
self.uart.flush().unwrap();
self.delay.delay_millis(500);
log::info!("Wrote command {}", command);
self.flush_rx();
@ -95,7 +92,7 @@ impl<'a, 'd> ATCommands<'a, 'd> {
}
self.uart.flush().unwrap();
self.read_response(cmd, command, timeout)
self.read_response(cmd, command)
}
pub fn readline(&mut self) -> Option<String> {
@ -107,23 +104,16 @@ impl<'a, 'd> ATCommands<'a, 'd> {
}
}
fn read_response<T: SimpleATCommand>(
&mut self,
cmd: &T,
start: String,
timeout: Duration,
) -> Result<Vec<String>, ATError> {
let start_time = Instant::now();
fn read_response<T: SimpleATCommand>(&mut self, cmd: &T, start: String) -> Vec<String> {
let mut response = None;
while response.is_none() {
if (start_time + timeout) < Instant::now() {
return Err(ATError::Timeout);
}
self.flush_rx();
log::info!("{:?}", self.lines);
response = self.parse_response(cmd, &start);
}
log::info!("{:?}", response);
response.unwrap()
}
@ -131,7 +121,7 @@ impl<'a, 'd> ATCommands<'a, 'd> {
&mut self,
cmd: &T,
start: &String,
) -> Option<Result<Vec<String>, ATError>> {
) -> Option<Vec<String>> {
let start_idx = self
.lines
.iter()
@ -148,8 +138,6 @@ impl<'a, 'd> ATCommands<'a, 'd> {
}
response.push(line.trim().to_string());
log::info!("{:?}", response);
let mut parser = ATResponseParser::from(response.clone());
match cmd.try_parse(&mut parser) {
@ -162,11 +150,11 @@ impl<'a, 'd> ATCommands<'a, 'd> {
for _ in 0..parser.lines {
output_lines.push(self.lines.remove(start_idx));
}
return Some(Ok(output_lines));
return Some(output_lines);
}
Err(err) => match err {
ATError::EOF => {}
_ => return Some(Err(err)),
ATParseError::EOF => {}
ATParseError::InvalidResponse => panic!("Invalid response"),
},
}
}
@ -183,9 +171,7 @@ impl<'a, 'd> ATCommands<'a, 'd> {
self.uart.read_ready()
} {
let length = self.uart.read(&mut buffer).unwrap();
for character in &buffer[..length] {
self.buffer += str::from_utf8(&[character.clone()]).unwrap_or("?")
}
self.buffer += str::from_utf8(&buffer[..length]).unwrap();
}
let lines = self
@ -231,18 +217,11 @@ impl ATResponseParser {
result.cloned()
}
pub fn params(&mut self) -> Option<Params> {
self.readline().map(|l| Params {
raw_params: l.to_owned(),
idx: 0,
})
}
pub fn expect(&mut self, text: String) -> Result<(), ATError> {
pub fn expect(&mut self, text: String) -> Result<(), ATParseError> {
let line = if let Some(curr_line) = self.curr_line.take() {
curr_line
} else {
self.readline().ok_or(ATError::EOF)?
self.readline().ok_or(ATParseError::EOF)?
};
if let Some(text) = line.strip_prefix(&text) {
@ -250,88 +229,34 @@ impl ATResponseParser {
Ok(())
} else {
self.curr_line = Some(line);
Err(ATError::InvalidResponse)
}
}
pub fn peek(&self) -> Option<String> {
if let Some(curr_line) = &self.curr_line {
return Some(curr_line.clone());
}
if let Some(text) = self.text.get(self.index) {
Some(text.clone())
} else {
None
}
}
}
pub struct Params {
raw_params: String,
idx: u32,
}
impl Params {
pub fn read_raw(&mut self) -> Option<String> {
if self.idx > 0 {
self.raw_params = self.raw_params.strip_prefix(",").unwrap().to_owned()
}
let returned;
if let Some(rest) = self.raw_params.strip_prefix("\"") {
let mut parts = rest.split("\"");
returned = parts.next().unwrap().to_owned();
self.raw_params = parts.collect::<Vec<_>>().join("\"")
} else {
let mut parts = self.raw_params.split(",");
returned = parts.next().unwrap().to_owned();
self.raw_params = parts.collect::<Vec<_>>().join(",")
}
Some(returned)
}
pub fn read_u32(&mut self) -> Option<u32> {
if let Some(part) = self.read_raw() {
u32::from_str_radix(&part, 10).ok()
} else {
None
}
}
pub fn read_string(&mut self) -> Option<String> {
if let Some(part) = self.read_raw() {
Some(part)
} else {
None
Err(ATParseError::InvalidResponse)
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum ATError {
pub enum ATParseError {
EOF,
InvalidResponse,
Timeout,
}
pub trait ATCommand: Send + Sync {
type Response: core::fmt::Debug + ATResponse;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError>;
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError>;
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String>;
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATParseError>;
}
pub trait SimpleATCommand: Send + Sync {
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError>;
fn try_parse(&self, parser: &mut ATResponseParser) -> Result<(), ATError>;
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String>;
fn try_parse(&self, parser: &mut ATResponseParser) -> Result<(), ATParseError>;
}
impl<T: ATCommand> SimpleATCommand for T {
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String> {
self.execute(at_commands)
}
fn try_parse(&self, parser: &mut ATResponseParser) -> Result<(), ATError> {
fn try_parse(&self, parser: &mut ATResponseParser) -> Result<(), ATParseError> {
let result = Self::parse_response(parser);
result.map(|_| ())
}
@ -375,11 +300,11 @@ pub struct ATInformationCommand;
impl ATCommand for ATInformationCommand {
type Response = ATInformation;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
at_commands.raw_command(self, "ATI".to_string(), Duration::from_millis(2000))
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String> {
at_commands.raw_command(self, "ATI".to_string())
}
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError> {
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATParseError> {
parser.expect("Manufacturer: ".to_string())?;
let manufacturer = parser.readline().unwrap();
parser.expect("Model: ".to_string())?;
@ -399,7 +324,7 @@ impl ATCommand for ATInformationCommand {
});
}
}
Err(ATError::EOF)
Err(ATParseError::EOF)
}
}
@ -424,15 +349,11 @@ pub struct EnterPinCommand(pub String);
impl ATCommand for EnterPinCommand {
type Response = EnterPinResult;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
at_commands.raw_command(
self,
format!("AT+CPIN={}", self.0),
Duration::from_millis(2000),
)
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String> {
at_commands.raw_command(self, format!("AT+CPIN={}", self.0))
}
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError> {
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATParseError> {
while let Some(line) = parser.readline() {
if line.starts_with("OK") {
return Ok(EnterPinResult::Ok);
@ -442,7 +363,7 @@ impl ATCommand for EnterPinCommand {
return Ok(EnterPinResult::ErrorMessage(status.to_string()));
}
}
Err(ATError::EOF)
Err(ATParseError::EOF)
}
}
@ -467,11 +388,11 @@ pub struct CheckPinCommand;
impl ATCommand for CheckPinCommand {
type Response = CheckPinResult;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
at_commands.raw_command(self, "AT+CPIN?".to_string(), Duration::from_millis(2000))
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String> {
at_commands.raw_command(self, "AT+CPIN?".to_string())
}
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError> {
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATParseError> {
if let Ok(_) = parser.expect("+CPIN: ".to_string()) {
let status = parser.readline().unwrap();
while let Some(line) = parser.readline() {
@ -488,7 +409,7 @@ impl ATCommand for CheckPinCommand {
return Ok(CheckPinResult::ErrorMessage(status.to_string()));
}
}
Err(ATError::EOF)
Err(ATParseError::EOF)
}
}
@ -503,15 +424,11 @@ pub struct SelectSMSFormatCommand(pub SMSFormat);
impl ATCommand for SelectSMSFormatCommand {
type Response = SimpleATResponse;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
at_commands.raw_command(
self,
format!("AT+CMGF={}", self.0 as u8),
Duration::from_millis(2000),
)
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String> {
at_commands.raw_command(self, format!("AT+CMGF={}", self.0 as u8))
}
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError> {
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATParseError> {
while let Some(line) = parser.readline() {
if line.starts_with("OK") {
return Ok(SimpleATResponse::Ok);
@ -519,7 +436,7 @@ impl ATCommand for SelectSMSFormatCommand {
return Ok(SimpleATResponse::Error);
}
}
Err(ATError::EOF)
Err(ATParseError::EOF)
}
}
@ -543,11 +460,11 @@ pub struct CheckSMSFormatCommand;
impl ATCommand for CheckSMSFormatCommand {
type Response = CheckSMSFormatResult;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
at_commands.raw_command(self, "AT+CMGF?".to_string(), Duration::from_millis(2000))
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String> {
at_commands.raw_command(self, "AT+CMGF?".to_string())
}
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError> {
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATParseError> {
if let Ok(_) = parser.expect("+CMGF: ".to_string()) {
let mode = parser.readline().unwrap();
while let Some(line) = parser.readline() {
@ -562,7 +479,7 @@ impl ATCommand for CheckSMSFormatCommand {
return Ok(CheckSMSFormatResult::Error);
}
}
Err(ATError::EOF)
Err(ATParseError::EOF)
}
}
@ -584,11 +501,11 @@ pub struct ListTECharacterSetsCommand;
impl ATCommand for ListTECharacterSetsCommand {
type Response = TECharacterSets;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
at_commands.raw_command(self, "AT+CSCS=?".to_string(), Duration::from_millis(2000))
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String> {
at_commands.raw_command(self, "AT+CSCS=?".to_string())
}
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError> {
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATParseError> {
parser.expect("+CSCS: ".to_owned())?;
let result = parser.readline().unwrap();
let result = result.strip_prefix("(").unwrap().strip_suffix(")").unwrap();
@ -599,7 +516,7 @@ impl ATCommand for ListTECharacterSetsCommand {
return Ok(TECharacterSets::Charsets(results));
}
}
Err(ATError::EOF)
Err(ATParseError::EOF)
}
}
@ -627,15 +544,11 @@ pub struct SetTECharsetCommand(pub TECharset);
impl ATCommand for SetTECharsetCommand {
type Response = SimpleATResponse;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
at_commands.raw_command(
self,
format!("AT+CSCS=\"{}\"", self.0.into_str()),
Duration::from_millis(2000),
)
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String> {
at_commands.raw_command(self, format!("AT+CSCS=\"{}\"", self.0.into_str()))
}
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError> {
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATParseError> {
while let Some(line) = parser.readline() {
if line.starts_with("OK") {
return Ok(SimpleATResponse::Ok);
@ -643,7 +556,7 @@ impl ATCommand for SetTECharsetCommand {
return Ok(SimpleATResponse::Error);
}
}
Err(ATError::EOF)
Err(ATParseError::EOF)
}
}
@ -672,16 +585,15 @@ pub struct SendSMSCommand {
impl ATCommand for SendSMSCommand {
type Response = SendSMSResponse;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
fn execute(&self, at_commands: &mut ATCommands) -> Vec<String> {
at_commands.raw_two_part_command(
self,
format!("AT+CMGS=\"{}\"", self.destination),
self.message.clone(),
Duration::from_millis(30000),
)
}
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError> {
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATParseError> {
if let Ok(_) = parser.expect("+CMS ERROR: ".to_owned()) {
let result = parser.readline().unwrap();
return Ok(SendSMSResponse::ErrorMessage(result));
@ -708,125 +620,6 @@ impl ATCommand for SendSMSCommand {
}
}
Err(ATError::EOF)
}
}
#[derive(Debug, Clone)]
pub enum ListSMSMessagesResponse {
Ok(Vec<SMSMessageListing>),
Error(String),
}
#[derive(Debug, Clone)]
pub struct SMSMessageListing {
pub index: u32,
pub stat: SMSMessageStat,
pub from_addr: String,
pub to_addr: String,
pub timestamp: String,
pub text: String,
}
impl ATResponse for ListSMSMessagesResponse {
fn is_error(&self) -> bool {
match self {
ListSMSMessagesResponse::Error(_) => true,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub enum SMSMessageStat {
RecUnread,
RecRead,
StoUnsent,
StoSent,
All,
}
impl SMSMessageStat {
pub fn into_str(&self) -> String {
match self {
SMSMessageStat::RecUnread => "REC UNREAD",
SMSMessageStat::RecRead => "REC READ",
SMSMessageStat::StoUnsent => "STO UNSENT",
SMSMessageStat::StoSent => "STO SENT",
SMSMessageStat::All => "ALL",
}
.to_string()
}
pub fn from(text: &str) -> SMSMessageStat {
match text {
"REC UNREAD" => Self::RecUnread,
"REC READ" => Self::RecRead,
"STO UNSENT" => Self::StoUnsent,
"STO SENT" => Self::StoSent,
_ => Self::All,
}
}
}
#[derive(Debug, Clone)]
pub struct ListSMSMessages {
pub stat: SMSMessageStat,
}
impl ATCommand for ListSMSMessages {
type Response = ListSMSMessagesResponse;
fn execute(&self, at_commands: &mut ATCommands) -> Result<Vec<String>, ATError> {
at_commands.raw_command(
self,
format!("AT+CMGL=\"{}\"", self.stat.into_str()),
Duration::from_millis(5000),
)
}
fn parse_response(parser: &mut ATResponseParser) -> Result<Self::Response, ATError> {
let mut messages = Vec::new();
while let Ok(_) = parser.expect("+CMGL: ".to_owned()) {
let mut params = parser.params().unwrap();
let idx = params.read_u32().unwrap();
let stat = SMSMessageStat::from(&params.read_string().unwrap());
let to_addr = params.read_string().unwrap();
let from_addr = params.read_string().unwrap();
let timestamp = params.read_string().unwrap();
let mut message = String::new();
while let Some(line) = parser.peek() {
if line.starts_with("OK") || line.starts_with("+CMGL: ") {
break;
}
message += &parser.readline().unwrap();
}
messages.push(SMSMessageListing {
index: idx,
stat,
from_addr,
to_addr,
timestamp,
text: message,
});
}
log::info!("Messages so far: {:?}", messages);
while let Some(line) = parser.readline() {
if line.starts_with("OK") {
return Ok(ListSMSMessagesResponse::Ok(messages));
}
if line.starts_with("+CMS ERROR: ") {
return Ok(ListSMSMessagesResponse::Error(
line.strip_prefix("+CMS ERROR: ").unwrap().to_owned(),
));
}
}
Err(ATError::EOF)
Err(ATParseError::EOF)
}
}

View File

@ -1,11 +1,10 @@
use alloc::string::ToString;
use embedded_hal::delay::DelayNs;
use esp_hal::DriverMode;
use rusttype::{Font, Point, PositionedGlyph, Scale};
use alloc::vec;
use alloc::vec::Vec;
use alloc::{borrow::ToOwned, string::String};
use alloc::{format, vec};
use crate::display::{self, Display, Position, Rgb565};
@ -67,46 +66,9 @@ impl<'a> FontRenderer<'a> {
v_align: VerticalAlignment,
bg: Rgb565,
fg: Rgb565,
wrap_text: bool,
) {
let text = text.into();
let mut rows = text.split("\n").map(|v| v.to_string()).collect::<Vec<_>>();
if wrap_text {
let mut i = 0;
while i < rows.len() {
let row = &rows[i];
let mut parts = row.split(" ").into_iter();
let mut replaced = String::new();
let mut overflow: Option<String> = None;
replaced += parts.next().unwrap();
while let Some(part) = parts.next() {
let data = self.prepare(format!("{} {}", replaced, part));
log::info!("text: {}, width: {}", replaced, data.pixel_width);
if data.pixel_width > 240 {
overflow = Some(part.to_owned());
break;
}
replaced += &format!(" {}", part);
}
while let Some(part) = parts.next() {
if let Some(overflowed) = overflow {
overflow = Some(format!("{} {}", overflowed, part));
} else {
overflow = Some(part.to_owned())
}
}
rows[i] = replaced;
if let Some(overflow) = overflow {
rows.insert(i + 1, overflow);
}
i += 1;
}
}
log::info!("{:?}", rows);
let rows = text.split("\n").collect::<Vec<&str>>();
let start_y = match v_align {
VerticalAlignment::TopToBottom => position.y,

View File

@ -155,7 +155,6 @@ fn main() -> ! {
font::VerticalAlignment::Center,
Rgb565::black(),
Rgb565::white(),
false,
);
at_commands.init();
@ -257,6 +256,10 @@ fn thread_2_main(
delay.delay_millis(5);
}
if buttons_pressed.len() > 0 {
log::info!("{:?}", buttons_pressed)
}
async_io.keypad.handle_presses(buttons_pressed);
}
}

View File

@ -3,7 +3,7 @@ use esp_hal::{Blocking, delay::Delay};
use crate::{
async_io::{ATPromise, AsyncIO},
at_commands::{ATCommand, ATError},
at_commands::{ATCommand, ATParseError},
display::{Color, Display, Position, Rgb565},
font::{FontRenderer, HorizontalAlignment, VerticalAlignment},
};
@ -14,7 +14,6 @@ pub struct TextSettings {
pub v_align: VerticalAlignment,
pub fg: Rgb565,
pub bg: Rgb565,
pub wrap: bool,
}
impl Default for TextSettings {
@ -24,39 +23,10 @@ impl Default for TextSettings {
v_align: VerticalAlignment::TopToBottom,
fg: Rgb565::white(),
bg: Rgb565::black(),
wrap: false,
}
}
}
impl TextSettings {
pub fn with_horizontal_align(self, align: HorizontalAlignment) -> TextSettings {
TextSettings {
h_align: align,
..self
}
}
pub fn with_vertical_align(self, align: VerticalAlignment) -> TextSettings {
TextSettings {
v_align: align,
..self
}
}
pub fn with_fg(self, color: Rgb565) -> TextSettings {
TextSettings { fg: color, ..self }
}
pub fn with_bg(self, color: Rgb565) -> TextSettings {
TextSettings { bg: color, ..self }
}
pub fn with_wrap(self, wrap: bool) -> TextSettings {
TextSettings { wrap, ..self }
}
}
#[derive(PartialEq, Eq, Clone)]
pub enum DrawCommands {
Clear(Color),
@ -109,16 +79,14 @@ pub struct StateManager<'a> {
impl<'a> StateManager<'a> {
pub fn update(&mut self) {
critical_section::with(|_| {
match self.curr_state.update(&mut self.data) {
Some(next_state) => {
self.curr_state = next_state;
self.curr_state.init(&mut self.data);
}
None => {}
match self.curr_state.update(&mut self.data) {
Some(next_state) => {
self.curr_state = next_state;
self.curr_state.init(&mut self.data);
}
self.data.io.keypad.clear();
})
None => {}
}
self.data.io.keypad.clear();
}
pub fn draw(&mut self) {
@ -138,7 +106,6 @@ impl<'a> StateManager<'a> {
text_settings.v_align,
text_settings.bg,
text_settings.fg,
text_settings.wrap,
)
}
}
@ -151,7 +118,7 @@ impl<'a> StateManager<'a> {
pub struct ATCommandHelper<T: ATCommand> {
command: Option<T>,
promise: Option<ATPromise<T>>,
response: Option<Result<T::Response, ATError>>,
response: Option<Result<T::Response, ATParseError>>,
}
impl<T: ATCommand + 'static> ATCommandHelper<T>
@ -166,16 +133,19 @@ where
}
}
pub fn poll(&mut self, io: &mut AsyncIO) -> Option<Result<T::Response, ATError>> {
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) => {
self.response = Some(response);
self.response.clone()
}
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() {
@ -187,82 +157,3 @@ where
}
}
}
const MENU_HEIGHT: usize = 240 / 30;
pub trait MenuItem {
fn as_text(&self) -> String;
}
#[derive(Default, Debug, Clone)]
pub struct Menu<T: MenuItem> {
pub items: Vec<T>,
idx: usize,
scroll: usize,
}
impl<T: MenuItem> Menu<T> {
pub fn with_item(mut self, item: T) -> Menu<T> {
self.items.push(item);
self
}
pub fn poll<'s>(&'s mut self, io: &mut AsyncIO) -> Option<&'s T> {
use crate::async_io::KeypadButton;
for _ in 0..io.keypad.get_presses(KeypadButton::KeypadC) {
self.previous();
}
for _ in 0..io.keypad.get_presses(KeypadButton::KeypadD) {
self.next();
}
if io.keypad.get_presses(KeypadButton::KeypadA) > 0 {
Some(&self.items[self.idx])
} else {
None
}
}
pub fn previous(&mut self) {
if self.idx == 0 {
// Wrap index around
self.idx = self.items.len() - 1;
if self.idx >= (self.scroll + MENU_HEIGHT) {
self.scroll = (self.idx - MENU_HEIGHT) + 1;
}
} else {
self.idx -= 1;
if self.idx < self.scroll {
self.scroll = self.idx;
}
}
}
pub fn next(&mut self) {
self.idx = (self.idx + 1) % self.items.len();
if self.idx == 0 {
self.scroll = 0;
} else if self.idx >= (self.scroll + MENU_HEIGHT) {
self.scroll = (self.idx - MENU_HEIGHT) + 1;
}
}
pub fn draw(&self, data: &mut StateData) {
for (i, (idx, item)) in self
.items
.iter()
.enumerate()
.skip(self.scroll)
.take(MENU_HEIGHT)
.enumerate()
{
let mut settings = TextSettings::default();
if idx == self.idx {
settings.bg = Rgb565::white();
settings.fg = Rgb565::black();
}
data.draw_text(item.as_text(), Position::new(0, (i * 30) as i16), settings);
}
}
}

View File

@ -5,21 +5,19 @@ use alloc::{
boxed::Box,
format,
string::{String, ToString},
vec::Vec,
};
use esp_hal::time::{Duration, Instant};
use crate::{
async_io::{ATPromise, KeypadButton, NumberInput, TextInput},
async_io::{ATPromise, KeypadButton, NumberInput},
at_commands::{
ATCommand, ATError, ATInformationCommand, CheckPinCommand, CheckPinResult, EnterPinCommand,
EnterPinResult, ListSMSMessages, ListSMSMessagesResponse, SMSFormat, SMSMessageListing,
SMSMessageStat, SelectSMSFormatCommand, SendSMSCommand, SendSMSResponse,
SetTECharsetCommand, SimpleATResponse, TECharset,
ATCommand, ATInformationCommand, ATParseError, CheckPinCommand, CheckPinResult,
EnterPinCommand, EnterPinResult, SMSFormat, SelectSMSFormatCommand, SendSMSCommand,
SendSMSResponse, SetTECharsetCommand, SimpleATResponse, TECharset,
},
display::{Position, Rgb565},
font::{HorizontalAlignment, VerticalAlignment},
state::{ATCommandHelper, Menu, MenuItem, State, StateData, TextSettings},
state::{ATCommandHelper, State, StateData, TextSettings},
};
#[derive(Debug, Clone)]
@ -55,7 +53,7 @@ impl DotsMessage {
pub struct ATCommandState<Cmd: ATCommand, F>
where
F: FnOnce(Result<Cmd::Response, ATError>) -> Box<dyn State>,
F: FnOnce(Result<Cmd::Response, ATParseError>) -> Box<dyn State>,
{
message: DotsMessage,
command: Cmd,
@ -65,7 +63,7 @@ where
impl<Cmd: ATCommand, F> ATCommandState<Cmd, F>
where
F: FnOnce(Result<Cmd::Response, ATError>) -> Box<dyn State>,
F: FnOnce(Result<Cmd::Response, ATParseError>) -> Box<dyn State>,
{
pub fn with(message: String, command: Cmd, after: F) -> ATCommandState<Cmd, F> {
ATCommandState {
@ -82,7 +80,7 @@ where
impl<Cmd: ATCommand + Clone + 'static, F: Clone> State for ATCommandState<Cmd, F>
where
F: FnOnce(Result<Cmd::Response, ATError>) -> Box<dyn State>,
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());
@ -94,10 +92,13 @@ where
if let Some(promise) = &self.promise {
match promise.poll(&mut data.io) {
Some(response) => {
return Some((self.fun.clone())(response));
}
None => {}
Some(response) => match response {
Some(response) => {
return Some((self.fun.clone())(response));
}
None => {}
},
None => self.promise = None,
}
return None;
}
@ -140,34 +141,18 @@ impl State for InitATState {
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(),
}));
}
}
resp.unwrap()
} 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) => {
match resp.unwrap() {
SimpleATResponse::Ok => {}
SimpleATResponse::Error => {
return Some(Box::new(TextState {
text: format!("Err: {:?}", err),
text: "ERROR!".to_owned(),
after: InitATState::default(),
}));
}
@ -178,19 +163,11 @@ impl State for InitATState {
};
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) => {
match resp.unwrap() {
SimpleATResponse::Ok => {}
SimpleATResponse::Error => {
return Some(Box::new(TextState {
text: format!("Err: {:?}", err),
text: "ERROR!".to_owned(),
after: InitATState::default(),
}));
}
@ -253,27 +230,22 @@ 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) => {
Some(response) => match response.unwrap() {
EnterPinResult::Ok => {
return Some(Box::new(TextState {
text: format!("Error:\n{:?}", err),
text: "SIM entered\nsuccessfully".to_owned(),
after: PhoneNumberState::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(),
}));
}
@ -303,173 +275,6 @@ impl State for EnterPinState {
}
}
#[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(ListSMSMessagesState::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>,
promise: Option<ATPromise<ListSMSMessages>>,
}
impl State for ListSMSMessagesState {
fn init(&mut self, data: &mut StateData) {
self.promise = data
.io
.send_at_command(ListSMSMessages {
stat: SMSMessageStat::All,
})
.ok()
}
fn update(&mut self, data: &mut StateData) -> Option<Box<dyn State>> {
if let Some(promise) = &self.promise {
match promise.poll(&mut data.io) {
Some(result) => {
match result {
Ok(response) => match response {
ListSMSMessagesResponse::Ok(messages) => {
for message in messages {
self.menu = self.menu.clone().with_item(MessageListing {
from: message.from_addr.clone(),
text: message.text.clone(),
})
}
}
ListSMSMessagesResponse::Error(err) => {
return Some(Box::new(TextState {
text: format!("Error\n{:?}", err),
after: MainMenuState::default(),
}));
}
},
Err(err) => {
return Some(Box::new(TextState {
text: format!("Error\n{:?}", err),
after: MainMenuState::default(),
}));
}
}
self.promise = None;
}
None => return None,
}
}
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) {
data.clear_screen(Rgb565::black().as_color());
if self.menu.items.len() == 0 {
data.draw_text(
"Loading\nmessages",
Position::new(0, 0),
TextSettings::default(),
);
} else {
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(ListSMSMessagesState::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().with_wrap(true),
);
}
}
#[derive(Clone, Default)]
pub struct PhoneNumberState {
input: NumberInput,
@ -502,43 +307,35 @@ impl State for PhoneNumberState {
pub struct MessageState {
number: String,
input: TextInput,
input: NumberInput,
}
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
}
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.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 {
None
}