Skip to content
Merged
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
13 changes: 6 additions & 7 deletions lib/ssd1327/ssd1327/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,20 +144,20 @@ def write_data(self):


class SSD1327_I2C(SSD1327):
def __init__(self, width, height, i2c, addr=0x3C):
def __init__(self, width, height, i2c, address=0x3C):
self.i2c = i2c
self.addr = addr
self.address = address
self.cmd_arr = bytearray([REG_CMD, 0]) # Co=1, D/C#=0
self.data_list = [bytes((REG_DATA,)), None]
super().__init__(width, height)

def write_cmd(self, cmd):
self.cmd_arr[1] = cmd
self.i2c.writeto(self.addr, self.cmd_arr)
self.i2c.writeto(self.address, self.cmd_arr)

def write_data(self, data_buf):
self.data_list[1] = data_buf
self.i2c.writevto(self.addr, self.data_list)
self.i2c.writevto(self.address, self.data_list)


class SSD1327_SPI(SSD1327):
Expand All @@ -166,7 +166,6 @@ def __init__(self, width, height, spi, dc, res, cs):
dc.init(dc.OUT, value=0)
res.init(res.OUT, value=1)
cs.init(cs.OUT, value=1)
print(dc, res, cs)
self.spi = spi
self.dc = dc
self.res = res
Expand Down Expand Up @@ -202,5 +201,5 @@ def __init__(self, spi, dc, res, cs):


class WS_OLED_128X128_I2C(SSD1327_I2C):
def __init__(self, i2c, addr=0x3C):
super().__init__(128, 128, i2c, addr)
def __init__(self, i2c, address=0x3C):
super().__init__(128, 128, i2c, address)
30 changes: 30 additions & 0 deletions tests/fake_machine/framebuf_stub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Stub for the MicroPython framebuf module."""

GS4_HMSB = 2


class FrameBuffer:
"""Minimal FrameBuffer stub for testing display drivers on CPython."""

def __init__(self, buffer, width, height, fmt):
self.buffer = buffer
self.width = width
self.height = height
self.format = fmt

def fill(self, col):
val = (col & 0x0F) | ((col & 0x0F) << 4)
for i in range(len(self.buffer)):
self.buffer[i] = val

def pixel(self, x, y, col=None):
pass

def text(self, string, x, y, col=15):
pass

def line(self, x1, y1, x2, y2, col):
pass

def scroll(self, dx, dy):
pass
9 changes: 9 additions & 0 deletions tests/fake_machine/i2c.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ def writeto_mem(self, addr, reg, buf, *, addrsize=8):
self._registers[reg] = bytes(buf)
self._write_log.append((reg, bytes(buf)))

def writeto(self, addr, buf, stop=True):
self._check_address(addr)
self._write_log.append((None, bytes(buf)))

def writevto(self, addr, bufs, stop=True):
self._check_address(addr)
data = b"".join(bytes(b) for b in bufs)
self._write_log.append((None, data))

def scan(self):
if self._address is not None:
return [self._address]
Expand Down
30 changes: 28 additions & 2 deletions tests/runner/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@
from pathlib import Path


def prompt_yes_no(prompt):
"""Prompt user and wait for a single key: Enter=yes, Escape=no.

Returns True for yes, False for no.
"""
print(f" [MANUAL] {prompt} [Entree=oui / Echap=non] ", end="", flush=True)
try:
import tty
import termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
print() # newline after keypress
return ch in ("\r", "\n")
except (ImportError, OSError):
# Fallback for non-Unix or piped stdin
response = input("")
return response.strip().lower() in ("", "y", "yes")


def load_driver_mock(driver_name, fake_i2c, module_name=None):
"""Load a driver using FakeI2C on CPython.

Expand All @@ -21,6 +45,7 @@ def load_driver_mock(driver_name, fake_i2c, module_name=None):
module_name = driver_name
from tests.fake_machine import FakeI2C, FakePin
from tests.fake_machine import micropython_stub
from tests.fake_machine import framebuf_stub

# Patch modules before importing driver
import types
Expand All @@ -32,6 +57,7 @@ def load_driver_mock(driver_name, fake_i2c, module_name=None):

sys.modules["machine"] = fake_machine
sys.modules["micropython"] = micropython_stub
sys.modules["framebuf"] = framebuf_stub
Comment thread
nedseb marked this conversation as resolved.

# Patch time module to add MicroPython-specific functions
import time
Expand Down Expand Up @@ -84,6 +110,7 @@ def cleanup_driver(driver_name, module_name=None):
del sys.modules[mod_name]
sys.modules.pop("machine", None)
sys.modules.pop("micropython", None)
sys.modules.pop("framebuf", None)


def run_action(action, driver_instance):
Expand All @@ -107,8 +134,7 @@ def run_action(action, driver_instance):

if action_type == "manual":
prompt = action.get("prompt", "Manual check required")
response = input(f"\n [MANUAL] {prompt} [y/n] ")
return response.strip().lower() == "y"
return prompt_yes_no(prompt)

if action_type == "interactive":
# Prompt first, then call method (used for hold-button-and-read tests)
Expand Down
116 changes: 116 additions & 0 deletions tests/scenarios/ssd1327.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
driver: ssd1327
driver_class: WS_OLED_128X128_I2C
i2c_address: 0x3C

# I2C config for hardware tests (STeaMi board - STM32WB55)
# The display uses SPI on hardware, but i2c config is required by the bridge.
i2c:
id: 1

# Custom hardware init (STeaMi display is on SPI, not I2C)
hardware_init: |
from ssd1327.device import WS_OLED_128X128_SPI
from machine import SPI, Pin
spi = SPI(1)
dc = Pin("DATA_COMMAND_DISPLAY")
res = Pin("RST_DISPLAY")
cs = Pin("CS_DISPLAY")
dev = WS_OLED_128X128_SPI(spi, dc, res, cs)

tests:
- name: "Fill black does not crash"
action: call
method: fill
args: [0]
mode: [mock]

- name: "Fill white does not crash"
action: call
method: fill
args: [15]
mode: [mock]

- name: "Text rendering does not crash"
action: call
method: text
args: ["Test", 0, 0, 15]
mode: [mock]

- name: "Show does not crash"
action: call
method: show
mode: [mock]

- name: "Display white screen"
action: hardware_script
script: |
dev.fill(15)
dev.show()
result = True
expect_true: true
mode: [hardware]

- name: "Screen is white"
action: manual
prompt: "L'écran affiche-t-il un fond blanc uniforme ?"
expect_true: true
mode: [hardware]

- name: "Display text"
action: hardware_script
script: |
dev.fill(0)
dev.text("STeaMi", 35, 56, 15)
dev.text("Test OK", 32, 68, 15)
dev.show()
result = True
expect_true: true
mode: [hardware]

- name: "Text is visible"
action: manual
prompt: "L'écran affiche-t-il 'STeaMi' et 'Test OK' sur fond noir ?"
expect_true: true
mode: [hardware]

- name: "Display grayscale gradient"
action: hardware_script
script: |
dev.fill(0)
w = 128 // 16
for level in range(16):
x = level * w
for row in range(128):
for col in range(x, x + w):
dev.pixel(col, row, level)
dev.show()
result = True
expect_true: true
mode: [hardware]

- name: "Grayscale gradient is visible"
action: manual
prompt: "L'écran affiche-t-il 16 bandes verticales du noir au blanc ?"
expect_true: true
mode: [hardware]

- name: "Scrolling animation"
action: hardware_script
script: |
from time import sleep_ms
dev.fill(0)
dev.text("STeaMi", 35, 60, 15)
dev.show()
for i in range(128):
dev.scroll(1, 0)
dev.show()
sleep_ms(20)
result = True
expect_true: true
mode: [hardware]

- name: "Animation was smooth"
action: manual
prompt: "Le texte 'STeaMi' a-t-il defile horizontalement de maniere fluide ?"
expect_true: true
mode: [hardware]
4 changes: 2 additions & 2 deletions tests/test_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
check_result,
cleanup_driver,
load_driver_mock,
prompt_yes_no,
run_action,
)

Expand Down Expand Up @@ -108,8 +109,7 @@ def test_scenario(scenario, test, mode, port):
else:
print(f" {label}: {value} {unit}")
prompt = test.get("prompt", "Manual check")
response = input(f" [MANUAL] {prompt} [y/n] ")
result = response.strip().lower() == "y"
result = prompt_yes_no(prompt)
elif action == "call":
result = bridge.call_method(
scenario["driver"],
Expand Down
Loading