diff --git a/Cargo.toml b/Cargo.toml index c0a5b26..065b8e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,8 @@ ili9341 = "0.5.0" display-interface-spi = "0.4.1" models = { path = "models" } graphics = { path = "graphics" } +crossbeam-channel = "0.5.13" +jpeg-decoder = "0.3.1" [build-dependencies] embuild = "0.31.3" diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index c93dec7..8e57acc 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -6,10 +6,18 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -esp-idf-hal = "=0.43.1" embedded-graphics = "0.7.1" embedded-graphics-core = "0.4.0" image = "0.25.1" -display-interface = "0.4.1" +embedded-canvas = "0.2.0" +embedded-layout = "0.2.0" ili9341 = "0.5.0" -display-interface-spi = "0.4.1" \ No newline at end of file +display-interface-spi = "0.4.1" +# embedded-graphics-core = "0.4.0" +# image = "0.25.1" +# embedded-canvas = "0.3.1" +# embedded-layout = "0.4.1" +# bytes = "1.6.0" +# embedded-graphics = "0.7.1" +# display-interface = "0.4.1" +# ili9341 = "0.5.0" diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 2a44fb1..f8e53f1 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -1,14 +1,21 @@ -use display_interface_spi::SPIInterfaceNoCS; +use std::io::Cursor; + +use embedded_canvas::CanvasAt; use embedded_graphics::{ draw_target::DrawTarget, - geometry::{Dimensions, Point}, - pixelcolor::{PixelColor, Rgb565}, - primitives::{Primitive, PrimitiveStyle, Rectangle, StyledDrawable}, + geometry::{Dimensions, Point, Size}, + image::{ImageDrawable, ImageRaw}, + mono_font::{ascii::FONT_10X20, MonoTextStyle}, + pixelcolor::{Rgb565, RgbColor}, + primitives::{PointsIter, PrimitiveStyle, Rectangle, StyledDrawable}, + text::Text, + Drawable, }; -use esp_idf_hal::gpio::{Gpio16, Output, OutputPin, PinDriver}; -use ili9341::Ili9341; +use embedded_layout::{layout::linear::LinearLayout, prelude::*}; +use ili9341::DisplayError; +use image::{io::Reader as ImageReader, DynamicImage, RgbImage}; -fn rgb888_to_rgb565(r: u8, g: u8, b: u8) -> u16 { +pub fn rgb888_to_rgb565(r: u8, g: u8, b: u8) -> u16 { let red = (r >> 3) as u16; let green = (g >> 2) as u16; let blue = (b >> 3) as u16; @@ -35,9 +42,109 @@ pub fn fill_display(display: &mut T, color: Rgb565) where T: DrawTarget, { - let display_area = Rectangle::new(Point::new(0, 0), display.bounding_box().size); + let display_area = Rectangle::new( + embedded_graphics::geometry::Point::new(0, 0), + display.bounding_box().size, + ); let fill_style = PrimitiveStyle::with_fill(color); display_area.draw_styled(&fill_style, display); -} \ No newline at end of file +} + +fn draw_canvas(display: &mut T, canvas: CanvasAt, background: Rgb565) +where + T: DrawTarget, +{ + display.fill_contiguous( + &canvas.bounding_box(), + canvas + .bounding_box() + .points() + .map(|point| canvas.get_pixel(point).unwrap_or(background)), + ); +} + +pub fn draw_album_cover(display: &mut T, image_bytes: Vec) +where + T: DrawTarget, +{ + let mut album_canvas = CanvasAt::new(Point::zero(), Size::new(240, 240)); + + let img = ImageReader::new(Cursor::new(image_bytes)) + .with_guessed_format() + .expect("Failed to guess image format") + .decode() + .expect("Failed to decode image"); + + let rgb_imag: RgbImage = img.into_rgb8(); + let resized_image = DynamicImage::ImageRgb8(rgb_imag) + .resize(240, 240, image::imageops::FilterType::Nearest) + .to_rgb8(); + + let raw = resized_image.clone().into_raw(); + let rgb565: Vec = convert_vec_rgb888_to_rgb565(&raw); + + let out: ImageRaw = ImageRaw::new(&rgb565, resized_image.width()); + out.draw(&mut album_canvas).expect("Could not draw image"); + + draw_canvas(display, album_canvas, Rgb565::BLACK); +} + +pub fn draw_title_and_artist(display: &mut T, title: String, artist: String) +where + T: DrawTarget, +{ + let mut displayed_title = title.clone(); + if displayed_title.len() > 23 { + displayed_title = title[..22].to_string(); + displayed_title.push_str("..") + // displayed_title = &title[..22].to_owned() + "..".to_string(); + } + + let mut displayed_artist = artist.clone(); + if displayed_artist.len() > 23 { + displayed_artist = artist[..22].to_string(); + displayed_artist.push_str(".."); + } + + let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); + + let mut combined_canvas = CanvasAt::::new(Point::new(0, 240), Size::new(240, 50)); + LinearLayout::vertical( + embedded_layout::object_chain::Chain::new(Text::new( + &displayed_title, + Point::zero(), + text_style, + )) + .append(Text::new(&displayed_artist, Point::zero(), text_style)), + ) + .with_alignment(horizontal::Left) + .arrange() + .align_to( + &combined_canvas.bounding_box(), + horizontal::Left, + vertical::Center, + ) + .draw(&mut combined_canvas) + .unwrap(); + + draw_canvas(display, combined_canvas, Rgb565::BLACK); +} + +pub fn something(display: &mut T) +where + T: DrawTarget, +{ + // Create a new character style + let style = MonoTextStyle::new(&FONT_10X20, Rgb565::RED); + + // Create a text at position (20, 30) and draw it using the previously defined style + Text::with_alignment( + "First line\nSecond line", + Point::new(20, 30), + style, + embedded_graphics::text::Alignment::Center, + ) + .draw(display); +} diff --git a/models/src/lib.rs b/models/src/lib.rs index c1d37ba..a7013c6 100644 --- a/models/src/lib.rs +++ b/models/src/lib.rs @@ -3,27 +3,27 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Artist { - name: String, - url: Option, + pub name: String, + pub url: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Track { - name: String, - artists: Vec, - image_url: Option, - url: Option, - duration: u32, + pub name: String, + pub artists: Vec, + pub image_url: Option, + pub url: Option, + pub duration: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CurrentlyPlaying { - device: Device, - track: Track, - progress_secs: u32, - shuffled: bool, - playing: bool, - repeat_status: RepeatState, + pub device: Device, + pub track: Track, + pub progress_secs: u32, + pub shuffled: bool, + pub playing: bool, + pub repeat_status: RepeatState, } // Holds the structs from the `rspotify` package. It's easier to just copy the structs because it diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 9ea5d73..62da83e 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -1,6 +1,8 @@ # Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000 - +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_80M=y # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). # This allows to use 1 ms granuality for thread sleeps (10 ms by default). #CONFIG_FREERTOS_HZ=1000 diff --git a/src/main.rs b/src/main.rs index fa666b3..59a27d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,12 @@ use display_interface_spi::SPIInterfaceNoCS; -// use display_interface_spi::SPIInterfaceNoCS; use dotenv::dotenv; use embedded_graphics::pixelcolor::{Rgb565, RgbColor}; -use embedded_hal::delay::DelayNs; -use embedded_svc::http::{client::Client, Headers, Status}; +use embedded_svc::http::{client::Client, Headers}; use esp_idf_hal::{ - delay::{Delay, FreeRtos}, - gpio::{AnyIOPin, IOPin, PinDriver}, + delay::Delay, + gpio::{IOPin, PinDriver}, io::Read, - peripherals::{self, Peripherals}, + peripherals::Peripherals, spi::{self, SpiDeviceDriver, SpiDriver, SpiDriverConfig, SPI2}, sys::{esp_crt_bundle_attach, esp_get_free_heap_size, esp_get_minimum_free_heap_size}, }; @@ -22,9 +20,9 @@ use ili9341::Ili9341; use log::{error, info}; use models::CurrentlyPlaying; use once_cell::sync::Lazy; -use serde_json::json; use std::{ - thread::{self, sleep}, + sync::{Arc, Mutex}, + thread::{self}, time::Duration, }; @@ -34,6 +32,11 @@ enum ButtonStatus { High, } +enum Signal { + ChangeSong(Option), + UpdateProgress(Option), +} + fn main() { esp_idf_svc::sys::link_patches(); esp_idf_svc::log::EspLogger::initialize_default(); @@ -65,23 +68,28 @@ fn main() { let rst = PinDriver::output(pins.gpio16).unwrap(); let spi = peripherals.spi2; - let driver = - SpiDriver::new::(spi, sclk, mosi, Some(miso), &SpiDriverConfig::new()).unwrap(); - let config = spi::config::Config::default().baudrate(esp_idf_hal::units::Hertz(10 * 1_000_000)); + let spidispplayinterface = { + let driver = + SpiDriver::new::(spi, sclk, mosi, Some(miso), &SpiDriverConfig::new()).unwrap(); + let config = + spi::config::Config::default().baudrate(esp_idf_hal::units::Hertz(10 * 1_000_000)); - let spi_device = SpiDeviceDriver::new(&driver, Some(pins.gpio17), &config).unwrap(); + let spi_device = SpiDeviceDriver::new(driver, Some(pins.gpio17), &config).unwrap(); - let spidispplayinterface = SPIInterfaceNoCS::new(spi_device, dc); + SPIInterfaceNoCS::new(spi_device, dc) + }; let mut display = Ili9341::new( spidispplayinterface, rst, &mut esp_idf_hal::delay::FreeRtos, - ili9341::Orientation::Landscape, + ili9341::Orientation::Portrait, ili9341::DisplaySize240x320, ) .expect("Failed to initialize LCD ILI9341."); + graphics::fill_display(&mut display, Rgb565::BLACK); + let mut wifi_driver = BlockingWifi::wrap( EspWifi::new(peripherals.modem, sys_loop.clone(), Some(nvs)).unwrap(), sys_loop, @@ -104,9 +112,17 @@ fn main() { .to_string() }); + // Using crossbeam channel instead of normal channels because it avoids blocking indefinitely + // and keeps watchdog happy :) + let (transmitter, receiver) = crossbeam_channel::unbounded(); + + // Used instead of song name because two songs can have the same name, but a url acts like an id + let stored_song_url: Arc>> = Arc::new(Mutex::new(None)); + let stored_song_position: Arc>> = Arc::new(Mutex::new(None)); + let check_playback = thread::Builder::new() .stack_size(64 * 1024) - .spawn(|| { + .spawn(move || { loop { let httpconnection = EspHttpConnection::new(&HttpConfig { // use_global_ca_store: true, @@ -125,12 +141,10 @@ fn main() { let mut response = request.submit().expect("could not get response"); let mut playing_buf = vec![0u8; response.content_len().unwrap() as usize]; - let mut image_buf = vec![0u8; 300 * 300]; + // let mut image_buf = vec![0u8; 300 * 300]; response.read_exact(&mut playing_buf).unwrap(); - let response_str = std::str::from_utf8(&playing_buf); - let playing_json: Result, serde_json::Error> = serde_json::from_slice(&playing_buf); @@ -139,56 +153,118 @@ fn main() { continue; } - // println!("{:?}", response_str); - // println!("{:#?}", playing_json); + let mut prev_song_url = stored_song_url.lock().unwrap(); + let mut prev_song_position = stored_song_position.lock().unwrap(); + + let playing_data = match playing_json { + Ok(Some(data)) => data, + _ => { + if prev_song_url.is_some() || prev_song_position.is_some() { + *prev_song_url = None; + *prev_song_position = None; + transmitter.send(Signal::ChangeSong(None)); + transmitter.send(Signal::UpdateProgress(None)); + } + Delay::new_default().delay_ms(5000); + continue; + } + }; + + if prev_song_url.is_none() + || (prev_song_url.is_some() + && *prev_song_url != Some(playing_data.track.name.clone())) + { + *prev_song_url = Some(playing_data.track.name.clone()); + transmitter.send(Signal::ChangeSong(Some(playing_data.clone()))); + } + + if prev_song_position.is_none() + || (prev_song_position.is_some() + && *prev_song_position != Some(playing_data.progress_secs)) + { + *prev_song_position = Some(playing_data.progress_secs); + transmitter.send(Signal::UpdateProgress(Some(playing_data.progress_secs))); + } Delay::new_default().delay_ms(1000); } }) .unwrap(); - loop { - if btn_pin1.is_high() && btn1_status == ButtonStatus::Low { - info!("Button 1 Pressed - Attempting to skip track"); - // Using a button lock to make sure register one button input at a time - btn_lock = false; - btn1_status = ButtonStatus::High; - - if !previous_track(&*API_URL_ROOT, &*AUTH_TOKEN) { - error!("could not go to previous track"); + let control_playback_thread = thread::Builder::new() + .stack_size(8 * 1024) + .spawn(move || loop { + if btn_pin1.is_high() && btn1_status == ButtonStatus::Low { + info!("Button 1 Pressed - Attempting to skip track"); + // Using a button lock to make sure register one button input at a time + btn_lock = false; + btn1_status = ButtonStatus::High; + + if !previous_track(&*API_URL_ROOT, &*AUTH_TOKEN) { + error!("could not go to previous track"); + } + } else if btn_pin1.is_low() && !btn_lock { + btn_lock = true; + btn1_status = ButtonStatus::Low; } - } else if btn_pin1.is_low() && !btn_lock { - btn_lock = true; - btn1_status = ButtonStatus::Low; - } - if btn_pin2.is_high() && btn2_status == ButtonStatus::Low { - info!("Button 2 Pressed - Attempting to toggle playback"); - btn_lock = false; - btn2_status = ButtonStatus::High; + if btn_pin2.is_high() && btn2_status == ButtonStatus::Low { + info!("Button 2 Pressed - Attempting to toggle playback"); + btn_lock = false; + btn2_status = ButtonStatus::High; - if !toggle_playback(&*API_URL_ROOT, &*AUTH_TOKEN) { - error!("could not toggle playback"); + if !toggle_playback(&*API_URL_ROOT, &*AUTH_TOKEN) { + error!("could not toggle playback"); + } + } else if btn_pin2.is_low() && !btn_lock { + btn_lock = true; + btn2_status = ButtonStatus::Low; } - } else if btn_pin2.is_low() && !btn_lock { - btn_lock = true; - btn2_status = ButtonStatus::Low; - } - if btn_pin3.is_high() && btn3_status == ButtonStatus::Low { - info!("Button 3 Pressed - Attempting to skip track"); - btn_lock = false; - btn3_status = ButtonStatus::High; + if btn_pin3.is_high() && btn3_status == ButtonStatus::Low { + info!("Button 3 Pressed - Attempting to skip track"); + btn_lock = false; + btn3_status = ButtonStatus::High; + + if !skip_track(&*API_URL_ROOT, &*AUTH_TOKEN) { + error!("could not go to next track"); + } + } else if btn_pin3.is_low() && !btn_lock { + btn_lock = true; + btn3_status = ButtonStatus::Low; + } - if !skip_track(&*API_URL_ROOT, &*AUTH_TOKEN) { - error!("could not go to next track"); + Delay::new_default().delay_ms(100); + }) + .unwrap(); + + loop { + match receiver.recv() { + Ok(received_signal) => match received_signal { + Signal::ChangeSong(optional_currently_playing) => { + match optional_currently_playing { + Some(currently_playing) => { + println!("Attempting to draw image "); + graphics::draw_title_and_artist( + &mut display, + currently_playing.track.name, + currently_playing.track.artists[0].name.clone(), + ); + } + None => { + println!("Nothing"); + } + } + } + Signal::UpdateProgress(optional_position) => {} + }, + Err(_) => { + error!("Error receiving signal"); + continue; } - } else if btn_pin3.is_low() && !btn_lock { - btn_lock = true; - btn3_status = ButtonStatus::Low; } - print_memory_info(); + // print_memory_info(); thread::sleep(Duration::from_millis(100)); }