pub mod ansi;
pub mod render;
use crate::serial::SerialPort;
use crate::state::render::{
init_viewport, render_about, render_active_users, render_empty_line, render_message,
render_prompt, render_status, render_system_message, render_time_info, render_welcome,
};
use crate::storage::Storage;
use crate::types::{FixedString, MAX_HISTORY, MAX_USERS, Message, RingBuffer, User};
use crate::utils::{Crc32, parse_u16};
use core::mem::size_of_val;
use core::ptr::{addr_of, addr_of_mut};
use core::slice::{from_raw_parts, from_raw_parts_mut};
pub struct AppState {
pub users: [User; MAX_USERS],
pub history: RingBuffer<Message, MAX_HISTORY>,
pub current_user_id: Option<u16>,
pub input_buffer: FixedString<256>,
pub relative_tick: u32,
pub boot_epoch: u32,
pub initialized: bool,
pub should_exit: bool,
}
impl AppState {
#[inline]
#[must_use]
pub const fn uninit() -> Self {
let empty_user = User {
id: 0,
status: 1,
padding: 0,
name: FixedString::new(),
};
Self {
users: [empty_user; MAX_USERS],
history: RingBuffer::new(),
current_user_id: None,
input_buffer: FixedString::new(),
relative_tick: 0,
boot_epoch: 0,
initialized: false,
should_exit: false,
}
}
pub fn init(
&mut self,
storage: &mut impl Storage,
port: &mut impl SerialPort,
) -> Result<(), &'static str> {
let mut crc = Crc32::new();
let mut offset = 0;
let mut header = [0u8; 9];
let Ok(len) = storage.read(offset, &mut header) else {
return self.factory_reset(port);
};
if len != 9 || &header[0..4] != b"EMBU" || header[4] != 1 {
return self.factory_reset(port);
}
crc.update(&header);
offset += 9;
self.boot_epoch = u32::from_le_bytes([header[5], header[6], header[7], header[8]]);
let users_bytes = unsafe {
from_raw_parts_mut(
self.users.as_mut_ptr().cast::<u8>(),
size_of_val(&self.users),
)
};
let Ok(len) = storage.read(offset, users_bytes) else {
return self.factory_reset(port);
};
if len != users_bytes.len() {
return self.factory_reset(port);
}
crc.update(users_bytes);
offset += users_bytes.len();
let history_bytes = unsafe {
from_raw_parts_mut(
addr_of_mut!(self.history).cast::<u8>(),
size_of_val(&self.history),
)
};
let Ok(len) = storage.read(offset, history_bytes) else {
return self.factory_reset(port);
};
if len != history_bytes.len() {
return self.factory_reset(port);
}
crc.update(history_bytes);
offset += history_bytes.len();
let mut stored_crc_bytes = [0u8; 4];
let Ok(len) = storage.read(offset, &mut stored_crc_bytes) else {
return self.factory_reset(port);
};
if len != 4 {
return self.factory_reset(port);
}
let stored_crc = u32::from_le_bytes(stored_crc_bytes);
if crc.finalize() != stored_crc {
return self.factory_reset(port);
}
self.initialized = true;
init_viewport(port);
render_welcome(port);
render_empty_line(port);
for i in 0..self.history.len() {
let Some(msg) = self.history.get(i) else {
continue;
};
render_message(self, msg, port);
}
render_prompt(self, port);
Ok(())
}
pub fn factory_reset(&mut self, port: &mut impl SerialPort) -> Result<(), &'static str> {
*self = AppState::uninit();
self.initialized = true;
init_viewport(port);
render_welcome(port);
render_empty_line(port);
render_system_message(port, 0, "CRC mismatch: factory reset applied");
render_empty_line(port);
render_prompt(self, port);
Ok(())
}
pub fn flush(&self, storage: &mut impl Storage) -> Result<(), &'static str> {
let mut crc = Crc32::new();
let mut offset = 0;
let mut header = [0u8; 9];
header[0..4].copy_from_slice(b"EMBU");
header[4] = 1;
header[5..9].copy_from_slice(&self.boot_epoch.to_le_bytes());
storage.write(offset, &header)?;
crc.update(&header);
offset += 9;
let users_bytes =
unsafe { from_raw_parts(self.users.as_ptr().cast::<u8>(), size_of_val(&self.users)) };
storage.write(offset, users_bytes)?;
crc.update(users_bytes);
offset += users_bytes.len();
let history_bytes = unsafe {
from_raw_parts(
addr_of!(self.history).cast::<u8>(),
size_of_val(&self.history),
)
};
storage.write(offset, history_bytes)?;
crc.update(history_bytes);
offset += history_bytes.len();
let crc_val = crc.finalize();
storage.write(offset, &crc_val.to_le_bytes())?;
storage.flush()?;
Ok(())
}
#[allow(clippy::collapsible_match)]
pub fn process_byte(
&mut self,
byte: u8,
port: &mut impl SerialPort,
storage: &mut impl Storage,
) -> Result<(), &'static str> {
if !self.initialized {
return Err("AppState not initialized. Call init() first.");
}
match byte {
0x08 | 0x7F => {
if self.input_buffer.pop().is_some() {
port.write_byte(0x08);
port.write_byte(b' ');
port.write_byte(0x08);
}
}
b'\r' | b'\n' => {
if self.input_buffer.is_empty() {
return Ok(());
}
self.relative_tick = self.relative_tick.wrapping_add(1);
let input_bytes = self.input_buffer.as_bytes();
if input_bytes.starts_with(b"/") {
self.dispatch_command(port);
} else {
self.dispatch_message(port);
}
self.input_buffer.clear();
self.flush(storage)?;
render_prompt(self, port);
}
0x20..=0x7E => {
if self.input_buffer.push(byte) {
port.write_byte(byte);
}
}
_ => {}
}
Ok(())
}
fn dispatch_command(&mut self, port: &mut impl SerialPort) {
let bytes = self.input_buffer.as_bytes();
if bytes == b"/exit" {
self.should_exit = true;
return;
}
if bytes == b"/clear" {
init_viewport(port);
render_welcome(port);
render_empty_line(port);
return;
}
render_empty_line(port);
if bytes.starts_with(b"/user ") || bytes.starts_with(b"/switch ") {
let prefix_len = if bytes.starts_with(b"/user ") { 6 } else { 8 };
let id_bytes = &bytes[prefix_len..];
let Some(id) = parse_u16(id_bytes) else {
render_system_message(port, self.relative_tick, "Error: Invalid user ID.");
render_empty_line(port);
return;
};
self.current_user_id = Some(id);
let mut exists = false;
let mut empty_slot = None;
for (i, user) in self.users.iter().enumerate() {
if user.id == id && user.status == 0 {
exists = true;
break;
}
if user.status == 1 && empty_slot.is_none() {
empty_slot = Some(i);
}
}
if !exists && let Some(index) = empty_slot {
self.users[index].id = id;
self.users[index].status = 0;
self.users[index].name.clear();
}
render_system_message(port, self.relative_tick, "User switched successfully.");
render_empty_line(port);
return;
}
if bytes.starts_with(b"/name ") {
let Some(user_id) = self.current_user_id else {
render_system_message(
port,
self.relative_tick,
"Error: Please switch to a user first.",
);
render_empty_line(port);
return;
};
let name_bytes = &bytes[6..];
for user in &mut self.users {
if user.id != user_id || user.status != 0 {
continue;
}
user.name.clear();
for &byte in name_bytes {
if !user.name.push(byte) {
break;
}
}
break;
}
render_system_message(port, self.relative_tick, "Display name updated.");
render_empty_line(port);
return;
}
match bytes {
b"/help" => render_system_message(
port,
self.relative_tick,
"Commands: /user <id>, /switch <id>, /name <str>, /users, /status, /time, /ping, /about, /clear, /exit, /help",
),
b"/users" => render_active_users(self, port),
b"/status" => render_status(self, port),
b"/ping" => {
render_system_message(port, self.relative_tick, "Pong! System is responsive.");
}
b"/time" => render_time_info(self, port),
b"/about" => render_about(self, port),
_ => render_system_message(
port,
self.relative_tick,
"Unknown command. Type /help for a list of commands.",
),
}
render_empty_line(port);
}
fn dispatch_message(&mut self, port: &mut impl SerialPort) {
let user_id = self.current_user_id.unwrap_or(0);
let mut message = Message {
user_id,
padding: 0,
tick: self.relative_tick,
content: FixedString::new(),
};
for &byte in self.input_buffer.as_bytes() {
message.content.push(byte);
}
self.history.push(message);
render_message(self, &message, port);
}
pub fn set_boot_epoch(&mut self, epoch: u32) {
self.boot_epoch = epoch;
}
}