Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Commit messages follow the [Conventional Commits](https://www.conventionalcommit

**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.

- Driver scopes: `apds9960`, `bme280`, `bq27441`, `daplink_bridge`, `daplink_flash`, `gc9a01`, `hts221`, `im34dt05`, `ism330dl`, `lis2mdl`, `mcp23009e`, `ssd1327`, `steami_config`, `vl53l1x`, `wsen-hids`, `wsen-pads`
- 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`
- Domain scopes: `ci`, `docs`, `style`, `tests`, `tooling`

### Examples
Expand Down
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module.exports = {
'style',
'tests',
'tooling',
'steami_screen'
],
],
'type-enum': [
Expand Down
218 changes: 218 additions & 0 deletions lib/steami_screen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# STeaMi Screen

High-level UI library for STeaMi displays.

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.

---

## Features

* Display abstraction (works with any FrameBuffer-based backend)
* Automatic layout for round screens (cardinal positioning, safe margins)
* Text rendering with alignment and scaling
* Drawing primitives (pixel, line, rect, circle)
* 10 widgets: title, subtitle, value, bar, gauge, graph, menu, compass, watch, face

---

## Basic Usage

```python
import ssd1327
from machine import SPI, Pin
from steami_screen import Screen

spi = SPI(1)
dc = Pin("DATA_COMMAND_DISPLAY")
res = Pin("RST_DISPLAY")
cs = Pin("CS_DISPLAY")

display = ssd1327.WS_OLED_128X128_SPI(spi, dc, res, cs)
screen = Screen(display)

screen.clear()
screen.title("STeaMi")
screen.value(42, label="Temp", unit="C")
screen.show()
```

---

## API Reference

### Initialization

```python
screen = Screen(display)
```

`display` must expose `fill()`, `pixel()`, `line()`, `rect()`, `fill_rect()`, `text()`, `show()`. Width and height are auto-detected from the display backend.

---

### Drawing Primitives

```python
screen.pixel(x, y, color)
screen.line(x1, y1, x2, y2, color)
screen.rect(x, y, w, h, color, fill=False)
screen.circle(x, y, r, color, fill=False)
```

---

### Text

```python
screen.text("Hello", at="CENTER")
screen.text("Top", at="N")
screen.text("Custom", at=(10, 20))
screen.text("Big", at="CENTER", scale=2)
```

Cardinal positions: `"N"`, `"NE"`, `"E"`, `"SE"`, `"S"`, `"SW"`, `"W"`, `"NW"`, `"CENTER"`.

---

### Widgets

#### Title

```python
screen.title("STeaMi")
```

Draws text centered at the top (N position).

---

#### Subtitle

```python
screen.subtitle("Line 1", "Line 2")
```

Draws text centered at the bottom (S position). Accepts multiple lines.

---

#### Value

```python
screen.value(23.5, label="Temp", unit="C")
```

Displays a large centered value with optional label above and unit below.

---

#### Progress Bar

```python
screen.bar(value=75, max_value=100)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README example uses keyword arguments value and max_value, but Screen.bar() is defined as bar(self, val, max_val=100, ...). Using the snippet as-is will raise a TypeError. Update the example to match the actual parameter names (or rename the function parameters for a stable public API).

Suggested change
screen.bar(value=75, max_value=100)
screen.bar(val=75, max_val=100)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7b86403: bar(value=75, max_value=100)bar(75, max_val=100) to match the actual API.

```

---

#### Gauge

```python
screen.gauge(value=60, min_value=0, max_value=100, unit="C")
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README gauge snippet uses value, min_value, and max_value, but Screen.gauge() is defined as gauge(self, val, min_val=0, max_val=100, ...). The snippet will fail when copy/pasted; update the example (or adjust the API) so parameter names match.

Suggested change
screen.gauge(value=60, min_value=0, max_value=100, unit="C")
screen.gauge(val=60, min_val=0, max_val=100, unit="C")

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7b86403: gauge(value=60, min_value=0, max_value=100)gauge(60, min_val=0, max_val=100) to match the actual API.

```

Draws a 270-degree arc gauge near the screen border.

---

#### Graph

```python
screen.graph([10, 20, 15, 30], min_val=0, max_val=100)
```

Draws a scrolling line graph with the last value displayed above.

---

#### Menu

```python
screen.menu(["Item 1", "Item 2", "Item 3"], selected=1)
```

---

#### Compass

```python
screen.compass(heading=45)
```

Draws a compass with cardinal labels and a rotating needle.

---

#### Watch

```python
screen.watch(hours=10, minutes=30, seconds=15)
```

Draws an analog clock face.

---

#### Face

```python
screen.face("happy")
```

Draws a pixel-art expression. Available: `"happy"`, `"sad"`, `"surprised"`, `"sleeping"`, `"angry"`, `"love"`.

---

### Control

```python
screen.clear()
screen.show()
```

---

### Properties

```python
screen.center # (64, 64) for 128x128
screen.radius # 64 for 128x128
screen.max_chars # 16 for 128px width
```

---

## Color Constants

```python
from steami_screen import BLACK, DARK, GRAY, LIGHT, WHITE
from steami_screen import RED, GREEN, BLUE, YELLOW
```

Colors are RGB tuples. On SSD1327 they degrade to greyscale automatically.

---

## Color Utilities

```python
from steami_screen import rgb_to_gray4, rgb_to_rgb565, rgb_to_rgb8
```

| Function | Output |
|----------|--------|
| `rgb_to_gray4(color)` | 4-bit greyscale (0-15) for SSD1327 |
| `rgb_to_rgb565(color)` | 16-bit RGB565 for GC9A01 |
| `rgb_to_rgb8(color)` | RGB tuple pass-through |

All accept int values for backward compatibility.
6 changes: 6 additions & 0 deletions lib/steami_screen/manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
metadata(
description="Library for controlling the STeaMi round display.",
version="0.0.1",
)

package("steami_screen")
29 changes: 29 additions & 0 deletions lib/steami_screen/steami_screen/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from steami_screen.colors import rgb_to_gray4, rgb_to_rgb8, rgb_to_rgb565
from steami_screen.device import (
BLACK,
BLUE,
DARK,
GRAY,
GREEN,
LIGHT,
RED,
WHITE,
YELLOW,
Screen,
)

__all__ = [
"BLACK",
"BLUE",
"DARK",
"GRAY",
"GREEN",
"LIGHT",
"RED",
"WHITE",
"YELLOW",
"Screen",
"rgb_to_gray4",
"rgb_to_rgb8",
"rgb_to_rgb565",
]
49 changes: 49 additions & 0 deletions lib/steami_screen/steami_screen/colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Color conversion utilities for STeaMi display backends.

Colors are represented as RGB tuples (r, g, b) with values 0-255.
Each backend converts to its native format:
- SSD1327 : grayscale 4-bit (0-15)
- GC9A01 : RGB565 (16-bit)
- Simulator: RGB tuple (pass-through)

All functions accept legacy int values for backward compatibility.
"""


def rgb_to_gray4(color):
"""Convert an RGB tuple to a 4-bit grayscale value (0-15).

Uses BT.601 luminance: Y = 0.299*R + 0.587*G + 0.114*B
Accepts int for backward compatibility (returned as-is, clamped to 0-15).
"""
if isinstance(color, int):
return max(0, min(15, color))
r, g, b = color
luminance = (r * 77 + g * 150 + b * 29) >> 8 # 0-255
return luminance >> 4 # 0-15


def rgb_to_rgb565(color):
"""Convert an RGB tuple to a 16-bit RGB565 integer.

Accepts int for backward compatibility (treated as gray4, expanded).
"""
if isinstance(color, int):
g = max(0, min(15, color)) * 17 # 0-255
r, b = g, g
else:
r, g, b = color
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)


def rgb_to_rgb8(color):
"""Convert a color to an RGB tuple (r, g, b).

If already a tuple, returns it unchanged.
Accepts int for backward compatibility (treated as gray4, expanded).
"""
if isinstance(color, int):
v = max(0, min(15, color)) * 17 # 0-255
return (v, v, v)
return color
Loading
Loading