Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
306a054
Add EEPROM experiment
bifurcation Sep 14, 2025
4233f1c
Add I2S
bifurcation Sep 14, 2025
b623a78
First stab at sound
bifurcation Sep 15, 2025
5c27d40
Play a sound
bifurcation Sep 19, 2025
e3645c8
First stab at audio rx
bifurcation Sep 20, 2025
0fe4dee
Use a parallel checkout of Embassy rather than the one on crates.io
bifurcation Sep 22, 2025
8e88ac3
Attempt loopback
bifurcation Sep 22, 2025
062da73
Play a tone to verify that write works
bifurcation Sep 22, 2025
9eba5cc
Move audio control logic to a new module
bifurcation Sep 23, 2025
c082464
Make I2C register semantic explicit
bifurcation Sep 23, 2025
900444d
Add machine translation of C HAL I2S
bifurcation Sep 25, 2025
8d7b07e
Checkpoint
bifurcation Sep 25, 2025
0bcac05
Produce sound with translated HAL
bifurcation Sep 26, 2025
914636a
Now receiving audio, but noise
bifurcation Sep 27, 2025
367214f
No longer getting distorted loopback, seems to be faithfully measurin…
bifurcation Sep 28, 2025
2744031
Show noise output
bifurcation Sep 29, 2025
41bc917
Make a working analog pass-through setup to verify mic config
bifurcation Sep 29, 2025
49352ab
Factor out I2C controls
bifurcation Sep 30, 2025
90359a6
Improve the field-setting API and prepare for experiments
bifurcation Sep 30, 2025
2d8885a
Analog loopback experiment works with the new API
bifurcation Sep 30, 2025
92aae60
Tone generation experiment
bifurcation Sep 30, 2025
c7631a6
Device loopback works
bifurcation Sep 30, 2025
8a127cf
Cleanup
bifurcation Sep 30, 2025
2432e53
Partially use safe API
bifurcation Oct 1, 2025
7f98d18
Turning off the SysTick seems to make things smoother
bifurcation Oct 1, 2025
a5aa326
Checkpoint
bifurcation Oct 1, 2025
a707e71
Remove unnecessary setup pause
bifurcation Oct 1, 2025
84505be
Get rid of documentation comments in hal_i2s.rs
bifurcation Oct 1, 2025
67df492
Replace config constants with enums
bifurcation Oct 1, 2025
d3eb3ba
Remove unsafe blocks from hal_i2s_init and a few others
bifurcation Oct 1, 2025
29988fa
Remove unsafe() from some helper functions
bifurcation Oct 1, 2025
e0fa010
Remove unsafe from hal_i2s_transmit
bifurcation Oct 1, 2025
0202c4e
Checkpoint in txrx
bifurcation Oct 2, 2025
c3f00e9
Checkpoint
bifurcation Oct 2, 2025
3ff709e
Checkpoint
bifurcation Oct 2, 2025
295b8e8
Cleanup
bifurcation Oct 2, 2025
cfba174
Objectize the I2S handle
bifurcation Oct 2, 2025
7976174
End of day checkpoint
bifurcation Oct 2, 2025
71e6ec1
Start introducing typed config
bifurcation Oct 2, 2025
1162088
Remove async from I2C setup
bifurcation Oct 2, 2025
f9f700a
Remove non-Embassy notion of time
bifurcation Oct 2, 2025
72d1fd6
Remove the custom Format enum
bifurcation Oct 2, 2025
a1319d4
Generate the square wave more compactly
bifurcation Oct 2, 2025
54ce3b2
Use bool instead of enum for master clock output
bifurcation Oct 2, 2025
b76beec
Use clock polarity enum and enum mappings
bifurcation Oct 2, 2025
b3d51a1
Use Standard enum
bifurcation Oct 2, 2025
d20eec5
Remove the ClockSource enum
bifurcation Oct 2, 2025
6816c2b
Use Hertz for the requested frequency
bifurcation Oct 2, 2025
652da30
Remove internal state tracking from I2S
bifurcation Oct 2, 2025
eaf4041
Remove remaining unsafe code from I2S module
bifurcation Oct 2, 2025
a58d82e
Consolidate peripheral initialization into I2S::new
bifurcation Oct 3, 2025
a95c77a
DMA transmit works
bifurcation Oct 3, 2025
1399faa
DMA Tx and Rx working
bifurcation Oct 4, 2025
8faeea5
Cleanup
bifurcation Oct 4, 2025
27d7c83
More cleanup
bifurcation Oct 4, 2025
1a362b9
Use RxDmaExt trait to capture variant request number
bifurcation Oct 5, 2025
09ed310
Use Config and Error from Embassy
bifurcation Oct 5, 2025
a94d73c
Start migrating I2S functionality to embassy_stm32
bifurcation Oct 6, 2025
3b51ad8
Move I2S to Embassy
bifurcation Oct 6, 2025
6a9ad98
Cleanup
bifurcation Oct 7, 2025
8d3bded
Merge branch 'main' into i2s
bifurcation Oct 7, 2025
cff7970
First attempt to move I2S into the board module
bifurcation Oct 7, 2025
455ba07
Move I2S to board module
bifurcation Oct 7, 2025
aa4d5da
Add audio flow to app logic
bifurcation Oct 7, 2025
304c206
Log transmission
bifurcation Oct 7, 2025
7c8062c
Bring ui-app tests up to date
bifurcation Oct 7, 2025
47a7039
Add a PTT receive test
bifurcation Oct 7, 2025
3daf098
Add a PTT transmit test
bifurcation Oct 8, 2025
0ac0699
Update stm32 to match latest ui-app changes
bifurcation Oct 8, 2025
00646c3
Implement a rough draft of TLV sending
bifurcation Oct 8, 2025
468235f
Implement proper TLV reading
bifurcation Oct 9, 2025
f09c5af
Implement TLV write
bifurcation Oct 9, 2025
12d99a5
Avoid overruns
bifurcation Oct 9, 2025
cc239fb
Use local checkout for all of Embassy
bifurcation Oct 10, 2025
279bac6
Make net rx less brittle
bifurcation Oct 10, 2025
84d0ef2
Send Message to NET
bifurcation Oct 11, 2025
11a57ac
Add Tx/Rx demos
bifurcation Oct 14, 2025
226e537
Checkpoint
bifurcation Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ui-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ embedded-graphics = "0.8.1"
embedded-graphics-core = { version = "0.4.0", default-features = false }
heapless = "0.9.1"
hex = { version = "0.4.3", default-features = false }

[dev-dependencies]
tokio = { version = "1.47.1", features = ["time", "rt", "macros", "sync"] }
289 changes: 197 additions & 92 deletions ui-app/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![no_std]
#![allow(async_fn_in_trait)]

use bitmap_font::{BitmapFont, TextStyle, tamzen::FONT_10x20};
use bitmap_font::{tamzen::FONT_10x20, BitmapFont, TextStyle};
use core::fmt::Write;
use defmt::Format;
use embedded_graphics::{
Expand All @@ -12,7 +13,6 @@ use embedded_graphics::{
text::Text,
};
use heapless::String;
use hex::ToHex;

#[derive(Copy, Clone, Debug, PartialEq, Format)]
pub enum Key {
Expand Down Expand Up @@ -73,17 +73,23 @@ pub enum Button {
B,
}

#[derive(Copy, Clone, Debug, PartialEq, Format)]
pub const MAX_MESSAGE_LEN: usize = 128;

#[derive(Clone, Debug, PartialEq, Format)]
pub enum FromNet {
Pong,
AudioFrame(Frame),
Chat(String<MAX_MESSAGE_LEN>),
}

#[derive(Copy, Clone, Debug, PartialEq, Format)]
#[derive(Clone, Debug, PartialEq, Format)]
pub enum ToNet {
Ping,
AudioFrame(Frame),
Chat(String<MAX_MESSAGE_LEN>),
}

#[derive(Copy, Clone, Debug, PartialEq, Format)]
#[derive(Clone, Debug, PartialEq, Format)]
pub enum Event {
ButtonDown(Button),
ButtonUp(Button),
Expand Down Expand Up @@ -196,19 +202,79 @@ pub trait Eeprom {
fn write(&mut self, data: &[u8; 256]);
}

pub const FRAME_SIZE: usize = 320;

#[derive(Clone, Debug, PartialEq, Format)]
pub struct Frame(pub [u16; FRAME_SIZE]);

impl Default for Frame {
fn default() -> Self {
Self([0; FRAME_SIZE])
}
}

pub trait AudioControl {
fn start(&mut self);
fn enable_input(&mut self, enabled: bool);
fn enable_output(&mut self, enabled: bool);
}

pub trait AudioData {
async fn start(&mut self);
async fn stop(&mut self);
async fn read(&mut self) -> Frame;
async fn write(&mut self, frame: &Frame);

async fn write_iter(&mut self, samples: impl Iterator<Item = u16>) {
let mut send = Frame::default();

let mut i_mod = 0;
for (i, sample) in samples.enumerate() {
i_mod = i % FRAME_SIZE;
send.0[i_mod] = sample;

if i_mod == FRAME_SIZE - 1 {
self.write(&send).await;
}
}

i_mod += 1;
if i_mod < FRAME_SIZE {
send.0[i_mod..].fill(0);
self.write(&send).await;
}
}
}

pub trait Outputs {
fn button_a_down(&self) -> bool;
fn button_b_down(&self) -> bool;
fn status_led(&mut self) -> &mut impl Led;
fn screen(&mut self) -> &mut impl DrawTarget<Color = Rgb565>;
fn net_tx(&mut self) -> &mut impl NetTx;
fn eeprom(&mut self) -> impl Eeprom;
fn audio_control(&mut self) -> impl AudioControl;
fn audio_data(&mut self) -> &mut impl AudioData;
fn log(&mut self, message: &str);
}

pub trait EventSource {
async fn receive(&mut self) -> Option<Event>;
}

#[derive(Debug, PartialEq, Format)]
enum PttState {
Idle,
Transmitting,
Receiving,
}

#[derive(Debug)]
pub struct App {
a_down: bool,
b_down: bool,
message_buffer: String<24>,
ptt_state: PttState,
message_buffer: String<MAX_MESSAGE_LEN>,
}

impl App {
Expand All @@ -220,6 +286,7 @@ impl App {
Self {
a_down: false,
b_down: false,
ptt_state: PttState::Idle,
message_buffer: Default::default(),
}
}
Expand All @@ -228,19 +295,10 @@ impl App {
// Extinguish the status LED
out.status_led().set_color(Color::Black);

// Read the EEPROM, XOR with 0xFF, write it, then read it back
let mut eeprom_data = [0u8; 256];
out.eeprom().read(&mut eeprom_data);
let hex: heapless::String<1024> = eeprom_data.encode_hex();
defmt::info!("eeprom before {}", hex);

eeprom_data.iter_mut().for_each(|x| *x ^= 0xff);
out.eeprom().write(&eeprom_data);

eeprom_data.fill(0);
out.eeprom().read(&mut eeprom_data);
let hex: heapless::String<1024> = eeprom_data.encode_hex();
defmt::info!("eeprom after {}", hex);
// Start up the audio interface
out.audio_control().start();
out.audio_control().enable_input(true);
out.audio_control().enable_output(true);

// Draw a test pattern to the screen
let rect = out.screen().bounding_box();
Expand Down Expand Up @@ -280,91 +338,138 @@ impl App {
.unwrap_or_else(|_| panic!("graphics error"));
}

pub fn handle(&mut self, event: Event, out: &mut impl Outputs) {
match event {
Event::ButtonDown(button) => match button {
Button::A => {
out.log("button a down");
self.a_down = true;
out.net_tx().write(&ToNet::Ping);
}
Button::B => {
out.log("button b down");
self.b_down = true;
}
},
async fn handle_button_down(&mut self, button: Button, out: &mut impl Outputs) {
match button {
Button::A => {
out.log("button a down");
self.a_down = true;
out.status_led().set(false, self.a_down, self.b_down);

Event::ButtonUp(button) => match button {
Button::A => {
out.log("button a up");
self.a_down = false;
}
Button::B => {
out.log("button b up");
self.b_down = false;
}
},

Event::KeyDown(key, value) => {
// Log the key press
let mut msg: String<32> = Default::default();
write!(&mut msg, "key down: {:?} {:?}", key, value).unwrap();
out.log(&msg);

// If this key press is a return, then clear things out
if let Key::Enter = key {
let mut msg: String<64> = Default::default();
write!(&mut msg, "sending message: {}", self.message_buffer).unwrap();
out.log(&msg);

self.message_buffer.clear();

let width = out.screen().bounding_box().size.width;
let height = Self::FONT.height();
Rectangle::new(Point::new(0, 0), Size::new(width, height))
.into_styled(PrimitiveStyle::with_fill(Self::BACKGROUND))
.draw(out.screen())
.unwrap_or_else(|_| panic!("graphics error"));
return;
out.net_tx().write(&ToNet::Ping);
}
Button::B => {
out.log("button b down");
self.b_down = true;
out.status_led().set(false, self.a_down, self.b_down);

self.ptt_state = PttState::Transmitting;
out.audio_data().start().await;

while out.button_b_down() {
out.log("frame");
let frame = out.audio_data().read().await;
out.net_tx().write(&ToNet::AudioFrame(frame));
}

// Otherwise, if it's a character, add it to the buffer and render it to the screen
let KeyValue::Char(c) = value else {
return;
};
out.log("button b up (via down)");
// TODO: Stop the audio buffer. If we have this line right now, it causes a
// DmaUnsynced error. Probably an error in the underlying driver code.
// out.audio_data().stop().await;
self.ptt_state = PttState::Idle;
}
}
}

if self.message_buffer.len() == self.message_buffer.capacity() {
// Ignore any characters beyond the capacity of the message buffer
return;
}
fn handle_button_up(&mut self, button: Button, out: &mut impl Outputs) {
match button {
Button::A => {
out.log("button a up");
self.a_down = false;
}
Button::B => {
out.log("button b up");
self.b_down = false;
}
}

out.status_led().set(false, self.a_down, self.b_down);
}

fn handle_key_down(&mut self, key: Key, value: KeyValue, out: &mut impl Outputs) {
// Log the key press
let mut msg: String<32> = Default::default();
write!(&mut msg, "key down: {:?} {:?}", key, value).unwrap();
out.log(&msg);

// If this key press is a return, then clear things out
if let Key::Enter = key {
let mut msg: String<{ MAX_MESSAGE_LEN + 20 }> = Default::default();
write!(&mut msg, "sending message: {}", self.message_buffer).unwrap();
out.log(&msg);

self.message_buffer.push(c).unwrap();
out.net_tx()
.write(&ToNet::Chat(self.message_buffer.clone()));

let text = Text::new(
&self.message_buffer,
Point { x: 0, y: 0 },
TextStyle::new(&Self::FONT, BinaryColor::On),
);
self.message_buffer.clear();

let mut binary_display =
BinaryDisplay::new(Rgb565::WHITE, Rgb565::BLACK, out.screen());
text.draw(&mut binary_display)
.unwrap_or_else(|_| panic!("graphics error"));
let width = out.screen().bounding_box().size.width;
let height = Self::FONT.height();
Rectangle::new(Point::new(0, 0), Size::new(width, height))
.into_styled(PrimitiveStyle::with_fill(Self::BACKGROUND))
.draw(out.screen())
.unwrap_or_else(|_| panic!("graphics error"));
return;
}

// Otherwise, if it's a character, add it to the buffer and render it to the screen
let KeyValue::Char(c) = value else {
return;
};

if self.message_buffer.len() == self.message_buffer.capacity() {
// Ignore any characters beyond the capacity of the message buffer
return;
}

self.message_buffer.push(c).unwrap();

let text = Text::new(
&self.message_buffer,
Point { x: 0, y: 0 },
TextStyle::new(&Self::FONT, BinaryColor::On),
);

let mut binary_display = BinaryDisplay::new(Rgb565::WHITE, Rgb565::BLACK, out.screen());
text.draw(&mut binary_display)
.unwrap_or_else(|_| panic!("graphics error"));
}

fn handle_key_up(&mut self, key: Key, out: &mut impl Outputs) {
let mut msg: heapless::String<32> = Default::default();
write!(&mut msg, "key up: {:?}", key).unwrap();
out.log(&msg);
}

async fn handle_from_net(&mut self, from_net: FromNet, out: &mut impl Outputs) {
match from_net {
FromNet::Pong => {
out.log("pong");
}

Event::KeyUp(key) => {
let mut msg: heapless::String<32> = Default::default();
write!(&mut msg, "key up: {:?}", key).unwrap();
out.log(&msg);
FromNet::AudioFrame(frame) if self.ptt_state == PttState::Receiving => {
out.audio_data().start().await;
out.audio_data().write(&frame).await
}

Event::FromNet(from_net) => match from_net {
FromNet::Pong => {
out.log("pong");
}
},
_ => {
out.log("Dropped out-of-context message from NET chip");
}
}
}

out.status_led().set(false, self.a_down, self.b_down);
pub async fn handle(&mut self, event: Event, out: &mut impl Outputs) {
match event {
Event::ButtonDown(button) => self.handle_button_down(button, out).await,
Event::ButtonUp(button) => self.handle_button_up(button, out),
Event::KeyDown(key, value) => self.handle_key_down(key, value, out),
Event::KeyUp(key) => self.handle_key_up(key, out),
Event::FromNet(from_net) => self.handle_from_net(from_net, out).await,
}
}

pub async fn run(&mut self, mut events: impl EventSource, mut board: impl Outputs) {
while let Some(event) = events.receive().await {
self.handle(event, &mut board).await;
}
}
}
Loading