Skip to content

Commit 028fd9c

Browse files
authored
Merge pull request #1 from colmarius/feature/flip-display
Flip display option
2 parents 096d004 + ccbea49 commit 028fd9c

6 files changed

Lines changed: 82 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ cargo clippy -- -D warnings
135135
cargo test
136136
```
137137

138+
### Display Orientation
139+
140+
Use `--orientation` to pick landscape or portrait and `--flip` to rotate output 180° (useful for upside-down installs).
141+
138142
## Git Workflow
139143

140144
Use plain git commands for version control.

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Options:
106106
-s, --font-size <SIZE> Font size in pixels [default: 14]
107107
-a, --auto Auto-fit text to largest readable size
108108
-o, --orientation <MODE> Display orientation: landscape or portrait [default: landscape]
109+
--flip Flip the display 180° (use if the screen is upside down)
109110
-d, --delay <SECONDS> Delay between pages [default: 2.0]
110111
-l, --loop Loop display continuously
111112
--detect Only check if display is connected
@@ -126,7 +127,7 @@ The `--auto` flag automatically calculates the largest font size that fits your
126127

127128
### Orientation Mode
128129

129-
The `--orientation` flag switches between landscape (160x80, default) and portrait (80x160) modes:
130+
The `--orientation` flag switches between landscape (160x80, default) and portrait (80x160) modes. Add `--flip` to rotate the output 180° in either orientation:
130131

131132
```bash
132133
# Landscape (default) - wider display
@@ -151,6 +152,9 @@ The `--orientation` flag switches between landscape (160x80, default) and portra
151152
# Portrait orientation with auto-fit
152153
./display-fs show --auto -o portrait "Tall"
153154

155+
# Landscape orientation but flipped 180°
156+
./display-fs show --auto --flip "Upside down"
157+
154158
# Larger font (manual)
155159
./display-fs show -s 24 "BIG"
156160

@@ -169,6 +173,9 @@ Display the currently playing Spotify track:
169173
# Live updates (refresh every 2 seconds)
170174
./display-fs spotify --loop
171175

176+
# Live updates with flipped display
177+
./display-fs spotify --loop --flip
178+
172179
# Faster refresh
173180
./display-fs spotify --loop --speed fast
174181
```

display-fs

-5.24 MB
Binary file not shown.

src/image.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ pub enum Orientation {
1414
Landscape,
1515
/// 80x160 - taller than wide
1616
Portrait,
17+
/// 160x80 - wider than tall, flipped 180°
18+
LandscapeFlip,
19+
/// 80x160 - taller than wide, flipped 180°
20+
PortraitFlip,
1721
}
1822

1923
impl Orientation {
@@ -22,6 +26,8 @@ impl Orientation {
2226
match self {
2327
Orientation::Landscape => PHYSICAL_HEIGHT, // 160
2428
Orientation::Portrait => PHYSICAL_WIDTH, // 80
29+
Orientation::LandscapeFlip => PHYSICAL_HEIGHT,
30+
Orientation::PortraitFlip => PHYSICAL_WIDTH,
2531
}
2632
}
2733

@@ -30,6 +36,8 @@ impl Orientation {
3036
match self {
3137
Orientation::Landscape => PHYSICAL_WIDTH, // 80
3238
Orientation::Portrait => PHYSICAL_HEIGHT, // 160
39+
Orientation::LandscapeFlip => PHYSICAL_WIDTH,
40+
Orientation::PortraitFlip => PHYSICAL_HEIGHT,
3341
}
3442
}
3543
}
@@ -252,6 +260,30 @@ pub fn image_to_rgb565_bytes_oriented(img: &RgbImage, orientation: Orientation)
252260
}
253261
}
254262
}
263+
Orientation::PortraitFlip => {
264+
// Portrait flip: rotate 180° (80x160)
265+
for py in 0..PHYSICAL_HEIGHT {
266+
for px in 0..PHYSICAL_WIDTH {
267+
let lx = (PHYSICAL_WIDTH - 1) - px; // 79 - px
268+
let ly = (PHYSICAL_HEIGHT - 1) - py; // 159 - py
269+
let pixel = img.get_pixel(lx, ly);
270+
push_rgb565(&mut data, pixel[0], pixel[1], pixel[2]);
271+
}
272+
}
273+
}
274+
Orientation::LandscapeFlip => {
275+
// Landscape flip: rotate 90° CCW to get 180° from landscape orientation
276+
// Input is 160w x 80h, output is 80w x 160h
277+
// Maps to logical: lx = 159 - py, ly = px
278+
for py in 0..PHYSICAL_HEIGHT {
279+
for px in 0..PHYSICAL_WIDTH {
280+
let lx = (PHYSICAL_HEIGHT - 1) - py; // 159 - py
281+
let ly = px;
282+
let pixel = img.get_pixel(lx, ly);
283+
push_rgb565(&mut data, pixel[0], pixel[1], pixel[2]);
284+
}
285+
}
286+
}
255287
}
256288

257289
data
@@ -365,6 +397,14 @@ mod tests {
365397
// Portrait: 80x160
366398
assert_eq!(Orientation::Portrait.width(), 80);
367399
assert_eq!(Orientation::Portrait.height(), 160);
400+
401+
// Landscape flip: 160x80
402+
assert_eq!(Orientation::LandscapeFlip.width(), 160);
403+
assert_eq!(Orientation::LandscapeFlip.height(), 80);
404+
405+
// Portrait flip: 80x160
406+
assert_eq!(Orientation::PortraitFlip.width(), 80);
407+
assert_eq!(Orientation::PortraitFlip.height(), 160);
368408
}
369409

370410
#[test]

src/main.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ enum OrientationArg {
4949
Portrait,
5050
}
5151

52-
impl From<OrientationArg> for Orientation {
53-
fn from(arg: OrientationArg) -> Self {
54-
match arg {
55-
OrientationArg::Landscape => Orientation::Landscape,
56-
OrientationArg::Portrait => Orientation::Portrait,
52+
impl OrientationArg {
53+
fn to_orientation(self, flip: bool) -> Orientation {
54+
match (self, flip) {
55+
(OrientationArg::Landscape, true) => Orientation::LandscapeFlip,
56+
(OrientationArg::Landscape, false) => Orientation::Landscape,
57+
(OrientationArg::Portrait, true) => Orientation::PortraitFlip,
58+
(OrientationArg::Portrait, false) => Orientation::Portrait,
5759
}
5860
}
5961
}
@@ -72,6 +74,10 @@ struct DisplayOptions {
7274
#[arg(short = 'o', long, value_enum, default_value = "landscape")]
7375
orientation: OrientationArg,
7476

77+
/// Flip the display 180° (use if the screen is upside down)
78+
#[arg(long)]
79+
flip: bool,
80+
7581
/// Delay between pages/updates in seconds (must be positive)
7682
#[arg(short, long, default_value = "2.0", value_parser = validate_positive_f32)]
7783
delay: f32,
@@ -91,7 +97,7 @@ impl DisplayOptions {
9197
}
9298

9399
pub fn orientation(&self) -> Orientation {
94-
self.orientation.into()
100+
self.orientation.to_orientation(self.flip)
95101
}
96102
}
97103

src/protocol.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,13 @@ pub fn send_image_to_display(
5353

5454
/// Create orientation command to initialize display orientation
5555
fn create_orientation_command(orientation: Orientation) -> [u8; 3] {
56-
// Orientation values: 0=portrait, 1=landscape, 2=portrait_flip, 3=landscape_flip
56+
// Orientation values: 0=portrait, 1=landscape
57+
// Flip variants are handled in image data rotation to avoid device quirks.
5758
let orientation_value = match orientation {
5859
Orientation::Portrait => 0,
5960
Orientation::Landscape => 1,
61+
Orientation::PortraitFlip => 0,
62+
Orientation::LandscapeFlip => 1,
6063
};
6164
[CMD_SET_ORIENTATION, orientation_value, CMD_END]
6265
}
@@ -108,7 +111,12 @@ mod tests {
108111
#[test]
109112
fn test_bitmap_header_always_physical_dimensions() {
110113
// Both orientations use physical 80x160 dimensions
111-
for orientation in [Orientation::Landscape, Orientation::Portrait] {
114+
for orientation in [
115+
Orientation::Landscape,
116+
Orientation::Portrait,
117+
Orientation::LandscapeFlip,
118+
Orientation::PortraitFlip,
119+
] {
112120
let header = create_bitmap_header_oriented(orientation);
113121
// x0 = 0, y0 = 0
114122
assert_eq!(header[1], 0x00); // x0 low
@@ -129,6 +137,14 @@ mod tests {
129137
assert_eq!(CMD_END, 0x0A);
130138
}
131139

140+
#[test]
141+
fn test_orientation_command_values() {
142+
assert_eq!(create_orientation_command(Orientation::Portrait)[1], 0);
143+
assert_eq!(create_orientation_command(Orientation::Landscape)[1], 1);
144+
assert_eq!(create_orientation_command(Orientation::PortraitFlip)[1], 0);
145+
assert_eq!(create_orientation_command(Orientation::LandscapeFlip)[1], 1);
146+
}
147+
132148
#[test]
133149
fn test_chunk_size_physical() {
134150
// Always uses physical width: 80 * 4 = 320

0 commit comments

Comments
 (0)