One major weekend coding session later...

I hate doing this as one giant commit, but I was under the gun and had
to just code as fast as I could, so some common software development
things had to be skipped. Mostly git commits.

Here we have several mini applications that reside on the microbit.
* Menu - The main app switching presentation.
* Badge - A name scroller.
* Snake - A snake game.

The switcher task is the main running task of the program. All the logic
goes through here. It will pass the running off to different apps. When
an app returns from its main function it will return the next app to
switch to.

The display task is a PWM LED matrix updater. It will turn on the LEDs
for what ever frame is passed to it. It keeps a backing frame for draw
calls that occur between new frames arriving.

There are several button listeners for input handling.

Everything is sent using channels, button input and presentation frames.
This commit is contained in:
2025-07-18 10:47:10 -04:00
parent 46a23631e4
commit 1181759ace
23 changed files with 4847 additions and 4 deletions

95
src/app.rs Normal file
View File

@ -0,0 +1,95 @@
use crate::channel::{ButtonReceiver, FrameSender};
#[derive(Debug, Clone, Copy)]
pub enum AppError
{
Unknown
}
impl core::fmt::Display for AppError
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
{
match self
{
AppError::Unknown =>
{
write!(f, "Application caused an unknown error.")
}
}
}
}
//#[cfg(not(feature = "std"))]
impl core::error::Error for AppError {}
#[repr(u8)]
pub enum AppId
{
Menu = 0,
Snake = 1,
Badge = 2,
Nfc = 3
}
impl From<AppId> for u8
{
fn from(value: AppId) -> u8
{
value as u8
}
}
impl From<AppId> for usize
{
fn from(value: AppId) -> usize
{
value as usize
}
}
impl core::convert::TryFrom<u8> for AppId
{
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error>
{
match value
{
0 => Ok(AppId::Menu),
1 => Ok(AppId::Snake),
2 => Ok(AppId::Badge),
3 => Ok(AppId::Nfc),
_ => Err(())
}
}
}
impl core::convert::TryFrom<usize> for AppId
{
type Error = ();
fn try_from(value: usize) -> Result<Self, Self::Error>
{
match value
{
0 => Ok(AppId::Menu),
1 => Ok(AppId::Snake),
2 => Ok(AppId::Badge),
3 => Ok(AppId::Nfc),
_ => Err(())
}
}
}
pub trait App
{
fn new(frame_output: FrameSender, button_input: ButtonReceiver) -> Self;
async fn run(&mut self) -> Result<AppId, AppError>;
}

4
src/badge.rs Normal file
View File

@ -0,0 +1,4 @@
mod badge;
mod renderer;
pub use crate::badge::badge::Badge;

275
src/badge/badge.rs Normal file
View File

@ -0,0 +1,275 @@
use embassy_time::Timer;
use crate::app::{App, AppError, AppId};
use crate::badge::renderer::{BadgeRenderer, Icon};
use crate::bounded::{BoundedU8, WrappedU8};
use crate::channel::{ButtonReceiver, FrameSender};
use crate::image::{Grayscale, Image, MicrobitImage};
use crate::microbit::Button;
use crate::renderer::Renderer;
use crate::string::String;
const MAX_NAME: usize = 128;
const SPACE_SIZE: usize = 2;
type AnimationImage =
Grayscale<{ MicrobitImage::WIDTH * 2 + SPACE_SIZE }, 5, 11, 4>;
pub struct Badge
{
renderer: BadgeRenderer,
button_input: ButtonReceiver,
text_images: [MicrobitImage; 6],
animation_image: AnimationImage,
text: String<MAX_NAME>,
current_letter: WrappedU8<0, 5>,
current_col: WrappedU8<0, 7>
}
impl Badge
{
fn load_text_images(&mut self)
{
self.text_images = [MicrobitImage::new(),
[[BoundedU8::new(9), BoundedU8::new(9), BoundedU8::new(9), BoundedU8::new(9), BoundedU8::new(9)],
[BoundedU8::new(0), BoundedU8::new(0), BoundedU8::new(9), BoundedU8::new(0), BoundedU8::new(0)],
[BoundedU8::new(0), BoundedU8::new(0), BoundedU8::new(9), BoundedU8::new(0), BoundedU8::new(0)],
[BoundedU8::new(9), BoundedU8::new(0), BoundedU8::new(9), BoundedU8::new(0), BoundedU8::new(0)],
[BoundedU8::new(0), BoundedU8::new(9), BoundedU8::new(0), BoundedU8::new(0), BoundedU8::new(0)]].into(),
[[BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0)],
[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0)],
[BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)]].into(),
[[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0)],
[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(0)],
[BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(0)]].into(),
[[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(0)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(0)]].into(),
[[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)]].into()];
}
fn animate_name(&mut self) -> bool
{
let mut pause: bool = false;
// Clear the image.
self.renderer.clear();
if self.current_col.get() == 0
{
pause = true;
}
// Now copy the sub image of the larger animation image.
for row in 0..MicrobitImage::HEIGHT
{
for col in 0..MicrobitImage::WIDTH
{
let adjusted_col = col + (self.current_col.get() as usize);
let level = self.animation_image
.get_pixel(adjusted_col.into(), row.into());
self.renderer.set_level(col.into(), row.into(), *level);
}
}
// If we have finished
if (self.current_col.get() as usize) ==
(AnimationImage::WIDTH - MicrobitImage::WIDTH)
{
self.current_letter += 1;
self.load_animation_image(self.current_letter.get() as usize,
(self.current_letter + 1).get() as usize);
}
self.current_col += 1;
pause
}
fn load_animation_image(&mut self, curr_char: usize, next_char: usize)
{
self.animation_image.clear();
let left: &MicrobitImage = &self.text_images[curr_char];
let right: &MicrobitImage = &self.text_images[next_char];
// Now write the animation image.
for row in 0..MicrobitImage::WIDTH
{
for col in 0..MicrobitImage::HEIGHT
{
let left_level = left.get_pixel(col.into(), row.into());
let right_level = right.get_pixel(col.into(), row.into());
self.animation_image
.set_pixel(col.into(), row.into(), *left_level);
self.animation_image.set_pixel((col +
MicrobitImage::WIDTH +
SPACE_SIZE)
.into(),
row.into(),
*right_level);
}
}
}
}
impl App for Badge
{
fn new(frame_output: FrameSender, button_input: ButtonReceiver) -> Self
{
// We create an image large enough to hold two
// letters and a space. Then we just cycle through
// each step on what we draw. The run timer will
// ultimately decide the animations speed.
//
// 0 1 1 1 1 0 0 0 1 1 1 0
// 1 0 0 0 0 0 0 1 0 0 0 1
// 0 1 1 1 0 -> 0 0 -> 1 0 0 0 1
// 0 0 0 0 1 0 0 1 0 0 0 1
// 1 1 1 1 0 0 0 0 1 1 1 0
let mut badge = Self { renderer: BadgeRenderer::new(frame_output),
button_input,
text_images: [MicrobitImage::new(); 6],
animation_image: AnimationImage::new(),
text: String::from_str("Jason"),
current_letter: 0.into(),
current_col: 0.into() };
badge.load_text_images();
badge.load_animation_image(0, 1);
badge
}
async fn run(&mut self) -> Result<AppId, AppError>
{
loop
{
// Get the button input.
if let Ok(button) = self.button_input.try_receive()
{
match button
{
Button::A |
Button::B |
Button::Start =>
{
return Ok(AppId::Menu);
}
}
}
// Draw the animation of a scrolling name.
if self.animate_name()
{
Timer::after_micros(150000).await;
}
// Swap the frame buffer so it can be drawn.
self.renderer.swap_buffers().await;
// Sleep enough to acheive 80Hz.
Timer::after_micros(150000).await;
}
}
}

83
src/badge/renderer.rs Normal file
View File

@ -0,0 +1,83 @@
use crate::bounded::BoundedUsize;
use crate::channel::FrameSender;
use crate::image::{Level, MicrobitImage};
use crate::renderer::{Col, Renderer, Row};
pub type Icon = MicrobitImage;
///
pub struct BadgeRenderer
{
sender: FrameSender,
frame: MicrobitImage
}
impl BadgeRenderer
{
pub fn clear(&mut self)
{
self.frame.clear();
}
pub fn turn_on(&mut self, x: Col, y: Row)
{
self.frame.set_pixel(x, y, Level::MAX.into());
}
pub fn set_level(&mut self, x: Col, y: Row, level: Level)
{
self.frame.set_pixel(x, y, level);
}
pub fn turn_off(&mut self, x: Col, y: Row)
{
self.frame.set_pixel(x, y, Level::MIN.into());
}
pub fn draw_icon(&mut self, icon: Icon)
{
self.frame = icon;
}
}
impl Renderer for BadgeRenderer
{
fn new(sender: FrameSender) -> Self
{
Self { sender,
frame: MicrobitImage::new() }
}
async fn swap_buffers(&mut self)
{
self.sender.send(self.frame.clone()).await;
}
}
// impl crate::snake::Renderer for Renderer
// {
// fn clear(&mut self)
// {
// self.clear();
// }
//
// fn draw_food(&mut self, position: (usize, usize))
// {
// self.turn_on(position.0.into(), position.1.into());
// }
//
// fn draw_snake(&mut self, position: &[(usize, usize)])
// {
// self.set_level(position[0].0.into(), position[0].1.into(), 5u8.into());
//
// for segment in position.iter().skip(1)
// {
// self.set_level(segment.0.into(), segment.1.into(), 2u8.into());
// }
// }
// }

2461
src/bounded.rs Normal file

File diff suppressed because it is too large Load Diff

17
src/channel.rs Normal file
View File

@ -0,0 +1,17 @@
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::{Channel, Receiver, Sender};
use crate::image::MicrobitImage;
use crate::microbit::Button;
pub type FrameChannel = Channel<CriticalSectionRawMutex, MicrobitImage, 1>;
pub type FrameReceiver =
Receiver<'static, CriticalSectionRawMutex, MicrobitImage, 1>;
pub type FrameSender =
Sender<'static, CriticalSectionRawMutex, MicrobitImage, 1>;
pub type ButtonChannel = Channel<CriticalSectionRawMutex, Button, 4>;
pub type ButtonReceiver = Receiver<'static, CriticalSectionRawMutex, Button, 4>;
pub type ButtonSender = Sender<'static, CriticalSectionRawMutex, Button, 4>;

144
src/display.rs Normal file
View File

@ -0,0 +1,144 @@
use embassy_nrf::gpio::{Output, OutputDrive};
use embassy_nrf::peripherals::{P0_11, P0_15, P0_19, P0_21, P0_22, P0_24, P0_28, P0_30, P0_31, P1_05};
use embassy_time::Timer;
use crate::channel::FrameReceiver;
use crate::image::{Level, MicrobitImage};
use crate::microbit::{LED_MATRIX_HEIGHT, LED_MATRIX_WIDTH};
const WIDTH: usize = LED_MATRIX_WIDTH;
const HEIGHT: usize = LED_MATRIX_HEIGHT;
const WIDTH_BOUND: usize = WIDTH - 1;
const HEIGHT_BOUND: usize = HEIGHT - 1;
const STEP_DELAY: u64 = 250;
/// This will own and control the pins for the 5x5 LED matrix.
///
/// row_1: P0_21
/// row_2: P0_22
/// row_3: P0_15
/// row_4: P0_24
/// row_5: P0_19
///
/// col_1: P0_28
/// col_2: P0_11
/// col_3: P0_31
/// col_4: P1_05
/// col_5: P0_30
pub struct Display<'a>
{
rows: [Output<'a>; 5],
cols: [Output<'a>; 5],
receiver: FrameReceiver
}
impl<'a> Display<'a>
{
pub fn new(row_1: P0_21, row_2: P0_22, row_3: P0_15, row_4: P0_24,
row_5: P0_19, col_1: P0_28, col_2: P0_11, col_3: P0_31,
col_4: P1_05, col_5: P0_30, receiver: FrameReceiver)
-> Self
{
Self { rows: [Output::new(row_1,
embassy_nrf::gpio::Level::Low,
OutputDrive::Standard),
Output::new(row_2,
embassy_nrf::gpio::Level::Low,
OutputDrive::Standard),
Output::new(row_3,
embassy_nrf::gpio::Level::Low,
OutputDrive::Standard),
Output::new(row_4,
embassy_nrf::gpio::Level::Low,
OutputDrive::Standard),
Output::new(row_5,
embassy_nrf::gpio::Level::Low,
OutputDrive::Standard)],
cols: [Output::new(col_1,
embassy_nrf::gpio::Level::High,
OutputDrive::Standard),
Output::new(col_2,
embassy_nrf::gpio::Level::High,
OutputDrive::Standard),
Output::new(col_3,
embassy_nrf::gpio::Level::High,
OutputDrive::Standard),
Output::new(col_4,
embassy_nrf::gpio::Level::High,
OutputDrive::Standard),
Output::new(col_5,
embassy_nrf::gpio::Level::High,
OutputDrive::Standard)],
receiver }
}
///
pub async fn present(&mut self)
{
// Get the frame from the renderer and present it.
let mut frame = MicrobitImage::new();
loop
{
if let Ok(new_frame) = self.receiver.try_receive()
{
frame = new_frame;
}
self.present_frame(frame).await;
}
}
async fn present_frame(&mut self, frame: MicrobitImage)
{
// Draw it row by row.
for pwm_counter in 0..Level::MAX + 1
{
for (y, row) in frame.row_iter().enumerate()
{
self.present_row(y, row, pwm_counter).await;
}
}
}
async fn present_row(&mut self, index: usize, row: &[Level], pwm_counter: u8)
{
// Clear the row we are drawing to.
for col in &mut self.cols
{
col.set_high();
}
self.rows[index].set_high();
// We need to flash draw the row Brightness step times.
self.pwm_cycle(pwm_counter, row).await;
// Unset the row.
self.rows[index].set_low();
}
async fn pwm_cycle(&mut self, pwm_counter: u8, row: &[Level])
{
//
for (x, &brightness) in row.iter().enumerate()
{
if brightness.get() > pwm_counter
{
// Turn on.
self.cols[x].set_low();
}
else
{
// Turn off.
self.cols[x].set_high();
}
}
Timer::after_micros(STEP_DELAY).await;
}
}

97
src/image.rs Normal file
View File

@ -0,0 +1,97 @@
// TODO: Change the BoundedUsize to be {W-1} and {H-1} afer
// [generic_const_exprs](https://github.com/rust-lang/rust/issues/76560)
// lands.. If it ever lands.
//
// Then this can just be Grayscale<W, H>.
use crate::bounded::{BoundedU8, BoundedUsize};
use crate::microbit::{LED_MATRIX_HEIGHT, LED_MATRIX_WIDTH};
pub type Level = BoundedU8<0, 9>;
pub type MicrobitImage = Grayscale<LED_MATRIX_WIDTH,
LED_MATRIX_HEIGHT,
{ LED_MATRIX_WIDTH - 1 },
{ LED_MATRIX_HEIGHT - 1 }>;
pub trait Image
{
fn width() -> usize;
fn height() -> usize;
}
#[derive(Clone, Copy)]
pub struct Grayscale<const W: usize,
const H: usize,
const WB: usize,
const HB: usize>
{
pixels: [[Level; W]; H]
}
impl<const W: usize, const H: usize, const WB: usize, const HB: usize>
Grayscale<W, H, WB, HB>
{
pub const HEIGHT: usize = H;
pub const WIDTH: usize = W;
pub fn new() -> Self
{
Self { pixels: [[Level::MIN.into(); W]; H] }
}
pub fn clear(&mut self)
{
self.pixels
.iter_mut()
.flatten()
.for_each(|level| *level = Level::MIN.into());
}
///
pub fn set_pixel(&mut self,
x: BoundedUsize<0, WB>, // TODO:Change to {W-1}
y: BoundedUsize<0, HB>, // TODO:Change to {H-1}
level: Level)
{
self.pixels[y.get()][x.get()] = level;
}
pub fn get_pixel(&self, x: BoundedUsize<0, WB>, y: BoundedUsize<0, HB>)
-> &Level
{
&self.pixels[y.get()][x.get()]
}
pub fn row_iter(&self) -> impl Iterator<Item = &[Level; W]>
{
self.pixels.iter()
}
}
impl<const W: usize, const H: usize, const WB: usize, const HB: usize>
From<[[Level; W]; H]> for Grayscale<W, H, WB, HB>
{
fn from(pixels: [[Level; W]; H]) -> Self
{
Self { pixels }
}
}
impl<const W: usize, const H: usize, const WB: usize, const HB: usize> Image
for Grayscale<W, H, WB, HB>
{
fn width() -> usize
{
W
}
fn height() -> usize
{
H
}
}

View File

@ -1,14 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
// Sealed with Magistamp.
//! This is where the cargo build information can be retrieved from.
/// The environment variable defined by Cargo for the name.
#[allow(dead_code)]
const NAME: Option<&str> = option_env!("CARGO_PKG_NAME");
/// The environment variable defined by Cargo for the version.
#[allow(dead_code)]
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
/// The string to display if a value is not defined during compile time.
#[allow(dead_code)]
const NOT_DEFINED: &'static str = "UNDEFINED";
@ -17,6 +23,7 @@ const NOT_DEFINED: &'static str = "UNDEFINED";
/// set at compile time and comes from the Cargo.toml file.
///
/// If a value is not found, then it will return the not defined value.
#[allow(dead_code)]
pub fn get_name() -> &'static str
{
NAME.unwrap_or(NOT_DEFINED)
@ -27,6 +34,7 @@ pub fn get_name() -> &'static str
/// This is set at compile time and comes from the Cargo.toml file.
///
/// If a value is not found, then it will return the not defined value.
#[allow(dead_code)]
pub fn get_version() -> &'static str
{
VERSION.unwrap_or(NOT_DEFINED)

View File

@ -1,4 +1,8 @@
//! MicroBadge is a software application suite for the BBC micro:bit v2, designed as a digital conference badge.
// SPDX-License-Identifier: Apache-2.0
// Sealed with Magistamp.
//! Examples of my code in an embedded environment. This is targeting the
//! Microbit v2 platform.
#![no_std]
#![no_main]
@ -7,13 +11,42 @@
mod info;
mod app;
mod bounded;
mod channel;
mod display;
mod image;
mod microbit;
mod renderer;
mod string;
mod switcher;
mod badge;
mod menu;
mod snake;
use defmt::info;
use defmt::{error, info, warn};
use embassy_executor::Spawner;
use {embassy_nrf as _};
use embassy_nrf::gpio::{AnyPin, Input, Pull};
use embassy_sync::channel::Channel;
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
use crate::channel::{ButtonChannel, ButtonSender, FrameChannel};
use crate::display::Display;
use crate::microbit::Button;
use crate::switcher::Switcher;
// Static channel for frame passing.
pub static FRAME_CHANNEL: FrameChannel = Channel::new();
// Create a channel for button events.
pub static BUTTON_CHANNEL: ButtonChannel = Channel::new();
/// Print the version.
@ -23,10 +56,101 @@ fn print_version()
}
#[embassy_executor::task(pool_size = 3)]
async fn button_listener(pin: AnyPin, button_id: Button, sender: ButtonSender)
{
let mut button: Input<'_> = Input::new(pin, Pull::None);
loop
{
// Wait for the button to be pressed.
button.wait_for_low().await;
if let Err(_error) = sender.try_send(button_id)
{
warn!("Button event dropped.");
}
// Debounce the button.
//
// I saw that others use 54ms for this, but I was still getting a
// few phantom signals, so I upped it to 100ms. Plenty fast enough.
Timer::after_millis(100).await;
// Then wait for the button to be released.
button.wait_for_high().await;
}
}
#[embassy_executor::task]
async fn display_task(mut display: Display<'static>)
{
display.present().await;
}
#[embassy_executor::task]
async fn app_task(mut switcher: Switcher)
{
switcher.run().await;
}
#[embassy_executor::main]
async fn main(_spawner: Spawner)
async fn main(spawner: Spawner)
{
// Print the version of our project.
print_version();
let p = embassy_nrf::init(Default::default());
let switcher =
Switcher::new(FRAME_CHANNEL.sender(), BUTTON_CHANNEL.receiver());
let display = Display::new(p.P0_21,
p.P0_22,
p.P0_15,
p.P0_24,
p.P0_19,
p.P0_28,
p.P0_11,
p.P0_31,
p.P1_05,
p.P0_30,
FRAME_CHANNEL.receiver());
// Create the Button listeners for the input we are interested in.
//
// Start (the logo) is actually a capacitive touch sensor, but we'll use it
// like a button.
if let Err(error) = spawner.spawn(button_listener(p.P0_14.into(),
Button::A,
BUTTON_CHANNEL.sender()))
{
error!("Unable to spawn Button Listener for A: {}", error);
}
if let Err(error) = spawner.spawn(button_listener(p.P0_23.into(),
Button::B,
BUTTON_CHANNEL.sender()))
{
error!("Unable to spawn Button Listener for B: {}", error);
}
if let Err(error) = spawner.spawn(button_listener(p.P1_04.into(),
Button::Start,
BUTTON_CHANNEL.sender()))
{
error!("Unable to spawn Button Listener for Start: {}", error);
}
// Create the display task. It will constantly render to the screen.
if let Err(error) = spawner.spawn(display_task(display))
{
error!("Unable to spawn Display Task: {}", error);
}
// Create the app task. It will handle running and switching the current
// application.
if let Err(error) = spawner.spawn(app_task(switcher))
{
error!("Unable to spawn App Task: {}", error);
}
}

4
src/menu.rs Normal file
View File

@ -0,0 +1,4 @@
mod menu;
mod renderer;
pub use crate::menu::menu::Menu;

125
src/menu/menu.rs Normal file
View File

@ -0,0 +1,125 @@
use crate::app::{App, AppError, AppId};
use crate::bounded::WrappedUsize;
use crate::channel::{ButtonReceiver, FrameSender};
use crate::menu::renderer::{Icon, MenuRenderer};
use crate::microbit::Button;
use crate::renderer::Renderer;
pub type ItemId = usize;
#[derive(Clone, Copy)]
pub struct Item
{
id: ItemId,
icon: Icon
}
pub struct Menu<const C: usize, const CB: usize>
{
items: [Option<Item>; C],
count: usize,
renderer: MenuRenderer,
button_input: ButtonReceiver
}
impl<const C: usize, const CB: usize> Menu<C, CB>
{
pub fn add(&mut self, id: ItemId, icon: Icon) -> Result<(), ()>
{
for (index, option_item) in self.items.iter_mut().enumerate()
{
if option_item.is_none()
{
*option_item = Some(Item { id: id, icon: icon });
self.count += 1;
return Ok(());
}
}
// No space in the array!
Err(())
}
pub fn remove(&mut self, id: ItemId, icon: Icon) -> Result<(), ()>
{
for (index, option_item) in self.items.iter_mut().enumerate()
{
if let Some(item) = option_item
{
if item.id == id
{
*option_item = None;
self.count -= 1;
return Ok(());
}
}
}
// Item wasn't in array!
Err(())
}
}
impl<const C: usize, const CB: usize> App for Menu<C, CB>
{
fn new(frame_output: FrameSender, button_input: ButtonReceiver) -> Self
{
Self { items: [None; C],
count: 0,
renderer: MenuRenderer::new(frame_output),
button_input }
}
async fn run(&mut self) -> Result<AppId, AppError>
{
// This cheats for now and assumes that C is the amount of items in the
// menu list. Really it is self.count, but I can't use that as a constant.
let mut selected: WrappedUsize<0, CB> = 0.into();
loop
{
// Get the button input.
if let Ok(button) = self.button_input.try_receive()
{
match button
{
Button::A =>
{
selected -= 1;
}
Button::B =>
{
selected += 1;
}
Button::Start =>
{
if let Some(item) = self.items[selected.get()]
{
let result: Result<AppId, ()> = (item.id as u8).try_into();
if let Ok(id) = result
{
return Ok(id);
}
}
return Ok(AppId::Menu);
}
}
}
// Draw the item currently selected.
if let Some(item) = self.items[selected.get()]
{
self.renderer.draw_icon(item.icon);
}
// Swap the frame buffer so it can be drawn.
self.renderer.swap_buffers().await;
}
}
}

83
src/menu/renderer.rs Normal file
View File

@ -0,0 +1,83 @@
use crate::bounded::BoundedUsize;
use crate::channel::FrameSender;
use crate::image::{Level, MicrobitImage};
use crate::renderer::{Col, Renderer, Row};
pub type Icon = MicrobitImage;
///
pub struct MenuRenderer
{
sender: FrameSender,
frame: MicrobitImage
}
impl MenuRenderer
{
pub fn clear(&mut self)
{
self.frame.clear();
}
pub fn turn_on(&mut self, x: Col, y: Row)
{
self.frame.set_pixel(x, y, Level::MAX.into());
}
pub fn set_level(&mut self, x: Col, y: Row, level: Level)
{
self.frame.set_pixel(x, y, level);
}
pub fn turn_off(&mut self, x: Col, y: Row)
{
self.frame.set_pixel(x, y, Level::MIN.into());
}
pub fn draw_icon(&mut self, icon: Icon)
{
self.frame = icon;
}
}
impl Renderer for MenuRenderer
{
fn new(sender: FrameSender) -> Self
{
Self { sender,
frame: MicrobitImage::new() }
}
async fn swap_buffers(&mut self)
{
self.sender.send(self.frame.clone()).await;
}
}
// impl crate::snake::Renderer for Renderer
// {
// fn clear(&mut self)
// {
// self.clear();
// }
//
// fn draw_food(&mut self, position: (usize, usize))
// {
// self.turn_on(position.0.into(), position.1.into());
// }
//
// fn draw_snake(&mut self, position: &[(usize, usize)])
// {
// self.set_level(position[0].0.into(), position[0].1.into(), 5u8.into());
//
// for segment in position.iter().skip(1)
// {
// self.set_level(segment.0.into(), segment.1.into(), 2u8.into());
// }
// }
// }

21
src/microbit.rs Normal file
View File

@ -0,0 +1,21 @@
///
pub const LED_MATRIX_WIDTH: usize = 5;
///
pub const LED_MATRIX_HEIGHT: usize = 5;
///
#[derive(Clone, Copy)]
pub enum Button
{
///
A,
///
B,
///
Start
}

90
src/renderer.rs Normal file
View File

@ -0,0 +1,90 @@
use crate::bounded::BoundedUsize;
use crate::channel::FrameSender;
use crate::image::{Level, MicrobitImage};
use crate::microbit::{LED_MATRIX_HEIGHT, LED_MATRIX_WIDTH};
pub const WIDTH: usize = LED_MATRIX_WIDTH;
pub const HEIGHT: usize = LED_MATRIX_HEIGHT;
pub type Row = BoundedUsize<0, { HEIGHT - 1 }>;
pub type Col = BoundedUsize<0, { WIDTH - 1 }>;
pub trait Renderer
{
fn new(sender: FrameSender) -> Self;
async fn swap_buffers(&mut self);
}
///
pub struct MicrobitRenderer
{
sender: FrameSender,
frame: MicrobitImage
}
impl MicrobitRenderer
{
pub fn clear(&mut self)
{
self.frame.clear();
}
pub fn turn_on(&mut self, x: Col, y: Row)
{
self.frame.set_pixel(x, y, Level::MAX.into());
}
pub fn set_level(&mut self, x: Col, y: Row, level: Level)
{
self.frame.set_pixel(x, y, level);
}
pub fn turn_off(&mut self, x: Col, y: Row)
{
self.frame.set_pixel(x, y, Level::MIN.into());
}
}
impl Renderer for MicrobitRenderer
{
fn new(sender: FrameSender) -> Self
{
Self { sender,
frame: MicrobitImage::new() }
}
async fn swap_buffers(&mut self)
{
self.sender.send(self.frame.clone()).await;
}
}
// impl crate::snake::Renderer for Renderer
// {
// fn clear(&mut self)
// {
// self.clear();
// }
//
// fn draw_food(&mut self, position: (usize, usize))
// {
// self.turn_on(position.0.into(), position.1.into());
// }
//
// fn draw_snake(&mut self, position: &[(usize, usize)])
// {
// self.set_level(position[0].0.into(), position[0].1.into(), 5u8.into());
//
// for segment in position.iter().skip(1)
// {
// self.set_level(segment.0.into(), segment.1.into(), 2u8.into());
// }
// }
// }

9
src/snake.rs Normal file
View File

@ -0,0 +1,9 @@
mod game;
mod position;
mod prng;
mod renderer;
mod snake;
pub use self::game::Game;

283
src/snake/game.rs Normal file
View File

@ -0,0 +1,283 @@
use embassy_time::{Instant, Timer};
use crate::app::{App, AppError, AppId};
use crate::bounded::ClampedU8;
use crate::channel::{ButtonReceiver, FrameSender};
use crate::microbit::Button;
use crate::renderer::Renderer;
use crate::snake::position::*;
use crate::snake::prng::Prng;
use crate::snake::renderer::SnakeRenderer;
use crate::snake::snake::Snake;
/// The different states that the snake game can be in.
#[derive(Clone, Copy, PartialEq)]
pub enum GameState
{
Difficulty,
Playing,
GameOver,
PulseScore
}
pub struct Game
{
renderer: SnakeRenderer,
button_input: ButtonReceiver,
game_state: GameState,
rng: Prng,
snake: Snake,
food: Position,
difficulty: ClampedU8<1, 24>,
speed: u8,
score: ClampedU8<0, 24>,
score_shown: ClampedU8<0, 24>
}
impl Game
{
pub fn reset(&mut self)
{
self.snake = Snake::new();
self.food = get_random_pos(&mut self.rng, self.snake.get_body()).unwrap_or(Position::new(0, 0));
self.difficulty = 3.into();
self.speed = 1;
self.score = 0.into();
self.score_shown = 0.into();
}
fn difficulty_input(&mut self)
{
// Get the button input.
if let Ok(button) = self.button_input.try_receive()
{
match button
{
Button::A =>
{
defmt::info!("Button A");
self.difficulty -= 1;
}
Button::B =>
{
defmt::info!("Button B");
self.difficulty += 1;
}
Button::Start =>
{
defmt::info!("Button Start");
self.game_state = GameState::Playing
}
}
}
}
fn difficulty_render(&mut self)
{
self.renderer.clear();
// Light up the chosen difficulty. Turn the linear value into an X,Y on
// the board.
let x: usize = (self.difficulty.get() as usize) % 5;
let y: usize = ((self.difficulty.get() as usize) - x) / 5;
defmt::info!("Draw {} -- ({}, {})", self.difficulty.get(), x, y);
for row in 0..y
{
for col in 0..5
{
self.renderer.turn_on(col.into(), row.into());
}
}
for col in 0..x
{
self.renderer.turn_on(col.into(), y.into());
}
}
fn playing_input(&mut self)
{
// Get the button input.
if let Ok(button) = self.button_input.try_receive()
{
match button
{
Button::A =>
{
defmt::info!("Button A");
self.snake.turn(Turn::Left);
}
Button::B =>
{
defmt::info!("Button B");
self.snake.turn(Turn::Right);
}
Button::Start =>
{
defmt::info!("Button Start");
self.game_state = GameState::Playing
}
}
}
}
fn playing_render(&mut self)
{
defmt::info!("Snake size: {}", self.snake.get_size());
let mut grow: bool = false;
// Check the next move of the snake for a collision.
let next_pos: Position = self.snake.get_next_position();
let grow = next_pos == self.food;
let collision = self.snake.check_collision();
// Generate the next food pellet.
if grow
{
self.food = get_random_pos(&mut self.rng, self.snake.get_body())
.unwrap_or_else(|| {
defmt::warn!("Board full, placing food at default.");
Position::new(0, 0)
});
// Increase the score if the snake grew.
self.score += 1;
}
// If the snake collided with itself, then it's gameover time.
if collision
{
self.game_state = GameState::GameOver;
}
else
{
// Move the snake.
self.snake.move_forward(grow);
}
self.renderer.clear();
self.renderer.draw_food(&self.food);
self.renderer.draw_snake(self.snake.get_body());
}
fn gameover_step(&mut self)
{
// Light up the number of LEDs for the score.
// One at a time.
self.score_shown += 1;
}
fn pulsescore_step(&mut self)
{
// Pulse the score LEDs.
}
}
impl App for Game
{
fn new(frame_output: FrameSender, button_input: ButtonReceiver) -> Self
{
let now = Instant::now().as_ticks() as u32;
let mut game = Game {
renderer: SnakeRenderer::new(frame_output),
button_input,
game_state: GameState::Difficulty,
rng: Prng::gen_state(200),
snake: Snake::new(),
food: Position::default(),
difficulty: 4.into(),
speed: 1,
score: 0.into(),
score_shown: 0.into() };
game.food = get_random_pos(&mut game.rng, game.snake.get_body()).unwrap_or(Position::new(0, 0));
game
}
async fn run(&mut self) -> Result<AppId, AppError>
{
loop
{
match self.game_state
{
GameState::Difficulty =>
{
self.difficulty_input();
self.difficulty_render();
}
GameState::Playing =>
{
self.playing_input();
self.playing_render();
}
GameState::GameOver =>
{
self.gameover_step();
}
GameState::PulseScore =>
{
self.pulsescore_step();
}
}
self.renderer.swap_buffers().await;
// Sleep enough to acheive 80Hz.
Timer::after_micros(250000).await;
}
}
}
fn get_random_pos(prng: &mut Prng, exclusion: &[Position]) -> Option<Position>
{
// This doesn't really work if there are no spaces left on the board.
if exclusion.len() == 25
{
defmt::info!("Default food: {}, {}", 0, 0);
return None;
}
// Calculate how many slots there are free on the board.
let slots: usize = 25 - exclusion.len();
// Fill in the board to find the slots. The board is a flat array here for
// easy enumeration.
//
// Free slots are true; Filled slots are false.
let mut temp_board: [bool; 25] = [true; 25];
for pos in exclusion.iter()
{
let index: usize = ((5u8 * pos.y.get()) + pos.x.get()) as usize;
temp_board[index] = false;
}
// Out of all the available slots on the board, pick one randomly.
// Then get it's index.
defmt::info!("Random stuff: {}, {}, {}, {}, {}", (prng.get() as usize), (prng.get() as usize), (prng.get() as usize), (prng.get() as usize), (prng.get() as usize));
let slot: usize = (prng.get() as usize) % slots;
let pos: usize = temp_board.iter()
.enumerate()
.filter(|&(_, &val)| val)
.nth(slot)
.map(|(i, _)| i)
.unwrap_or(0);
// Now turn our flat array of a board into a x and y coordinate on
// our grid.
let x: u8 = (pos as u8) % 5;
let y: u8 = ((pos as u8) - x) / 5;
defmt::info!("Next food: {}, {}", x, y);
Some(Position::new(x, y))
}

127
src/snake/position.rs Normal file
View File

@ -0,0 +1,127 @@
use crate::bounded::WrappedU8;
/// Global game Direction.
#[derive(Clone, Copy, PartialEq)]
pub enum Direction
{
/// Towards the top of the board.
Up,
/// Towarfs the bottom of the board.
Down,
/// Towards the Left of the board.
Left,
/// Towards the Right of the board.
Right
}
/// The way to Turn the Snake relative to it's global Direction.
///
/// For example if the Snake is heading Right and it is told to go Left then
/// it's next Direction would be Up.
#[derive(Clone, Copy, PartialEq)]
pub enum Turn
{
/// Turn left!
Left,
/// Turn right!
Right,
/// Go straight.
Straight
}
/// Defines a position on the Game board.
///
/// The game board is a grid with the upper left being (0, 0)
/// with positive X to the right and positive y going down.
///
/// Below is a 5x5 representation.
///
/// [ (0,0) (1,0), (2,0), (3,0), (4,0) ]
/// [ (0,1) (1,1), (2,1), (3,1), (4,1) ]
/// [ (0,2) (1,2), (2,2), (3,2), (4,2) ]
/// [ (0,3) (1,3), (2,3), (3,3), (4,3) ]
/// [ (0,4) (1,4), (2,4), (3,4), (4,4) ]
#[derive(Clone, Copy, Default, PartialEq)]
pub struct Position
{
/// The column we are in.
pub x: WrappedU8<0, 4>,
/// The row we are in.
pub y: WrappedU8<0, 4>
}
impl Position
{
pub fn new(x: u8, y: u8) -> Self
{
Position { x: x.into(),
y: y.into() }
}
pub fn set(&mut self, new_x: u8, new_y: u8)
{
self.x = new_x.into();
self.y = new_y.into();
}
}
impl From<&Position> for (usize, usize)
{
fn from(pos: &Position) -> Self
{
(pos.x.get() as usize, pos.y.get() as usize)
}
}
#[cfg(test)]
mod tests
{
// use super::bounded::WrappedU8;
use super::Position;
#[test]
fn position_new_sets_coordinates_correctly()
{
let pos = Position::new(2, 3);
assert_eq!(pos.x.get(), 2);
assert_eq!(pos.y.get(), 3);
}
#[test]
fn position_set_updates_coordinates()
{
let mut pos = Position::new(0, 0);
pos.set(4, 1);
assert_eq!(pos.x.get(), 4);
assert_eq!(pos.y.get(), 1);
}
#[test]
fn position_new_wraps_coordinates()
{
let pos = Position::new(5, 6);
assert_eq!(pos.x.get(), 1);
assert_eq!(pos.y.get(), 2);
}
#[test]
fn position_default_is_zero_zero()
{
let pos = Position::default();
assert_eq!(pos.x.get(), 0);
assert_eq!(pos.y.get(), 0);
}
}

111
src/snake/prng.rs Normal file
View File

@ -0,0 +1,111 @@
/// Defines the size of the state array needed for the Well algorithm.
const RNG_STATE_MAX: usize = 16;
/// A magic number for the Well algorithm.
const WELL_MAGIC_NUM: u32 = 3660128548; // 0xDA442D24
/// Generate psuedo random numbers.
pub struct Prng
{
/// This is the state of the Well algorithm.
state: [u32; RNG_STATE_MAX],
/// The index into the state machine array.
index: usize
}
impl Prng
{
/// Create a new pseudo random number generator from a given seed of states.
pub fn new(seed: [u32; RNG_STATE_MAX]) -> Self
{
Prng { state: seed,
index: 0 }
}
/// Create a new pseudo random number generator from a single given seed.
///
/// A basic XOR PRNG is used to fill in the Well state array.
pub fn gen_state(seed: u32) -> Self
{
let mut input: u32 = seed;
let mut seeded: [u32; RNG_STATE_MAX] = [0; RNG_STATE_MAX];
for i in 0..RNG_STATE_MAX {
seeded[i] = Prng::xor_rng(&mut input);
}
Prng { state: seeded,
index: 0 }
}
/// Get the next pseudo random number.
///
/// This is using Well 512 state algorithm from this paper:
/// [Lomont PRNG 2008](https://lomont.org/papers/2008/Lomont_PRNG_2008.pdf)
pub fn get(&mut self) -> u32
{
let mut a: u32 = self.state[self.index];
let mut c: u32 = self.state[(self.index + 13) & 15];
let b: u32 = a ^ c ^ (a << 16) ^ (c << 15);
c = self.state[(self.index + 9) & 15];
c = c ^ (c >> 11);
self.state[self.index] = b ^ c;
a = self.state[self.index];
let d: u32 = a ^ ((a << 5) & WELL_MAGIC_NUM);
self.index = (self.index + 15) & 15;
a = self.state[self.index];
self.state[self.index] = a ^ b ^ d ^ (a << 2) ^ (b << 18) ^ (c << 28);
self.state[self.index]
}
/// An XOR PRNG algorithm.
fn xor_rng(input: &mut u32) -> u32
{
*input ^= *input << 13;
*input ^= *input >> 17;
*input ^= *input << 5;
*input
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gen_state_produces_different_values() {
let mut prng = Prng::gen_state(12345);
let a = prng.get();
let b = prng.get();
assert_ne!(a, b); // Should not produce the same number twice
}
#[test]
fn test_deterministic_output() {
let mut prng1 = Prng::gen_state(42);
let mut prng2 = Prng::gen_state(42);
for _ in 0..100 {
assert_eq!(prng1.get(), prng2.get());
}
}
#[test]
fn test_state_based_initialization() {
let seed = [1u32; 16];
let mut prng = Prng::new(seed);
let _ = prng.get(); // Just ensure it runs
}
}

88
src/snake/renderer.rs Normal file
View File

@ -0,0 +1,88 @@
use crate::bounded::BoundedU8;
use crate::channel::FrameSender;
use crate::bounded::BoundedUsize;
use crate::image::{Level, MicrobitImage};
use crate::renderer::{Col, Renderer, Row};
use crate::snake::position::Position;
const FOOD_LEVEL: Level = BoundedU8::<0,9>::new(5);
const HEAD_LEVEL: Level = BoundedU8::<0,9>::new(9);
const TAIL_LEVEL: Level = BoundedU8::<0,9>::new(1);
/// Defines the functionality needed to render the Snake game.
pub struct SnakeRenderer
{
sender: FrameSender,
frame: MicrobitImage
}
impl SnakeRenderer
{
pub fn clear(&mut self)
{
self.frame.clear();
}
pub fn turn_on(&mut self, x: Col, y: Row)
{
self.frame.set_pixel(x, y, Level::MAX.into());
}
pub fn set_level(&mut self, x: Col, y: Row, level: Level)
{
self.frame.set_pixel(x, y, level);
}
pub fn turn_off(&mut self, x: Col, y: Row)
{
self.frame.set_pixel(x, y, Level::MIN.into());
}
/// Draw the food the snake is after.
pub fn draw_food(&mut self, position: &Position)
{
self.set_level((position.x.get() as usize).into(), (position.y.get() as usize).into(), FOOD_LEVEL);
}
/// Draw the snake.
pub fn draw_snake(&mut self, positions: &[Position])
{
if positions.is_empty() {
return;
}
// Draw the whole snake at body brightness
for segment in positions {
self.set_level(
(segment.x.get() as usize).into(),
(segment.y.get() as usize).into(),
TAIL_LEVEL
);
}
// Draw the head brighter (it's the last element)
let head = positions[positions.len() - 1];
self.set_level(
(head.x.get() as usize).into(),
(head.y.get() as usize).into(),
HEAD_LEVEL
);
}
}
impl Renderer for SnakeRenderer
{
fn new(sender: FrameSender) -> Self
{
Self { sender,
frame: MicrobitImage::new() }
}
async fn swap_buffers(&mut self)
{
self.sender.send(self.frame.clone()).await;
}
}

164
src/snake/snake.rs Normal file
View File

@ -0,0 +1,164 @@
use crate::bounded::ClampedUsize;
use crate::snake::position::{Direction, Position, Turn};
///
const BODY_MAX: usize = 25;
/// The main star of the game. The Snake will start off with a head and a
/// single body piece so that everyone knows that the snake isn't foot. It
/// also makes it easy to tell after a single move what direction the snake is
/// headed in.
pub struct Snake
{
/// The locations of the body of the snake.
/// The grid is a 5x5 so a full board would
/// be 25 body pieces.
body: [Position; BODY_MAX],
/// The location in the body array of the head.
head: ClampedUsize<1, {BODY_MAX - 1}>,
/// The Direction the Snake is headed in.
direction: Direction
}
impl Snake
{
pub fn new() -> Self
{
let mut snake_body: [Position; BODY_MAX] =
[Position::default(); BODY_MAX];
snake_body[1].set(2, 2); // Head
snake_body[0].set(2, 3); // Tail
Snake { body: snake_body,
head: 1.into(),
direction: Direction::Up }
}
pub fn check_collision(&self) -> bool
{
// Check if the head is in the same spot as any of the tail pieces.
for segment in 0..self.head.get()
{
if self.body[segment] == self.body[self.head.get()]
{
return true;
}
}
false
}
pub fn get_size(&self) -> usize
{
self.head.get() + 1
}
pub fn get_body(&self) -> &[Position]
{
defmt::info!("body({})", self.body[0..=self.head.get()].len());
&self.body[0..=self.head.get()]
}
pub fn get_next_position(&self) -> Position
{
let mut next_pos: Position = self.body[self.head.get()];
match self.direction
{
Direction::Up =>
{
next_pos.y -= 1;
}
Direction::Down =>
{
next_pos.y += 1;
}
Direction::Left =>
{
next_pos.x -= 1;
}
Direction::Right =>
{
next_pos.x += 1;
}
}
next_pos
}
pub fn move_forward(&mut self, grow: bool)
{
// Get the next position of the Snake.
let next_pos: Position = self.get_next_position();
// If the snake is supposed to grow, then we will add the head value to
// a new index in the array.
if grow
{
self.head += 1;
self.body[self.head.get()] = next_pos;
}
else
{
// Ripple the movement forward through the body by copying the
// positions backwards through the array.
let mut prev_val: Position = self.body[self.head.get()];
self.body[self.head.get()] = next_pos;
for index in (0..self.head.get()).rev()
{
let temp = self.body[index];
self.body[index] = prev_val;
prev_val = temp;
}
}
}
pub fn turn(&mut self, direction: Turn)
{
match direction
{
Turn::Left =>
{
self.turn_left();
}
Turn::Right =>
{
self.turn_right();
}
_ =>
{}
}
}
fn turn_left(&mut self)
{
self.direction = match self.direction
{
Direction::Up => Direction::Left,
Direction::Down => Direction::Right,
Direction::Left => Direction::Down,
Direction::Right => Direction::Up
}
}
fn turn_right(&mut self)
{
self.direction = match self.direction
{
Direction::Up => Direction::Right,
Direction::Down => Direction::Left,
Direction::Left => Direction::Up,
Direction::Right => Direction::Down
}
}
}

264
src/string.rs Normal file
View File

@ -0,0 +1,264 @@
#![no_std]
use core::fmt;
use core::ops::{Deref, Index, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive};
/// A UTF-8-safe, fixed-capacity string backed by a `[u8; N]` buffer.
pub struct String<const N: usize>
{
buffer: [u8; N],
len: usize
}
impl<const N: usize> String<N>
{
/// Creates a new, empty string.
pub const fn new() -> Self
{
Self { buffer: [0; N],
len: 0 }
}
/// Creates a new string from a `&str`, truncating safely if needed.
pub fn from_str(s: &str) -> Self
{
let mut string = Self::new();
string.push_str(s);
string
}
/// Returns the current length of the string in UTF-8 bytes.
pub fn len(&self) -> usize
{
self.len
}
/// Returns whether the string is empty.
pub fn is_empty(&self) -> bool
{
self.len == 0
}
/// Returns the string as a `&str`.
pub fn as_str(&self) -> &str
{
// Safety: Only valid UTF-8 bytes are inserted.
unsafe { core::str::from_utf8_unchecked(&self.buffer[..self.len]) }
}
/// Returns an iterator over the characters in the string.
pub fn chars(&self) -> core::str::Chars<'_>
{
self.as_str().chars()
}
/// Clears the string to empty.
pub fn clear(&mut self)
{
self.len = 0;
}
/// Appends a string slice, truncating safely if it doesnt fit.
pub fn push_str(&mut self, s: &str)
{
let available = N - self.len;
let bytes = s.as_bytes();
if bytes.len() <= available
{
self.buffer[self.len..self.len + bytes.len()].copy_from_slice(bytes);
self.len += bytes.len();
}
else
{
// Truncate on UTF-8 boundary.
let mut end = available;
while end > 0 && core::str::from_utf8(&bytes[..end]).is_err()
{
end -= 1;
}
self.buffer[self.len..self.len + end].copy_from_slice(&bytes[..end]);
self.len += end;
}
}
/// Appends a single character, if space permits.
pub fn push(&mut self, c: char) -> bool
{
let size = c.len_utf8();
if self.len + size > N
{
return false;
}
let mut buf = [0u8; 4];
c.encode_utf8(&mut buf);
self.buffer[self.len..self.len + size].copy_from_slice(&buf[..size]);
self.len += size;
true
}
/// Removes and returns the last character, if any.
pub fn pop(&mut self) -> Option<char>
{
if self.len == 0
{
return None;
}
// Walk backward to find the start of the last UTF-8 character.
let mut idx = self.len - 1;
while idx > 0 && (self.buffer[idx] & 0b1100_0000) == 0b1000_0000
{
idx -= 1;
}
let ch = core::str::from_utf8(&self.buffer[idx..self.len]).ok()
.and_then(|s| {
s.chars()
.next()
});
if let Some(_) = ch
{
self.len = idx;
}
ch
}
/// Truncates the string to the first `n` characters, if it contains more.
pub fn truncate_to_char_len(&mut self, max_chars: usize)
{
let mut count = 0;
let mut byte_pos = 0;
for (i, c) in self.as_str().char_indices()
{
if count == max_chars
{
break;
}
byte_pos = i + c.len_utf8();
count += 1;
}
self.len = byte_pos;
}
}
// Allow dereferencing to &str.
impl<const N: usize> Deref for String<N>
{
type Target = str;
fn deref(&self) -> &Self::Target
{
self.as_str()
}
}
// Allow equality comparison with another String of same capacity.
impl<const N: usize> PartialEq for String<N>
{
fn eq(&self, other: &Self) -> bool
{
self.as_str() == other.as_str()
}
}
// Allow equality comparison with &str.
impl<const N: usize> PartialEq<&str> for String<N>
{
fn eq(&self, other: &&str) -> bool
{
self.as_str() == *other
}
}
// Implement Index for various slicing types, returning &str.
impl<const N: usize> Index<Range<usize>> for String<N>
{
type Output = str;
fn index(&self, index: Range<usize>) -> &Self::Output
{
&self.as_str()[index]
}
}
impl<const N: usize> Index<RangeFrom<usize>> for String<N>
{
type Output = str;
fn index(&self, index: RangeFrom<usize>) -> &Self::Output
{
&self.as_str()[index]
}
}
impl<const N: usize> Index<RangeTo<usize>> for String<N>
{
type Output = str;
fn index(&self, index: RangeTo<usize>) -> &Self::Output
{
&self.as_str()[index]
}
}
impl<const N: usize> Index<RangeFull> for String<N>
{
type Output = str;
fn index(&self, _: RangeFull) -> &Self::Output
{
self.as_str()
}
}
impl<const N: usize> Index<RangeInclusive<usize>> for String<N>
{
type Output = str;
fn index(&self, index: RangeInclusive<usize>) -> &Self::Output
{
&self.as_str()[index]
}
}
impl<const N: usize> Index<RangeToInclusive<usize>> for String<N>
{
type Output = str;
fn index(&self, index: RangeToInclusive<usize>) -> &Self::Output
{
&self.as_str()[index]
}
}
// Implement core::fmt::Write so it can be used with `write!` macros.
impl<const N: usize> fmt::Write for String<N>
{
fn write_str(&mut self, s: &str) -> fmt::Result
{
self.push_str(s);
Ok(())
}
fn write_char(&mut self, c: char) -> fmt::Result
{
if self.push(c)
{
Ok(())
}
else
{
Err(fmt::Error)
}
}
}

166
src/switcher.rs Normal file
View File

@ -0,0 +1,166 @@
use crate::app::{App, AppId};
use crate::badge::Badge;
use crate::bounded::BoundedU8;
use crate::channel::{ButtonReceiver, FrameSender};
use crate::image::MicrobitImage;
use crate::menu::Menu;
use crate::renderer::Renderer;
use crate::snake::Game;
pub struct Switcher
{
menu: Menu<3, 2>,
snake: Game,
badge: Badge
}
impl Switcher
{
pub fn new(frame_output: FrameSender, button_input: ButtonReceiver) -> Self
{
let mut menu_app = Menu::new(frame_output, button_input);
menu_app.add(AppId::Snake.into(),
[[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0)],
[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(0)],
[BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(0)]].into());
menu_app.add(AppId::Badge.into(),
[[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(9),
BoundedU8::new(0)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0)],
[BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0)],
[BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(0)]].into());
menu_app.add(AppId::Nfc.into(),
[[BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0)],
[BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9)],
[BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0)],
[BoundedU8::new(0),
BoundedU8::new(0),
BoundedU8::new(9),
BoundedU8::new(0),
BoundedU8::new(0)]].into());
let snake_app = Game::new(frame_output, button_input);
let badge_app = Badge::new(frame_output, button_input);
Self { menu: menu_app,
snake: snake_app,
badge: badge_app }
}
pub async fn run(&mut self)
{
// Default to the menu.
let mut curr_app: AppId = AppId::Menu;
loop
{
match curr_app
{
AppId::Menu =>
{
match self.menu.run().await
{
Ok(next_app) =>
{
curr_app = next_app;
}
Err(_error) =>
{ /* TODO: Log the error. */ }
}
}
AppId::Snake =>
{
match self.snake.run().await
{
Ok(next_app) =>
{
curr_app = next_app;
}
Err(error) =>
{ /* TODO: Log the error. */ }
}
}
AppId::Badge =>
{
match self.badge.run().await
{
Ok(next_app) =>
{
curr_app = next_app;
}
Err(error) =>
{ /* TODO: Log the error. */ }
}
}
AppId::Nfc =>
{
curr_app = AppId::Menu;
}
}
}
}
}