From f237bcaf9c901b13124d07b3ccf756c96579860d Mon Sep 17 00:00:00 2001
From: Stephen D <webmaster@scd31.com>
Date: Sun, 30 Jun 2024 15:49:41 -0300
Subject: [PATCH] message storage

---
 Cargo.lock             |   7 +
 Cargo.toml             |   1 +
 src/controller.rs      |  63 +++++----
 src/gui/chat.rs        |  43 +++---
 src/gui/chat_list.rs   |  48 ++++---
 src/gui/textbox.rs     |  16 ++-
 src/main.rs            |   1 -
 src/message.rs         |  90 -------------
 src/storage/contact.rs |   7 +-
 src/storage/message.rs | 296 +++++++++++++++++++++++++++++++++++++++++
 src/storage/mod.rs     |  54 ++++++--
 11 files changed, 454 insertions(+), 172 deletions(-)
 delete mode 100644 src/message.rs
 create mode 100644 src/storage/message.rs

diff --git a/Cargo.lock b/Cargo.lock
index 81ceba8..f4f3f53 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -86,6 +86,7 @@ dependencies = [
  "rp2040-hal",
  "rtic-monotonics",
  "rtt-target",
+ "sectorize",
  "usb-device 0.2.9",
  "usbd-serial",
 ]
@@ -755,6 +756,12 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
+[[package]]
+name = "sectorize"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad334105ac17abdd7ba69b0e76658de21fc02b2a093ae5135b1e3e378ccffd3"
+
 [[package]]
 name = "semver"
 version = "0.9.0"
diff --git a/Cargo.toml b/Cargo.toml
index 4a48507..83b14ab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,3 +24,4 @@ panic-rtt-target = "0.1.3"
 embedded-text = "0.7.1"
 crc = "3.2.1"
 rp2040-flash = "0.5.0"
+sectorize = "0.1.0"
diff --git a/src/controller.rs b/src/controller.rs
index 2bf76ae..ca76689 100644
--- a/src/controller.rs
+++ b/src/controller.rs
@@ -46,10 +46,10 @@ impl View {
         Self::ContactOptions(opts, contact_id)
     }
 
-    pub fn from_main_menu_id(id: usize, contacts: &ContactIter) -> Option<Self> {
+    pub fn from_main_menu_id(id: usize, contacts: &ContactIter, storage: &Storage) -> Option<Self> {
         match id {
             0 => Some(Self::new_contacts(0, contacts)),
-            1 => Some(Self::ChatList(ChatList::new(0))),
+            1 => Some(Self::ChatList(ChatList::new(storage, 0))),
             _ => None,
         }
     }
@@ -73,8 +73,7 @@ impl View {
             0 => {
                 let contact = contacts
                     .nth(contact_id)
-                    .unwrap_or_else(|| storage.new_contact())
-                    .clone();
+                    .unwrap_or_else(|| storage.new_contact());
                 Some(View::ContactView(
                     ContactView::new(contact, false),
                     contact_id,
@@ -85,8 +84,7 @@ impl View {
             1 => {
                 let contact = contacts
                     .nth(contact_id)
-                    .unwrap_or_else(|| storage.new_contact())
-                    .clone();
+                    .unwrap_or_else(|| storage.new_contact());
                 Some(View::ContactView(
                     ContactView::new(contact, true),
                     contact_id,
@@ -94,35 +92,34 @@ impl View {
                 ))
             }
 
-            _ => None,
-        }
-    }
-}
+            2 => {
+                let contact = contacts
+                    .nth(contact_id)
+                    .unwrap_or_else(|| storage.new_contact());
 
-impl Default for View {
-    fn default() -> Self {
-        Self::new_main_menu(0)
-    }
-}
+                Some(View::Chat(Chat::new(contact.callsign, contact.ssid), 0))
+            }
 
-impl Element for View {
-    type KeyPushReturn = ();
+            _ => unreachable!(),
+        }
+    }
 
     fn render<E, DT: DrawTarget<Color = Rgb565, Error = E>>(
         &mut self,
         dt: &mut DT,
+        storage: &Storage,
     ) -> Result<(), E> {
         match self {
             View::MainMenu(m) => m.render(dt),
             View::Contacts(c) => c.render(dt),
             View::ContactOptions(c, _) => c.render(dt),
             View::ContactView(c, _, _) => c.render(dt),
-            View::ChatList(c) => c.render(dt),
-            View::Chat(c, _) => c.render(dt),
+            View::ChatList(c) => c.render(dt, storage),
+            View::Chat(c, _) => c.render(dt, storage),
         }
     }
 
-    fn key_push(&mut self, k: KeyCode) {
+    fn key_push(&mut self, k: KeyCode, storage: &mut Storage) {
         match self {
             View::MainMenu(m) => m.key_push(k),
             View::Contacts(c) => c.key_push(k),
@@ -131,7 +128,7 @@ impl Element for View {
                 c.key_push(k);
             }
             View::ChatList(c) => c.key_push(k),
-            View::Chat(c, _) => c.key_push(k),
+            View::Chat(c, _) => c.key_push(k, storage),
         }
     }
 
@@ -142,11 +139,17 @@ impl Element for View {
             View::ContactOptions(c, _) => c.touchpad_scroll(x, y),
             View::ContactView(c, _, _) => c.touchpad_scroll(x, y),
             View::ChatList(c) => c.touchpad_scroll(x, y),
-            View::Chat(c, _) => c.touchpad_scroll(x, y),
+            View::Chat(_c, _) => {} // c.touchpad_scroll(x, y),
         }
     }
 }
 
+impl Default for View {
+    fn default() -> Self {
+        Self::new_main_menu(0)
+    }
+}
+
 pub struct Controller {
     status: StatusBar,
     cur_view: View,
@@ -176,7 +179,7 @@ impl Element for Controller {
 
         self.status.render(dt)?;
 
-        self.cur_view.render(dt)?;
+        self.cur_view.render(dt, &self.storage)?;
 
         Ok(())
     }
@@ -185,7 +188,7 @@ impl Element for Controller {
         match (&mut self.cur_view, k) {
             (View::MainMenu(m), KeyCode::Touchpad) => {
                 if let Some(new_view) =
-                    View::from_main_menu_id(m.selected(), &self.storage.contacts())
+                    View::from_main_menu_id(m.selected(), &self.storage.contacts(), &self.storage)
                 {
                     self.cur_view = new_view;
                 }
@@ -210,7 +213,13 @@ impl Element for Controller {
             }
 
             (View::ChatList(cl), KeyCode::Touchpad) => {
-                self.cur_view = View::Chat(Chat::new(), cl.selected());
+                let (callsign, ssid) = self
+                    .storage
+                    .message_addressees()
+                    .nth(cl.selected())
+                    .unwrap();
+
+                self.cur_view = View::Chat(Chat::new(callsign, ssid), cl.selected());
             }
 
             (View::MainMenu(_), KeyCode::Back) => {}
@@ -227,7 +236,7 @@ impl Element for Controller {
             }
 
             (View::Chat(_, selected_id), KeyCode::Back) => {
-                self.cur_view = View::ChatList(ChatList::new(*selected_id));
+                self.cur_view = View::ChatList(ChatList::new(&self.storage, *selected_id));
             }
 
             (_, KeyCode::Back) => {
@@ -243,7 +252,7 @@ impl Element for Controller {
                 }
             }
 
-            (_, k) => self.cur_view.key_push(k),
+            (_, k) => self.cur_view.key_push(k, &mut self.storage),
         }
     }
 
diff --git a/src/gui/chat.rs b/src/gui/chat.rs
index c4e791c..0a378bd 100644
--- a/src/gui/chat.rs
+++ b/src/gui/chat.rs
@@ -7,10 +7,13 @@ use embedded_graphics::{
     Drawable,
 };
 use embedded_text::{style::TextBoxStyleBuilder, TextBox};
+use heapless::String;
+use rtt_target::rdbg;
 
 use crate::{
     app::WIDTH,
-    message::{MessageDirection, MessageGroup},
+    keyboard::KeyCode,
+    storage::{Message, MessageDirection, Storage, MAX_CONTACT_CALLSIGN_LENGTH},
 };
 
 use super::{textbox::TextBox as MyTextBox, Element};
@@ -19,23 +22,21 @@ const MESSAGE_BOX_WIDTH: u32 = 200;
 const MESSAGE_BOX_HEIGHT: u32 = 64;
 
 pub struct Chat {
-    message_group: MessageGroup,
+    callsign: String<MAX_CONTACT_CALLSIGN_LENGTH>,
+    ssid: u8,
     textbox: MyTextBox,
 }
 
 impl Chat {
-    pub fn new() -> Self {
+    pub fn new(callsign: String<MAX_CONTACT_CALLSIGN_LENGTH>, ssid: u8) -> Self {
         Self {
-            message_group: MessageGroup::default(),
+            callsign,
+            ssid,
             textbox: MyTextBox::new(1, 218, 31, 1),
         }
     }
-}
-
-impl Element for Chat {
-    type KeyPushReturn = ();
 
-    fn render<
+    pub fn render<
         E,
         DT: embedded_graphics::prelude::DrawTarget<
             Color = embedded_graphics::pixelcolor::Rgb565,
@@ -44,6 +45,7 @@ impl Element for Chat {
     >(
         &mut self,
         dt: &mut DT,
+        storage: &Storage,
     ) -> Result<(), E> {
         let textbox_style = TextBoxStyleBuilder::new().build();
         let character_style = MonoTextStyle::new(&FONT_10X20, Rgb565::BLACK);
@@ -52,17 +54,17 @@ impl Element for Chat {
             .build();
 
         let mut y = 28;
-        for msg in self.message_group.iter() {
+        for msg in storage.messages(&self.callsign, self.ssid) {
             let bounds = Rectangle::new(
                 Point::new(4, y),
                 Size::new(MESSAGE_BOX_WIDTH, MESSAGE_BOX_HEIGHT),
             );
 
             let mut text_box =
-                TextBox::with_textbox_style(msg.content, bounds, character_style, textbox_style);
+                TextBox::with_textbox_style(msg.content(), bounds, character_style, textbox_style);
 
             let mut actual_bounds = calculate_bounding_box(&text_box);
-            if msg.direction == MessageDirection::To {
+            if msg.direction() == MessageDirection::To {
                 let x = i32::try_from(WIDTH).unwrap()
                     - i32::try_from(actual_bounds.size.width).unwrap()
                     - 4;
@@ -83,8 +85,16 @@ impl Element for Chat {
         Ok(())
     }
 
-    fn key_push(&mut self, k: crate::keyboard::KeyCode) {
-        self.textbox.key_push(k);
+    pub fn key_push(&mut self, k: KeyCode, storage: &mut Storage) {
+        if k == KeyCode::Enter {
+            let text = self.textbox.clear();
+            let message =
+                Message::new(self.callsign.clone(), self.ssid, text, MessageDirection::To);
+
+            storage.push_message(message);
+        } else {
+            self.textbox.key_push(k);
+        }
     }
 }
 
@@ -101,7 +111,10 @@ fn calculate_bounding_box<S: TextRenderer>(tb: &TextBox<'_, S>) -> Rectangle {
             y += 1;
         } else {
             x += 1;
-            max_x = x;
+
+            if x > max_x {
+                max_x = x;
+            }
         }
     }
 
diff --git a/src/gui/chat_list.rs b/src/gui/chat_list.rs
index bae3605..a3741d9 100644
--- a/src/gui/chat_list.rs
+++ b/src/gui/chat_list.rs
@@ -10,12 +10,9 @@ use embedded_graphics::{
 };
 use heapless::String;
 
-use crate::{app::WIDTH, message::MessageGroup};
+use crate::{app::WIDTH, storage::Storage};
 
-use super::{
-    scroll_tracker::{ScrollAction, ScrollTracker},
-    Element,
-};
+use super::scroll_tracker::{ScrollAction, ScrollTracker};
 
 const STATUS_BAR_HEIGHT: i32 = 40;
 const GROUP_HEIGHT: i32 = 40;
@@ -24,33 +21,27 @@ const BACKGROUND_COLOR: Rgb565 = Rgb565::new(31, 41, 0); // #FFA500
 const BORDER_COLOR: Rgb565 = Rgb565::new(31, 26, 0); // #FF6600
 
 pub struct ChatList {
-    groups: [MessageGroup; 3],
     tracker: ScrollTracker,
     selected: usize,
+    len: usize,
 }
 
 impl ChatList {
-    pub fn new(selected: usize) -> Self {
+    pub fn new(storage: &Storage, selected: usize) -> Self {
+        let len = storage.message_addressees().count();
+
         Self {
-            groups: [
-                MessageGroup::default(),
-                MessageGroup::default(),
-                MessageGroup::default(),
-            ],
             tracker: ScrollTracker::new(),
             selected,
+            len,
         }
     }
 
     pub fn selected(&self) -> usize {
         self.selected
     }
-}
-
-impl Element for ChatList {
-    type KeyPushReturn = ();
 
-    fn render<
+    pub fn render<
         E,
         DT: embedded_graphics::prelude::DrawTarget<
             Color = embedded_graphics::pixelcolor::Rgb565,
@@ -59,6 +50,7 @@ impl Element for ChatList {
     >(
         &mut self,
         dt: &mut DT,
+        storage: &Storage,
     ) -> Result<(), E> {
         let unselected_rect_style = PrimitiveStyleBuilder::new()
             .stroke_color(BORDER_COLOR)
@@ -72,7 +64,7 @@ impl Element for ChatList {
             .fill_color(Rgb565::WHITE)
             .build();
 
-        for (idx, group) in self.groups.iter().enumerate() {
+        for (idx, (callsign, ssid)) in storage.message_addressees().enumerate() {
             let i = i32::try_from(idx).unwrap();
 
             let x = 0;
@@ -104,8 +96,20 @@ impl Element for ChatList {
                 .text_color(Rgb565::BLACK)
                 .build();
 
+            let name = storage
+                .contacts()
+                .find(|c| c.callsign == callsign && c.ssid == ssid)
+                .map(|c| c.name);
+
             let mut text: String<25> = String::new();
-            write!(&mut text, "{}", group).unwrap();
+            match name {
+                Some(x) => {
+                    text = String::try_from(x.as_str()).unwrap();
+                }
+                None => {
+                    write!(&mut text, "{}-{}", callsign, ssid).unwrap();
+                }
+            };
             Text::with_baseline(&text, Point::new(text_x, text_y), text_style, Baseline::Top)
                 .draw(dt)?;
         }
@@ -113,9 +117,9 @@ impl Element for ChatList {
         Ok(())
     }
 
-    fn key_push(&mut self, _: crate::keyboard::KeyCode) {}
+    pub fn key_push(&mut self, _: crate::keyboard::KeyCode) {}
 
-    fn touchpad_scroll(&mut self, _x: i8, y: i8) {
+    pub fn touchpad_scroll(&mut self, _x: i8, y: i8) {
         match self.tracker.scroll(y) {
             ScrollAction::Previous => {
                 if self.selected > 0 {
@@ -123,7 +127,7 @@ impl Element for ChatList {
                 }
             }
             ScrollAction::Next => {
-                if self.selected < self.groups.len() - 1 {
+                if self.selected < self.len - 1 {
                     self.selected += 1;
                 }
             }
diff --git a/src/gui/textbox.rs b/src/gui/textbox.rs
index 9f1e506..e989fbf 100644
--- a/src/gui/textbox.rs
+++ b/src/gui/textbox.rs
@@ -1,3 +1,5 @@
+use core::mem;
+
 use embedded_graphics::{
     draw_target::DrawTarget,
     geometry::{Point, Size},
@@ -18,6 +20,7 @@ const BACKGROUND_COLOR: Rgb565 = Rgb565::new(0xFF, 0xFF, 0xFF);
 pub const BORDER_WIDTH: u32 = 3;
 const CHAR_WIDTH: u32 = 10;
 const CHAR_HEIGHT: u32 = 20;
+const MAX_TEXT_LENGTH: usize = 500;
 
 pub struct TextBox {
     // in # of chars
@@ -32,7 +35,7 @@ pub struct TextBox {
     x: i32,
     y: i32,
 
-    text: String<32>,
+    text: String<MAX_TEXT_LENGTH>,
     pub selected: bool,
 }
 
@@ -77,6 +80,17 @@ impl TextBox {
         &self.text
     }
 
+    pub fn clear(&mut self) -> String<MAX_TEXT_LENGTH> {
+        let mut other = String::new();
+        mem::swap(&mut self.text, &mut other);
+
+        self.cursor_x = 0;
+        self.cursor_y = 0;
+        self.cursor_i = 0;
+
+        other
+    }
+
     fn width(&self) -> u32 {
         BORDER_WIDTH * 2 + CHAR_WIDTH * self.char_width
     }
diff --git a/src/main.rs b/src/main.rs
index eb5bcff..1e52d22 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,7 +6,6 @@ mod controller;
 mod drivers;
 mod gui;
 mod keyboard;
-mod message;
 mod storage;
 mod touchpad;
 mod voltage;
diff --git a/src/message.rs b/src/message.rs
deleted file mode 100644
index 111ac81..0000000
--- a/src/message.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-use core::fmt::Display;
-
-#[derive(Copy, Clone, PartialEq, Eq)]
-pub enum MessageDirection {
-    From,
-    To,
-}
-
-#[derive(Copy, Clone)]
-pub struct Message {
-    pub content: &'static str,
-    pub direction: MessageDirection,
-}
-
-// A single column in the message list
-// TODO rename this to MessageIdentity? May be more straightforward
-pub struct MessageGroup {
-    callsign: &'static str,
-    ssid: u8,
-}
-
-impl MessageGroup {
-    pub fn iter(&self) -> MessageGroupIter {
-        MessageGroupIter::new(self)
-    }
-}
-
-impl Display for MessageGroup {
-    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
-        // TODO will need to somehow look up the actual contact name
-        write!(f, "{}-{}", self.callsign, self.ssid)
-    }
-}
-
-impl Default for MessageGroup {
-    // Stubbing for now
-    fn default() -> Self {
-        Self {
-            callsign: "VA3QAK",
-            ssid: 3,
-        }
-    }
-}
-
-pub struct MessageGroupIter<'a> {
-    group: &'a MessageGroup,
-    i: usize,
-}
-
-impl<'a> MessageGroupIter<'a> {
-    pub fn new(group: &'a MessageGroup) -> Self {
-        Self { group, i: 0 }
-    }
-}
-
-impl<'a> Iterator for MessageGroupIter<'a> {
-    type Item = Message;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        // Stubbing for now
-        let stubbed = [
-            Message {
-                content: "Hello",
-                direction: MessageDirection::From,
-            },
-            Message {
-                content: "Hi",
-                direction: MessageDirection::To,
-            },
-            Message {
-                content: "How are you?",
-                direction: MessageDirection::From,
-            },
-            Message {
-                content: "I'm good",
-                direction: MessageDirection::To,
-            },
-        ];
-
-        if self.i >= stubbed.len() {
-            None
-        } else {
-            let out = stubbed[self.i];
-
-            self.i += 1;
-
-            Some(out)
-        }
-    }
-}
diff --git a/src/storage/contact.rs b/src/storage/contact.rs
index ce8fd81..ae8f865 100644
--- a/src/storage/contact.rs
+++ b/src/storage/contact.rs
@@ -6,14 +6,15 @@ use crate::storage::DATA_START;
 
 use super::{Storage, X25};
 
-const CONTACT_HEADER_START: usize = DATA_START;
-const CONTACT_HEADER_SIZE: usize = 3;
+pub const CONTACT_HEADER_START: usize = DATA_START;
+pub const CONTACT_HEADER_SIZE: usize = 3;
 
 pub const MAX_CONTACTS: usize = 100;
 pub const MAX_CONTACT_NAME_LENGTH: usize = 20;
 pub const MAX_CONTACT_CALLSIGN_LENGTH: usize = 10;
 // 1 byte for each length, 1 byte for SSID, 2 bytes for checksum
-const CONTACT_LENGTH: usize = MAX_CONTACT_NAME_LENGTH + 1 + 1 + MAX_CONTACT_CALLSIGN_LENGTH + 1 + 2;
+pub const CONTACT_LENGTH: usize =
+    MAX_CONTACT_NAME_LENGTH + 1 + 1 + MAX_CONTACT_CALLSIGN_LENGTH + 1 + 2;
 
 #[derive(Copy, Clone)]
 pub struct ContactGroup {
diff --git a/src/storage/message.rs b/src/storage/message.rs
new file mode 100644
index 0000000..a0ba89b
--- /dev/null
+++ b/src/storage/message.rs
@@ -0,0 +1,296 @@
+use core::slice;
+
+use heapless::{FnvIndexSet, String, Vec};
+
+use super::{
+    contact::{CONTACT_HEADER_SIZE, CONTACT_HEADER_START, CONTACT_LENGTH},
+    Storage, MAX_CONTACTS, MAX_CONTACT_CALLSIGN_LENGTH, X25,
+};
+
+const MESSAGE_HEADER_START: usize =
+    CONTACT_HEADER_START + CONTACT_HEADER_SIZE + MAX_CONTACTS * CONTACT_LENGTH;
+// Includes 2 byte checksum
+const MESSAGE_HEADER_SIZE: usize = 6;
+const MAX_MESSAGE_BODY_LENGTH: usize = 500;
+// 1 byte callsign length
+// 2 byte message length
+// 1 byte SSID
+// 1 byte direction + received
+// 1 byte send attempts
+// 2 byte checksum
+const MESSAGE_LENGTH: usize =
+    1 + 2 + 1 + 1 + 1 + MAX_MESSAGE_BODY_LENGTH + MAX_CONTACT_CALLSIGN_LENGTH + 2;
+const MAX_MESSAGES: usize = 1024;
+
+#[derive(Copy, Clone)]
+pub struct MessageGroup {
+    header: MessageHeader,
+}
+
+impl MessageGroup {
+    pub fn new() -> Self {
+        Self {
+            header: MessageHeader::read(),
+        }
+    }
+
+    pub fn push_message(&mut self, storage: &mut Storage, message: Message) {
+        if usize::from(self.header.len) == MAX_MESSAGES {
+            // overwrite the first message
+            message.write(storage, self.header.start.into());
+            self.header.start += 1;
+            self.header.start %= self.header.len;
+        } else {
+            // append to end
+            message.write(storage, self.header.len.into());
+            self.header.len += 1;
+        }
+
+        self.header.write(storage);
+    }
+
+    pub fn iter<'a, 'b, 'c>(
+        &'a self,
+        storage: &'b Storage,
+        callsign: &'c str,
+        ssid: u8,
+    ) -> MessageIter<'a, 'b, 'c> {
+        MessageIter::new(self, storage, callsign, ssid)
+    }
+
+    pub fn iter_addressees<'a, 'b>(&'a self, storage: &'b Storage) -> AddresseeIter<'a, 'b> {
+        AddresseeIter::new(self, storage)
+    }
+}
+
+#[derive(Copy, Clone)]
+pub struct MessageHeader {
+    start: u16,
+    len: u16,
+}
+
+impl MessageHeader {
+    fn read() -> Self {
+        let bytes =
+            unsafe { slice::from_raw_parts(MESSAGE_HEADER_START as *mut u8, MESSAGE_HEADER_SIZE) };
+        let start = u16::from_le_bytes([bytes[0], bytes[1]]);
+        let len = u16::from_le_bytes([bytes[2], bytes[3]]);
+        let checksum_expected = u16::from_le_bytes([bytes[4], bytes[5]]);
+        let checksum_actual = X25.checksum(&bytes[0..4]);
+
+        if checksum_expected == checksum_actual {
+            Self { start, len }
+        } else {
+            Self { start: 0, len: 0 }
+        }
+    }
+
+    fn write(&self, storage: &mut Storage) {
+        let mut buf = [0; MESSAGE_HEADER_SIZE];
+        buf[0..2].copy_from_slice(&self.start.to_le_bytes());
+        buf[2..4].copy_from_slice(&self.len.to_le_bytes());
+        let checksum = X25.checksum(&buf[0..4]);
+        buf[4..].copy_from_slice(&checksum.to_le_bytes());
+
+        storage.write_data(MESSAGE_HEADER_START, &buf);
+    }
+}
+
+pub struct Message {
+    // Storing the entire callsign/ssid per message is kind of a waste
+    // but it's simple. We can fix it later if it becomes an issue
+    callsign: String<MAX_CONTACT_CALLSIGN_LENGTH>,
+    ssid: u8,
+    message: String<MAX_MESSAGE_BODY_LENGTH>,
+    direction: MessageDirection,
+    received: bool,
+    send_attempts: u8,
+}
+
+impl Message {
+    pub fn new(
+        callsign: String<MAX_CONTACT_CALLSIGN_LENGTH>,
+        ssid: u8,
+        message: String<MAX_MESSAGE_BODY_LENGTH>,
+        direction: MessageDirection,
+    ) -> Self {
+        Self {
+            callsign,
+            ssid,
+            message,
+            direction,
+            received: false,
+            send_attempts: 0,
+        }
+    }
+
+    pub fn content(&self) -> &str {
+        &self.message
+    }
+
+    pub fn direction(&self) -> MessageDirection {
+        self.direction
+    }
+
+    fn read(storage: &Storage, idx: usize) -> Option<Self> {
+        let start_addr = MESSAGE_HEADER_START + MESSAGE_HEADER_SIZE + idx * MESSAGE_LENGTH;
+
+        let bytes = storage.read_slice::<MESSAGE_LENGTH>(start_addr);
+        let checksum_expected =
+            u16::from_le_bytes([bytes[MESSAGE_LENGTH - 2], bytes[MESSAGE_LENGTH - 1]]);
+        let checksum_actual = X25.checksum(&bytes[..(MESSAGE_LENGTH - 2)]);
+
+        if checksum_expected != checksum_actual {
+            return None;
+        }
+
+        let callsign_length: usize = bytes[0].into();
+        let message_length: usize = u16::from_le_bytes([bytes[1], bytes[2]]).into();
+        let ssid = bytes[3];
+        let direction = MessageDirection::from(bytes[4] & 0x01 > 0);
+        let received = bytes[4] & 0x02 > 0;
+        let send_attempts = bytes[5];
+        let callsign_bytes = &bytes[6..][..callsign_length];
+        let message_bytes = &bytes[(6 + MAX_CONTACT_CALLSIGN_LENGTH)..][..message_length];
+
+        let callsign = String::from_utf8(Vec::from_slice(callsign_bytes).ok()?).ok()?;
+        let message = String::from_utf8(Vec::from_slice(message_bytes).ok()?).ok()?;
+
+        Some(Self {
+            callsign,
+            ssid,
+            message,
+            direction,
+            received,
+            send_attempts,
+        })
+    }
+
+    fn write(&self, storage: &mut Storage, idx: usize) {
+        let start_addr = MESSAGE_HEADER_START + MESSAGE_HEADER_SIZE + idx * MESSAGE_LENGTH;
+
+        let callsign_bytes = self.callsign.as_bytes();
+        let message_bytes = self.message.as_bytes();
+
+        let mut buf = [0; MESSAGE_LENGTH];
+        buf[0] = callsign_bytes.len().try_into().unwrap();
+        buf[1..3].copy_from_slice(&u16::try_from(self.message.len()).unwrap().to_le_bytes());
+        buf[3] = self.ssid;
+        buf[4] = (u8::from(self.received) << 1) | u8::from(bool::from(self.direction));
+        buf[5] = self.send_attempts;
+        buf[6..][..callsign_bytes.len()].copy_from_slice(callsign_bytes);
+        buf[(6 + MAX_CONTACT_CALLSIGN_LENGTH)..][..(message_bytes.len())]
+            .copy_from_slice(message_bytes);
+
+        let checksum = X25.checksum(&buf[..(MESSAGE_LENGTH - 2)]);
+        buf[(MESSAGE_LENGTH - 2)..].copy_from_slice(&checksum.to_le_bytes());
+
+        storage.write_data(start_addr, &buf);
+    }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum MessageDirection {
+    From,
+    To,
+}
+
+impl From<bool> for MessageDirection {
+    fn from(value: bool) -> Self {
+        if value {
+            MessageDirection::From
+        } else {
+            MessageDirection::To
+        }
+    }
+}
+
+impl From<MessageDirection> for bool {
+    fn from(value: MessageDirection) -> Self {
+        match value {
+            MessageDirection::From => true,
+            MessageDirection::To => false,
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct MessageIter<'a, 'b, 'c> {
+    group: &'a MessageGroup,
+    storage: &'b Storage,
+    callsign: &'c str,
+    ssid: u8,
+    i: u16,
+}
+
+impl<'a, 'b, 'c> MessageIter<'a, 'b, 'c> {
+    pub fn new(group: &'a MessageGroup, storage: &'b Storage, callsign: &'c str, ssid: u8) -> Self {
+        Self {
+            group,
+            storage,
+            callsign,
+            ssid,
+            i: 0,
+        }
+    }
+}
+
+impl<'a, 'b, 'c> Iterator for MessageIter<'a, 'b, 'c> {
+    type Item = Message;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        while self.i < self.group.header.len {
+            let idx = (self.i + self.group.header.start) % self.group.header.len;
+            self.i += 1;
+
+            if let Some(msg) = Message::read(self.storage, idx.into()) {
+                if msg.callsign == self.callsign && msg.ssid == self.ssid {
+                    return Some(msg);
+                }
+            }
+        }
+
+        None
+    }
+}
+
+// Orders from newest to oldest
+pub struct AddresseeIter<'a, 'b> {
+    group: &'a MessageGroup,
+    storage: &'b Storage,
+    i: u16,
+    seen: FnvIndexSet<(String<MAX_CONTACT_CALLSIGN_LENGTH>, u8), MAX_MESSAGES>,
+}
+
+impl<'a, 'b> AddresseeIter<'a, 'b> {
+    pub fn new(group: &'a MessageGroup, storage: &'b Storage) -> Self {
+        Self {
+            group,
+            storage,
+            i: group.header.len,
+            seen: FnvIndexSet::new(),
+        }
+    }
+}
+
+impl<'a, 'b> Iterator for AddresseeIter<'a, 'b> {
+    type Item = (String<MAX_CONTACT_CALLSIGN_LENGTH>, u8);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        while self.i > 0 {
+            self.i -= 1;
+
+            let idx = (self.i + self.group.header.start) % self.group.header.len;
+            if let Some(msg) = Message::read(self.storage, idx.into()) {
+                let ident = (msg.callsign, msg.ssid);
+                if !self.seen.contains(&ident) {
+                    self.seen.insert(ident.clone()).unwrap();
+
+                    return Some(ident);
+                }
+            }
+        }
+
+        None
+    }
+}
diff --git a/src/storage/mod.rs b/src/storage/mod.rs
index 0b628ea..12e2c1b 100644
--- a/src/storage/mod.rs
+++ b/src/storage/mod.rs
@@ -1,13 +1,18 @@
 mod contact;
+mod message;
 
 use contact::ContactGroup;
 pub use contact::{
     Contact, ContactIter, MAX_CONTACTS, MAX_CONTACT_CALLSIGN_LENGTH, MAX_CONTACT_NAME_LENGTH,
 };
+pub use message::{Message, MessageDirection, MessageGroup, MessageIter};
+use sectorize::SectorizeIter;
 
 use core::slice::from_raw_parts;
 use rp2040_flash::flash;
 
+use self::message::AddresseeIter;
+
 const SECTOR_SIZE: usize = 4096;
 const FLASH_START: usize = 0x10000000;
 // First 6MB is for code
@@ -19,12 +24,14 @@ const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
 
 pub struct Storage {
     contacts: ContactGroup,
+    messages: MessageGroup,
 }
 
 impl Storage {
     pub fn new() -> Self {
         Self {
             contacts: ContactGroup::new(),
+            messages: MessageGroup::new(),
         }
     }
 
@@ -54,21 +61,42 @@ impl Storage {
         self.contacts.len()
     }
 
+    // Punch through our message methods
+
+    pub fn push_message(&mut self, message: Message) {
+        let mut messages = self.messages;
+        messages.push_message(self, message);
+        self.messages = messages;
+    }
+
+    pub fn messages<'a, 'c>(&'a self, callsign: &'c str, ssid: u8) -> MessageIter<'a, 'a, 'c> {
+        self.messages.iter(self, callsign, ssid)
+    }
+
+    pub fn message_addressees(&self) -> AddresseeIter {
+        self.messages.iter_addressees(self)
+    }
+
     fn write_data(&mut self, start_addr: usize, buf: &[u8]) {
-        let start_sector = start_addr / SECTOR_SIZE;
-        let num_sectors = buf.len() / SECTOR_SIZE + 1;
-        let diff = start_addr % SECTOR_SIZE;
-
-        for s in 0..num_sectors {
-            let existing_start = if s == 0 { diff } else { 0 };
-            let existing_end = (diff + (s + 1) * SECTOR_SIZE).min(existing_start + buf.len());
-            let mut existing = self.read_slice((start_sector + s) * SECTOR_SIZE);
-            let buf_start = (s * SECTOR_SIZE).min(buf.len());
-            let buf_end = (buf_start + SECTOR_SIZE).min(buf.len());
+        // rprintln!("Write {:x?} to {:x}", buf, start_addr);
+
+        for s in SectorizeIter::new(SECTOR_SIZE, start_addr, buf.len()) {
+            let mut existing = self.read_slice(s.sector_index * SECTOR_SIZE);
+
+            /* rprintln!(
+                    "Sector {}: {}..{} <- {}..{}",
+                    s.sector_index,
+                    s.sector_start,
+                    s.sector_end,
+                    s.input_start,
+                    s.input_end
+            ); */
+
             // only write if there's a difference
-            if existing[existing_start..existing_end] != buf[buf_start..buf_end] {
-                existing[existing_start..existing_end].copy_from_slice(&buf[buf_start..buf_end]);
-                self.write_sector((start_sector + s) * SECTOR_SIZE, &existing);
+            if existing[s.sector_start..s.sector_end] != buf[s.input_start..s.input_end] {
+                existing[s.sector_start..s.sector_end]
+                    .copy_from_slice(&buf[s.input_start..s.input_end]);
+                self.write_sector(s.sector_index * SECTOR_SIZE, &existing);
             }
         }
     }
-- 
GitLab