Skip to content

Commit cf55e75

Browse files
committed
feat(steami_screen): Add SSD1327 display wrapper.
1 parent 0aa034e commit cf55e75

4 files changed

Lines changed: 112 additions & 49 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
YELLOW,
1212
Screen,
1313
)
14+
from steami_screen.ssd1327 import SSD1327Display
1415

1516
__all__ = [
1617
"BLACK",
@@ -22,6 +23,7 @@
2223
"RED",
2324
"WHITE",
2425
"YELLOW",
26+
"SSD1327Display",
2527
"Screen",
2628
"rgb_to_gray4",
2729
"rgb_to_rgb8",

lib/steami_screen/steami_screen/device.py

Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
import math
1616

1717
# --- Color constants (RGB tuples) ---
18-
from steami_screen.colors import rgb_to_gray4
19-
2018
# Grays map to exact SSD1327 levels: gray4 * 17 gives R=G=B
2119
BLACK = (0, 0, 0)
2220
DARK = (102, 102, 102) # gray4=6
@@ -52,14 +50,6 @@ def __init__(self, display, width=None, height=None):
5250
self._d = display
5351
self.width = width or getattr(display, 'width', 128)
5452
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
6353

6454
# --- Adaptive properties ---
6555

@@ -121,7 +111,7 @@ def _resolve(self, at, text_len=0, scale=1):
121111
def title(self, text, color=GRAY):
122112
"""Draw title text at the top (N)."""
123113
x, y = self._resolve("N", len(text))
124-
self._text(text, x, y, color)
114+
self._d.text(text, x, y, color)
125115

126116
def value(self, val, unit=None, at="CENTER", label=None,
127117
color=WHITE, scale=2, y_offset=0):
@@ -156,7 +146,7 @@ def value(self, val, unit=None, at="CENTER", label=None,
156146
# Optional label above
157147
if label:
158148
lx = x + tw // 2 - len(label) * self.CHAR_W // 2
159-
self._text(label, lx, y - self.CHAR_H - 4, GRAY)
149+
self._d.text(label, lx, y - self.CHAR_H - 4, GRAY)
160150

161151
# Value (large)
162152
self._draw_scaled_text(text, x, y, color, scale)
@@ -168,7 +158,7 @@ def value(self, val, unit=None, at="CENTER", label=None,
168158
if hasattr(self._d, 'draw_medium_text'):
169159
self._d.draw_medium_text(unit, ux, unit_y, LIGHT)
170160
else:
171-
self._text(unit, ux, unit_y, LIGHT)
161+
self._d.text(unit, ux, unit_y, LIGHT)
172162

173163
def subtitle(self, *lines, color=DARK):
174164
"""Draw subtitle text at the bottom (S). Accepts multiple lines."""
@@ -185,7 +175,7 @@ def subtitle(self, *lines, color=DARK):
185175
block_h = (n - 1) * line_h
186176
start_y = base_y - block_h // 2
187177

188-
draw = getattr(self._d, 'draw_small_text', self._text)
178+
draw = getattr(self._d, 'draw_small_text', self._d.text)
189179
for i, line in enumerate(lines):
190180
x, _ = self._resolve("S", len(line))
191181
y = start_y + i * line_h
@@ -246,7 +236,7 @@ def gauge(self, val, min_val=0, max_val=100, unit=None, color=LIGHT):
246236
if hasattr(self._d, 'draw_medium_text'):
247237
self._d.draw_medium_text(unit, ux, uy, LIGHT)
248238
else:
249-
self._text(unit, ux, uy, LIGHT)
239+
self._d.text(unit, ux, uy, LIGHT)
250240

251241
# Min/max labels at arc endpoints (slightly inward to stay visible)
252242
min_t = str(int(min_val))
@@ -259,7 +249,7 @@ def gauge(self, val, min_val=0, max_val=100, unit=None, color=LIGHT):
259249
ly = int(cy + r_label * math.sin(angle_s))
260250
rx = int(cx + r_label * math.cos(angle_e)) - len(max_t) * self.CHAR_W // 2
261251
ry = int(cy + r_label * math.sin(angle_e))
262-
draw_sm = getattr(self._d, 'draw_small_text', self._text)
252+
draw_sm = getattr(self._d, 'draw_small_text', self._d.text)
263253
draw_sm(min_t, lx, ly, GRAY)
264254
draw_sm(max_t, rx, ry, GRAY)
265255

@@ -280,7 +270,7 @@ def graph(self, data, min_val=0, max_val=100, color=LIGHT):
280270
if data:
281271
text = str(int(data[-1]))
282272
draw_fn = getattr(self._d, 'draw_medium_text',
283-
self._text)
273+
self._d.text)
284274
tw = len(text) * self.CHAR_W
285275
vx = cx - tw // 2
286276
vy = 31
@@ -292,7 +282,7 @@ def _fmt(v):
292282
return str(int(v // 1000)) + "k"
293283
return str(int(v))
294284

295-
draw_sm = getattr(self._d, 'draw_small_text', self._text)
285+
draw_sm = getattr(self._d, 'draw_small_text', self._d.text)
296286
mid_val = (min_val + max_val) / 2
297287
for val, yp in [(max_val, gy),
298288
(mid_val, gy + gh // 2),
@@ -349,9 +339,9 @@ def menu(self, items, selected=0, color=WHITE):
349339
iy = y + (i - start) * item_h
350340
if i == selected:
351341
self._fill_rect(15, iy - 2, self.width - 30, item_h, DARK)
352-
self._text("> " + items[i], 18, iy, color)
342+
self._d.text("> " + items[i], 18, iy, color)
353343
else:
354-
self._text(" " + items[i], 18, iy, GRAY)
344+
self._d.text(" " + items[i], 18, iy, GRAY)
355345

356346
def compass(self, heading, color=LIGHT):
357347
"""Draw a compass with a rotating needle."""
@@ -367,7 +357,7 @@ def compass(self, heading, color=LIGHT):
367357
lx = cx + int((r + 5) * math.sin(math.radians(angle)))
368358
ly = cy - int((r + 5) * math.cos(math.radians(angle)))
369359
c = WHITE if label == "N" else GRAY
370-
self._text(label, lx - self.CHAR_W // 2, ly - self.CHAR_H // 2, c)
360+
self._d.text(label, lx - self.CHAR_W // 2, ly - self.CHAR_H // 2, c)
371361

372362
# Tick marks (8 directions)
373363
for angle in range(0, 360, 45):
@@ -435,7 +425,7 @@ def watch(self, hours, minutes, seconds=0, color=LIGHT):
435425
lx = cx + int((r - 15) * math.sin(rad))
436426
ly = cy - int((r - 15) * math.cos(rad))
437427
tw = len(text) * self.CHAR_W
438-
self._text(text, lx - tw // 2, ly - self.CHAR_H // 2, WHITE)
428+
self._d.text(text, lx - tw // 2, ly - self.CHAR_H // 2, WHITE)
439429

440430
# Hour hand (short, thick)
441431
h_angle = (hours % 12 + minutes / 60) * 30
@@ -514,7 +504,7 @@ def text(self, text, at="CENTER", color=WHITE, scale=1):
514504
if scale > 1:
515505
self._draw_scaled_text(text, x, y, color, scale)
516506
else:
517-
self._text(text, x, y, color)
507+
self._d.text(text, x, y, color)
518508

519509
def line(self, x1, y1, x2, y2, color=WHITE):
520510
self._line(x1, y1, x2, y2, color)
@@ -532,49 +522,37 @@ def rect(self, x, y, w, h, color=WHITE, fill=False):
532522
self._rect(x, y, w, h, color)
533523

534524
def pixel(self, x, y, color=WHITE):
535-
self._pixel(x, y, self._c(color))
525+
self._d.pixel(x, y, color)
536526

537527
# --- Control ---
538528

539529
def clear(self, color=BLACK):
540-
self._d.fill(self._c(color))
530+
self._d.fill(color)
541531

542532
def show(self):
543533
self._d.show()
544534

545535
# --- Internal drawing helpers ---
546536

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-
553537
def _line(self, x1, y1, x2, y2, c):
554-
self._d.line(x1, y1, x2, y2, self._c(c))
538+
self._d.line(x1, y1, x2, y2, c)
555539

556540
def _hline(self, x, y, w, c):
557-
self._d.line(x, y, x + w - 1, y, self._c(c))
541+
self._d.line(x, y, x + w - 1, y, c)
558542

559543
def _vline(self, x, y, h, c):
560-
self._d.line(x, y, x, y + h - 1, self._c(c))
544+
self._d.line(x, y, x, y + h - 1, c)
561545

562546
def _fill_rect(self, x, y, w, h, c):
563-
cc = self._c(c)
564547
if hasattr(self._d, 'fill_rect'):
565-
self._d.fill_rect(x, y, w, h, cc)
566-
elif hasattr(self._d, 'framebuf'):
567-
self._d.framebuf.fill_rect(x, y, w, h, cc)
548+
self._d.fill_rect(x, y, w, h, c)
568549
else:
569550
for row in range(h):
570-
self._d.line(x, y + row, x + w - 1, y + row, cc)
551+
self._d.line(x, y + row, x + w - 1, y + row, c)
571552

572553
def _rect(self, x, y, w, h, c):
573-
cc = self._c(c)
574554
if hasattr(self._d, 'rect'):
575-
self._d.rect(x, y, w, h, cc)
576-
elif hasattr(self._d, 'framebuf'):
577-
self._d.framebuf.rect(x, y, w, h, cc)
555+
self._d.rect(x, y, w, h, c)
578556
else:
579557
self._hline(x, y, w, c)
580558
self._hline(x, y + h - 1, w, c)
@@ -594,21 +572,21 @@ def _draw_scaled_text(self, text, x, y, color, scale):
594572
# On real hardware without scaled text support, draw at scale=1
595573
# centered at the same position (best effort)
596574
if not hasattr(self._d, 'pixel'):
597-
self._text(text, x, y, color)
575+
self._d.text(text, x, y, color)
598576
return
599577
# Render at 1x to a temporary buffer, then scale up
600578
# For MicroPython: draw each character using the display's text method
601579
# but multiple times offset for a bold effect at scale 2
602580
if scale == 2:
603581
for dx in range(2):
604582
for dy in range(2):
605-
self._text(text, x + dx, y + dy, color)
583+
self._d.text(text, x + dx, y + dy, color)
606584
elif scale == 3:
607585
for dx in range(3):
608586
for dy in range(3):
609-
self._text(text, x + dx, y + dy, color)
587+
self._d.text(text, x + dx, y + dy, color)
610588
else:
611-
self._text(text, x, y, color)
589+
self._d.text(text, x, y, color)
612590

613591
def _draw_arc(self, cx, cy, r, start_deg, sweep_deg, color, width=3):
614592
"""Draw a thick arc using individual pixels."""
@@ -623,7 +601,7 @@ def _draw_arc(self, cx, cy, r, start_deg, sweep_deg, color, width=3):
623601
x = int(cx + (r + dr) * math.cos(angle))
624602
y = int(cy + (r + dr) * math.sin(angle))
625603
if 0 <= x < self.width and 0 <= y < self.height:
626-
self._pixel(x, y, color)
604+
self._d.pixel(x, y, color)
627605

628606
def _draw_circle(self, cx, cy, r, color):
629607
"""Bresenham circle."""
@@ -633,7 +611,7 @@ def _draw_circle(self, cx, cy, r, color):
633611
(x, -y), (y, -x), (-x, -y), (-y, -x)):
634612
px, py = cx + sx, cy + sy
635613
if 0 <= px < self.width and 0 <= py < self.height:
636-
self._pixel(px, py, color)
614+
self._d.pixel(px, py, color)
637615
y += 1
638616
if d < 0:
639617
d += 2 * y + 1
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""SSD1327 display wrapper — converts RGB colors to 4-bit grayscale.
2+
3+
Wraps the raw SSD1327 driver so that steami_screen can pass RGB tuples
4+
while the hardware receives grayscale values (0-15).
5+
6+
Usage on the STeaMi board:
7+
import ssd1327
8+
from steami_screen import SSD1327Display
9+
raw = ssd1327.WS_OLED_128X128_SPI(spi, dc, res, cs)
10+
display = SSD1327Display(raw)
11+
"""
12+
13+
from steami_screen.colors import rgb_to_gray4
14+
15+
16+
class SSD1327Display:
17+
"""Thin wrapper around an SSD1327 driver that accepts RGB colors."""
18+
19+
def __init__(self, raw):
20+
self._raw = raw
21+
self.width = getattr(raw, 'width', 128)
22+
self.height = getattr(raw, 'height', 128)
23+
24+
def fill(self, color):
25+
self._raw.fill(rgb_to_gray4(color))
26+
27+
def pixel(self, x, y, color):
28+
self._raw.pixel(x, y, rgb_to_gray4(color))
29+
30+
def text(self, string, x, y, color):
31+
self._raw.text(string, x, y, rgb_to_gray4(color))
32+
33+
def line(self, x1, y1, x2, y2, color):
34+
self._raw.line(x1, y1, x2, y2, rgb_to_gray4(color))
35+
36+
def fill_rect(self, x, y, w, h, color):
37+
gray = rgb_to_gray4(color)
38+
if hasattr(self._raw, 'fill_rect'):
39+
self._raw.fill_rect(x, y, w, h, gray)
40+
else:
41+
self._raw.framebuf.fill_rect(x, y, w, h, gray)
42+
43+
def rect(self, x, y, w, h, color):
44+
gray = rgb_to_gray4(color)
45+
if hasattr(self._raw, 'rect'):
46+
self._raw.rect(x, y, w, h, gray)
47+
else:
48+
self._raw.framebuf.rect(x, y, w, h, gray)
49+
50+
def show(self):
51+
self._raw.show()

0 commit comments

Comments
 (0)