diff --git a/Cargo.lock b/Cargo.lock index a70d839..1e2a5e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,7 +76,7 @@ checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" [[package]] name = "b1display" -version = "0.1.3" +version = "0.1.4" dependencies = [ "cortex-m", "cortex-m-rt", @@ -144,7 +144,7 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "c1minimal" -version = "0.1.3" +version = "0.1.4" dependencies = [ "cortex-m", "cortex-m-rt", @@ -705,7 +705,7 @@ dependencies = [ [[package]] name = "inputmodule-control" -version = "0.1.3" +version = "0.1.4" dependencies = [ "chrono", "clap", @@ -780,7 +780,7 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "ledmatrix" -version = "0.1.3" +version = "0.1.4" dependencies = [ "cortex-m", "cortex-m-rt", @@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "lotus-inputmodules" -version = "0.1.3" +version = "0.1.4" dependencies = [ "cortex-m", "cortex-m-rt", @@ -1476,7 +1476,7 @@ dependencies = [ [[package]] name = "st7306-lcd" version = "0.8.2" -source = "git+ssh://git@github.com/FrameworkComputer/st7306.git#f953feca1623a13346d9b21940acb2a4aaeb2d87" +source = "git+ssh://git@github.com/FrameworkComputer/st7306.git#b5148a927071421562eafa4a19c75d3849b11daf" dependencies = [ "embedded-graphics", "embedded-hal", diff --git a/README.md b/README.md index 228e5f0..d5ec1d1 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,10 @@ Features that all modules share To build your own application see the: [API command documentation](commands.md) -Or use our `inputmodule-control` app. Optionally there are is also a -[Python script](python.md). +Or use our `inputmodule-control` app, which you can download from the latest +[GH Actions](https://github.com/FrameworkComputer/led_matrix_fw/actions) run or +the [release page](https://github.com/FrameworkComputer/led_matrix_fw/releases). +Optionally there are is also a [Python script](python.md). For device specific commands, see their individual documentation pages. diff --git a/b1display/Cargo.toml b/b1display/Cargo.toml index a8c18db..f4c1871 100644 --- a/b1display/Cargo.toml +++ b/b1display/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "b1display" -version = "0.1.3" +version = "0.1.4" [dependencies] cortex-m = "0.7" diff --git a/b1display/README.md b/b1display/README.md index 5249d22..98ddbf3 100644 --- a/b1display/README.md +++ b/b1display/README.md @@ -1 +1,105 @@ -## B1 Display +# B1 Display + +A transmissive, mono-color (black/white) screen that's 300x400px in size. +It's 4.2 inches in size and mounted in portrait orientation. +Because it's optimized for power, the recommended framerate is 1 FPS. +But it can go up to 32 FPS. + +The current panel is susceptible to image retention, so the display will start +up with the screen saver. If you send a command to draw anything on the display, +the screensaver will exit. +Currently it does not re-appear after a timeout, it will only re-appear on the +next power-on or after waking from sleep. + +## Controlling + +### Display System Status + +For a similar type of display, there's an +[open-source software](https://github.com/mathoudebine/turing-smart-screen-python) +to get systems stats, render them into an image file and send it to the screen. + +For this display, we have [a fork](https://github.com/FrameworkComputer/lotus-smart-screen-python). +To run it, just install Python and the dependencies, then run `main.py`. +The configuration (`config.yaml`) is already adapted for this display - +it should be able to find the display by itself (Windows or Linux). + +###### Configuration + +Check out the [upstream documentation](https://github.com/mathoudebine/turing-smart-screen-python/wiki/System-monitor-%3A-themes) +for more information about editing themes. + +Currently we have two themes optimized for this display: `B1Terminal` and `B1Blank`. + +`B1Terminal` comes pre-configured with lots of system stats. + +`B1Blank` comes configured as rendering the text in `file1.txt` onto the screen. + +Both can be fully customized by changing the background image and the displayed statistics +in `res/themes/{B1Blank,B1Terminal}/background.png` and `res/themes/{B1Blank,B1Terminal}/theme.yaml` +respectively. + +### Commandline + +``` +> ./inputmodule-control b1-display +B1 Display + +Usage: ipc b1-display [OPTIONS] + +Options: + --sleeping [] + Set sleep status or get, if no value provided [possible values: true, false] + --bootloader + Jump to the bootloader + --panic + Crash the firmware (TESTING ONLY!) + -v, --version + Get the device version + --display-on [] + Turn display on/off [possible values: true, false] + --pattern + Display a simple pattern [possible values: white, black] + --invert-screen [] + Invert screen on/off [possible values: true, false] + --screen-saver [] + Screensaver on/off [possible values: true, false] + --image-bw + Display black&white image (300x400px) + --clear-ram + Clear display RAM + -h, --help + Print help +``` + +### Non-trivial Examples + +###### Display an Image + +Display an image (tested with PNG and GIF). It must be 300x400 pixels in size. +It doesn't have to be black/white. The program will calculate the brightness of +each pixel. But if the brightness doesn't vary enough, it won't look good. One +example image is included in the repository. + +```sh +# Should show the Framework Logo and a Lotus flower +inputmodule-control b1-display --image-bw b1display.gif +``` + +###### Invert the colors (dark-mode) + +Since the screen is just black and white, you can display black text on a +white/light background. This can be turned into dark mode by inverting the +colors, making it show light text on a black background. + +```sh +# Invert on +> inputmodule-control b1-display --invert-screen true + +# Invert off +> inputmodule-control b1-display --invert-screen false + +# Check if currently inverted +> inputmodule-control b1-display --invert-screen +Currently inverted: false +``` diff --git a/b1display/src/main.rs b/b1display/src/main.rs index a0343a3..d6e49a9 100644 --- a/b1display/src/main.rs +++ b/b1display/src/main.rs @@ -18,7 +18,7 @@ use embedded_graphics::pixelcolor::Rgb565; use embedded_graphics::prelude::*; use embedded_graphics::primitives::*; use embedded_hal::blocking::spi; -use st7306_lcd::ST7306; +use st7306_lcd::{FpsConfig, HpmFps, LpmFps, PowerMode, ST7306}; // Provide an alias for our BSP so we can switch targets quickly. // Uncomment the BSP you included in Cargo.toml, the rest of the code does not need to change. @@ -43,8 +43,9 @@ use usb_device::{class_prelude::*, prelude::*}; use usbd_serial::{SerialPort, USB_CLASS_CDC}; // Used to demonstrate writing formatted strings -use core::fmt::{Debug, Write}; -use heapless::String; +use core::fmt::Debug; +//use core::fmt::Write; +//use heapless::String; use lotus_inputmodules::control::*; use lotus_inputmodules::graphics::*; @@ -66,6 +67,12 @@ type B1ST7306 = ST7306< 200, >; +const DEBUG: bool = false; +const SCRNS_DELTA: i32 = 10; +const WIDTH: i32 = 300; +const HEIGHT: i32 = 400; +const SIZE: Size = Size::new(WIDTH as u32, HEIGHT as u32); + #[entry] fn main() -> ! { let mut pac = pac::Peripherals::take().unwrap(); @@ -128,7 +135,6 @@ fn main() -> ! { let spi = bsp::hal::Spi::<_, _, 8>::new(pac.SPI0); // Display control pins let dc = pins.dc.into_push_pull_output(); - //let mut lcd_led = pins.backlight.into_push_pull_output(); let mut cs = pins.cs.into_push_pull_output(); cs.set_low().unwrap(); let rst = pins.rstb.into_push_pull_output(); @@ -140,43 +146,68 @@ fn main() -> ! { &embedded_hal::spi::MODE_0, ); - let mut disp: B1ST7306 = ST7306::new(spi, dc, cs, rst, false, 300, 400); + const INVERTED: bool = false; + const AUTO_PWRDOWN: bool = true; + const TE_ENABLE: bool = true; + const COL_START: u16 = 0x12; + const ROW_START: u16 = 0x00; + let mut disp: B1ST7306 = ST7306::new( + spi, + dc, + cs, + rst, + INVERTED, + AUTO_PWRDOWN, + TE_ENABLE, + FpsConfig { + hpm: HpmFps::ThirtyTwo, + lpm: LpmFps::One, + }, + WIDTH as u16, + HEIGHT as u16, + COL_START, + ROW_START, + ); disp.init(&mut delay).unwrap(); + // Clear display, might have garbage in display memory // TODO: Seems broken //disp.clear(Rgb565::WHITE).unwrap(); - Rectangle::new(Point::new(0, 0), Size::new(300, 400)) + Rectangle::new(Point::new(0, 0), SIZE) .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE)) .draw(&mut disp) .unwrap(); - let logo_rect = draw_logo(&mut disp).unwrap(); - Rectangle::new(Point::new(10, 10), Size::new(10, 10)) - .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) - .draw(&mut disp) - .unwrap(); - Rectangle::new(Point::new(20, 20), Size::new(10, 10)) - .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) - .draw(&mut disp) - .unwrap(); - Rectangle::new(Point::new(30, 30), Size::new(10, 10)) - .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) - .draw(&mut disp) - .unwrap(); - Rectangle::new(Point::new(40, 40), Size::new(10, 10)) - .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) - .draw(&mut disp) - .unwrap(); - Rectangle::new(Point::new(50, 50), Size::new(10, 10)) - .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) - .draw(&mut disp) + let logo_rect = draw_logo(&mut disp, Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y)).unwrap(); + if DEBUG { + Rectangle::new(Point::new(10, 10), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + Rectangle::new(Point::new(20, 20), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + Rectangle::new(Point::new(30, 30), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + Rectangle::new(Point::new(40, 40), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + Rectangle::new(Point::new(50, 50), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + draw_text( + &mut disp, + "Framework", + Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y + logo_rect.size.height as i32), + ) .unwrap(); - draw_text( - &mut disp, - "Framework", - Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y + logo_rect.size.height as i32), - ) - .unwrap(); + } + disp.flush().unwrap(); let sleep = pins.sleep.into_pull_down_input(); @@ -184,38 +215,57 @@ fn main() -> ! { sleeping: SimpleSleepState::Awake, screen_inverted: false, screen_on: true, + screensaver: Some(ScreenSaverState::default()), }; - let mut said_hello = false; - let timer = Timer::new(pac.TIMER, &mut pac.RESETS); let mut prev_timer = timer.get_counter().ticks(); + let mut logo_pos = Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y); + loop { // Go to sleep if the host is sleeping let host_sleeping = sleep.is_low().unwrap(); handle_sleep(host_sleeping, &mut state, &mut delay, &mut disp); // Handle period display updates. Don't do it too often - if timer.get_counter().ticks() > prev_timer + 20_000 { - // TODO: Update display + if timer.get_counter().ticks() > prev_timer + 500_000 { prev_timer = timer.get_counter().ticks(); - } - // A welcome message at the beginning - if !said_hello && timer.get_counter().ticks() >= 2_000_000 { - said_hello = true; - let _ = serial.write(b"Hello, World!\r\n"); + if let Some(ref mut screensaver) = state.screensaver { + logo_pos = { + let (x, y) = (logo_pos.x, logo_pos.y); + let w = logo_rect.size.width as i32; + let h = logo_rect.size.height as i32; - let time = timer.get_counter(); - let mut text: String<64> = String::new(); - write!(&mut text, "Current timer ticks: {}\r\n", time).unwrap(); + // Bounce off the walls + if x <= 0 || x + w >= WIDTH { + screensaver.rightwards *= -1; + } + if y <= 0 || y + h >= HEIGHT { + screensaver.downwards *= -1; + } - // This only works reliably because the number of bytes written to - // the serial port is smaller than the buffers available to the USB - // peripheral. In general, the return value should be handled, so that - // bytes not transferred yet don't get lost. - let _ = serial.write(text.as_bytes()); + Point::new( + x + screensaver.rightwards * SCRNS_DELTA, + y + screensaver.downwards * SCRNS_DELTA, + ) + }; + // Draw a border around the new logo, to clear previously drawn adjacent logos + let style = PrimitiveStyleBuilder::new() + .stroke_color(Rgb565::WHITE) + .stroke_width(2 * SCRNS_DELTA as u32) + .build(); + Rectangle::new( + logo_pos - Point::new(SCRNS_DELTA, SCRNS_DELTA), + logo_rect.size + Size::new(2 * SCRNS_DELTA as u32, 2 * SCRNS_DELTA as u32), + ) + .into_styled(style) + .draw(&mut disp) + .unwrap(); + draw_logo(&mut disp, logo_pos).unwrap(); + disp.flush().unwrap(); + } } // Check for new data @@ -250,14 +300,14 @@ fn main() -> ! { }; // Must write AFTER writing response, otherwise the // client interprets this debug message as the response - let mut text: String<64> = String::new(); - write!( - &mut text, - "Handled command {}:{}:{}:{}\r\n", - buf[0], buf[1], buf[2], buf[3] - ) - .unwrap(); - let _ = serial.write(text.as_bytes()); + //let mut text: String<64> = String::new(); + //write!( + // &mut text, + // "Handled command {}:{}:{}:{}\r\n", + // buf[0], buf[1], buf[2], buf[3] + //) + //.unwrap(); + //let _ = serial.write(text.as_bytes()); } _ => {} } @@ -270,7 +320,7 @@ fn main() -> ! { fn handle_sleep( go_sleeping: bool, state: &mut B1DIsplayState, - _delay: &mut Delay, + delay: &mut Delay, disp: &mut ST7306, ) where SPI: spi::Write, @@ -285,7 +335,8 @@ fn handle_sleep( state.sleeping = SimpleSleepState::Sleeping; // Turn off display - disp.on_off(false).unwrap(); + //disp.on_off(false).unwrap(); + disp.sleep_in(delay).unwrap(); // TODO: Power Display controller down @@ -298,7 +349,14 @@ fn handle_sleep( state.sleeping = SimpleSleepState::Awake; // Turn display back on - disp.on_off(true).unwrap(); + //disp.on_off(true).unwrap(); + disp.sleep_out(delay).unwrap(); + // Sleep-in has to go into HPM first, so we'll be in HPM after wake-up as well + disp.switch_mode(delay, PowerMode::Lpm).unwrap(); + + // Turn screensaver on when resuming from sleep + // TODO Subject to change, but currently I want to avoid burn-in by default + state.screensaver = Some(ScreenSaverState::default()); // TODO: Power display controller back on } diff --git a/c1minimal/Cargo.toml b/c1minimal/Cargo.toml index 85c545b..9b44ec5 100644 --- a/c1minimal/Cargo.toml +++ b/c1minimal/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "c1minimal" -version = "0.1.3" +version = "0.1.4" [dependencies] cortex-m = "0.7" diff --git a/c1minimal/src/main.rs b/c1minimal/src/main.rs index 7708300..f9bb4a3 100644 --- a/c1minimal/src/main.rs +++ b/c1minimal/src/main.rs @@ -40,8 +40,8 @@ use usb_device::{class_prelude::*, prelude::*}; use usbd_serial::{SerialPort, USB_CLASS_CDC}; // Used to demonstrate writing formatted strings -use core::fmt::Write; -use heapless::String; +// use core::fmt::Write; +// use heapless::String; // RGB LED use smart_leds::{colors, SmartLedsWrite, RGB8}; @@ -125,8 +125,6 @@ fn main() -> ! { brightness: 10, }; - let mut said_hello = false; - let timer = Timer::new(pac.TIMER, &mut pac.RESETS); let mut prev_timer = timer.get_counter().ticks(); @@ -157,22 +155,6 @@ fn main() -> ! { prev_timer = timer.get_counter().ticks(); } - // A welcome message at the beginning - if !said_hello && timer.get_counter().ticks() >= 2_000_000 { - said_hello = true; - let _ = serial.write(b"Hello, World!\r\n"); - - let time = timer.get_counter(); - let mut text: String<64> = String::new(); - write!(&mut text, "Current timer ticks: {}\r\n", time).unwrap(); - - // This only works reliably because the number of bytes written to - // the serial port is smaller than the buffers available to the USB - // peripheral. In general, the return value should be handled, so that - // bytes not transferred yet don't get lost. - let _ = serial.write(text.as_bytes()); - } - // Check for new data if usb_dev.poll(&mut [&mut serial]) { let mut buf = [0u8; 64]; diff --git a/control.py b/control.py index 7406f1c..1252a64 100755 --- a/control.py +++ b/control.py @@ -39,6 +39,8 @@ class CommandVals(IntEnum): InvertScreen = 0x15 SetPixelColumn = 0x16 FlushFramebuffer = 0x17 + ClearRam = 0x18 + ScreenSaver = 0x19 Version = 0x20 @@ -192,6 +194,8 @@ def main(): action=argparse.BooleanOptionalAction) parser.add_argument("--invert-screen", help="Invert display", action=argparse.BooleanOptionalAction) + parser.add_argument("--screen-saver", help="Turn on/off screensaver", + action=argparse.BooleanOptionalAction) parser.add_argument("--b1image", help="On the B1 display, show a PNG or GIF image in black and white only)", type=argparse.FileType('rb')) @@ -256,7 +260,7 @@ def main(): snake_embedded() elif args.game_of_life_embedded is not None: game_of_life_embedded(args.game_of_life_embedded) - elif args.quit_embedded_game is not None: + elif args.quit_embedded_game: send_command(CommandVals.GameControl, [GameControlVal.Quit]) elif args.pong_embedded: pong_embedded() @@ -276,6 +280,8 @@ def main(): display_on_cmd(args.display_on) elif args.invert_screen is not None: invert_screen_cmd(args.invert_screen) + elif args.screen_saver is not None: + screen_saver_cmd(args.screen_saver) elif args.b1image is not None: b1image_bl(args.b1image) elif args.version: @@ -1063,6 +1069,10 @@ def invert_screen_cmd(invert): send_command(CommandVals.InvertScreen, [invert]) +def screen_saver_cmd(on): + send_command(CommandVals.ScreenSaver, [on]) + + # 5x6 symbol font. Leaves 2 pixels on each side empty # We can leave one row empty below and then the display fits 5 of these digits. diff --git a/inputmodule-control/Cargo.toml b/inputmodule-control/Cargo.toml index 692578a..b1279d2 100644 --- a/inputmodule-control/Cargo.toml +++ b/inputmodule-control/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "inputmodule-control" -version = "0.1.3" +version = "0.1.4" [dependencies] clap = { version = "4.0", features = ["derive"] } diff --git a/inputmodule-control/src/b1display.rs b/inputmodule-control/src/b1display.rs index f5293de..cd7391d 100644 --- a/inputmodule-control/src/b1display.rs +++ b/inputmodule-control/src/b1display.rs @@ -4,6 +4,7 @@ use clap::Parser; pub enum B1Pattern { White, Black, + //Checkerboard, } /// B1 Display @@ -40,7 +41,15 @@ pub struct B1DisplaySubcommand { #[arg(long)] pub invert_screen: Option>, + /// Screensaver on/off + #[arg(long)] + pub screen_saver: Option>, + /// Display black&white image (300x400px) #[arg(long)] pub image_bw: Option, + + /// Clear display RAM + #[arg(long)] + pub clear_ram: bool, } diff --git a/inputmodule-control/src/inputmodule.rs b/inputmodule-control/src/inputmodule.rs index 723d196..78e7e55 100644 --- a/inputmodule-control/src/inputmodule.rs +++ b/inputmodule-control/src/inputmodule.rs @@ -38,6 +38,8 @@ enum Command { InvertScreen = 0x15, SetPixelColumn = 0x16, FlushFramebuffer = 0x17, + ClearRam = 0x18, + ScreenSaver = 0x19, Version = 0x20, } @@ -259,9 +261,15 @@ pub fn serial_commands(args: &crate::ClapCli) { if let Some(invert_screen) = b1display_args.invert_screen { invert_screen_cmd(serialdev, invert_screen); } + if let Some(screensaver_on) = b1display_args.screen_saver { + screensaver_cmd(serialdev, screensaver_on); + } if let Some(image_path) = &b1display_args.image_bw { b1display_bw_image_cmd(serialdev, image_path); } + if b1display_args.clear_ram { + simple_cmd(serialdev, Command::ClearRam, &[0x00]); + } if let Some(pattern) = b1display_args.pattern { b1_display_pattern(serialdev, pattern); } @@ -488,27 +496,27 @@ fn blinking_cmd(serialdevs: &Vec) { fn breathing_cmd(serialdevs: &Vec) { loop { // Go quickly from 250 to 50 - for i in 0..10 { - thread::sleep(Duration::from_millis(30)); - simple_cmd_multiple(serialdevs, Command::Brightness, &[250 - i * 20]); + for i in 0..40 { + simple_cmd_multiple(serialdevs, Command::Brightness, &[250 - i * 5]); + thread::sleep(Duration::from_millis(25)); } // Go slowly from 50 to 0 - for i in 0..10 { - thread::sleep(Duration::from_millis(60)); - simple_cmd_multiple(serialdevs, Command::Brightness, &[50 - i * 5]); + for i in 0..50 { + simple_cmd_multiple(serialdevs, Command::Brightness, &[50 - i]); + thread::sleep(Duration::from_millis(10)); } // Go slowly from 0 to 50 - for i in 0..10 { - thread::sleep(Duration::from_millis(60)); - simple_cmd_multiple(serialdevs, Command::Brightness, &[i * 5]); + for i in 0..50 { + simple_cmd_multiple(serialdevs, Command::Brightness, &[i]); + thread::sleep(Duration::from_millis(10)); } // Go quickly from 50 to 250 - for i in 0..10 { - thread::sleep(Duration::from_millis(30)); - simple_cmd_multiple(serialdevs, Command::Brightness, &[50 + i * 20]); + for i in 0..40 { + simple_cmd_multiple(serialdevs, Command::Brightness, &[50 + i * 5]); + thread::sleep(Duration::from_millis(25)); } } } @@ -735,6 +743,26 @@ fn invert_screen_cmd(serialdev: &str, arg: Option) { } } +fn screensaver_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(display_on) = arg { + simple_cmd_port(&mut port, Command::ScreenSaver, &[display_on as u8]); + } else { + simple_cmd_port(&mut port, Command::ScreenSaver, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let on = response[0] == 1; + println!("Currently on: {on}"); + } +} + fn set_color_cmd(serialdev: &str, color: Color) { let args = match color { Color::White => &[0xFF, 0xFF, 0xFF], diff --git a/ledmatrix/Cargo.toml b/ledmatrix/Cargo.toml index 74ac04c..902dc89 100644 --- a/ledmatrix/Cargo.toml +++ b/ledmatrix/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "ledmatrix" -version = "0.1.3" +version = "0.1.4" [dependencies] cortex-m = "0.7" diff --git a/ledmatrix/src/main.rs b/ledmatrix/src/main.rs index c15c994..f4b61f2 100644 --- a/ledmatrix/src/main.rs +++ b/ledmatrix/src/main.rs @@ -190,7 +190,7 @@ fn main() -> ! { ); let mut state = State { - grid: percentage(100), + grid: percentage(0), col_buffer: Grid::default(), animate: false, brightness: 120, @@ -207,22 +207,37 @@ fn main() -> ! { .set_scaling(state.brightness) .expect("failed to set scaling"); - let mut said_hello = false; - fill_grid_pixels(&state.grid, &mut matrix); let timer = Timer::new(pac.TIMER, &mut pac.RESETS); let mut prev_timer = timer.get_counter().ticks(); let mut game_timer = timer.get_counter().ticks(); + let mut startup_percentage = Some(0); + loop { // TODO: Current hardware revision does not have the sleep pin wired up :( // Go to sleep if the host is sleeping let _host_sleeping = sleep.is_low().unwrap(); - //handle_sleep(host_sleeping, &mut state, &mut matrix, &mut delay); + //handle_sleep( + // host_sleeping, + // &mut state, + // &mut matrix, + // &mut delay, + // &mut led_enable, + //); // Handle period display updates. Don't do it too often if timer.get_counter().ticks() > prev_timer + 20_000 { + // On startup slowly turn the screen on - it's a pretty effect :) + match startup_percentage { + Some(p) if p <= 100 => { + state.grid = percentage(p); + startup_percentage = Some(p + 5); + } + _ => {} + } + fill_grid_pixels(&state.grid, &mut matrix); if state.animate { for x in 0..WIDTH { @@ -232,22 +247,6 @@ fn main() -> ! { prev_timer = timer.get_counter().ticks(); } - // A welcome message at the beginning - if !said_hello && timer.get_counter().ticks() >= 2_000_000 { - said_hello = true; - let _ = serial.write(b"Hello, World!\r\n"); - - let time = timer.get_counter(); - let mut text: String<64> = String::new(); - write!(&mut text, "Current timer ticks: {}\r\n", time).unwrap(); - - // This only works reliably because the number of bytes written to - // the serial port is smaller than the buffers available to the USB - // peripheral. In general, the return value should be handled, so that - // bytes not transferred yet don't get lost. - let _ = serial.write(text.as_bytes()); - } - // Check for new data if usb_dev.poll(&mut [&mut serial]) { let mut buf = [0u8; 64]; @@ -279,6 +278,9 @@ fn main() -> ! { }; } (Some(command), SleepState::Awake) => { + // If there's a very early command, cancel the startup animation + startup_percentage = None; + // While sleeping no command is handled, except waking up if let Some(response) = handle_command(&command, &mut state, &mut matrix, random) diff --git a/lotus-inputmodules/Cargo.toml b/lotus-inputmodules/Cargo.toml index 839d46a..4460724 100644 --- a/lotus-inputmodules/Cargo.toml +++ b/lotus-inputmodules/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "lotus-inputmodules" -version = "0.1.3" +version = "0.1.4" [dependencies] cortex-m = "0.7" diff --git a/lotus-inputmodules/src/control.rs b/lotus-inputmodules/src/control.rs index 55887dd..b08f7fd 100644 --- a/lotus-inputmodules/src/control.rs +++ b/lotus-inputmodules/src/control.rs @@ -22,7 +22,7 @@ use embedded_hal::digital::v2::OutputPin; #[cfg(feature = "b1display")] use heapless::String; #[cfg(feature = "b1display")] -use st7306_lcd::{instruction::Instruction, ST7306}; +use st7306_lcd::ST7306; #[cfg(feature = "ledmatrix")] use crate::games::pong; @@ -57,6 +57,8 @@ pub enum CommandVals { InvertScreen = 0x15, SetPixelColumn = 0x16, FlushFramebuffer = 0x17, + ClearRam = 0x18, + ScreenSaver = 0x19, Version = 0x20, } @@ -108,6 +110,14 @@ pub enum GameOfLifeStartParam { Glider = 0x05, } +#[derive(Copy, Clone, num_derive::FromPrimitive)] +pub enum DisplayMode { + /// Low Power Mode + Lpm = 0x00, + /// High Power Mode + Hpm = 0x01, +} + // TODO: Reduce size for modules that don't require other commands pub enum Command { /// Get current brightness scaling @@ -149,6 +159,9 @@ pub enum Command { GetInvertScreen, SetPixelColumn(usize, [u8; 50]), FlushFramebuffer, + ClearRam, + ScreenSaver(bool), + GetScreenSaver, _Unknown, } @@ -166,11 +179,27 @@ pub struct C1MinimalState { pub brightness: u8, } +#[derive(Copy, Clone)] +pub struct ScreenSaverState { + pub rightwards: i32, + pub downwards: i32, +} + +impl Default for ScreenSaverState { + fn default() -> Self { + Self { + rightwards: 1, + downwards: 1, + } + } +} + #[cfg(feature = "b1display")] pub struct B1DIsplayState { pub sleeping: SimpleSleepState, pub screen_inverted: bool, pub screen_on: bool, + pub screensaver: Option, } pub fn parse_command(count: usize, buf: &[u8]) -> Option { @@ -353,6 +382,12 @@ pub fn parse_module_command(count: usize, buf: &[u8]) -> Option { } } Some(CommandVals::FlushFramebuffer) => Some(Command::FlushFramebuffer), + Some(CommandVals::ClearRam) => Some(Command::ClearRam), + Some(CommandVals::ScreenSaver) => Some(if let Some(on) = arg { + Command::ScreenSaver(on == 1) + } else { + Command::GetScreenSaver + }), _ => None, } } else { @@ -515,6 +550,9 @@ where } Command::Panic => panic!("Ahhh"), Command::SetText(text) => { + // Turn screensaver off, when drawing something + state.screensaver = None; + clear_text( disp, Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y + logo_rect.size.height as i32), @@ -528,6 +566,7 @@ where Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y + logo_rect.size.height as i32), ) .unwrap(); + disp.flush().unwrap(); None } Command::DisplayOn(on) => { @@ -542,11 +581,7 @@ where } Command::InvertScreen(invert) => { state.screen_inverted = *invert; - if *invert { - disp.write_command(Instruction::INVON, &[]).unwrap(); - } else { - disp.write_command(Instruction::INVOFF, &[]).unwrap(); - } + disp.invert_screen(state.screen_inverted).unwrap(); None } Command::GetInvertScreen => { @@ -555,6 +590,9 @@ where Some(response) } Command::SetPixelColumn(column, pixel_bytes) => { + // Turn screensaver off, when drawing something + state.screensaver = None; + let mut pixels: [bool; 400] = [false; 400]; for (i, byte) in pixel_bytes.iter().enumerate() { pixels[8 * i] = byte & 0b00000001 != 0; @@ -582,6 +620,27 @@ where disp.flush().unwrap(); None } + Command::ClearRam => { + // Turn screensaver off, when drawing something + state.screensaver = None; + + disp.clear_ram().unwrap(); + None + } + Command::ScreenSaver(on) => { + state.screensaver = match (*on, state.screensaver) { + (true, Some(x)) => Some(x), + (true, None) => Some(ScreenSaverState::default()), + (false, Some(_)) => None, + (false, None) => None, + }; + None + } + Command::GetScreenSaver => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.screensaver.is_some() as u8; + Some(response) + } _ => handle_generic_command(command), } } diff --git a/lotus-inputmodules/src/graphics.rs b/lotus-inputmodules/src/graphics.rs index f32e9b1..69e9bb2 100644 --- a/lotus-inputmodules/src/graphics.rs +++ b/lotus-inputmodules/src/graphics.rs @@ -42,12 +42,12 @@ where Ok(()) } -pub fn draw_logo(target: &mut D) -> Result +pub fn draw_logo(target: &mut D, offset: Point) -> Result where D: DrawTarget, { let bmp: Bmp = Bmp::from_slice(include_bytes!("../assets/logo.bmp")).unwrap(); - let image = Image::new(&bmp, Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y)); + let image = Image::new(&bmp, offset); image.draw(target)?; Ok(image.bounding_box())