Skip to content

Commit 737f8de

Browse files
feat(steami_screen): Add steami_screen widget library. (#354)
* feat: Add 'steami_screen ' scope to the config and Contributing.md * feat(steami_screen): Add file structure * feat(steami_screen): migrate steami_screen from tutorial repo * fix(steami_screen): remove unused constants * feat(steami_screen): make width and height parameters adaptive * fix(steami_screen): handle division by zero in ratio calculation * fix(steami_screen): Use absolute import for colors module. * docs(steami_screen): Add README with API reference. * test(steami_screen): Add mock tests and rename screen.py to device.py. * docs(steami_screen): Document text scaling limitation. * fix(steami_screen): Add color conversion for SSD1327 and fix bar div by zero. * fix(steami_screen): Fix parameter names in README bar and gauge examples. --------- Co-authored-by: Sébastien NEDJAR <sebastien@nedjar.com>
1 parent f14cfa8 commit 737f8de

8 files changed

Lines changed: 1313 additions & 1 deletion

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Commit messages follow the [Conventional Commits](https://www.conventionalcommit
100100

101101
**Scopes** (optional but enforced): if provided, the scope **must** be one of the allowed values. The scope is recommended for driver-specific changes but can be omitted for cross-cutting changes.
102102

103-
- Driver scopes: `apds9960`, `bme280`, `bq27441`, `daplink_bridge`, `daplink_flash`, `gc9a01`, `hts221`, `im34dt05`, `ism330dl`, `lis2mdl`, `mcp23009e`, `ssd1327`, `steami_config`, `vl53l1x`, `wsen-hids`, `wsen-pads`
103+
- Driver scopes: `apds9960`, `bme280`, `bq27441`, `daplink_bridge`, `daplink_flash`, `gc9a01`, `hts221`, `im34dt05`, `ism330dl`, `lis2mdl`, `mcp23009e`, `ssd1327`, `steami_config`, `vl53l1x`, `wsen-hids`, `wsen-pads`, `steami_screen`
104104
- Domain scopes: `ci`, `docs`, `style`, `tests`, `tooling`
105105

106106
### Examples

commitlint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ module.exports = {
3434
'style',
3535
'tests',
3636
'tooling',
37+
'steami_screen'
3738
],
3839
],
3940
'type-enum': [

lib/steami_screen/README.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# STeaMi Screen
2+
3+
High-level UI library for STeaMi displays.
4+
5+
Provides a device-agnostic abstraction layer on top of display drivers (SSD1327, GC9A01) with a simple API to draw UI elements: text layouts, widgets, menus, icons.
6+
7+
---
8+
9+
## Features
10+
11+
* Display abstraction (works with any FrameBuffer-based backend)
12+
* Automatic layout for round screens (cardinal positioning, safe margins)
13+
* Text rendering with alignment and scaling
14+
* Drawing primitives (pixel, line, rect, circle)
15+
* 10 widgets: title, subtitle, value, bar, gauge, graph, menu, compass, watch, face
16+
17+
---
18+
19+
## Basic Usage
20+
21+
```python
22+
import ssd1327
23+
from machine import SPI, Pin
24+
from steami_screen import Screen
25+
26+
spi = SPI(1)
27+
dc = Pin("DATA_COMMAND_DISPLAY")
28+
res = Pin("RST_DISPLAY")
29+
cs = Pin("CS_DISPLAY")
30+
31+
display = ssd1327.WS_OLED_128X128_SPI(spi, dc, res, cs)
32+
screen = Screen(display)
33+
34+
screen.clear()
35+
screen.title("STeaMi")
36+
screen.value(42, label="Temp", unit="C")
37+
screen.show()
38+
```
39+
40+
---
41+
42+
## API Reference
43+
44+
### Initialization
45+
46+
```python
47+
screen = Screen(display)
48+
```
49+
50+
`display` must expose `fill()`, `pixel()`, `line()`, `rect()`, `fill_rect()`, `text()`, `show()`. Width and height are auto-detected from the display backend.
51+
52+
---
53+
54+
### Drawing Primitives
55+
56+
```python
57+
screen.pixel(x, y, color)
58+
screen.line(x1, y1, x2, y2, color)
59+
screen.rect(x, y, w, h, color, fill=False)
60+
screen.circle(x, y, r, color, fill=False)
61+
```
62+
63+
---
64+
65+
### Text
66+
67+
```python
68+
screen.text("Hello", at="CENTER")
69+
screen.text("Top", at="N")
70+
screen.text("Custom", at=(10, 20))
71+
screen.text("Big", at="CENTER", scale=2)
72+
```
73+
74+
Cardinal positions: `"N"`, `"NE"`, `"E"`, `"SE"`, `"S"`, `"SW"`, `"W"`, `"NW"`, `"CENTER"`.
75+
76+
Note: `scale=2` produces a bold effect (text drawn with 1px offset), not a true pixel-scale zoom. Backends can provide `draw_scaled_text()` for true scaling.
77+
78+
---
79+
80+
### Widgets
81+
82+
#### Title
83+
84+
```python
85+
screen.title("STeaMi")
86+
```
87+
88+
Draws text centered at the top (N position).
89+
90+
---
91+
92+
#### Subtitle
93+
94+
```python
95+
screen.subtitle("Line 1", "Line 2")
96+
```
97+
98+
Draws text centered at the bottom (S position). Accepts multiple lines.
99+
100+
---
101+
102+
#### Value
103+
104+
```python
105+
screen.value(23.5, label="Temp", unit="C")
106+
```
107+
108+
Displays a large centered value with optional label above and unit below.
109+
110+
---
111+
112+
#### Progress Bar
113+
114+
```python
115+
screen.bar(75, max_val=100)
116+
```
117+
118+
---
119+
120+
#### Gauge
121+
122+
```python
123+
screen.gauge(60, min_val=0, max_val=100, unit="C")
124+
```
125+
126+
Draws a 270-degree arc gauge near the screen border.
127+
128+
---
129+
130+
#### Graph
131+
132+
```python
133+
screen.graph([10, 20, 15, 30], min_val=0, max_val=100)
134+
```
135+
136+
Draws a scrolling line graph with the last value displayed above.
137+
138+
---
139+
140+
#### Menu
141+
142+
```python
143+
screen.menu(["Item 1", "Item 2", "Item 3"], selected=1)
144+
```
145+
146+
---
147+
148+
#### Compass
149+
150+
```python
151+
screen.compass(heading=45)
152+
```
153+
154+
Draws a compass with cardinal labels and a rotating needle.
155+
156+
---
157+
158+
#### Watch
159+
160+
```python
161+
screen.watch(hours=10, minutes=30, seconds=15)
162+
```
163+
164+
Draws an analog clock face.
165+
166+
---
167+
168+
#### Face
169+
170+
```python
171+
screen.face("happy")
172+
```
173+
174+
Draws a pixel-art expression. Available: `"happy"`, `"sad"`, `"surprised"`, `"sleeping"`, `"angry"`, `"love"`.
175+
176+
---
177+
178+
### Control
179+
180+
```python
181+
screen.clear()
182+
screen.show()
183+
```
184+
185+
---
186+
187+
### Properties
188+
189+
```python
190+
screen.center # (64, 64) for 128x128
191+
screen.radius # 64 for 128x128
192+
screen.max_chars # 16 for 128px width
193+
```
194+
195+
---
196+
197+
## Color Constants
198+
199+
```python
200+
from steami_screen import BLACK, DARK, GRAY, LIGHT, WHITE
201+
from steami_screen import RED, GREEN, BLUE, YELLOW
202+
```
203+
204+
Colors are RGB tuples. On SSD1327 they degrade to greyscale automatically.
205+
206+
---
207+
208+
## Color Utilities
209+
210+
```python
211+
from steami_screen import rgb_to_gray4, rgb_to_rgb565, rgb_to_rgb8
212+
```
213+
214+
| Function | Output |
215+
|----------|--------|
216+
| `rgb_to_gray4(color)` | 4-bit greyscale (0-15) for SSD1327 |
217+
| `rgb_to_rgb565(color)` | 16-bit RGB565 for GC9A01 |
218+
| `rgb_to_rgb8(color)` | RGB tuple pass-through |
219+
220+
All accept int values for backward compatibility.

lib/steami_screen/manifest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
metadata(
2+
description="Library for controlling the STeaMi round display.",
3+
version="0.0.1",
4+
)
5+
6+
package("steami_screen")
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from steami_screen.colors import rgb_to_gray4, rgb_to_rgb8, rgb_to_rgb565
2+
from steami_screen.device import (
3+
BLACK,
4+
BLUE,
5+
DARK,
6+
GRAY,
7+
GREEN,
8+
LIGHT,
9+
RED,
10+
WHITE,
11+
YELLOW,
12+
Screen,
13+
)
14+
15+
__all__ = [
16+
"BLACK",
17+
"BLUE",
18+
"DARK",
19+
"GRAY",
20+
"GREEN",
21+
"LIGHT",
22+
"RED",
23+
"WHITE",
24+
"YELLOW",
25+
"Screen",
26+
"rgb_to_gray4",
27+
"rgb_to_rgb8",
28+
"rgb_to_rgb565",
29+
]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Color conversion utilities for STeaMi display backends.
3+
4+
Colors are represented as RGB tuples (r, g, b) with values 0-255.
5+
Each backend converts to its native format:
6+
- SSD1327 : grayscale 4-bit (0-15)
7+
- GC9A01 : RGB565 (16-bit)
8+
- Simulator: RGB tuple (pass-through)
9+
10+
All functions accept legacy int values for backward compatibility.
11+
"""
12+
13+
14+
def rgb_to_gray4(color):
15+
"""Convert an RGB tuple to a 4-bit grayscale value (0-15).
16+
17+
Uses BT.601 luminance: Y = 0.299*R + 0.587*G + 0.114*B
18+
Accepts int for backward compatibility (returned as-is, clamped to 0-15).
19+
"""
20+
if isinstance(color, int):
21+
return max(0, min(15, color))
22+
r, g, b = color
23+
luminance = (r * 77 + g * 150 + b * 29) >> 8 # 0-255
24+
return luminance >> 4 # 0-15
25+
26+
27+
def rgb_to_rgb565(color):
28+
"""Convert an RGB tuple to a 16-bit RGB565 integer.
29+
30+
Accepts int for backward compatibility (treated as gray4, expanded).
31+
"""
32+
if isinstance(color, int):
33+
g = max(0, min(15, color)) * 17 # 0-255
34+
r, b = g, g
35+
else:
36+
r, g, b = color
37+
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
38+
39+
40+
def rgb_to_rgb8(color):
41+
"""Convert a color to an RGB tuple (r, g, b).
42+
43+
If already a tuple, returns it unchanged.
44+
Accepts int for backward compatibility (treated as gray4, expanded).
45+
"""
46+
if isinstance(color, int):
47+
v = max(0, min(15, color)) * 17 # 0-255
48+
return (v, v, v)
49+
return color

0 commit comments

Comments
 (0)