diff --git a/Cargo.toml b/Cargo.toml index bfd3fa3..7aa10cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,9 @@ resolver = "2" -members = [ "cardputer-bsc", +members = [ + "cardputer-bsc", + "snake", "test-app", ] @@ -11,4 +13,4 @@ opt-level = "s" [profile.dev] debug = true # Symbols are nice and they don't increase the size on Flash -opt-level = "z" \ No newline at end of file +opt-level = "z" diff --git a/snake/Cargo.toml b/snake/Cargo.toml new file mode 100644 index 0000000..cbf0095 --- /dev/null +++ b/snake/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "snake" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "snake" +harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors + +[features] +default = [] + +experimental = ["esp-idf-svc/experimental"] + +[dependencies] +log = "0.4" +esp-idf-svc = { version = "0.51", features = ["critical-section", "embassy-time-driver", "embassy-sync"] } +cardputer-bsc = { version = "0.1.0", path = "../cardputer-bsc" } +embedded-graphics-core = "0.4.0" +anyhow = "1.0.98" + +[build-dependencies] +embuild = "0.33" diff --git a/snake/build.rs b/snake/build.rs new file mode 100644 index 0000000..112ec3f --- /dev/null +++ b/snake/build.rs @@ -0,0 +1,3 @@ +fn main() { + embuild::espidf::sysenv::output(); +} diff --git a/snake/src/main.rs b/snake/src/main.rs new file mode 100644 index 0000000..9e20f13 --- /dev/null +++ b/snake/src/main.rs @@ -0,0 +1,193 @@ +use std::{collections::VecDeque, ops::Rem, thread::sleep, time::Duration}; + +use anyhow::Context; +use cardputer_bsc::display::{DISPLAY_SIZE_HEIGHT, DISPLAY_SIZE_WIDTH, Drawable}; +use embedded_graphics_core::{pixelcolor::Rgb565, prelude::RgbColor}; +use esp_idf_svc::{ + hal::{ + ledc::{self, LedcDriver, LedcTimerDriver, SpeedMode, config::TimerConfig}, + prelude::Peripherals, + rmt::config::DutyPercent, + units::Hertz, + }, + sys::esp_random, +}; + +const GRID_SQUARE_SIZE: u16 = 15; +const GRID_WIDTH: i32 = 16; +const GRID_HEIGHT: i32 = 9; + +fn main() -> Result<(), anyhow::Error> { + // It is necessary to call this function once. Otherwise some patches to the runtime + // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 + esp_idf_svc::sys::link_patches(); + + // Bind the log crate to the ESP Logging facilities + esp_idf_svc::log::EspLogger::initialize_default(); + + log::info!("Hello, world!"); + + let peripherals = Peripherals::take()?; + + let mut display = cardputer_bsc::display::build( + peripherals.spi2, + peripherals.pins.gpio36, + peripherals.pins.gpio35, + peripherals.pins.gpio37, + peripherals.pins.gpio34, + peripherals.pins.gpio33, + ) + .context("Failed to initialise display")?; + + let timer_config = TimerConfig::new() + .resolution(ledc::Resolution::Bits8) + .frequency(Hertz(256).into()); + let bl_timer_driver = LedcTimerDriver::new(peripherals.ledc.timer3, &timer_config) + .context("Failed to create backlight timer")?; + let mut bl_driver = LedcDriver::new( + peripherals.ledc.channel7, + bl_timer_driver, + peripherals.pins.gpio38, + ) + .context("Failed to create backlight driver")?; + + dbg!(display.is_sleeping()); + + clear_screen(&mut display, Rgb565::BLACK)?; + bl_driver + .set_duty(bl_driver.get_max_duty()) + .context("Failed to set duty")?; + bl_driver.enable().context("Failed to enable driver")?; + + let start_position = (GRID_WIDTH / 2, GRID_HEIGHT / 2); + let mut direction = (1, 0); + let mut snake_tiles = VecDeque::new(); + snake_tiles.push_back(start_position); + snake_tiles.push_back((start_position.0 - 1, start_position.1)); + snake_tiles.push_back((start_position.0 - 2, start_position.1)); + snake_tiles.push_back((start_position.0 - 3, start_position.1)); + + let mut keyboard = cardputer_bsc::keyboard::Keyboard::new( + peripherals.pins.gpio8, + peripherals.pins.gpio9, + peripherals.pins.gpio11, + peripherals.pins.gpio13, + peripherals.pins.gpio15, + peripherals.pins.gpio3, + peripherals.pins.gpio4, + peripherals.pins.gpio5, + peripherals.pins.gpio6, + peripherals.pins.gpio7, + )?; + + let mut apple = random_tile(); + + loop { + let pressed_keys = keyboard.scan_pressed_keys()?; + dbg!(&pressed_keys); + for key in pressed_keys { + let new_direction = match key { + cardputer_bsc::keyboard::KeyImprint::SemiColon => (0, -1), + cardputer_bsc::keyboard::KeyImprint::Comma => (-1, 0), + cardputer_bsc::keyboard::KeyImprint::Period => (0, 1), + cardputer_bsc::keyboard::KeyImprint::Slash => (1, 0), + _ => continue, + }; + match (snake_tiles.get(0), snake_tiles.get(1)) { + (Some(&first), Some(&second)) => { + let disallowed_direction = ( + (second.0 - first.0).rem(GRID_WIDTH), + (second.1 - first.1).rem(GRID_HEIGHT), + ); + dbg!(disallowed_direction); + if disallowed_direction != new_direction { + direction = new_direction; + } + } + _ => { + direction = new_direction; + } + } + } + + sleep(Duration::from_millis(200)); + + let front_file = *snake_tiles.get(0).unwrap(); + + let new_tile = ( + (front_file.0 + direction.0).rem_euclid(GRID_WIDTH), + (front_file.1 + direction.1).rem_euclid(GRID_HEIGHT), + ); + + if snake_tiles.iter().any(|&tile| tile == new_tile) { + for x in 0..GRID_WIDTH { + for y in 0..GRID_HEIGHT { + draw_tile(&mut display, x, y, Rgb565::new(31, 63, 31))?; + sleep(Duration::from_millis(20)); + } + } + sleep(Duration::from_secs(1)); + clear_screen(&mut display, Rgb565::BLACK)?; + snake_tiles.clear(); + direction = (1, 0); + snake_tiles.push_back(start_position); + snake_tiles.push_back((start_position.0 - 1, start_position.1)); + snake_tiles.push_back((start_position.0 - 2, start_position.1)); + snake_tiles.push_back((start_position.0 - 3, start_position.1)); + apple = random_tile(); + continue; + } + + snake_tiles.push_front(new_tile); + + let old_tile = if new_tile == apple { + apple = random_tile(); + None + } else { + snake_tiles.pop_back() + }; + if let Some(old_tile) = old_tile { + draw_tile(&mut display, old_tile.0, old_tile.1, Rgb565::BLACK)?; + } + for &tile in &snake_tiles { + draw_tile(&mut display, tile.0, tile.1, Rgb565::RED)?; + } + draw_tile(&mut display, apple.0, apple.1, Rgb565::GREEN)?; + } +} + +fn random_tile() -> (i32, i32) { + ( + unsafe { esp_random() as i32 }.rem_euclid(GRID_WIDTH), + unsafe { esp_random() as i32 }.rem_euclid(GRID_HEIGHT), + ) +} + +fn clear_screen(display: &mut Drawable, colour: Rgb565) -> Result<(), anyhow::Error> { + display + .set_pixels( + 0, + 0, + DISPLAY_SIZE_WIDTH - 1, + DISPLAY_SIZE_HEIGHT - 1, + std::iter::repeat_n( + colour, + DISPLAY_SIZE_WIDTH as usize * DISPLAY_SIZE_HEIGHT as usize, + ), + ) + .map_err(|e| anyhow::anyhow!("{e:?}"))?; + Ok(()) +} + +fn draw_tile(display: &mut Drawable, x: i32, y: i32, colour: Rgb565) -> Result<(), anyhow::Error> { + display + .set_pixels( + x as u16 * GRID_SQUARE_SIZE, + y as u16 * GRID_SQUARE_SIZE, + (x + 1) as u16 * GRID_SQUARE_SIZE - 1, + (y + 1) as u16 * GRID_SQUARE_SIZE - 1, + std::iter::repeat_n(colour, (GRID_SQUARE_SIZE as usize).pow(2)), + ) + .map_err(|e| anyhow::anyhow!("{e:?}"))?; + Ok(()) +}