Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Options:
-s, --font-size <SIZE> Font size in pixels [default: 14]
-a, --auto Auto-fit text to largest readable size
-o, --orientation <MODE> Display orientation: landscape or portrait [default: landscape]
--flip Flip the display 180° (use if the screen is upside down)
-d, --delay <SECONDS> Delay between pages [default: 2.0]
-l, --loop Loop display continuously
--detect Only check if display is connected
Expand All @@ -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
Expand All @@ -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"

Expand All @@ -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
```
Expand Down
Binary file removed display-fs
Binary file not shown.
40 changes: 40 additions & 0 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
}

Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
18 changes: 12 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ enum OrientationArg {
Portrait,
}

impl From<OrientationArg> 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,
}
}
}
Expand All @@ -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,
Expand All @@ -91,7 +97,7 @@ impl DisplayOptions {
}

pub fn orientation(&self) -> Orientation {
self.orientation.into()
self.orientation.to_orientation(self.flip)
}
}

Expand Down
20 changes: 18 additions & 2 deletions src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading