Skip to content

Commit fa9d992

Browse files
Charly-sketchnedseb
authored andcommitted
feat(steami_screen): Add SSD1327 and GC9A01 display wrappers
with example for analog clock
1 parent 5b84080 commit fa9d992

6 files changed

Lines changed: 153 additions & 45 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Tutorial 09 — Analog Watch
3+
Displays an analog clock face using the built-in RTC.
4+
"""
5+
6+
import time
7+
8+
import ssd1327
9+
from machine import RTC, SPI, Pin
10+
from steami_screen import Screen, SSD1327Display
11+
12+
# --- Screen setup ---
13+
spi = SPI(1)
14+
dc = Pin("DATA_COMMAND_DISPLAY")
15+
res = Pin("RST_DISPLAY")
16+
cs = Pin("CS_DISPLAY")
17+
18+
display = SSD1327Display(ssd1327.WS_OLED_128X128_SPI(spi, dc, res, cs))
19+
screen = Screen(display)
20+
21+
# --- RTC setup ---
22+
rtc = RTC()
23+
24+
# --- Main loop ---
25+
while True:
26+
_, _, _, _, h, m, s, _ = rtc.datetime()
27+
28+
screen.clear()
29+
screen.watch(h, m, s)
30+
screen.show()
31+
32+
time.sleep(0.5)

lib/steami_screen/steami_screen/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
YELLOW,
1212
Screen,
1313
)
14+
from steami_screen.gc9a01 import GC9A01Display
15+
from steami_screen.ssd1327 import SSD1327Display
1416

1517
__all__ = [
1618
"BLACK",
@@ -22,6 +24,8 @@
2224
"RED",
2325
"WHITE",
2426
"YELLOW",
27+
"GC9A01Display",
28+
"SSD1327Display",
2529
"Screen",
2630
"rgb_to_gray4",
2731
"rgb_to_rgb8",

lib/steami_screen/steami_screen/device.py

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,6 @@ def __init__(self, display, width=None, height=None):
5252
self._d = display
5353
self.width = width or getattr(display, 'width', 128)
5454
self.height = height or getattr(display, 'height', 128)
55-
# Detect if the backend needs greyscale ints (SSD1327) or accepts RGB tuples
56-
self._needs_gray = not hasattr(display, 'fill_rect') or hasattr(display, 'framebuf')
57-
58-
def _c(self, color):
59-
"""Convert color for the backend. Returns gray4 int for SSD1327, pass-through otherwise."""
60-
if self._needs_gray:
61-
return rgb_to_gray4(color)
62-
return color
6355

6456
# --- Adaptive properties ---
6557

@@ -121,7 +113,7 @@ def _resolve(self, at, text_len=0, scale=1):
121113
def title(self, text, color=GRAY):
122114
"""Draw title text at the top (N)."""
123115
x, y = self._resolve("N", len(text))
124-
self._text(text, x, y, color)
116+
self._d.text(text, x, y, color)
125117

126118
def value(self, val, unit=None, at="CENTER", label=None,
127119
color=WHITE, scale=2, y_offset=0):
@@ -156,7 +148,7 @@ def value(self, val, unit=None, at="CENTER", label=None,
156148
# Optional label above
157149
if label:
158150
lx = x + tw // 2 - len(label) * self.CHAR_W // 2
159-
self._text(label, lx, y - self.CHAR_H - 4, GRAY)
151+
self._d.text(label, lx, y - self.CHAR_H - 4, GRAY)
160152

161153
# Value (large)
162154
self._draw_scaled_text(text, x, y, color, scale)
@@ -168,7 +160,7 @@ def value(self, val, unit=None, at="CENTER", label=None,
168160
if hasattr(self._d, 'draw_medium_text'):
169161
self._d.draw_medium_text(unit, ux, unit_y, LIGHT)
170162
else:
171-
self._text(unit, ux, unit_y, LIGHT)
163+
self._d.text(unit, ux, unit_y, LIGHT)
172164

173165
def subtitle(self, *lines, color=DARK):
174166
"""Draw subtitle text at the bottom (S). Accepts multiple lines."""
@@ -185,7 +177,7 @@ def subtitle(self, *lines, color=DARK):
185177
block_h = (n - 1) * line_h
186178
start_y = base_y - block_h // 2
187179

188-
draw = getattr(self._d, 'draw_small_text', self._text)
180+
draw = getattr(self._d, 'draw_small_text', self._d.text)
189181
for i, line in enumerate(lines):
190182
x, _ = self._resolve("S", len(line))
191183
y = start_y + i * line_h
@@ -246,7 +238,7 @@ def gauge(self, val, min_val=0, max_val=100, unit=None, color=LIGHT):
246238
if hasattr(self._d, 'draw_medium_text'):
247239
self._d.draw_medium_text(unit, ux, uy, LIGHT)
248240
else:
249-
self._text(unit, ux, uy, LIGHT)
241+
self._d.text(unit, ux, uy, LIGHT)
250242

251243
# Min/max labels at arc endpoints (slightly inward to stay visible)
252244
min_t = str(int(min_val))
@@ -259,7 +251,7 @@ def gauge(self, val, min_val=0, max_val=100, unit=None, color=LIGHT):
259251
ly = int(cy + r_label * math.sin(angle_s))
260252
rx = int(cx + r_label * math.cos(angle_e)) - len(max_t) * self.CHAR_W // 2
261253
ry = int(cy + r_label * math.sin(angle_e))
262-
draw_sm = getattr(self._d, 'draw_small_text', self._text)
254+
draw_sm = getattr(self._d, 'draw_small_text', self._d.text)
263255
draw_sm(min_t, lx, ly, GRAY)
264256
draw_sm(max_t, rx, ry, GRAY)
265257

@@ -280,7 +272,7 @@ def graph(self, data, min_val=0, max_val=100, color=LIGHT):
280272
if data:
281273
text = str(int(data[-1]))
282274
draw_fn = getattr(self._d, 'draw_medium_text',
283-
self._text)
275+
self._d.text)
284276
tw = len(text) * self.CHAR_W
285277
vx = cx - tw // 2
286278
vy = 31
@@ -292,7 +284,7 @@ def _fmt(v):
292284
return str(int(v // 1000)) + "k"
293285
return str(int(v))
294286

295-
draw_sm = getattr(self._d, 'draw_small_text', self._text)
287+
draw_sm = getattr(self._d, 'draw_small_text', self._d.text)
296288
mid_val = (min_val + max_val) / 2
297289
for val, yp in [(max_val, gy),
298290
(mid_val, gy + gh // 2),
@@ -349,9 +341,9 @@ def menu(self, items, selected=0, color=WHITE):
349341
iy = y + (i - start) * item_h
350342
if i == selected:
351343
self._fill_rect(15, iy - 2, self.width - 30, item_h, DARK)
352-
self._text("> " + items[i], 18, iy, color)
344+
self._d.text("> " + items[i], 18, iy, color)
353345
else:
354-
self._text(" " + items[i], 18, iy, GRAY)
346+
self._d.text(" " + items[i], 18, iy, GRAY)
355347

356348
def compass(self, heading, color=LIGHT):
357349
"""Draw a compass with a rotating needle."""
@@ -367,7 +359,7 @@ def compass(self, heading, color=LIGHT):
367359
lx = cx + int((r + 5) * math.sin(math.radians(angle)))
368360
ly = cy - int((r + 5) * math.cos(math.radians(angle)))
369361
c = WHITE if label == "N" else GRAY
370-
self._text(label, lx - self.CHAR_W // 2, ly - self.CHAR_H // 2, c)
362+
self._d.text(label, lx - self.CHAR_W // 2, ly - self.CHAR_H // 2, c)
371363

372364
# Tick marks (8 directions)
373365
for angle in range(0, 360, 45):
@@ -435,7 +427,7 @@ def watch(self, hours, minutes, seconds=0, color=LIGHT):
435427
lx = cx + int((r - 15) * math.sin(rad))
436428
ly = cy - int((r - 15) * math.cos(rad))
437429
tw = len(text) * self.CHAR_W
438-
self._text(text, lx - tw // 2, ly - self.CHAR_H // 2, WHITE)
430+
self._d.text(text, lx - tw // 2, ly - self.CHAR_H // 2, WHITE)
439431

440432
# Hour hand (short, thick)
441433
h_angle = (hours % 12 + minutes / 60) * 30
@@ -514,7 +506,7 @@ def text(self, text, at="CENTER", color=WHITE, scale=1):
514506
if scale > 1:
515507
self._draw_scaled_text(text, x, y, color, scale)
516508
else:
517-
self._text(text, x, y, color)
509+
self._d.text(text, x, y, color)
518510

519511
def line(self, x1, y1, x2, y2, color=WHITE):
520512
self._line(x1, y1, x2, y2, color)
@@ -532,49 +524,41 @@ def rect(self, x, y, w, h, color=WHITE, fill=False):
532524
self._rect(x, y, w, h, color)
533525

534526
def pixel(self, x, y, color=WHITE):
535-
self._pixel(x, y, self._c(color))
527+
self._d.pixel(x, y, color)
536528

537529
# --- Control ---
538530

539531
def clear(self, color=BLACK):
540-
self._d.fill(self._c(color))
532+
self._d.fill(color)
541533

542534
def show(self):
543535
self._d.show()
544536

545537
# --- Internal drawing helpers ---
546538

547-
def _text(self, text, x, y, c):
548-
self._d.text(text, x, y, self._c(c))
549-
550-
def _pixel(self, x, y, c):
551-
self._d.pixel(x, y, self._c(c))
552-
553539
def _line(self, x1, y1, x2, y2, c):
554-
self._d.line(x1, y1, x2, y2, self._c(c))
540+
self._d.line(x1, y1, x2, y2, c)
555541

556542
def _hline(self, x, y, w, c):
557-
self._d.line(x, y, x + w - 1, y, self._c(c))
543+
self._d.line(x, y, x + w - 1, y, c)
558544

559545
def _vline(self, x, y, h, c):
560-
self._d.line(x, y, x, y + h - 1, self._c(c))
546+
self._d.line(x, y, x, y + h - 1, c)
561547

562548
def _fill_rect(self, x, y, w, h, c):
563-
cc = self._c(c)
564549
if hasattr(self._d, 'fill_rect'):
565-
self._d.fill_rect(x, y, w, h, cc)
550+
self._d.fill_rect(x, y, w, h, c)
566551
elif hasattr(self._d, 'framebuf'):
567-
self._d.framebuf.fill_rect(x, y, w, h, cc)
552+
self._d.framebuf.fill_rect(x, y, w, h, rgb_to_gray4(c))
568553
else:
569554
for row in range(h):
570-
self._d.line(x, y + row, x + w - 1, y + row, cc)
555+
self._d.line(x, y + row, x + w - 1, y + row, c)
571556

572557
def _rect(self, x, y, w, h, c):
573-
cc = self._c(c)
574558
if hasattr(self._d, 'rect'):
575-
self._d.rect(x, y, w, h, cc)
559+
self._d.rect(x, y, w, h, c)
576560
elif hasattr(self._d, 'framebuf'):
577-
self._d.framebuf.rect(x, y, w, h, cc)
561+
self._d.framebuf.rect(x, y, w, h, rgb_to_gray4(c))
578562
else:
579563
self._hline(x, y, w, c)
580564
self._hline(x, y + h - 1, w, c)
@@ -594,21 +578,21 @@ def _draw_scaled_text(self, text, x, y, color, scale):
594578
# On real hardware without scaled text support, draw at scale=1
595579
# centered at the same position (best effort)
596580
if not hasattr(self._d, 'pixel'):
597-
self._text(text, x, y, color)
581+
self._d.text(text, x, y, color)
598582
return
599583
# Render at 1x to a temporary buffer, then scale up
600584
# For MicroPython: draw each character using the display's text method
601585
# but multiple times offset for a bold effect at scale 2
602586
if scale == 2:
603587
for dx in range(2):
604588
for dy in range(2):
605-
self._text(text, x + dx, y + dy, color)
589+
self._d.text(text, x + dx, y + dy, color)
606590
elif scale == 3:
607591
for dx in range(3):
608592
for dy in range(3):
609-
self._text(text, x + dx, y + dy, color)
593+
self._d.text(text, x + dx, y + dy, color)
610594
else:
611-
self._text(text, x, y, color)
595+
self._d.text(text, x, y, color)
612596

613597
def _draw_arc(self, cx, cy, r, start_deg, sweep_deg, color, width=3):
614598
"""Draw a thick arc using individual pixels."""
@@ -623,7 +607,7 @@ def _draw_arc(self, cx, cy, r, start_deg, sweep_deg, color, width=3):
623607
x = int(cx + (r + dr) * math.cos(angle))
624608
y = int(cy + (r + dr) * math.sin(angle))
625609
if 0 <= x < self.width and 0 <= y < self.height:
626-
self._pixel(x, y, color)
610+
self._d.pixel(x, y, color)
627611

628612
def _draw_circle(self, cx, cy, r, color):
629613
"""Bresenham circle."""
@@ -633,7 +617,7 @@ def _draw_circle(self, cx, cy, r, color):
633617
(x, -y), (y, -x), (-x, -y), (-y, -x)):
634618
px, py = cx + sx, cy + sy
635619
if 0 <= px < self.width and 0 <= py < self.height:
636-
self._pixel(px, py, color)
620+
self._d.pixel(px, py, color)
637621
y += 1
638622
if d < 0:
639623
d += 2 * y + 1
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
GC9A01 display wrapper — converts RGB colors to RGB565.
3+
4+
Wraps the raw GC9A01 driver so that steami_screen can pass RGB tuples
5+
while the hardware receives 16-bit RGB565 values.
6+
7+
Usage on the STeaMi board:
8+
import gc9a01
9+
from steami_gc9a01 import GC9A01Display
10+
raw = gc9a01.GC9A01(spi, dc, cs, rst, ...)
11+
display = GC9A01Display(raw)
12+
"""
13+
14+
from steami_screen.colors import rgb_to_rgb565
15+
16+
17+
class GC9A01Display:
18+
"""Thin wrapper around a GC9A01 driver that accepts RGB colors."""
19+
20+
def __init__(self, raw, width=240, height=240):
21+
self._raw = raw
22+
self.width = width
23+
self.height = height
24+
25+
def fill(self, color):
26+
self._raw.fill(rgb_to_rgb565(color))
27+
28+
def pixel(self, x, y, color):
29+
self._raw.pixel(x, y, rgb_to_rgb565(color))
30+
31+
def text(self, string, x, y, color):
32+
self._raw.text(string, x, y, rgb_to_rgb565(color))
33+
34+
def line(self, x1, y1, x2, y2, color):
35+
self._raw.line(x1, y1, x2, y2, rgb_to_rgb565(color))
36+
37+
def fill_rect(self, x, y, w, h, color):
38+
self._raw.fill_rect(x, y, w, h, rgb_to_rgb565(color))
39+
40+
def rect(self, x, y, w, h, color):
41+
self._raw.rect(x, y, w, h, rgb_to_rgb565(color))
42+
43+
def show(self):
44+
self._raw.show()

lib/steami_screen/steami_screen/screen.py

Whitespace-only changes.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
SSD1327 display wrapper — converts RGB colors to 4-bit grayscale.
3+
4+
Wraps the raw SSD1327 driver so that steami_screen can pass RGB tuples
5+
while the hardware receives grayscale values (0-15).
6+
7+
Usage on the STeaMi board:
8+
import ssd1327
9+
from steami_ssd1327 import SSD1327Display
10+
raw = ssd1327.WS_OLED_128X128_SPI(spi, dc, res, cs)
11+
display = SSD1327Display(raw)
12+
"""
13+
14+
from steami_screen.colors import rgb_to_gray4
15+
16+
17+
class SSD1327Display:
18+
"""Thin wrapper around an SSD1327 driver that accepts RGB colors."""
19+
20+
def __init__(self, raw):
21+
self._raw = raw
22+
self.width = getattr(raw, 'width', 128)
23+
self.height = getattr(raw, 'height', 128)
24+
25+
def fill(self, color):
26+
self._raw.fill(rgb_to_gray4(color))
27+
28+
def pixel(self, x, y, color):
29+
self._raw.pixel(x, y, rgb_to_gray4(color))
30+
31+
def text(self, string, x, y, color):
32+
self._raw.text(string, x, y, rgb_to_gray4(color))
33+
34+
def line(self, x1, y1, x2, y2, color):
35+
self._raw.line(x1, y1, x2, y2, rgb_to_gray4(color))
36+
37+
def fill_rect(self, x, y, w, h, color):
38+
self._raw.fill_rect(x, y, w, h, rgb_to_gray4(color))
39+
40+
def rect(self, x, y, w, h, color):
41+
self._raw.rect(x, y, w, h, rgb_to_gray4(color))
42+
43+
def show(self):
44+
self._raw.show()

0 commit comments

Comments
 (0)