Last active
February 14, 2026 07:59
-
-
Save Aketzu/9512287f0e19c74320b4220237e45e9b to your computer and use it in GitHub Desktop.
Disobey 2026 badge app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Convert PNG to raw RGB565 data for embedding in Rust code. | |
| Usage: python3 convert_png.py input.png output.bin | |
| Created with GitHub Copilot | |
| """ | |
| import sys | |
| from PIL import Image | |
| import struct | |
| def rgb888_to_rgb565(r, g, b): | |
| """Convert RGB888 to RGB565""" | |
| # Scale down to 5-6-5 bits | |
| r5 = (r * 31) // 255 | |
| g6 = (g * 63) // 255 | |
| b5 = (b * 31) // 255 | |
| # Pack into 16-bit value (big endian) | |
| rgb565 = (r5 << 11) | (g6 << 5) | b5 | |
| return rgb565 | |
| def convert_png_to_rgb565(input_path, output_path): | |
| """Convert PNG to raw RGB565 binary data""" | |
| try: | |
| # Open and convert to RGB | |
| img = Image.open(input_path).convert("RGB") | |
| width, height = img.size | |
| print(f"Converting {input_path}: {width}x{height}") | |
| # Convert each pixel to RGB565 | |
| rgb565_data = [] | |
| for y in range(height): | |
| for x in range(width): | |
| r, g, b = img.getpixel((x, y)) | |
| rgb565 = rgb888_to_rgb565(r, g, b) | |
| rgb565_data.append(rgb565) | |
| # Write binary data (little endian for ESP32) | |
| with open(output_path, "wb") as f: | |
| for pixel in rgb565_data: | |
| f.write(struct.pack("<HHH", width, height, pixel)) # Little endian 16-bit | |
| print( | |
| f"Wrote {len(rgb565_data)} pixels ({4+len(rgb565_data) * 2} bytes) to {output_path}" | |
| ) | |
| print(f"Add this to your Rust code:") | |
| print(f'const EMBEDDED_IMAGE_DATA: &[u8] = include_bytes!("{output_path}");') | |
| print(f"const EMBEDDED_IMAGE: EmbeddedImage = EmbeddedImage::new(EMBEDDED_IMAGE_DATA);") | |
| except Exception as e: | |
| print(f"Error: {e}") | |
| return False | |
| return True | |
| if __name__ == "__main__": | |
| if len(sys.argv) != 3: | |
| print("Usage: python3 convert_png.py input.png output.bin") | |
| sys.exit(1) | |
| input_file = sys.argv[1] | |
| output_file = sys.argv[2] | |
| if convert_png_to_rgb565(input_file, output_file): | |
| print("Conversion successful!") | |
| else: | |
| print("Conversion failed!") | |
| sys.exit(1) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //! Side-scrolling space shooter using ST7789 hardware vertical scrolling. | |
| //! | |
| //! The ST7789's VSCRDEF/VSCRSADD commands scroll the background starfield | |
| //! horizontally (the native 320px axis becomes X with Deg90 rotation). | |
| //! Fixed scroll regions create HUD strips on the left and right screen edges. | |
| //! | |
| //! Only the newly-revealed background column and dirty sprite regions are | |
| //! redrawn each frame — no full framebuffer needed. | |
| //! | |
| //! Controls: | |
| //! - D-pad Up/Down: move ship | |
| //! - A: fire | |
| //! - Start: restart after game over | |
| #![no_std] | |
| #![no_main] | |
| use core::sync::atomic::{ | |
| AtomicBool, | |
| Ordering, | |
| }; | |
| use defmt::info; | |
| #[allow(clippy::wildcard_imports)] | |
| use disobey2026badge::*; | |
| use embassy_executor::Spawner; | |
| use embassy_sync::channel::Channel; | |
| use embassy_time::{ | |
| Duration, | |
| Instant, | |
| Timer, | |
| }; | |
| use embedded_graphics::{ | |
| mono_font::{ | |
| MonoTextStyle, | |
| ascii::{ | |
| FONT_4X6, | |
| FONT_6X10, | |
| }, | |
| iso_8859_1::FONT_10X20, | |
| }, | |
| pixelcolor::Rgb565, | |
| prelude::*, | |
| primitives::{ | |
| PrimitiveStyle, | |
| Rectangle, | |
| }, | |
| text::Text, | |
| }; | |
| use esp_backtrace as _; | |
| use esp_hal::timer::timg::TimerGroup; | |
| use esp_println as _; | |
| use palette::Srgb; | |
| extern crate alloc; | |
| esp_bootloader_esp_idf::esp_app_desc!(); | |
| // ── Display geometry ──────────────────────────────────────────────────────── | |
| const SCREEN_W: i32 = 320; | |
| const SCREEN_H: i32 = 170; | |
| // HUD strips in the scroll axis (native rows). | |
| const HUD_RIGHT: u16 = 24; | |
| const HUD_LEFT: u16 = 24; | |
| const SCROLL_AREA: u16 = 320 - HUD_RIGHT - HUD_LEFT; // = 272 | |
| const GAME_X: i32 = HUD_LEFT as i32; | |
| const GAME_W: i32 = SCROLL_AREA as i32; | |
| const GAME_H: i32 = SCREEN_H; | |
| // ── Tuning ────────────────────────────────────────────────────────────────── | |
| const TICK_MS: u64 = 16; | |
| const SCROLL_SPEED: u16 = 1; | |
| const PLAYER_SPEED: i32 = 2; | |
| const BULLET_SPEED: i32 = 3; | |
| const ENEMY_SPEED: i32 = 1; | |
| const MAX_BULLETS: usize = 12; | |
| const MAX_ENEMIES: usize = 8; | |
| const ENEMY_HP: u8 = 3; | |
| const FIRE_COOLDOWN: u8 = 12; | |
| // ── Input atomics ─────────────────────────────────────────────────────────── | |
| static INPUT_UP: AtomicBool = AtomicBool::new(false); | |
| static INPUT_DOWN: AtomicBool = AtomicBool::new(false); | |
| static INPUT_CHANGE: AtomicBool = AtomicBool::new(false); | |
| static INPUT_FIRE: AtomicBool = AtomicBool::new(false); | |
| static INPUT_START: AtomicBool = AtomicBool::new(false); | |
| // ── Simple RNG ────────────────────────────────────────────────────────────── | |
| struct Rng(u32); | |
| impl Rng { | |
| const fn new(seed: u32) -> Self { | |
| Self(seed) | |
| } | |
| fn next(&mut self) -> u32 { | |
| self.0 ^= self.0 << 13; | |
| self.0 ^= self.0 >> 17; | |
| self.0 ^= self.0 << 5; | |
| self.0 | |
| } | |
| fn range(&mut self, max: u32) -> u32 { | |
| self.next() % max | |
| } | |
| } | |
| // ── Sine table for fire shader (fixed-point, 0..1023 → -120..120) ────────── | |
| const SIN_Q: [i16; 65] = [ | |
| 0, 3, 6, 9, 12, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 51, 54, 57, 60, 62, 65, 67, 70, | |
| 72, 75, 77, 79, 81, 84, 86, 88, 90, 92, 93, 95, 97, 99, 100, 102, 103, 105, 106, 107, 108, 110, | |
| 111, 112, 113, 114, 114, 115, 116, 117, 117, 118, 118, 119, 119, 119, 120, 120, 120, 120, | |
| ]; | |
| fn isin(angle: i32) -> i32 { | |
| let a = ((angle % 1024) + 1024) as u32 % 1024; | |
| let quadrant = a / 256; | |
| let idx = (a % 256) as usize; | |
| let i = idx * 64 / 256; | |
| match quadrant { | |
| 0 => SIN_Q[i] as i32, | |
| 1 => SIN_Q[64 - i] as i32, | |
| 2 => -(SIN_Q[i] as i32), | |
| _ => -(SIN_Q[64 - i] as i32), | |
| } | |
| } | |
| fn icos(angle: i32) -> i32 { | |
| isin(angle + 256) | |
| } | |
| /// Space nebula shader — slowly cycles through nebula hues as world_x advances. | |
| fn fire_bg(x: i32, y: i32, frame: i32) -> Rgb565 { | |
| let fy = GAME_H as i32 - 1 - y; | |
| let n1 = isin(x * 7 + fy * 3 - frame * 8); | |
| let n2 = icos(x * 3 + fy * 9 - frame * 12); | |
| let n3 = isin((x + fy) * 5 - frame * 6); | |
| let heat = (n1 + n2 + n3 + 360) * fy / (GAME_H as i32 * 2); | |
| let heat = heat.clamp(0, 180) as u32; | |
| // Contrast curve: h² / 180 maps 0..180 → 0..180 with darker darks | |
| let h = (heat * heat / 180) as u32; | |
| // Slowly rotating hue based on world x position (full cycle ~2048 px) | |
| let phase = x / 3 + 512; // start in blue range | |
| let wr = (isin(phase) + 120) as u32; // 0..240 | |
| let wg = (isin(phase + 341) + 120) as u32; // 120° offset | |
| let wb = (isin(phase + 682) + 120) as u32; // 240° offset | |
| let r = (h * wr / (240 * 2)).min(31) as u8; | |
| let g = (h * wg / (240 * 1)).min(63) as u8; | |
| let b = (h * wb / (240 * 1)).min(31) as u8; | |
| Rgb565::new(r, g, b) | |
| } | |
| // Embedded image functions provided by Github Copilot | |
| // 1. Convert PNG to raw RGB565 data using: python3 convert_png.py input.png output.bin | |
| // 2. Include the binary data using: include_bytes!("output.bin") | |
| // 3. Wrap in EmbeddedImage struct with width/height metadata | |
| // 4. Access RGB565 pixel data using as_rgb565() method | |
| // 5. Draw to display using fill_contiguous() with RGB565 iterator | |
| // Embed the converted RGB565 binary data directly into the binary | |
| const EMBEDDED_IMAGE_DATA_SKROLLI: &[u8] = include_bytes!("skrolli.bin"); | |
| const EMBEDDED_IMAGE_DATA_IKI: &[u8] = include_bytes!("iki.bin"); | |
| // Structure to hold image data and metadata | |
| #[derive(Debug)] | |
| pub struct EmbeddedImage { | |
| pub data: &'static [u8], | |
| pub width: i32, | |
| pub height: i32, | |
| } | |
| impl EmbeddedImage { | |
| pub const fn new(data: &'static [u8]) -> Self { | |
| let wh = unsafe { core::slice::from_raw_parts(self.data.as_ptr() as *const u16, 2) }; | |
| assert_eq!(data.len()-4, wh[0] * wh[1] * 2); | |
| Self { | |
| data, | |
| wh[0], | |
| wh[1], | |
| } | |
| } | |
| /// Get pixel data as RGB565 values (slice of u16) | |
| pub fn as_rgb565(&self) -> &[u16] { | |
| // Safety: We know the data is properly aligned RGB565 data | |
| unsafe { | |
| core::slice::from_raw_parts(self.data.as_ptr().offset(4) as *const u16, (self.data.len()-4) / 2) | |
| } | |
| } | |
| } | |
| // Create a constant that holds the embedded image with actual dimensions from skrolli.png | |
| const EMBEDDED_IMAGE_SKROLLI: EmbeddedImage = | |
| EmbeddedImage::new(EMBEDDED_IMAGE_DATA_SKROLLI, 692, 170); | |
| const EMBEDDED_IMAGE_IKI: EmbeddedImage = EmbeddedImage::new(EMBEDDED_IMAGE_DATA_IKI, 409, 170); | |
| /// Get the background color at specific x,y coordinates from the embedded image | |
| /// Returns Rgb565 color for the pixel at position (x, y) | |
| /// Returns black if coordinates are out of bounds | |
| fn embed_bg(x: i32, y: i32, frame: i32, image_id: u8) -> Rgb565 { | |
| //fn embed_bg(x: u32, y: u32) -> Rgb565 { | |
| // select image_data, width and height from EMBEDDED_IMAGE_SKROLLI or EMBEDDED_IMAGE_IKI based on image_id | |
| let embedded_image = match image_id { | |
| 0 => &EMBEDDED_IMAGE_SKROLLI, | |
| 1 => &EMBEDDED_IMAGE_IKI, | |
| _ => &EMBEDDED_IMAGE_SKROLLI, // default to skrolli if invalid id | |
| }; | |
| let image_data = embedded_image.as_rgb565(); | |
| let width = embedded_image.width; | |
| let height = embedded_image.height; | |
| let x = x % width; | |
| // Check bounds | |
| if x >= width || y >= height { | |
| return Rgb565::BLACK; | |
| } | |
| // Calculate pixel index (row-major order) | |
| let index = (y * width + x) as usize; | |
| // Get the RGB565 value and convert to Rgb565 object | |
| if let Some(&pixel) = image_data.get(index) { | |
| // pixel is = (r5 << 11) | (g6 << 5) | b5 | |
| Rgb565::new( | |
| ((pixel >> 11) & 0x1F) as u8, // Extract red (5 bits) | |
| ((pixel >> 5) & 0x3F) as u8, // Extract green (6 bits) | |
| (pixel & 0x1F) as u8, // Extract blue (5 bits) | |
| ) | |
| } else { | |
| Rgb565::BLACK | |
| } | |
| } | |
| fn bg_image(x: i32, y: i32, frame: i32) -> Rgb565 { | |
| // return fire_bg(x, y, frame); | |
| return embed_bg(x, y, frame, 1); | |
| } | |
| // ── Weapon configurations ─────────────────────────────────────────────────── | |
| #[derive(Clone, Copy)] | |
| struct WeaponConfig { | |
| count: u8, | |
| offsets: [i32; 4], | |
| color: Rgb565, | |
| damage: u8, | |
| name: &'static [u8], | |
| } | |
| const WEAPON_SINGLE: WeaponConfig = WeaponConfig { | |
| count: 1, | |
| offsets: [0, 0, 0, 0], | |
| color: Rgb565::YELLOW, | |
| damage: 1, | |
| name: b"SNGL", | |
| }; | |
| const WEAPON_DOUBLE: WeaponConfig = WeaponConfig { | |
| count: 2, | |
| offsets: [-4, 4, 0, 0], | |
| color: Rgb565::CYAN, | |
| damage: 1, | |
| name: b"DUAL", | |
| }; | |
| const WEAPON_SPREAD: WeaponConfig = WeaponConfig { | |
| count: 3, | |
| offsets: [-6, 0, 6, 0], | |
| color: Rgb565::CSS_ORANGE, | |
| damage: 1, | |
| name: b"SPRD", | |
| }; | |
| const WEAPONS: &[WeaponConfig] = &[WEAPON_SINGLE, WEAPON_DOUBLE, WEAPON_SPREAD]; | |
| // ── Entity types ──────────────────────────────────────────────────────────── | |
| #[derive(Clone, Copy)] | |
| struct Bullet { | |
| x: i32, | |
| y: i32, | |
| alive: bool, | |
| damage: u8, | |
| color: Rgb565, | |
| } | |
| impl Bullet { | |
| const DEAD: Self = Self { | |
| x: 0, | |
| y: 0, | |
| alive: false, | |
| damage: 0, | |
| color: Rgb565::BLACK, | |
| }; | |
| } | |
| #[derive(Clone, Copy)] | |
| struct Enemy { | |
| x: i32, | |
| y: i32, | |
| hp: u8, | |
| alive: bool, | |
| } | |
| impl Enemy { | |
| const DEAD: Self = Self { | |
| x: 0, | |
| y: 0, | |
| hp: 0, | |
| alive: false, | |
| }; | |
| const W: i32 = 12; | |
| const H: i32 = 11; | |
| } | |
| struct Player { | |
| y: i32, | |
| weapon_idx: usize, | |
| fire_cooldown: u8, | |
| automove_cooldown: u16, | |
| } | |
| impl Player { | |
| const X: i32 = GAME_X + 20; | |
| const W: i32 = 14; | |
| const H: i32 = 10; | |
| fn new() -> Self { | |
| Self { | |
| y: GAME_H / 2, | |
| weapon_idx: 0, | |
| fire_cooldown: 0, | |
| automove_cooldown: 500, | |
| } | |
| } | |
| fn weapon(&self) -> &'static WeaponConfig { | |
| &WEAPONS[self.weapon_idx] | |
| } | |
| fn cycle_weapon(&mut self) { | |
| self.weapon_idx = (self.weapon_idx + 1) % WEAPONS.len(); | |
| } | |
| } | |
| // ── Game state ────────────────────────────────────────────────────────────── | |
| struct Game { | |
| player: Player, | |
| bullets: [Bullet; MAX_BULLETS], | |
| enemies: [Enemy; MAX_ENEMIES], | |
| score: u32, | |
| tick: u32, | |
| scroll_offset: u16, | |
| alive: bool, | |
| rng: Rng, | |
| enemy_spawn_timer: u8, | |
| } | |
| impl Game { | |
| fn new() -> Self { | |
| Self { | |
| player: Player::new(), | |
| bullets: [Bullet::DEAD; MAX_BULLETS], | |
| enemies: [Enemy::DEAD; MAX_ENEMIES], | |
| score: 0, | |
| tick: 0, | |
| scroll_offset: 0, | |
| alive: true, | |
| rng: Rng::new(0xDEAD_BEEF), | |
| enemy_spawn_timer: 0, | |
| } | |
| } | |
| fn update(&mut self) { | |
| self.tick += 1; | |
| if INPUT_CHANGE.load(Ordering::Relaxed) { | |
| // self.bg_image_sel += 1; | |
| } | |
| let mut nearest_enemy_x = i32::MAX; | |
| let mut nearest_enemy_y = 0; | |
| for e in &self.enemies { | |
| if !e.alive { | |
| continue; | |
| } | |
| if e.x < nearest_enemy_x { | |
| nearest_enemy_x = e.x; | |
| nearest_enemy_y = e.y; | |
| } | |
| } | |
| if self.player.automove_cooldown > 0 { | |
| self.player.automove_cooldown -= 1; | |
| } | |
| if self.player.automove_cooldown == 0 && self.player.y - 2 < nearest_enemy_y { | |
| self.player.y = (self.player.y + PLAYER_SPEED).min(GAME_H - Player::H / 2 - 1); | |
| } | |
| if self.player.automove_cooldown == 0 && self.player.y - 2 > nearest_enemy_y { | |
| self.player.y = (self.player.y - PLAYER_SPEED).max(Player::H / 2); | |
| } | |
| if INPUT_UP.load(Ordering::Relaxed) { | |
| self.player.y = (self.player.y - PLAYER_SPEED).max(Player::H / 2); | |
| self.player.automove_cooldown = 200; | |
| } | |
| if INPUT_DOWN.load(Ordering::Relaxed) { | |
| self.player.y = (self.player.y + PLAYER_SPEED).min(GAME_H - Player::H / 2 - 1); | |
| self.player.automove_cooldown = 200; | |
| } | |
| let px = Player::X; | |
| let py = self.player.y - Player::H / 2; | |
| let mut autoshooter = false; | |
| for e in &self.enemies { | |
| if !e.alive { | |
| continue; | |
| } | |
| if self.player.automove_cooldown == 0 && e.y < py + Player::H && e.y + Enemy::H > py { | |
| autoshooter = true; | |
| break; | |
| } | |
| } | |
| if self.player.fire_cooldown > 0 { | |
| self.player.fire_cooldown -= 1; | |
| } | |
| if (autoshooter || INPUT_FIRE.load(Ordering::Relaxed)) && self.player.fire_cooldown == 0 { | |
| let w = self.player.weapon(); | |
| for i in 0..w.count as usize { | |
| if let Some(slot) = self.bullets.iter_mut().find(|b| !b.alive) { | |
| *slot = Bullet { | |
| x: Player::X + Player::W, | |
| y: self.player.y + w.offsets[i], | |
| alive: true, | |
| damage: w.damage, | |
| color: w.color, | |
| }; | |
| } | |
| } | |
| self.player.fire_cooldown = FIRE_COOLDOWN; | |
| } | |
| for b in &mut self.bullets { | |
| if b.alive { | |
| b.x += BULLET_SPEED; | |
| if b.x > GAME_X + GAME_W { | |
| b.alive = false; | |
| } | |
| } | |
| } | |
| if self.enemy_spawn_timer == 0 { | |
| let interval = 60u8.saturating_sub((self.score / 5) as u8).max(20); | |
| self.enemy_spawn_timer = interval; | |
| if let Some(slot) = self.enemies.iter_mut().find(|e| !e.alive) { | |
| let y = (self.rng.range((GAME_H - Enemy::H) as u32) as i32).max(0); | |
| *slot = Enemy { | |
| x: GAME_X + GAME_W, | |
| y, | |
| hp: ENEMY_HP, | |
| alive: true, | |
| }; | |
| } | |
| } else { | |
| self.enemy_spawn_timer -= 1; | |
| } | |
| for e in &mut self.enemies { | |
| if e.alive { | |
| e.x -= ENEMY_SPEED; | |
| if e.x + Enemy::W < GAME_X { | |
| e.alive = false; | |
| } | |
| } | |
| } | |
| for b in &mut self.bullets { | |
| if !b.alive { | |
| continue; | |
| } | |
| for e in &mut self.enemies { | |
| if !e.alive { | |
| continue; | |
| } | |
| if b.x >= e.x && b.x <= e.x + Enemy::W && b.y >= e.y && b.y <= e.y + Enemy::H { | |
| b.alive = false; | |
| if e.hp <= b.damage { | |
| e.alive = false; | |
| self.score += 1; | |
| LED_CHANNEL.try_send(LedEvent::EnemyKill).ok(); | |
| } else { | |
| e.hp -= b.damage; | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| let px = Player::X; | |
| let py = self.player.y - Player::H / 2; | |
| for e in &self.enemies { | |
| if !e.alive { | |
| continue; | |
| } | |
| // AutoSkrolli is invulnerable | |
| if self.player.automove_cooldown == 0 { | |
| break; | |
| } | |
| if e.x < px + Player::W | |
| && e.x + Enemy::W > px | |
| && e.y < py + Player::H | |
| && e.y + Enemy::H > py | |
| { | |
| self.alive = false; | |
| break; | |
| } | |
| } | |
| self.scroll_offset = (self.scroll_offset + SCROLL_SPEED) % SCROLL_AREA; | |
| } | |
| } | |
| // ── Rendering helpers ─────────────────────────────────────────────────────── | |
| /// Per-column background metadata so we can regenerate fire pixels under sprites. | |
| /// Index = framebuffer column offset (0..GAME_W), stores (world_x, bg_frame). | |
| struct BgMap { | |
| wx: [i32; SCROLL_AREA as usize], | |
| frame: [i32; SCROLL_AREA as usize], | |
| } | |
| impl BgMap { | |
| fn new() -> Self { | |
| Self { | |
| wx: [0; SCROLL_AREA as usize], | |
| frame: [0; SCROLL_AREA as usize], | |
| } | |
| } | |
| /// Record that framebuffer column `fb_x` was drawn with these params. | |
| fn set(&mut self, fb_x: i32, world_x: i32, bg_frame: i32) { | |
| let idx = (fb_x - GAME_X) as usize; | |
| if idx < SCROLL_AREA as usize { | |
| self.wx[idx] = world_x; | |
| self.frame[idx] = bg_frame; | |
| } | |
| } | |
| /// Get (world_x, bg_frame) for a framebuffer column. | |
| fn get(&self, fb_x: i32) -> (i32, i32) { | |
| let idx = (fb_x - GAME_X) as usize; | |
| (self.wx[idx], self.frame[idx]) | |
| } | |
| } | |
| /// Convert a screen-space X in the game area to the framebuffer X that the | |
| /// hardware scroll will display at that position. | |
| fn screen_to_fb_x(sx: i32, scroll_offset: u16) -> i32 { | |
| if sx < GAME_X || sx >= GAME_X + GAME_W { | |
| return sx; | |
| } | |
| let local = sx - GAME_X; | |
| GAME_X + (local + scroll_offset as i32) % GAME_W | |
| } | |
| /// Draw a filled rectangle at raw framebuffer coordinates (no scroll compensation). | |
| fn draw_rect_fb(display: &mut Display, x: i32, y: i32, w: i32, h: i32, color: Rgb565) { | |
| if w <= 0 || h <= 0 { | |
| return; | |
| } | |
| let x0 = x.max(0); | |
| let y0 = y.max(0); | |
| let x1 = (x + w).min(SCREEN_W); | |
| let y1 = (y + h).min(SCREEN_H); | |
| let cw = (x1 - x0) as u32; | |
| let ch = (y1 - y0) as u32; | |
| if cw == 0 || ch == 0 { | |
| return; | |
| } | |
| Rectangle::new(Point::new(x0, y0), Size::new(cw, ch)) | |
| .into_styled(PrimitiveStyle::with_fill(color)) | |
| .draw(display) | |
| .unwrap(); | |
| } | |
| /// Draw a filled rectangle in screen-space, compensating for hardware scroll. | |
| /// Clamps to the game area — nothing is ever drawn into the HUD regions. | |
| fn draw_rect_scr(display: &mut Display, x: i32, y: i32, w: i32, h: i32, color: Rgb565, so: u16) { | |
| if w <= 0 || h <= 0 { | |
| return; | |
| } | |
| let x0 = x.max(GAME_X); | |
| let x1 = (x + w).min(GAME_X + GAME_W); | |
| if x0 >= x1 { | |
| return; | |
| } | |
| let fb_start = screen_to_fb_x(x0, so); | |
| let fb_end = screen_to_fb_x(x1 - 1, so); | |
| if fb_start <= fb_end { | |
| draw_rect_fb(display, fb_start, y, fb_end - fb_start + 1, h, color); | |
| } else { | |
| draw_rect_fb(display, fb_start, y, GAME_X + GAME_W - fb_start, h, color); | |
| draw_rect_fb(display, GAME_X, y, fb_end - GAME_X + 1, h, color); | |
| } | |
| } | |
| fn draw_player(display: &mut Display, py: i32, color: Rgb565, so: u16) { | |
| let x = Player::X; | |
| let y = py - Player::H / 2; | |
| // Fuselage | |
| draw_rect_scr(display, x + 2, y + 2, 10, 6, color, so); | |
| // Nose | |
| draw_rect_scr(display, x + 12, y + 3, 2, 4, color, so); | |
| // Wings | |
| draw_rect_scr(display, x, y, 4, 2, color, so); | |
| draw_rect_scr(display, x, y + Player::H - 2, 4, 2, color, so); | |
| } | |
| fn erase_player(display: &mut Display, py: i32, so: u16, bg: &BgMap) { | |
| let x = Player::X; | |
| let y = py - Player::H / 2; | |
| restore_fire_rect(display, x, y, Player::W, Player::H, so, bg); | |
| } | |
| fn draw_enemy(display: &mut Display, e: &Enemy, color: Rgb565, so: u16) { | |
| // Standard enemy | |
| // xxxxxxxx | |
| // xxxxxxxx | |
| // zzxxxxxxxxzz | |
| // zzxxxxxxxxzz | |
| // zzxxxxxxxxzz | |
| // zzxxxxxxxxzz | |
| // xxxxxxxx | |
| // xxxxxxxx | |
| /* | |
| draw_rect_scr(display, e.x + 2, e.y + 1, 8, 8, color, so); | |
| draw_rect_scr(display, e.x, e.y + 3, 2, 4, color, so); | |
| draw_rect_scr(display, e.x + 10, e.y + 3, 2, 4, color, so); | |
| if color != Rgb565::BLACK { | |
| let eye = if e.hp <= 1 { Rgb565::RED } else { Rgb565::WHITE }; | |
| draw_rect_scr(display, e.x + 4, e.y + 3, 2, 2, eye, so); | |
| draw_rect_scr(display, e.x + 7, e.y + 3, 2, 2, eye, so); | |
| } | |
| */ | |
| // Skrolli trolli | |
| // 12345678901 | |
| // 1 x x | |
| // 2 xx xx | |
| // 3 xxxxxxxxx | |
| // 4 xxx xxx xxx | |
| // 5 x x x | |
| // 6 xxxxxxxxx | |
| // 7 xxxxxxxxxxx | |
| // 8 xxxxxxx | |
| // 9 xxxxxxxxx | |
| // 0 xxxxxxxxxxx | |
| draw_rect_scr(display, e.x + 4, e.y + 1, 1, 1, color, so); | |
| draw_rect_scr(display, e.x + 8, e.y + 1, 1, 1, color, so); | |
| draw_rect_scr(display, e.x + 3, e.y + 2, 2, 1, color, so); | |
| draw_rect_scr(display, e.x + 8, e.y + 2, 2, 1, color, so); | |
| draw_rect_scr(display, e.x + 2, e.y + 3, 9, 1, color, so); | |
| draw_rect_scr(display, e.x + 1, e.y + 4, 3, 1, color, so); | |
| draw_rect_scr(display, e.x + 5, e.y + 4, 3, 1, color, so); | |
| draw_rect_scr(display, e.x + 9, e.y + 4, 3, 1, color, so); | |
| draw_rect_scr(display, e.x + 3, e.y + 5, 1, 1, color, so); | |
| draw_rect_scr(display, e.x + 6, e.y + 5, 1, 1, color, so); | |
| draw_rect_scr(display, e.x + 9, e.y + 5, 1, 1, color, so); | |
| draw_rect_scr(display, e.x + 2, e.y + 6, 9, 1, color, so); | |
| draw_rect_scr(display, e.x + 1, e.y + 7, 11, 1, color, so); | |
| draw_rect_scr(display, e.x + 3, e.y + 8, 7, 1, color, so); | |
| draw_rect_scr(display, e.x + 2, e.y + 9, 9, 1, color, so); | |
| draw_rect_scr(display, e.x + 1, e.y + 10, 11, 1, color, so); | |
| // eyes | |
| if color != Rgb565::BLACK { | |
| let eye = if e.hp <= 1 { | |
| Rgb565::RED | |
| } else { | |
| Rgb565::BLACK | |
| }; | |
| draw_rect_scr(display, e.x + 4, e.y + 4, 1, 1, eye, so); | |
| draw_rect_scr(display, e.x + 8, e.y + 4, 1, 1, eye, so); | |
| draw_rect_scr(display, e.x + 4, e.y + 5, 2, 1, eye, so); | |
| draw_rect_scr(display, e.x + 7, e.y + 5, 2, 1, eye, so); | |
| } | |
| } | |
| fn erase_enemy(display: &mut Display, e: &Enemy, so: u16, bg: &BgMap) { | |
| restore_fire_rect(display, e.x, e.y, Enemy::W, Enemy::H, so, bg); | |
| } | |
| fn draw_bullet(display: &mut Display, b: &Bullet, color: Rgb565, so: u16) { | |
| draw_rect_scr(display, b.x, b.y, 3, 2, color, so); | |
| } | |
| fn erase_bullet(display: &mut Display, b: &Bullet, so: u16, bg: &BgMap) { | |
| restore_fire_rect(display, b.x, b.y, 3, 2, so, bg); | |
| } | |
| /// Paint a fire-shader column into the framebuffer at raw FB coordinate fb_x. | |
| fn draw_fire_column(display: &mut Display, fb_x: i32, world_x: i32, frame: i32, bg: &mut BgMap) { | |
| let w = SCROLL_SPEED as u32; | |
| let area = Rectangle::new(Point::new(fb_x, 0), Size::new(w, GAME_H as u32)); | |
| let pixels = (0..GAME_H as i32).flat_map(|y| { | |
| let c = bg_image(world_x, y, frame); | |
| core::iter::repeat_n(c, w as usize) | |
| }); | |
| display.fill_contiguous(&area, pixels).unwrap(); | |
| for dx in 0..w as i32 { | |
| bg.set(fb_x + dx, world_x + dx, frame); | |
| } | |
| } | |
| /// Fill the entire scrollable background with the fire shader at the given frame. | |
| fn fill_fire_background(display: &mut Display, frame: i32, bg: &mut BgMap) { | |
| let w = SCROLL_SPEED as usize; | |
| for col in (GAME_X..(GAME_X + GAME_W)).step_by(w) { | |
| let wx = col - GAME_X; | |
| let area = Rectangle::new(Point::new(col, 0), Size::new(w as u32, GAME_H as u32)); | |
| let pixels = (0..GAME_H as i32).flat_map(move |y| { | |
| let c = bg_image(wx, y, frame); | |
| core::iter::repeat_n(c, w) | |
| }); | |
| display.fill_contiguous(&area, pixels).unwrap(); | |
| for dx in 0..w as i32 { | |
| bg.set(col + dx, wx + dx, frame); | |
| } | |
| } | |
| } | |
| /// Restore fire background for a screen-space rectangle (erase a sprite). | |
| /// Each column is regenerated from the BgMap metadata. | |
| fn restore_fire_rect(display: &mut Display, x: i32, y: i32, w: i32, h: i32, so: u16, bg: &BgMap) { | |
| if w <= 0 || h <= 0 { | |
| return; | |
| } | |
| let x0 = x.max(GAME_X); | |
| let x1 = (x + w).min(GAME_X + GAME_W); | |
| let y0 = y.max(0); | |
| let y1 = (y + h).min(GAME_H); | |
| if x0 >= x1 || y0 >= y1 { | |
| return; | |
| } | |
| // Check if the rect wraps around the scroll boundary | |
| let fb_start = screen_to_fb_x(x0, so); | |
| let fb_end = screen_to_fb_x(x1 - 1, so); | |
| if fb_start <= fb_end { | |
| // Contiguous in framebuffer — single fill_contiguous call | |
| let fw = (fb_end - fb_start + 1) as u32; | |
| let fh = (y1 - y0) as u32; | |
| let area = Rectangle::new(Point::new(fb_start, y0), Size::new(fw, fh)); | |
| let pixels = (y0..y1).flat_map(|py| { | |
| (fb_start..=fb_end).map(move |fb_x| { | |
| let (wx, frame) = bg.get(fb_x); | |
| bg_image(wx, py, frame) | |
| }) | |
| }); | |
| display.fill_contiguous(&area, pixels).unwrap(); | |
| } else { | |
| // Wraps — two contiguous fills (right part + left part) | |
| // Right part: fb_start .. GAME_X + GAME_W | |
| let rw = (GAME_X + GAME_W - fb_start) as u32; | |
| let fh = (y1 - y0) as u32; | |
| let area_r = Rectangle::new(Point::new(fb_start, y0), Size::new(rw, fh)); | |
| let pixels_r = (y0..y1).flat_map(|py| { | |
| (fb_start..GAME_X + GAME_W).map(move |fb_x| { | |
| let (wx, frame) = bg.get(fb_x); | |
| bg_image(wx, py, frame) | |
| }) | |
| }); | |
| display.fill_contiguous(&area_r, pixels_r).unwrap(); | |
| // Left part: GAME_X .. fb_end + 1 | |
| let lw = (fb_end - GAME_X + 1) as u32; | |
| let area_l = Rectangle::new(Point::new(GAME_X, y0), Size::new(lw, fh)); | |
| let pixels_l = (y0..y1).flat_map(|py| { | |
| (GAME_X..=fb_end).map(move |fb_x| { | |
| let (wx, frame) = bg.get(fb_x); | |
| bg_image(wx, py, frame) | |
| }) | |
| }); | |
| display.fill_contiguous(&area_l, pixels_l).unwrap(); | |
| } | |
| } | |
| fn format_u32(mut n: u32, buf: &mut [u8; 16]) -> &str { | |
| if n == 0 { | |
| buf[0] = b'0'; | |
| return unsafe { core::str::from_utf8_unchecked(&buf[..1]) }; | |
| } | |
| let mut i = 0; | |
| while n > 0 { | |
| buf[i] = b'0' + (n % 10) as u8; | |
| n /= 10; | |
| i += 1; | |
| } | |
| buf[..i].reverse(); | |
| unsafe { core::str::from_utf8_unchecked(&buf[..i]) } | |
| } | |
| /// Right-side HUD (score). Fixed region — no scroll compensation. | |
| fn draw_hud_score(display: &mut Display, score: u32) { | |
| let hx = SCREEN_W - HUD_RIGHT as i32; | |
| draw_rect_fb( | |
| display, | |
| hx, | |
| 0, | |
| HUD_RIGHT as i32, | |
| SCREEN_H, | |
| Rgb565::new(0, 0, 4), | |
| ); | |
| draw_rect_fb(display, hx, 0, 1, SCREEN_H, Rgb565::new(2, 6, 12)); | |
| let lx = hx + 4; | |
| // "S" label | |
| draw_rect_fb(display, lx, 4, 5, 1, Rgb565::CSS_LIGHT_GRAY); | |
| draw_rect_fb(display, lx, 5, 1, 2, Rgb565::CSS_LIGHT_GRAY); | |
| draw_rect_fb(display, lx, 7, 5, 1, Rgb565::CSS_LIGHT_GRAY); | |
| draw_rect_fb(display, lx + 4, 8, 1, 2, Rgb565::CSS_LIGHT_GRAY); | |
| draw_rect_fb(display, lx, 10, 5, 1, Rgb565::CSS_LIGHT_GRAY); | |
| // Digits | |
| let mut buf = [0u8; 16]; | |
| let s = format_u32(score, &mut buf); | |
| for (i, ch) in s.bytes().enumerate() { | |
| let d = ch - b'0'; | |
| let dy = 18 + i as i32 * 14; | |
| let b = 8 + d * 2; | |
| draw_rect_fb(display, lx, dy, 10, 10, Rgb565::new(b, b * 2, b)); | |
| draw_rect_fb(display, lx + 2, dy + 2, 6, 6, Rgb565::new(0, 0, 4)); | |
| draw_rect_fb(display, lx + 3, dy + 3, 4, 4, Rgb565::new(b / 2, b, b)); | |
| } | |
| } | |
| /// Left-side HUD (weapon info). Fixed region — no scroll compensation. | |
| fn draw_hud_weapon(display: &mut Display, weapon: &WeaponConfig) { | |
| draw_rect_fb( | |
| display, | |
| 0, | |
| 0, | |
| HUD_LEFT as i32, | |
| SCREEN_H, | |
| Rgb565::new(0, 0, 4), | |
| ); | |
| draw_rect_fb( | |
| display, | |
| HUD_LEFT as i32 - 1, | |
| 0, | |
| 1, | |
| SCREEN_H, | |
| Rgb565::new(2, 6, 12), | |
| ); | |
| let lx = 4; | |
| for i in 0..weapon.count as i32 { | |
| let dy = 8 + i * 14; | |
| draw_rect_fb(display, lx, dy, 14, 8, weapon.color); | |
| draw_rect_fb(display, lx + 2, dy + 2, 8, 4, Rgb565::BLACK); | |
| draw_rect_fb(display, lx + 3, dy + 3, 6, 2, weapon.color); | |
| } | |
| let ny = SCREEN_H - 50; | |
| let style = MonoTextStyle::new(&FONT_6X10, weapon.color); | |
| for (i, &ch) in weapon.name.iter().enumerate() { | |
| let dy = ny + i as i32 * 12; | |
| let hud_bg = Rgb565::new(0, 0, 4); | |
| draw_rect_fb(display, lx, dy, 14, 12, hud_bg); | |
| let buf = [ch]; | |
| let s = unsafe { core::str::from_utf8_unchecked(&buf) }; | |
| Text::new(s, Point::new(lx + 4, dy + 9), style) | |
| .draw(display) | |
| .unwrap(); | |
| } | |
| } | |
| /// Draw FPS counter in the right HUD (score side), at the bottom. | |
| fn draw_hud_fps(display: &mut Display, fps: u32, delay_ms: u32) { | |
| let fps = fps.min(99); | |
| let color = if fps >= 25 { | |
| Rgb565::CSS_LIME_GREEN | |
| } else { | |
| Rgb565::RED | |
| }; | |
| let hud_bg = Rgb565::new(0, 0, 4); | |
| let hx = SCREEN_W - HUD_RIGHT as i32; | |
| let style = MonoTextStyle::new(&FONT_4X6, color); | |
| // Frame delay (ms idle at end of frame) | |
| let dy_delay = SCREEN_H - 16; | |
| draw_rect_fb(display, hx + 2, dy_delay, 20, 8, hud_bg); | |
| let mut buf2 = [0u8; 16]; | |
| let ds = format_u32(delay_ms.min(99), &mut buf2); | |
| Text::new(ds, Point::new(hx + 4, dy_delay + 5), style) | |
| .draw(display) | |
| .unwrap(); | |
| // FPS | |
| let dy = SCREEN_H - 8; | |
| draw_rect_fb(display, hx + 2, dy, 20, 8, hud_bg); | |
| let mut buf = [0u8; 16]; | |
| let s = format_u32(fps, &mut buf); | |
| Text::new(s, Point::new(hx + 4, dy + 5), style) | |
| .draw(display) | |
| .unwrap(); | |
| } | |
| // ── LED signalling ────────────────────────────────────────────────────────── | |
| #[derive(Clone, Copy)] | |
| enum LedEvent { | |
| /// Enemy destroyed — white flash | |
| EnemyKill, | |
| /// Score changed — update bar (carries score) | |
| Score(u32), | |
| /// Game over — red flash then idle | |
| GameOver, | |
| } | |
| static LED_CHANNEL: Channel< | |
| embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, | |
| LedEvent, | |
| 4, | |
| > = Channel::new(); | |
| // ── Tasks ─────────────────────────────────────────────────────────────────── | |
| #[embassy_executor::task] | |
| async fn led_task(leds: &'static mut Leds<'static>) { | |
| info!("LED task started"); | |
| loop { | |
| let event = LED_CHANNEL.receive().await; | |
| match event { | |
| LedEvent::EnemyKill => { | |
| // Bright flash fading to black over ~160ms | |
| for i in (0..=8).rev() { | |
| let brightness = i * 3; // MAX flashbang: * 30 | |
| leds.fill(Srgb::new(brightness, brightness, brightness)); | |
| leds.update().await; | |
| Timer::after(Duration::from_millis(20)).await; | |
| } | |
| } | |
| LedEvent::Score(score) => { | |
| let lit = ((score as usize).min(BAR_COUNT * 5)) / 5; | |
| let mut bar = [Srgb::new(0u8, 0, 0); BAR_COUNT]; | |
| for i in 0..lit.min(BAR_COUNT) { | |
| bar[i] = Srgb::new(0, 4, 2); | |
| } | |
| leds.set_both_bars(&bar); | |
| leds.update().await; | |
| } | |
| LedEvent::GameOver => { | |
| for _ in 0..3 { | |
| leds.fill(Srgb::new(20, 0, 0)); | |
| leds.update().await; | |
| Timer::after(Duration::from_millis(300)).await; | |
| leds.clear(); | |
| leds.update().await; | |
| Timer::after(Duration::from_millis(300)).await; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| #[embassy_executor::task] | |
| async fn input_task(buttons: &'static mut Buttons) { | |
| info!("Input task started"); | |
| loop { | |
| INPUT_UP.store(buttons.up.is_low(), Ordering::Relaxed); | |
| INPUT_DOWN.store(buttons.down.is_low(), Ordering::Relaxed); | |
| INPUT_FIRE.store(buttons.a.is_low(), Ordering::Relaxed); | |
| INPUT_CHANGE.store(buttons.b.is_low(), Ordering::Relaxed); | |
| INPUT_START.store(buttons.start.is_low(), Ordering::Relaxed); | |
| Timer::after(Duration::from_millis(10)).await; | |
| } | |
| } | |
| #[embassy_executor::task] | |
| async fn game_task(display: &'static mut Display<'static>, backlight: &'static mut Backlight) { | |
| backlight.on(); | |
| info!("Space shooter started"); | |
| loop { | |
| display | |
| .set_vertical_scroll_region(HUD_RIGHT, HUD_LEFT) | |
| .unwrap(); | |
| let mut game = Game::new(); | |
| let mut bg_frame: i32 = 0; | |
| let mut bg = BgMap::new(); | |
| let mut world_x: i32 = GAME_W as i32; | |
| fill_fire_background(display, bg_frame, &mut bg); | |
| draw_hud_score(display, 0); | |
| draw_hud_weapon(display, game.player.weapon()); | |
| let mut prev_player_y = game.player.y; | |
| let mut prev_weapon_idx = game.player.weapon_idx; | |
| let mut prev_score = game.score; | |
| let mut prev_scroll = game.scroll_offset; | |
| let mut prev_bullets = [Bullet::DEAD; MAX_BULLETS]; | |
| let mut prev_enemies = [Enemy::DEAD; MAX_ENEMIES]; | |
| let tick = Duration::from_millis(TICK_MS); | |
| let mut next_frame = Instant::now() + tick; | |
| let mut fps_accum: u32 = 0; | |
| let mut fps_timer = Instant::now(); | |
| while game.alive { | |
| let so_old = prev_scroll; | |
| // Advance game state | |
| if game.tick % 200 == 0 { | |
| game.player.cycle_weapon(); | |
| } | |
| game.update(); | |
| // Update scroll and paint new background column first | |
| display | |
| .set_vertical_scroll_offset(HUD_RIGHT + game.scroll_offset) | |
| .unwrap(); | |
| let so = game.scroll_offset; | |
| bg_frame += 1; | |
| let fb_col = GAME_X + ((so as i32 + GAME_W - SCROLL_SPEED as i32) % GAME_W); | |
| draw_fire_column(display, fb_col, world_x, bg_frame, &mut bg); | |
| world_x += SCROLL_SPEED as i32; | |
| // Erase old bullets (they move in FB space) | |
| for b in &prev_bullets { | |
| if b.alive { | |
| erase_bullet(display, b, so_old, &bg); | |
| } | |
| } | |
| // Erase enemies that just died | |
| for (pe, ne) in prev_enemies.iter().zip(game.enemies.iter()) { | |
| if pe.alive && !ne.alive { | |
| erase_enemy(display, pe, so_old, &bg); | |
| } | |
| } | |
| // Player always needs erase+redraw (fixed screen X, moves in FB space with scroll) | |
| erase_player(display, prev_player_y, so_old, &bg); | |
| draw_player(display, game.player.y, Rgb565::CSS_LIME_GREEN, so); | |
| for b in &game.bullets { | |
| if b.alive { | |
| draw_bullet(display, b, b.color, so); | |
| } | |
| } | |
| // Enemies are stationary in FB space (ENEMY_SPEED == SCROLL_SPEED), | |
| // so just overdraw them — no erase needed, no blink. | |
| for e in &game.enemies { | |
| if e.alive { | |
| draw_enemy(display, e, Rgb565::CSS_CHARTREUSE, so); | |
| } | |
| } | |
| if game.score != prev_score { | |
| draw_hud_score(display, game.score); | |
| LED_CHANNEL.try_send(LedEvent::Score(game.score)).ok(); | |
| prev_score = game.score; | |
| } | |
| if game.player.weapon_idx != prev_weapon_idx { | |
| draw_hud_weapon(display, game.player.weapon()); | |
| prev_weapon_idx = game.player.weapon_idx; | |
| } | |
| prev_player_y = game.player.y; | |
| prev_bullets = game.bullets; | |
| prev_enemies = game.enemies; | |
| prev_scroll = so; | |
| // FPS counter | |
| fps_accum += 1; | |
| let now = Instant::now(); | |
| let delay_ms = if next_frame > now { | |
| (next_frame - now).as_millis() as u32 | |
| } else { | |
| 0 | |
| }; | |
| if now.duration_since(fps_timer).as_millis() >= 1000 { | |
| let fps = fps_accum; | |
| fps_accum = 0; | |
| fps_timer = now; | |
| draw_hud_fps(display, fps, delay_ms); | |
| } | |
| Timer::at(next_frame).await; | |
| next_frame += tick; | |
| } | |
| // Game over — reset scroll so text renders at correct screen positions | |
| info!("Game over! Score: {}", game.score); | |
| display.set_vertical_scroll_offset(HUD_RIGHT).unwrap(); | |
| draw_rect_fb(display, GAME_X, 0, GAME_W, GAME_H, Rgb565::BLACK); | |
| // Box | |
| draw_rect_fb(display, GAME_X + 50, 40, 172, 90, Rgb565::new(12, 0, 0)); | |
| draw_rect_fb(display, GAME_X + 52, 42, 168, 86, Rgb565::new(4, 0, 0)); | |
| let style = MonoTextStyle::new(&FONT_10X20, Rgb565::RED); | |
| Text::new("GAME OVER", Point::new(GAME_X + 86, 75), style) | |
| .draw(display) | |
| .unwrap(); | |
| let score_style = MonoTextStyle::new(&FONT_10X20, Rgb565::CSS_ORANGE); | |
| let mut buf = [0u8; 16]; | |
| let s = format_u32(game.score, &mut buf); | |
| // Center the score: each char is 10px wide | |
| let sx = GAME_X + 136 - (s.len() as i32 * 10) / 2; | |
| Text::new("Score:", Point::new(GAME_X + 76, 105), score_style) | |
| .draw(display) | |
| .unwrap(); | |
| Text::new(s, Point::new(sx + 70, 105), score_style) | |
| .draw(display) | |
| .unwrap(); | |
| LED_CHANNEL.try_send(LedEvent::GameOver).ok(); | |
| loop { | |
| if INPUT_START.load(Ordering::Relaxed) { | |
| break; | |
| } | |
| Timer::after(Duration::from_millis(50)).await; | |
| } | |
| Timer::after(Duration::from_millis(200)).await; | |
| } | |
| } | |
| #[esp_rtos::main] | |
| async fn main(spawner: Spawner) -> ! { | |
| let peripherals = disobey2026badge::init(); | |
| let resources = split_resources!(peripherals); | |
| esp_alloc::heap_allocator!(size: 64 * 1024); | |
| let timg0 = TimerGroup::new(peripherals.TIMG0); | |
| esp_rtos::start(timg0.timer0); | |
| let buttons = mk_static!(Buttons, resources.buttons.into()); | |
| let display = mk_static!(Display<'static>, resources.display.into()); | |
| let backlight = mk_static!(Backlight, resources.backlight.into()); | |
| let leds = mk_static!(Leds<'static>, resources.leds.into()); | |
| spawner.must_spawn(input_task(buttons)); | |
| spawner.must_spawn(led_task(leds)); | |
| spawner.must_spawn(game_task(display, backlight)); | |
| loop { | |
| Timer::after(Duration::from_secs(600)).await; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment