diff --git a/AGENTS.md b/AGENTS.md index 38c8c3b..8aa1c38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,6 +135,10 @@ cargo clippy -- -D warnings cargo test ``` +### Display Orientation + +Use `--orientation` to pick landscape or portrait and `--flip` to rotate output 180° (useful for upside-down installs). + ## Git Workflow Use plain git commands for version control. diff --git a/README.md b/README.md index b1460d1..e98394f 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ Options: -s, --font-size Font size in pixels [default: 14] -a, --auto Auto-fit text to largest readable size -o, --orientation Display orientation: landscape or portrait [default: landscape] + --flip Flip the display 180° (use if the screen is upside down) -d, --delay Delay between pages [default: 2.0] -l, --loop Loop display continuously --detect Only check if display is connected @@ -126,7 +127,7 @@ The `--auto` flag automatically calculates the largest font size that fits your ### Orientation Mode -The `--orientation` flag switches between landscape (160x80, default) and portrait (80x160) modes: +The `--orientation` flag switches between landscape (160x80, default) and portrait (80x160) modes. Add `--flip` to rotate the output 180° in either orientation: ```bash # Landscape (default) - wider display @@ -151,6 +152,9 @@ The `--orientation` flag switches between landscape (160x80, default) and portra # Portrait orientation with auto-fit ./display-fs show --auto -o portrait "Tall" +# Landscape orientation but flipped 180° +./display-fs show --auto --flip "Upside down" + # Larger font (manual) ./display-fs show -s 24 "BIG" @@ -169,6 +173,9 @@ Display the currently playing Spotify track: # Live updates (refresh every 2 seconds) ./display-fs spotify --loop +# Live updates with flipped display +./display-fs spotify --loop --flip + # Faster refresh ./display-fs spotify --loop --speed fast ``` diff --git a/display-fs b/display-fs deleted file mode 100755 index 792cd69..0000000 Binary files a/display-fs and /dev/null differ diff --git a/src/image.rs b/src/image.rs index 887290d..63edc17 100644 --- a/src/image.rs +++ b/src/image.rs @@ -14,6 +14,10 @@ pub enum Orientation { Landscape, /// 80x160 - taller than wide Portrait, + /// 160x80 - wider than tall, flipped 180° + LandscapeFlip, + /// 80x160 - taller than wide, flipped 180° + PortraitFlip, } impl Orientation { @@ -22,6 +26,8 @@ impl Orientation { match self { Orientation::Landscape => PHYSICAL_HEIGHT, // 160 Orientation::Portrait => PHYSICAL_WIDTH, // 80 + Orientation::LandscapeFlip => PHYSICAL_HEIGHT, + Orientation::PortraitFlip => PHYSICAL_WIDTH, } } @@ -30,6 +36,8 @@ impl Orientation { match self { Orientation::Landscape => PHYSICAL_WIDTH, // 80 Orientation::Portrait => PHYSICAL_HEIGHT, // 160 + Orientation::LandscapeFlip => PHYSICAL_WIDTH, + Orientation::PortraitFlip => PHYSICAL_HEIGHT, } } } @@ -252,6 +260,30 @@ pub fn image_to_rgb565_bytes_oriented(img: &RgbImage, orientation: Orientation) } } } + Orientation::PortraitFlip => { + // Portrait flip: rotate 180° (80x160) + for py in 0..PHYSICAL_HEIGHT { + for px in 0..PHYSICAL_WIDTH { + let lx = (PHYSICAL_WIDTH - 1) - px; // 79 - px + let ly = (PHYSICAL_HEIGHT - 1) - py; // 159 - py + let pixel = img.get_pixel(lx, ly); + push_rgb565(&mut data, pixel[0], pixel[1], pixel[2]); + } + } + } + Orientation::LandscapeFlip => { + // Landscape flip: rotate 90° CCW to get 180° from landscape orientation + // Input is 160w x 80h, output is 80w x 160h + // Maps to logical: lx = 159 - py, ly = px + for py in 0..PHYSICAL_HEIGHT { + for px in 0..PHYSICAL_WIDTH { + let lx = (PHYSICAL_HEIGHT - 1) - py; // 159 - py + let ly = px; + let pixel = img.get_pixel(lx, ly); + push_rgb565(&mut data, pixel[0], pixel[1], pixel[2]); + } + } + } } data @@ -365,6 +397,14 @@ mod tests { // Portrait: 80x160 assert_eq!(Orientation::Portrait.width(), 80); assert_eq!(Orientation::Portrait.height(), 160); + + // Landscape flip: 160x80 + assert_eq!(Orientation::LandscapeFlip.width(), 160); + assert_eq!(Orientation::LandscapeFlip.height(), 80); + + // Portrait flip: 80x160 + assert_eq!(Orientation::PortraitFlip.width(), 80); + assert_eq!(Orientation::PortraitFlip.height(), 160); } #[test] diff --git a/src/main.rs b/src/main.rs index 5908de3..3a48ea0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,11 +49,13 @@ enum OrientationArg { Portrait, } -impl From for Orientation { - fn from(arg: OrientationArg) -> Self { - match arg { - OrientationArg::Landscape => Orientation::Landscape, - OrientationArg::Portrait => Orientation::Portrait, +impl OrientationArg { + fn to_orientation(self, flip: bool) -> Orientation { + match (self, flip) { + (OrientationArg::Landscape, true) => Orientation::LandscapeFlip, + (OrientationArg::Landscape, false) => Orientation::Landscape, + (OrientationArg::Portrait, true) => Orientation::PortraitFlip, + (OrientationArg::Portrait, false) => Orientation::Portrait, } } } @@ -72,6 +74,10 @@ struct DisplayOptions { #[arg(short = 'o', long, value_enum, default_value = "landscape")] orientation: OrientationArg, + /// Flip the display 180° (use if the screen is upside down) + #[arg(long)] + flip: bool, + /// Delay between pages/updates in seconds (must be positive) #[arg(short, long, default_value = "2.0", value_parser = validate_positive_f32)] delay: f32, @@ -91,7 +97,7 @@ impl DisplayOptions { } pub fn orientation(&self) -> Orientation { - self.orientation.into() + self.orientation.to_orientation(self.flip) } } diff --git a/src/protocol.rs b/src/protocol.rs index 76cb804..4129137 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -53,10 +53,13 @@ pub fn send_image_to_display( /// Create orientation command to initialize display orientation fn create_orientation_command(orientation: Orientation) -> [u8; 3] { - // Orientation values: 0=portrait, 1=landscape, 2=portrait_flip, 3=landscape_flip + // Orientation values: 0=portrait, 1=landscape + // Flip variants are handled in image data rotation to avoid device quirks. let orientation_value = match orientation { Orientation::Portrait => 0, Orientation::Landscape => 1, + Orientation::PortraitFlip => 0, + Orientation::LandscapeFlip => 1, }; [CMD_SET_ORIENTATION, orientation_value, CMD_END] } @@ -108,7 +111,12 @@ mod tests { #[test] fn test_bitmap_header_always_physical_dimensions() { // Both orientations use physical 80x160 dimensions - for orientation in [Orientation::Landscape, Orientation::Portrait] { + for orientation in [ + Orientation::Landscape, + Orientation::Portrait, + Orientation::LandscapeFlip, + Orientation::PortraitFlip, + ] { let header = create_bitmap_header_oriented(orientation); // x0 = 0, y0 = 0 assert_eq!(header[1], 0x00); // x0 low @@ -129,6 +137,14 @@ mod tests { assert_eq!(CMD_END, 0x0A); } + #[test] + fn test_orientation_command_values() { + assert_eq!(create_orientation_command(Orientation::Portrait)[1], 0); + assert_eq!(create_orientation_command(Orientation::Landscape)[1], 1); + assert_eq!(create_orientation_command(Orientation::PortraitFlip)[1], 0); + assert_eq!(create_orientation_command(Orientation::LandscapeFlip)[1], 1); + } + #[test] fn test_chunk_size_physical() { // Always uses physical width: 80 * 4 = 320