Skip to content
Open
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
6 changes: 4 additions & 2 deletions lib/steami_screen/steami_screen/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
from steami_screen.colors import rgb_to_gray4, rgb_to_rgb8, rgb_to_rgb565
from steami_screen.device import (
from steami_screen.const import (
BLACK,
BLUE,
DARK,
FACES,
GRAY,
GREEN,
LIGHT,
RED,
WHITE,
YELLOW,
Screen,
)
from steami_screen.device import Screen
from steami_screen.ssd1327 import SSD1327Display

__all__ = [
"BLACK",
"BLUE",
"DARK",
"FACES",
"GRAY",
"GREEN",
"LIGHT",
Expand Down
51 changes: 51 additions & 0 deletions lib/steami_screen/steami_screen/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Constants for the steami_screen module."""

from micropython import const

# --- Color constants (RGB tuples) ---
# Grays map to exact SSD1327 levels: gray4 * 17 gives R=G=B
BLACK = (0, 0, 0)
DARK = (102, 102, 102) # gray4=6
GRAY = (153, 153, 153) # gray4=9
LIGHT = (187, 187, 187) # gray4=11
WHITE = (255, 255, 255) # gray4=15

# Accent colors (used on color displays, degrade gracefully to gray on SSD1327)
GREEN = (119, 255, 119)
RED = (255, 85, 85)
BLUE = (85, 85, 255)
YELLOW = (255, 255, 85)

# Internal grid color (used by graph widget)
GRID_DARK = (51, 51, 51)

# --- Pixel-art face bitmaps (8x8, MSB = left) ---
FACES = {
"happy": (0x00, 0x24, 0x24, 0x00, 0x00, 0x42, 0x3C, 0x00),
"sad": (0x00, 0x24, 0x24, 0x00, 0x00, 0x3C, 0x42, 0x00),
"surprised": (0x00, 0x24, 0x24, 0x00, 0x18, 0x24, 0x24, 0x18),
"sleeping": (0x00, 0x00, 0x66, 0x00, 0x00, 0x18, 0x18, 0x00),
"angry": (0x00, 0x42, 0x24, 0x24, 0x00, 0x3C, 0x42, 0x00),
"love": (0x00, 0x66, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00),
}

# --- Framebuf built-in font dimensions ---
STEAMI_CHAR_W = const(8)
STEAMI_CHAR_H = const(8)

# --- Default screen dimensions (SSD1327) ---
STEAMI_DEFAULT_WIDTH = const(128)
STEAMI_DEFAULT_HEIGHT = const(128)

# --- Gauge widget ---
STEAMI_GAUGE_START_ANGLE = const(135)
STEAMI_GAUGE_SWEEP = const(270)

# --- Graph widget layout ---
STEAMI_GRAPH_MARGIN = const(15)
STEAMI_GRAPH_X_OFFSET = const(6)
STEAMI_GRAPH_Y = const(38)
STEAMI_GRAPH_HEIGHT = const(52)
STEAMI_GRAPH_VALUE_Y = const(31)
STEAMI_GRAPH_DASH = const(3)
STEAMI_GRAPH_GAP = const(3)
136 changes: 65 additions & 71 deletions lib/steami_screen/steami_screen/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,41 @@

import math

# --- Color constants (RGB tuples) ---
# Grays map to exact SSD1327 levels: gray4 * 17 gives R=G=B
BLACK = (0, 0, 0)
DARK = (102, 102, 102) # gray4=6
GRAY = (153, 153, 153) # gray4=9
LIGHT = (187, 187, 187) # gray4=11
WHITE = (255, 255, 255) # gray4=15

# Accent colors (used on color displays, degrade gracefully to gray on SSD1327)
GREEN = (119, 255, 119)
RED = (255, 85, 85)
BLUE = (85, 85, 255)
YELLOW = (255, 255, 85)

# --- Pixel-art face bitmaps (8x8, MSB = left) ---
FACES = {
"happy": (0x00, 0x24, 0x24, 0x00, 0x00, 0x42, 0x3C, 0x00),
"sad": (0x00, 0x24, 0x24, 0x00, 0x00, 0x3C, 0x42, 0x00),
"surprised": (0x00, 0x24, 0x24, 0x00, 0x18, 0x24, 0x24, 0x18),
"sleeping": (0x00, 0x00, 0x66, 0x00, 0x00, 0x18, 0x18, 0x00),
"angry": (0x00, 0x42, 0x24, 0x24, 0x00, 0x3C, 0x42, 0x00),
"love": (0x00, 0x66, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00),
}

# --- Cardinal position names ---
from steami_screen.const import (
BLACK,
DARK,
FACES,
GRAY,
GRID_DARK,
LIGHT,
STEAMI_CHAR_H,
STEAMI_CHAR_W,
STEAMI_DEFAULT_HEIGHT,
STEAMI_DEFAULT_WIDTH,
STEAMI_GAUGE_START_ANGLE,
STEAMI_GAUGE_SWEEP,
STEAMI_GRAPH_DASH,
STEAMI_GRAPH_GAP,
STEAMI_GRAPH_HEIGHT,
STEAMI_GRAPH_MARGIN,
STEAMI_GRAPH_VALUE_Y,
STEAMI_GRAPH_X_OFFSET,
STEAMI_GRAPH_Y,
WHITE,
)


class Screen:
"""High-level wrapper around a raw display backend."""

CHAR_W = 8 # framebuf built-in font width
CHAR_H = 8 # framebuf built-in font height
# Exposed as class attributes for backward compatibility (public API).
CHAR_W = STEAMI_CHAR_W
CHAR_H = STEAMI_CHAR_H

def __init__(self, display, width=None, height=None):
self._d = display
self.width = width or getattr(display, 'width', 128)
self.height = height or getattr(display, 'height', 128)
self.width = width or getattr(display, "width", STEAMI_DEFAULT_WIDTH)
self.height = height or getattr(display, "height", STEAMI_DEFAULT_HEIGHT)

# --- Adaptive properties ---

Expand Down Expand Up @@ -92,17 +90,17 @@ def _resolve(self, at, text_len=0, scale=1):
# Margins adapted to circular screen
# Floor at ch*2+4 ensures titles stay at a consistent height
margin_ns = self._safe_margin(tw, ch * 2 + 4) # N/S
margin_ew = ch + 4 # E/W: fixed side margin
margin_ew = ch + 4 # E/W: fixed side margin

positions = {
"N": (cx - tw // 2, margin_ns),
"NE": (self.width - margin_ew - tw, margin_ns),
"E": (self.width - margin_ew - tw, cy - ch // 2),
"SE": (self.width - margin_ew - tw, self.height - margin_ns - ch),
"S": (cx - tw // 2, self.height - margin_ns - ch),
"SW": (margin_ew, self.height - margin_ns - ch),
"W": (margin_ew, cy - ch // 2),
"NW": (margin_ew, margin_ns),
"N": (cx - tw // 2, margin_ns),
"NE": (self.width - margin_ew - tw, margin_ns),
"E": (self.width - margin_ew - tw, cy - ch // 2),
"SE": (self.width - margin_ew - tw, self.height - margin_ns - ch),
"S": (cx - tw // 2, self.height - margin_ns - ch),
"SW": (margin_ew, self.height - margin_ns - ch),
"W": (margin_ew, cy - ch // 2),
"NW": (margin_ew, margin_ns),
"CENTER": (cx - tw // 2, cy - ch // 2),
}
return positions.get(at, positions["CENTER"])
Expand All @@ -114,8 +112,7 @@ def title(self, text, color=GRAY):
x, y = self._resolve("N", len(text))
self._d.text(text, x, y, color)

def value(self, val, unit=None, at="CENTER", label=None,
color=WHITE, scale=2, y_offset=0):
def value(self, val, unit=None, at="CENTER", label=None, color=WHITE, scale=2, y_offset=0):
"""Draw a large value, optionally with unit below and label above."""
text = str(val)
cx, cy = self.center
Expand Down Expand Up @@ -156,7 +153,7 @@ def value(self, val, unit=None, at="CENTER", label=None,
if unit:
unit_y = y + char_h
ux = x + tw // 2 - len(unit) * self.CHAR_W // 2
if hasattr(self._d, 'draw_medium_text'):
if hasattr(self._d, "draw_medium_text"):
self._d.draw_medium_text(unit, ux, unit_y, LIGHT)
else:
self._d.text(unit, ux, unit_y, LIGHT)
Expand All @@ -176,7 +173,7 @@ def subtitle(self, *lines, color=DARK):
block_h = (n - 1) * line_h
start_y = base_y - block_h // 2

draw = getattr(self._d, 'draw_small_text', self._d.text)
draw = getattr(self._d, "draw_small_text", self._d.text)
for i, line in enumerate(lines):
x, _ = self._resolve("S", len(line))
y = start_y + i * line_h
Expand Down Expand Up @@ -206,17 +203,19 @@ def gauge(self, val, min_val=0, max_val=100, color=LIGHT):
cx, cy = self.center
arc_w = max(5, self.radius // 9)
r = self.radius - arc_w // 2 - 1
start_angle = 135
sweep = 270
ratio = (val - min_val) / (max_val - min_val) if max_val != min_val else 0 # Avoid division by zero; show empty gauge if min=max
start_angle = STEAMI_GAUGE_START_ANGLE
sweep = STEAMI_GAUGE_SWEEP
# Avoid division by zero; show empty gauge if min=max
ratio = (val - min_val) / (max_val - min_val) if max_val != min_val else 0
ratio = max(0.0, min(1.0, ratio))

# Background arc
self._draw_arc(cx, cy, r, start_angle, sweep, DARK, arc_w)
# Filled arc
if ratio > 0:
self._draw_arc(cx, cy, r, start_angle, int(sweep * ratio),
color, arc_w + 2) # +1 to fill gaps between segments
self._draw_arc(
cx, cy, r, start_angle, int(sweep * ratio), color, arc_w + 2
) # +1 to fill gaps between segments

# Min/max labels at arc endpoints (slightly inward to stay visible)
min_t = str(int(min_val))
Expand All @@ -229,7 +228,7 @@ def gauge(self, val, min_val=0, max_val=100, color=LIGHT):
ly = int(cy + r_label * math.sin(angle_s))
rx = int(cx + r_label * math.cos(angle_e)) - len(max_t) * self.CHAR_W // 2
ry = int(cy + r_label * math.sin(angle_e))
draw_sm = getattr(self._d, 'draw_small_text', self._d.text)
draw_sm = getattr(self._d, "draw_small_text", self._d.text)
draw_sm(min_t, lx, ly, GRAY)
draw_sm(max_t, rx, ry, GRAY)

Expand All @@ -240,20 +239,19 @@ def graph(self, data, min_val=0, max_val=100, color=LIGHT):
graph area. Call title() before graph() for proper layout.
"""
cx, _cy = self.center
margin = 15
gx = margin + 6
gy = 38
margin = STEAMI_GRAPH_MARGIN
gx = margin + STEAMI_GRAPH_X_OFFSET
gy = STEAMI_GRAPH_Y
gw = self.width - margin - gx
gh = 52
gh = STEAMI_GRAPH_HEIGHT

# Current value just below title area (fixed position)
if data:
text = str(int(data[-1]))
draw_fn = getattr(self._d, 'draw_medium_text',
self._d.text)
draw_fn = getattr(self._d, "draw_medium_text", self._d.text)
tw = len(text) * self.CHAR_W
vx = cx - tw // 2
vy = 31
vy = STEAMI_GRAPH_VALUE_Y
draw_fn(text, vx, vy, WHITE)

# Y-axis labels (max, mid, min)
Expand All @@ -262,11 +260,9 @@ def _fmt(v):
return str(int(v // 1000)) + "k"
return str(int(v))

draw_sm = getattr(self._d, 'draw_small_text', self._d.text)
draw_sm = getattr(self._d, "draw_small_text", self._d.text)
mid_val = (min_val + max_val) / 2
for val, yp in [(max_val, gy),
(mid_val, gy + gh // 2),
(min_val, gy + gh)]:
for val, yp in [(max_val, gy), (mid_val, gy + gh // 2), (min_val, gy + gh)]:
label = _fmt(val)
cw = int(self.CHAR_W * 0.85)
lx = gx - len(label) * cw - 1
Expand All @@ -275,11 +271,11 @@ def _fmt(v):

# Dashed grid line at midpoint
mid_y = gy + gh // 2
dash, gap = 3, 3
dash, gap = STEAMI_GRAPH_DASH, STEAMI_GRAPH_GAP
x = gx + 1
while x < gx + gw:
x2 = min(x + dash - 1, gx + gw - 1)
self._line(x, mid_y, x2, mid_y, (51, 51, 51))
self._line(x, mid_y, x2, mid_y, GRID_DARK)
x += dash + gap

# Y axis (extend +1 to meet X axis corner)
Expand Down Expand Up @@ -458,7 +454,7 @@ def face(self, expression, compact=False, color=LIGHT):

cx, cy = self.center
if compact:
scale = self.width // 16 # 8 on 128px
scale = self.width // 16 # 8 on 128px
ox = cx - 4 * scale
oy = cy - 4 * scale - scale // 2
else:
Expand All @@ -470,8 +466,7 @@ def face(self, expression, compact=False, color=LIGHT):
byte = bitmap[row]
for col in range(8):
if byte & (0x80 >> col):
self._fill_rect(ox + col * scale, oy + row * scale,
scale, scale, color)
self._fill_rect(ox + col * scale, oy + row * scale, scale, scale, color)

# --- Level 2: Cardinal text & shapes ---

Expand Down Expand Up @@ -524,14 +519,14 @@ def _vline(self, x, y, h, c):
self._d.line(x, y, x, y + h - 1, c)

def _fill_rect(self, x, y, w, h, c):
if hasattr(self._d, 'fill_rect'):
if hasattr(self._d, "fill_rect"):
self._d.fill_rect(x, y, w, h, c)
else:
for row in range(h):
self._d.line(x, y + row, x + w - 1, y + row, c)

def _rect(self, x, y, w, h, c):
if hasattr(self._d, 'rect'):
if hasattr(self._d, "rect"):
self._d.rect(x, y, w, h, c)
else:
self._hline(x, y, w, c)
Expand All @@ -546,12 +541,12 @@ def _draw_scaled_text(self, text, x, y, color, scale):
Otherwise, the text is drawn multiple times with a 1px offset to
produce a bold effect (not a true pixel-scale zoom).
"""
if hasattr(self._d, 'draw_scaled_text'):
if hasattr(self._d, "draw_scaled_text"):
self._d.draw_scaled_text(text, x, y, color, scale)
return
# On real hardware without scaled text support, draw at scale=1
# centered at the same position (best effort)
if not hasattr(self._d, 'pixel'):
if not hasattr(self._d, "pixel"):
self._d.text(text, x, y, color)
return
# Render at 1x to a temporary buffer, then scale up
Expand All @@ -570,7 +565,7 @@ def _draw_scaled_text(self, text, x, y, color, scale):

def _draw_arc(self, cx, cy, r, start_deg, sweep_deg, color, width=3):
"""Draw a thick arc."""
if hasattr(self._d, 'draw_arc'):
if hasattr(self._d, "draw_arc"):
self._d.draw_arc(cx, cy, r, start_deg, sweep_deg, color, width)
return

Expand Down Expand Up @@ -604,8 +599,7 @@ def _draw_circle(self, cx, cy, r, color):
"""Bresenham circle."""
x, y, d = r, 0, 1 - r
while x >= y:
for sx, sy in ((x, y), (y, x), (-x, y), (-y, x),
(x, -y), (y, -x), (-x, -y), (-y, -x)):
for sx, sy in ((x, y), (y, x), (-x, y), (-y, x), (x, -y), (y, -x), (-x, -y), (-y, -x)):
px, py = cx + sx, cy + sy
if 0 <= px < self.width and 0 <= py < self.height:
self._d.pixel(px, py, color)
Expand Down
Loading