diff --git a/python_payload/apps/cap_touch_demo.py b/python_payload/apps/cap_touch_demo.py index c1d9c89eb7e8f2220c7fce0edfe03ff358e90cd0..8a866b7f164717f32a763cc355c80a4016267a28 100644 --- a/python_payload/apps/cap_touch_demo.py +++ b/python_payload/apps/cap_touch_demo.py @@ -1,4 +1,8 @@ from st3m import logging +from st4m import application +from st4m.goose import List +from st4m.ui.ctx import Ctx +from st4m.input import InputState log = logging.Log(__name__, level=logging.INFO) log.info("import") @@ -8,16 +12,15 @@ import math import time import hardware -from st4m import application class Dot: - def __init__(self, size, imag, real): + def __init__(self, size: float, imag: float, real: float) -> None: self.size = size self.imag = imag self.real = real - def draw(self, i, ctx): + def draw(self, i: int, ctx: Ctx) -> None: imag = self.imag real = self.real size = self.size @@ -31,9 +34,9 @@ class Dot: class CapTouchDemo(application.Application): - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__(name) - self.dots = [] + self.dots: List[Dot] = [] self.last_calib = None # self.ui_autocalib = ui.IconLabel("Autocalib done", size=30) @@ -52,36 +55,35 @@ class CapTouchDemo(application.Application): def think(self, ins: InputState, delta_ms: int) -> None: super().think(ins, delta_ms) self.dots = [] - cps = captouch.read() for i in range(10): - petal = cps.petals[i] - (rad, phi) = petal.position + (rad, phi) = ins.petal_pos[i] size = 4 - if petal.pressed: + if ins.petal_pressed[i]: size += 4 - x = 70 + (rad / 1000) + x = 70 + (rad / 1000) + 0j x += (phi / 600) * 1j rot = cmath.exp(2j * math.pi * i / 10) x = x * rot self.dots.append(Dot(size, x.imag, x.real)) - def draw(self, ctx): + def draw(self, ctx: Ctx) -> None: # print(self.last_calib) # TODO (q3k) bug: we have to do this, otherwise we have horrible blinking ctx.rgb(1, 1, 1) ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill() + for i, dot in enumerate(self.dots): dot.draw(i, ctx) # if not self.last_calib is None and self.last_calib > 0: # self.last_calib -= 1 # self.ui_autocalib.draw(ctx) - def do_autocalib(self, data): - log.info("Performing captouch autocalibration") - captouch.calibration_request() - self.last_calib = 50 + # def do_autocalib(self, data) -> None: + # log.info("Performing captouch autocalibration") + # captouch.calibration_request() + # self.last_calib = 50 app = CapTouchDemo("cap touch") diff --git a/python_payload/apps/demo_worms4.py b/python_payload/apps/demo_worms4.py index 74261e3a9f8756bd2ff61f89548c40b4a7df64c2..b0d9de8c66a197077a1f87a9faeefea2f2ffbae3 100644 --- a/python_payload/apps/demo_worms4.py +++ b/python_payload/apps/demo_worms4.py @@ -4,9 +4,11 @@ import time import math # flow3r imports -from st3m import event, ui from st4m import application from st4m import Ctx, InputState +from st4m.property import BLUE, WHITE +from st4m.goose import Optional +from st4m.utils import xy_from_polar tau = 2 * math.pi @@ -14,7 +16,7 @@ tau = 2 * math.pi # Subclass Application class AppWorms(application.Application): - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__(name) # HACK: we work against double buffering by keeping note of how many @@ -38,32 +40,26 @@ class AppWorms(application.Application): self.just_shown = True - def on_enter(self): + def on_enter(self) -> None: # print("on foreground") super().on_enter() self.just_shown = True - def draw(self, ctx): + def draw(self, ctx: Ctx) -> None: if self.bufn <= 5: # TODO (q3k) bug: we have to do this, otherwise we have horrible blinking - ctx.rgb(*ui.BLUE).rectangle( - -ui.WIDTH / 2, -ui.HEIGHT / 2, ui.WIDTH, ui.HEIGHT - ).fill() + ctx.rgb(*BLUE).rectangle(-120, -120, 240, 240).fill() ctx.rgb(1, 1, 1) - ctx.rgb(*ui.BLUE).rectangle( - -ui.WIDTH / 2, -ui.HEIGHT / 2, ui.WIDTH, ui.HEIGHT - ).fill() + ctx.rgb(*BLUE).rectangle(-120, -120, 240, 240).fill() ctx.rgb(1, 1, 1) - ctx.rgb(*ui.BLUE).rectangle( - -ui.WIDTH / 2, -ui.HEIGHT / 2, ui.WIDTH, ui.HEIGHT - ).fill() + ctx.rgb(*BLUE).rectangle(-120, -120, 240, 240).fill() ctx.text_align = ctx.CENTER ctx.text_baseline = ctx.MIDDLE - ctx.move_to(0, 0).rgb(*ui.WHITE).text("touch me :)") + ctx.move_to(0, 0).rgb(*WHITE).text("touch me :)") self.bufn += 1 return @@ -83,32 +79,32 @@ class AppWorms(application.Application): for index, petal in enumerate(self.input.captouch.petals): if petal.pressed or petal.repeated: self.worms.append(Worm(tau * index / 10 + math.pi)) + while len(self.worms) > 10: + self.worms.pop(0) - def handle_input(self, data): - worms = self.worms - worms.append(Worm(data.get("index", 0) * 2 * math.pi / 10 + math.pi)) - while len(worms) > 10: - worms.pop(0) + +def randrgb() -> tuple[float, float, float]: + return (random.random(), random.random(), random.random()) class Worm: - def __init__(self, direction=None): - self.color = ui.randrgb() + def __init__(self, direction: Optional[float] = None) -> None: + self.color = randrgb() - if direction: + if direction is not None: self.direction = direction else: self.direction = random.random() * math.pi * 2 self.size = 50 self.speed = self.size / 5 - (x, y) = ui.xy_from_polar(100, self.direction) + (x, y) = xy_from_polar(100, self.direction) self.x = x self.y = y # (self.dx,self.dy) = xy_from_polar(1,self.direction) self._lastdist = 0.0 - def draw(self, ctx): + def draw(self, ctx: Ctx) -> None: ctx.rgb(*self.color) ctx.round_rectangle( self.x - self.size / 2, @@ -118,12 +114,14 @@ class Worm: self.size // 2, ).fill() - def mutate(self): - self.color = [ - max(0, min(1, x + ((random.random() - 0.5) * 0.3))) for x in self.color - ] + def mutate(self) -> None: + self.color = ( + max(0, min(1, self.color[0] + ((random.random() - 0.5) * 0.3))), + max(0, min(1, self.color[1] + ((random.random() - 0.5) * 0.3))), + max(0, min(1, self.color[2] + ((random.random() - 0.5) * 0.3))), + ) - def move(self): + def move(self) -> None: dist = math.sqrt(self.x**2 + self.y**2) target_size = (130 - dist) / 3 @@ -137,7 +135,7 @@ class Worm: self.direction += (random.random() - 0.5) * math.pi / 4 - (dx, dy) = ui.xy_from_polar(self.speed, self.direction) + (dx, dy) = xy_from_polar(self.speed, self.direction) self.x += dx self.y += dy diff --git a/python_payload/apps/harmonic_demo.py b/python_payload/apps/harmonic_demo.py index ec9ac5305bbc24e2a16b92129b13f7874000fa31..46398229f4992fe7e078a5a6b94d651504f5d2c3 100644 --- a/python_payload/apps/harmonic_demo.py +++ b/python_payload/apps/harmonic_demo.py @@ -1,7 +1,11 @@ from bl00mbox import tinysynth -from hardware import * import captouch import leds +import hardware + +from st4m.ui.ctx import Ctx +from st4m.goose import List +from st4m.input import InputState chords = [ [-4, 0, 3, 8, 10], @@ -16,12 +20,12 @@ from st4m.application import Application class HarmonicApp(Application): - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__(name) - self.color_intensity = 0 - self.chord_index = None - self.chord = None + self.color_intensity = 0.0 + self.chord_index = 0 + self.chord: List[int] = [] self.synths = [tinysynth(440) for i in range(15)] for i, synth in enumerate(self.synths): synth.decay_ms(100) @@ -44,7 +48,7 @@ class HarmonicApp(Application): synth.release_ms(800) self._set_chord(3) - def _set_chord(self, i): + def _set_chord(self, i: int) -> None: hue = int(72 * (i + 0.5)) % 360 if i != self.chord_index: self.chord_index = i @@ -53,12 +57,12 @@ class HarmonicApp(Application): self.chord = chords[i] leds.update() - def draw(self, ctx): + def draw(self, ctx: Ctx) -> None: i = self.color_intensity ctx.rgb(i, i, i).rectangle(-120, -120, 240, 240).fill() ctx.rgb(0, 0, 0) - scope_draw(ctx) + hardware.scope_draw(ctx) ctx.fill() def think(self, ins: InputState, delta_ms: int) -> None: diff --git a/python_payload/apps/melodic_demo.py b/python_payload/apps/melodic_demo.py index c1496508385418ecfd5bbd649cc5a2c34c897222..78000e5dd3dfa0a5e71fcc7f5b6d829670edc470 100644 --- a/python_payload/apps/melodic_demo.py +++ b/python_payload/apps/melodic_demo.py @@ -3,18 +3,22 @@ from hardware import * import captouch import leds +from st4m.goose import List +from st4m.input import InputState +from st4m.ui.ctx import Ctx + octave = 0 -synths = [] +synths: List[tinysynth] = [] scale = [0, 2, 4, 5, 7, 9, 11] -def highlight_bottom_petal(num, r, g, b): +def highlight_bottom_petal(num: int, r: int, g: int, b: int) -> None: start = 4 + 8 * num for i in range(7): leds.set_rgb(((i + start) % 40), r, g, b) -def change_playing_field_color(r, g, b): +def change_playing_field_color(r: int, g: int, b: int) -> None: highlight_bottom_petal(0, r, g, b) highlight_bottom_petal(1, r, g, b) highlight_bottom_petal(3, r, g, b) @@ -27,7 +31,7 @@ def change_playing_field_color(r, g, b): leds.update() -def adjust_playing_field_to_octave(): +def adjust_playing_field_to_octave() -> None: global octave if octave == -1: change_playing_field_color(0, 0, 55) @@ -37,13 +41,12 @@ def adjust_playing_field_to_octave(): change_playing_field_color(0, 55, 0) -def run(): +def run(ins: InputState) -> None: global scale global octave global synths - cts = captouch.read() for i in range(10): - if cts.petals[i].pressed: + if ins.petal_pressed[i]: if i == 4: octave = -1 adjust_playing_field_to_octave() @@ -63,7 +66,7 @@ def run(): synths[0].start() -def init(): +def init() -> None: global synths for i in range(1): synths += [tinysynth(440)] @@ -71,30 +74,31 @@ def init(): synth.decay_ms(100) -def foreground(): +def foreground() -> None: adjust_playing_field_to_octave() from st4m.application import Application +# TODO(q3k): properly port this app class MelodicApp(Application): - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__(name) init() - def draw(self, ctx): + def draw(self, ctx: Ctx) -> None: ctx.rgb(1, 1, 1).rectangle(-120, -120, 240, 240).fill() ctx.rgb(0, 0, 0) scope_draw(ctx) ctx.fill() - def on_enter(self): + def on_enter(self) -> None: foreground() def think(self, ins: InputState, delta_ms: int) -> None: super().think(ins, delta_ms) - run() + run(ins) app = MelodicApp("melodic") diff --git a/python_payload/apps/nick.py b/python_payload/apps/nick.py index e4b2df6e2ca5177607bf865116b822aee6c6622d..085f6cdfb4fd7b412f4ed273e295d0e610b16cc2 100644 --- a/python_payload/apps/nick.py +++ b/python_payload/apps/nick.py @@ -1,30 +1,65 @@ from st4m.application import Application from st4m.property import PUSH_RED, GO_GREEN, BLACK +from st4m.goose import Dict, Any +from st4m.ui.ctx import Ctx +from st4m.input import InputState import leds import json +class Configuration: + def __init__(self) -> None: + self.name = "flow3r" + self.size: int = 75 + self.font: int = 5 + + @classmethod + def load(cls, path: str) -> "Configuration": + res = cls() + try: + with open(path) as f: + jsondata = f.read() + data = json.loads(jsondata) + except OSError: + data = {} + if "name" in data and type(data["name"]) == str: + res.name = data["name"] + if "size" in data: + if type(data["size"]) == float: + res.size = int(data["size"]) + if type(data["size"]) == int: + res.size = data["size"] + if "font" in data and type(data["font"]) == int: + res.font = data["font"] + return res + + def save(self, path: str) -> None: + d = { + "name": self.name, + "size": self.size, + "font": self.font, + } + jsondata = json.dumps(d) + with open(path, "w") as f: + f.write(jsondata) + f.close() + + class NickApp(Application): - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__(name) - self._scale = 1 + self._scale = 1.0 self._dir = 1 self._led = 0.0 - self._filename = "nick.json" - self._defaults = { - "name": "flow3r", - "size": 75, - "font": 5, - } - self._data = {} - self._load() + self._filename = "/flash/nick.json" + self._config = Configuration.load(self._filename) - def draw(self, ctx): + def draw(self, ctx: Ctx) -> None: ctx.text_align = ctx.CENTER ctx.text_baseline = ctx.MIDDLE - ctx.font_size = self._data["size"] - ctx.font = ctx.get_font_name(self._data["font"]) + ctx.font_size = self._config.size + ctx.font = ctx.get_font_name(self._config.font) # TODO (q3k) bug: we have to do this, otherwise we have horrible blinking ctx.rgb(1, 1, 1) @@ -36,7 +71,7 @@ class NickApp(Application): ctx.move_to(0, 0) ctx.save() ctx.scale(self._scale, 1) - ctx.text(self._data["name"]) + ctx.text(self._config.name) ctx.restore() leds.set_hsv(int(self._led), abs(self._scale) * 360, 1, 0.2) @@ -44,11 +79,8 @@ class NickApp(Application): leds.update() # ctx.fill() - def on_enter(self): - super().on_enter() - - def on_exit(self): - self._save_to_json(self._data) + def on_exit(self) -> None: + self._config.save(self._filename) def think(self, ins: InputState, delta_ms: int) -> None: super().think(ins, delta_ms) @@ -62,27 +94,5 @@ class NickApp(Application): if self._led >= 40: self._led = 0 - def _load(self): - self._data = self._defaults.copy() - self._data.update(self._load_from_json()) - - def _load_from_json(self): - jsondata = "" - data = {} - try: - with open(self._filename) as f: - jsondata = f.read() - data = json.loads(jsondata) - except OSError: - pass - return data - - def _save_to_json(self, data): - jsondata = json.dumps(data) - - with open(self._filename, "w") as f: - f.write(jsondata) - f.close() - app = NickApp("nick") diff --git a/python_payload/main.py b/python_payload/main.py index 1807af46193c8d20e0440f4007d7d6e66fdf9215..76cbbda2f40b5cb86eb20ee3dcfc09ef75467a7a 100644 --- a/python_payload/main.py +++ b/python_payload/main.py @@ -44,7 +44,7 @@ from apps.demo_worms4 import app as worms from apps.harmonic_demo import app as harmonic from apps.melodic_demo import app as melodic from apps.nick import app as nick -from apps.cap_touch_demo import app as captouch +from apps.cap_touch_demo import app as captouch_demo # Set the view_managers for the apps, otherwise leaving the app (back) will not work @@ -52,7 +52,7 @@ worms._view_manager = vm harmonic._view_manager = vm melodic._view_manager = vm nick._view_manager = vm -captouch._view_manager = vm +captouch_demo._view_manager = vm menu_music = SimpleMenu( [ @@ -69,7 +69,7 @@ menu_music = SimpleMenu( menu_apps = SimpleMenu( [ MenuItemBack(), - MenuItemForeground("captouch", captouch), + MenuItemForeground("captouch", captouch_demo), MenuItemForeground("worms", worms), ], vm, diff --git a/python_payload/main_st4m.py b/python_payload/main_st4m.py index 04aa335d21a94119dc3be8e240c2875c2417f583..437e8e2b02984326a3e7e83a377f7792ede1602c 100644 --- a/python_payload/main_st4m.py +++ b/python_payload/main_st4m.py @@ -47,6 +47,7 @@ menu_apps = SimpleMenu( vm, ) + class USBIcon(Responder): """ Found in the bargain bin at an Aldi Süd. diff --git a/python_payload/st3m/logging.py b/python_payload/st3m/logging.py index 7ff504555e892757792978aea5a8a36075a8d8a6..a5e37649846aea70c8a8b7dbc1dd0653dc118dc8 100644 --- a/python_payload/st3m/logging.py +++ b/python_payload/st3m/logging.py @@ -1,43 +1,63 @@ import sys import time +from st4m.goose import Enum -DEBUG = 0 -INFO = 1 -WARNING = 2 -ERROR = 3 -# this is so ugly, but works -levels = ["DEBUG", "INFO", "WARNING", "ERROR"] +class Level(Enum): + DEBUG = 0 + INFO = 1 + WARNING = 2 + ERROR = 3 + + +DEBUG = Level.DEBUG +INFO = Level.INFO +WARNING = Level.WARNING +ERROR = Level.ERROR + +_log_levels = { + DEBUG: 0, + INFO: 1, + WARNING: 2, + ERROR: 3, +} + +_log_level_names = { + DEBUG: "DEBUG", + INFO: "INFO", + WARNING: "WARNING", + ERROR: "ERROR", +} class Log: - def __init__(self, name="log", level=INFO): + def __init__(self, name: str = "log", level: Level = INFO): self.name = name self.level = level self.logstring = "{timestamp} {name} ({level}): {msg}" - def debug(self, msg): + def debug(self, msg: str) -> None: self.message(msg, DEBUG) - def info(self, msg): + def info(self, msg: str) -> None: self.message(msg, INFO) - def warning(self, msg): + def warning(self, msg: str) -> None: self.message(msg, WARNING) - def error(self, msg): + def error(self, msg: str) -> None: self.message(msg, ERROR) - def message(self, msg, level): - if self.level <= level: + def message(self, msg: str, level: Level) -> None: + if _log_levels[self.level] <= _log_levels[level]: self._emit( self.logstring.format( timestamp=time.ticks_ms() / 1000, name=self.name, msg=msg, - level=levels[level], + level=_log_level_names[level], ) ) - def _emit(self, line): + def _emit(self, line: str) -> None: print(line) diff --git a/python_payload/st3m/system/__init__.py b/python_payload/st3m/system/__init__.py index f2512322c2b05e13b73849779ca80d1a941cfcfd..256751f87b74bc15b48b9b0ccb629204a6999e35 100644 --- a/python_payload/st3m/system/__init__.py +++ b/python_payload/st3m/system/__init__.py @@ -1,41 +1,7 @@ import hardware as _hardware +import audio as _audio import captouch as _captouch - -class NamedObject: - def __init__(self, name="foo"): - self.__name = name - - def __repr__(self): - return self.__name - - -class MockObject(NamedObject): - def __getattr__(self, attr): - attr_name = "{}.{}".format(str(self), attr) - print("mock attr", attr_name) - return MockObject(attr_name) - - def __call__(self, *args, **kwargs): - call_name = "{}({}{})".format(str(self), args, kwargs) - print("mock call", call_name) - return MockObject(call_name) - - -try: - import audio as _audio -except ModuleNotFoundError: - print("no real audio, using mock module") - _audio = MockObject("audio") - - -try: - import audio as _audio -except ModuleNotFoundError: - print("no real audio, using mock module") - _audio = MockObject("audio") - - hardware = _hardware captouch = _captouch audio = _audio diff --git a/python_payload/st4m/application.py b/python_payload/st4m/application.py index 5cb0255db7967a7a2d50f083eb83c4e14b57a759..165ab5fccca80f811e964e5b82d764c6784c1aed 100644 --- a/python_payload/st4m/application.py +++ b/python_payload/st4m/application.py @@ -1,14 +1,15 @@ -from st4m.ui.view import ViewWithInputState, ViewTransitionSwipeRight +from st4m.ui.view import ViewWithInputState, ViewTransitionSwipeRight, ViewManager from st4m.input import InputState +from st4m.goose import Optional class Application(ViewWithInputState): - def __init__(self, name: str = __name__): + def __init__(self, name: str = __name__) -> None: self._name = name - self._view_manager = None + self._view_manager: Optional[ViewManager] = None super().__init__() - def on_exit(self): + def on_exit(self) -> None: pass def think(self, ins: InputState, delta_ms: int) -> None: diff --git a/python_payload/st4m/goose.py b/python_payload/st4m/goose.py index bb1c0224894db616163d3cefba86b7897c6868f1..8fb29609b4f36d94518a1b54729918f629f4ef7a 100644 --- a/python_payload/st4m/goose.py +++ b/python_payload/st4m/goose.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: class ABCBase(metaclass=ABCMeta): pass - from typing import List, Optional, Tuple + from typing import List, Optional, Tuple, Dict, Any from enum import Enum else: # We're in CPython or Micropython. @@ -31,13 +31,15 @@ else: return _fail try: - from typing import List, Optional, Tuple + from typing import List, Optional, Tuple, Dict, Any from enum import Enum except ImportError: # We're in Micropython. List = None Optional = None Tuple = None + Dict = None + Any = None class Enum: pass @@ -51,4 +53,6 @@ __all__ = [ "Optional", "Enum", "Tuple", + "Dict", + "Any", ] diff --git a/python_payload/st4m/input.py b/python_payload/st4m/input.py index b982bfe62392e566c26354274739b925dc88a9a8..b85f918a762d464e46f9eefa6605ae88002fffcc 100644 --- a/python_payload/st4m/input.py +++ b/python_payload/st4m/input.py @@ -1,4 +1,4 @@ -from st4m.goose import List, Optional, Enum +from st4m.goose import List, Optional, Enum, Tuple from st4m.ui.ctx import Ctx import hardware @@ -19,7 +19,7 @@ class InputState: self, petal_pressed: List[bool], # petal_pads: List[List[int]], - petal_pos: List[List[int]], + petal_pos: List[Tuple[int, int]], left_button: int, right_button: int, ) -> None: @@ -41,10 +41,7 @@ class InputState: # [hardware.captouch_get_petal_pad(petal_ix, pad_ix) for pad_ix in range(3)] # for petal_ix in range(10) # ] - petal_pos = [ - ctx.petals[petal_ix].position - for petal_ix in range(10) - ] + petal_pos = [cts.petals[petal_ix].position for petal_ix in range(10)] left_button = hardware.left_button_get() right_button = hardware.right_button_get() @@ -192,82 +189,82 @@ class PetalState(Pressable): super().__init__(False) -class Touchable(Pressable): - """ - An object that can receive touch gestures (captouch petal) - """ - - BEGIN = "begin" - RESTING = "resting" - MOVED = "moved" - ENDED = "ended" - - def __init__(self, pos=(0, 0)): - super().__init__(False) - self._pos = pos - self._prev_pos = pos - self._polar = self._prev_polar = (0, 0) - self._dx = 0.0 - self._dy = 0.0 - self._dphi = 0.0 - self._dr = 0.0 - - def _update(self, ts, state, pos): - self._prev_pos = self._pos - self._pos = pos - - self._prev_polar = self._polar - - self._dx = self._pos[0] - self._prev_pos[0] - self._dy = self._pos[1] - self._prev_pos[1] - - x0 = -pos[0] / 500 - x1 = pos[1] / 500 - - phi = math.atan2(x0, x1) - math.pi / 2 - r = math.sqrt(x0 * x0 + x1 * x1) - self._polar = (r, phi) - - self._dr = self._polar[0] - self._prev_polar[0] - - v = self._polar[1] - self._prev_polar[1] - - for sign in [1, -1]: - t = v + sign * 2 * math.pi - if abs(t) < abs(v): - v = t - - self._dphi = v - - super()._update(ts, state) - if self.state != self.DOWN: - self._dx = self._dy = self._dphi = self._dr = 0 - else: - pass - # print(r, phi, self._dr, self._dphi) - - def phase(self) -> str: - if self.state == self.UP: - return self.UP - if self.state == self.RELEASED: - return self.ENDED - if self.state == self.PRESSED: - return self.BEGIN - if self.state == self.DOWN or self.state == self.REPEATED: - if abs(self._dr) > 1 or abs(self._dphi) > 0.01: - return self.MOVED - else: - return self.RESTING - return "HUHUHU" - - -class PetalGestureState(Touchable): - def __init__(self, ix: int) -> None: - self.ix = ix - super().__init__() - - def _update(self, ts: int, hr: InputState) -> None: - super()._update(ts, hr.petal_pressed[self.ix], hr.petal_pos[self.ix]) +# class Touchable(Pressable): +# """ +# An object that can receive touch gestures (captouch petal) +# """ +# +# BEGIN = "begin" +# RESTING = "resting" +# MOVED = "moved" +# ENDED = "ended" +# +# def __init__(self, pos: tuple[int,int] = (0, 0)) -> None: +# super().__init__(False) +# self._pos = pos +# self._prev_pos = pos +# self._polar = self._prev_polar = (0, 0) +# self._dx = 0.0 +# self._dy = 0.0 +# self._dphi = 0.0 +# self._dr = 0.0 +# +# def _update(self, ts, state, pos) -> None: +# self._prev_pos = self._pos +# self._pos = pos +# +# self._prev_polar = self._polar +# +# self._dx = self._pos[0] - self._prev_pos[0] +# self._dy = self._pos[1] - self._prev_pos[1] +# +# x0 = -pos[0] / 500 +# x1 = pos[1] / 500 +# +# phi = math.atan2(x0, x1) - math.pi / 2 +# r = math.sqrt(x0 * x0 + x1 * x1) +# self._polar = (r, phi) +# +# self._dr = self._polar[0] - self._prev_polar[0] +# +# v = self._polar[1] - self._prev_polar[1] +# +# for sign in [1, -1]: +# t = v + sign * 2 * math.pi +# if abs(t) < abs(v): +# v = t +# +# self._dphi = v +# +# super()._update(ts, state) +# if self.state != self.DOWN: +# self._dx = self._dy = self._dphi = self._dr = 0 +# else: +# pass +# # print(r, phi, self._dr, self._dphi) +# +# def phase(self) -> str: +# if self.state == self.UP: +# return self.UP +# if self.state == self.RELEASED: +# return self.ENDED +# if self.state == self.PRESSED: +# return self.BEGIN +# if self.state == self.DOWN or self.state == self.REPEATED: +# if abs(self._dr) > 1 or abs(self._dphi) > 0.01: +# return self.MOVED +# else: +# return self.RESTING +# return "HUHUHU" + + +# class PetalGestureState(Touchable): +# def __init__(self, ix: int) -> None: +# self.ix = ix +# super().__init__() +# +# def _update(self, ts: int, hr: InputState) -> None: +# super()._update(ts, hr.petal_pressed[self.ix], hr.petal_pos[self.ix]) class CaptouchState: @@ -373,14 +370,15 @@ class InputController: self.right_shoulder._ignore_pressed() -class PetalController: - def __init__(self, ix): - self._ts = 0 - self._input = PetalGestureState(ix) - - def think(self, hr: InputState, delta_ms: int) -> None: - self._ts += delta_ms - self._input._update(self._ts, hr) - - def _ignore_pressed(self) -> None: - self._input._ignore_pressed() +# class PetalController: +# def __init__(self, ix): +# self._ts = 0 +# self._input = PetalGestureState(ix) +# +# def think(self, hr: InputState, delta_ms: int) -> None: +# self._ts += delta_ms +# self._input._update(self._ts, hr) +# +# def _ignore_pressed(self) -> None: +# self._input._ignore_pressed() +# diff --git a/python_payload/st4m/property.py b/python_payload/st4m/property.py index fa4292cf443d9f8d1fd1b82c340ebd8349b453ac..55311e5e5eef39a89afa8ac2eb40cf229b99db89 100644 --- a/python_payload/st4m/property.py +++ b/python_payload/st4m/property.py @@ -3,43 +3,42 @@ class Property: class PropertyFloat(Property): - def __init__(self): + def __init__(self) -> None: self.min = 0.0 self.max = 1.0 - def set_value(self, value): + def set_value(self, value: float) -> None: self._value = value - def mod_value(self, delta): - self._value += delta_ms + def mod_value(self, delta: float) -> None: + self._value += delta - def get_value(self): + def get_value(self) -> float: return self._value # Define a few RGB (0.0 to 1.0) colors -BLACK = (0, 0, 0) -RED = (1, 0, 0) -GREEN = (0, 1, 0) -BLUE = (0, 0, 1) -WHITE = (1, 1, 1) +BLACK = (0.0, 0.0, 0.0) +RED = (1.0, 0.0, 0.0) +GREEN = (0.0, 1.0, 0.0) +BLUE = (0.0, 0.0, 1.0) +WHITE = (1.0, 1.0, 1.0) GREY = (0.5, 0.5, 0.5) GO_GREEN = (63 / 255, 255 / 255, 33 / 255) PUSH_RED = (251 / 255, 72 / 255, 196 / 255) -class Color: - @classmethod - def from_rgb888(cls, r: int = 0, g: int = 0, b: int = 0): - return cls(r / 255, g / 255, b / 255) - - def __init__(self, r: float = 0.0, g: float = 0.0, b: float = 0.0) -> None: - self.r = r - self.g = g - self.b = b - - def as_normal_tuple(self) -> tuple[float]: - return (self.r, self.g, self.b) - - -print(Color.from_rgb888(255, 255, 128).as_normal_tuple()) +# Unused? +# class Color: +# @classmethod +# def from_rgb888(cls, r: int = 0, g: int = 0, b: int = 0) -> Color: +# return cls(r / 255, g / 255, b / 255) +# +# def __init__(self, r: float = 0.0, g: float = 0.0, b: float = 0.0) -> None: +# self.r = r +# self.g = g +# self.b = b +# +# def as_normal_tuple(self) -> tuple[float, float, float]: +# return (self.r, self.g, self.b) +# diff --git a/python_payload/st4m/ui/ctx.py b/python_payload/st4m/ui/ctx.py index ddd00768fcd721e8f92291ca1cb8e9f84e45dc15..7119f205d3157721035212e87c4654de555a034d 100644 --- a/python_payload/st4m/ui/ctx.py +++ b/python_payload/st4m/ui/ctx.py @@ -38,6 +38,24 @@ class Ctx(ABCBase): self.text_baseline: str = "alphabetic" self.line_width: float = 1.0 self.global_alpha: float = 1.0 + self.font: str = "" + + @abstractmethod + def text_width(self, text: str) -> float: + """ + Calculates width of rendered text, without rendering it. + """ + pass + + @abstractmethod + def get_font_name(self, ix: int) -> str: + """ + Returns font name from internal font index. See ctx_config.h for + defined fonts. + + TODO(q3k): expose these font indices into mpy + """ + pass @abstractmethod def begin_path(self) -> "Ctx": diff --git a/python_payload/st4m/ui/elements/menus.py b/python_payload/st4m/ui/elements/menus.py index f3d8b7f93fd3271c3de8ec33931dc9df583a528b..7afab42b6131e091d0bda9a8697496f31baf2708 100644 --- a/python_payload/st4m/ui/elements/menus.py +++ b/python_payload/st4m/ui/elements/menus.py @@ -5,9 +5,8 @@ from st4m.ui.menu import MenuController, MenuItem from st4m import Ctx, InputState -from st4m.utils import lerp +from st4m.utils import lerp, tau import math -from st4m.vector import tau class SimpleMenu(MenuController): @@ -95,14 +94,15 @@ class FlowerMenu(MenuController): "_sun", ) - def __init__(self, items: List[MenuItem], vm: ViewManager, name="flow3r") -> None: + def __init__( + self, items: List[MenuItem], vm: ViewManager, name: str = "flow3r" + ) -> None: self._ts = 0 self.name = name self.ui = GroupRing(r=80) for item in items: self.ui.items_ring.append(FlowerIcon(label=item.label())) super().__init__(items, vm) - self._scroll_controller.wrap = True self.icon = FlowerIcon(label=self.name) self.icon.rotation_time = -5000 @@ -119,14 +119,14 @@ class FlowerMenu(MenuController): def draw(self, ctx: Ctx) -> None: ctx.gray(0) ctx.rectangle(-120, -120, 240, 240).fill() - for item in self.ui.items_ring: - item.highlighted = False - item.rotation_time = 10000 + # for item in self.ui.items_ring: + # item.highlighted = False + # item.rotation_time = 10000 current = self._scroll_controller.current_position() current_int = round(current) % len(self._items) # print("current", current, current_int) - self.ui.items_ring[current_int].highlighted = True - self.ui.items_ring[current_int].rotation_time = 3000 + # self.ui.items_ring[current_int].highlighted = True + # self.ui.items_ring[current_int].rotation_time = 3000 self.ui.angle_offset = math.pi - (tau * current / len(self.ui.items_ring)) self.ui.draw(ctx) diff --git a/python_payload/st4m/ui/elements/visuals.py b/python_payload/st4m/ui/elements/visuals.py index f975eedae4420b7a39257a239aa47ac998632b2c..64f68d045e1b2fef7c714ac6624f505d594eaab9 100644 --- a/python_payload/st4m/ui/elements/visuals.py +++ b/python_payload/st4m/ui/elements/visuals.py @@ -1,11 +1,11 @@ -from st4m.utils import xy_from_polar +from st4m.utils import xy_from_polar, tau from st4m.property import PUSH_RED, GO_GREEN, BLACK +from st4m.goose import List, Optional from st4m import Responder, Ctx, InputState import random import math -from st4m.vector import tau class Sun(Responder): @@ -52,26 +52,25 @@ class Sun(Responder): class GroupRing(Responder): - def __init__(self, r=100, x=0, y=0): + def __init__(self, r: float = 100, x: float = 0, y: float = 0) -> None: self.r = r self.x = x self.y = y - self.items_ring = [] - self.item_center = None - self.angle_offset = 0 + self.items_ring: List[Responder] = [] + self.item_center: Optional[Responder] = None + self.angle_offset = 0.0 self.ts = 0.0 def think(self, ins: InputState, delta_ms: int) -> None: self.ts += delta_ms - self.item_center.think(ins, delta_ms) + if self.item_center is not None: + self.item_center.think(ins, delta_ms) for item in self.items_ring: item.think(ins, delta_ms) def draw(self, ctx: Ctx) -> None: if self.item_center: - self.item_center.has_highlight = False self.item_center.draw(ctx) - # self.items_ring[0].draw(ctx) # ctx.save() for index, item in enumerate(self.items_ring): @@ -91,7 +90,7 @@ class FlowerIcon(Responder): A flower icon """ - def __init__(self, label="?") -> None: + def __init__(self, label: str = "?") -> None: self.x = 0.0 self.y = 0.0 self.size = 50.0 @@ -114,7 +113,7 @@ class FlowerIcon(Responder): def draw(self, ctx: Ctx) -> None: x = self.x y = self.y - petal_size = 0 + petal_size = 0.0 if self.petal_count: petal_size = 2.3 * self.size / self.petal_count + self.size_offset diff --git a/python_payload/st4m/ui/interactions.py b/python_payload/st4m/ui/interactions.py index e9dac9ceb22c09a292ef020f1b69fbe9eeb8e0a3..0bfcb74ef3eb89bd8c6bf5c0a6916ee63d0006cc 100644 --- a/python_payload/st4m/ui/interactions.py +++ b/python_payload/st4m/ui/interactions.py @@ -1,6 +1,6 @@ import st4m -from st4m.input import InputState, PetalController +from st4m.input import InputState from st4m.ui.ctx import Ctx from st4m import Responder @@ -31,12 +31,11 @@ class ScrollController(st4m.Responder): "_velocity", ) - def __init__(self, wrap=False) -> None: + def __init__(self) -> None: self._nitems = 0 self._target_position = 0 self._current_position = 0.0 self._velocity: float = 0.0 - self.wrap = wrap def set_item_count(self, count: int) -> None: """ @@ -56,7 +55,7 @@ class ScrollController(st4m.Responder): press). """ self._target_position -= 1 - self._velocity = -30 + self._velocity = -10 def scroll_right(self) -> None: """ @@ -64,7 +63,7 @@ class ScrollController(st4m.Responder): press). """ self._target_position += 1 - self._velocity = 30 + self._velocity = 10 def think(self, ins: InputState, delta_ms: int) -> None: if self._nitems == 0: @@ -72,13 +71,10 @@ class ScrollController(st4m.Responder): self._current_position = 0 self._velocity = 0 return - if self.wrap: - self._target_position = self._target_position % self._nitems - else: - if self._target_position < 0: - self._target_position = 0 - if self._target_position >= self._nitems: - self._target_position = self._nitems - 1 + if self._target_position < 0: + self._target_position = 0 + if self._target_position >= self._nitems: + self._target_position = self._nitems - 1 self._physics_step(delta_ms / 1000.0) @@ -96,7 +92,7 @@ class ScrollController(st4m.Responder): Use this value to animate the scroll list. """ - return round(self._current_position, 4) % self._nitems + return round(self._current_position, 4) def target_position(self) -> int: """ @@ -121,27 +117,16 @@ class ScrollController(st4m.Responder): return self._target_position >= self._nitems - 1 def _physics_step(self, delta: float) -> None: - diff = self._nitems - for i in [0, 1, -1]: - d = ( - float(self._target_position) + self._nitems * i - ) - self._current_position - if abs(d) < abs(diff): - diff = d - # diff = - # print(diff) - max_velocity = 0.2 + diff = float(self._target_position) - self._current_position + max_velocity = 500 velocity = self._velocity - if abs(velocity) < 1: - self._target_position = int(self._current_position) - - if abs(diff) > 0.2: + if abs(diff) > 0.1: # Apply force to reach target position. if diff > 0: - velocity += 5 * delta + velocity += 80 * delta else: - velocity -= 5 * delta + velocity -= 80 * delta # Clamp velocity. if velocity > max_velocity: @@ -149,149 +134,144 @@ class ScrollController(st4m.Responder): if velocity < -max_velocity: velocity = -max_velocity self._velocity = velocity - elif diff == 0: - pass - print("at target") else: # Try to snap to target position. pos = self._velocity > 0 and diff > 0 neg = self._velocity < 0 and diff < 0 - if self.wrap or pos or neg: - print("snapped") - self._current_position = self._target_position % self._nitems + if pos or neg: + self._current_position = self._target_position self._velocity = 0 self._physics_integrate(delta) def _physics_integrate(self, delta: float) -> None: self._velocity -= self._velocity * delta * 10 - self._current_position = ( - self._current_position + self._velocity * delta - ) % self._nitems - - -class GestureScrollController(ScrollController): - """ - GestureScrollController extends ScrollController to also react to swipe gestures - on a configurable petal. - - #TODO (iggy): rewrite both to extend a e.g. "PhysicsController" - """ - - def __init__(self, petal_index, wrap=False): - super().__init__(wrap) - self._petal = PetalController(petal_index) - self._speedbuffer = [0.0] - self._ignore = 0 - - def scroll_left(self) -> None: - """ - Call when the user wants to scroll left by discrete action (eg. button - press). - """ - # self._target_position -= 1 - self._speedbuffer = [] - self._velocity = -1 - self._current_position -= 0.3 - - def scroll_right(self) -> None: - """ - Call when the user wants to scroll right by discrete action (eg. button - press). - """ - # self._target_position += 1 - self._speedbuffer = [] - self._velocity = 1 - self._current_position += 0.3 - - def think(self, ins: InputState, delta_ms: int) -> None: - # super().think(ins, delta_ms) - - self._petal.think(ins, delta_ms) - - if self._ignore: - self._ignore -= 1 - return - - dphi = self._petal._input._dphi - phase = self._petal._input.phase() - - self._speedbuffer.append(self._velocity) - - while len(self._speedbuffer) > 3: - self._speedbuffer.pop(0) - - speed = sum(self._speedbuffer) / len(self._speedbuffer) - - speed = min(speed, 0.008) - speed = max(speed, -0.008) - - if phase == self._petal._input.ENDED: - # self._speedbuffer = [0 if abs(speed) < 0.005 else speed] - while len(self._speedbuffer) > 3: - print("sb:", self._speedbuffer) - self._speedbuffer.pop() - elif phase == self._petal._input.UP: - pass - elif phase == self._petal._input.BEGIN: - self._ignore = 5 - # self._speedbuffer = [0.0] - elif phase == self._petal._input.RESTING: - self._speedbuffer.append(0.0) - elif phase == self._petal._input.MOVED: - impulse = -dphi / delta_ms - self._speedbuffer.append(impulse) - - if abs(speed) < 0.0001: - speed = 0 - - self._velocity = speed - - self._current_position = self._current_position + self._velocity * delta_ms - - if self.wrap: - self._current_position = self._current_position % self._nitems - elif round(self._current_position) < 0: - self._current_position = 0 - elif round(self._current_position) >= self._nitems: - self._current_position = self._nitems - 1 - - if phase != self._petal._input.UP: - return - - pos = round(self._current_position) - microstep = round(self._current_position) - self._current_position - # print("micro:", microstep) - # print("v", self._velocity) - # print("pos", self._current_position) - - if ( - abs(microstep) < 0.1 - and abs(self._velocity) < 0.001 - # and abs(self._velocity) - ): - self._velocity = 0 - self._speedbuffer.append(0) - self._current_position = round(self._current_position) - self._target_position = self._current_position - # print("LOCK") - return - - if abs(self._velocity) > 0.001: - self._speedbuffer.append(-self._velocity) - # print("BREAKING") - return - - if self._velocity >= 0 and microstep > 0: - self._speedbuffer.append(max(self._velocity, 0.01) * microstep * 10) - # print("1") - elif self._velocity < 0 and microstep > 0: - self._speedbuffer.append(-self._velocity) - # print("2") - elif self._velocity > 0 and microstep < 0: - self._speedbuffer.append(-self._velocity * 0.5) - # print("3") - - elif self._velocity <= 0 and microstep < 0: - self._speedbuffer.append(min(self._velocity, -0.01) * abs(microstep) * 10) - # print("4") + self._current_position += self._velocity * delta + + +# class GestureScrollController(ScrollController): +# """ +# GestureScrollController extends ScrollController to also react to swipe gestures +# on a configurable petal. +# +# #TODO (iggy): rewrite both to extend a e.g. "PhysicsController" +# """ +# +# def __init__(self, petal_index, wrap=False): +# super().__init__(wrap) +# self._petal = PetalController(petal_index) +# self._speedbuffer = [0.0] +# self._ignore = 0 +# +# def scroll_left(self) -> None: +# """ +# Call when the user wants to scroll left by discrete action (eg. button +# press). +# """ +# # self._target_position -= 1 +# self._speedbuffer = [] +# self._velocity = -1 +# self._current_position -= 0.3 +# +# def scroll_right(self) -> None: +# """ +# Call when the user wants to scroll right by discrete action (eg. button +# press). +# """ +# # self._target_position += 1 +# self._speedbuffer = [] +# self._velocity = 1 +# self._current_position += 0.3 +# +# def think(self, ins: InputState, delta_ms: int) -> None: +# # super().think(ins, delta_ms) +# +# self._petal.think(ins, delta_ms) +# +# if self._ignore: +# self._ignore -= 1 +# return +# +# dphi = self._petal._input._dphi +# phase = self._petal._input.phase() +# +# self._speedbuffer.append(self._velocity) +# +# while len(self._speedbuffer) > 3: +# self._speedbuffer.pop(0) +# +# speed = sum(self._speedbuffer) / len(self._speedbuffer) +# +# speed = min(speed, 0.008) +# speed = max(speed, -0.008) +# +# if phase == self._petal._input.ENDED: +# # self._speedbuffer = [0 if abs(speed) < 0.005 else speed] +# while len(self._speedbuffer) > 3: +# print("sb:", self._speedbuffer) +# self._speedbuffer.pop() +# elif phase == self._petal._input.UP: +# pass +# elif phase == self._petal._input.BEGIN: +# self._ignore = 5 +# # self._speedbuffer = [0.0] +# elif phase == self._petal._input.RESTING: +# self._speedbuffer.append(0.0) +# elif phase == self._petal._input.MOVED: +# impulse = -dphi / delta_ms +# self._speedbuffer.append(impulse) +# +# if abs(speed) < 0.0001: +# speed = 0 +# +# self._velocity = speed +# +# self._current_position = self._current_position + self._velocity * delta_ms +# +# if self.wrap: +# self._current_position = self._current_position % self._nitems +# elif round(self._current_position) < 0: +# self._current_position = 0 +# elif round(self._current_position) >= self._nitems: +# self._current_position = self._nitems - 1 +# +# if phase != self._petal._input.UP: +# return +# +# pos = round(self._current_position) +# microstep = round(self._current_position) - self._current_position +# # print("micro:", microstep) +# # print("v", self._velocity) +# # print("pos", self._current_position) +# +# if ( +# abs(microstep) < 0.1 +# and abs(self._velocity) < 0.001 +# # and abs(self._velocity) +# ): +# self._velocity = 0 +# self._speedbuffer.append(0) +# self._current_position = round(self._current_position) +# self._target_position = self._current_position +# # print("LOCK") +# return +# +# if abs(self._velocity) > 0.001: +# self._speedbuffer.append(-self._velocity) +# # print("BREAKING") +# return +# +# if self._velocity >= 0 and microstep > 0: +# self._speedbuffer.append(max(self._velocity, 0.01) * microstep * 10) +# # print("1") +# elif self._velocity < 0 and microstep > 0: +# self._speedbuffer.append(-self._velocity) +# # print("2") +# elif self._velocity > 0 and microstep < 0: +# self._speedbuffer.append(-self._velocity * 0.5) +# # print("3") +# +# elif self._velocity <= 0 and microstep < 0: +# self._speedbuffer.append(min(self._velocity, -0.01) * abs(microstep) * 10) +# # print("4") +# diff --git a/python_payload/st4m/ui/menu.py b/python_payload/st4m/ui/menu.py index 96b09c3c7bf8c287428a85c63626cbd8914fba38..e076dedd52dce11e28f6bb4ca76de0c89384d636 100644 --- a/python_payload/st4m/ui/menu.py +++ b/python_payload/st4m/ui/menu.py @@ -10,7 +10,7 @@ from st4m.ui.view import ( ViewTransitionSwipeLeft, ViewTransitionSwipeRight, ) -from st4m.ui.interactions import ScrollController, GestureScrollController +from st4m.ui.interactions import ScrollController from st4m.ui.ctx import Ctx @@ -101,7 +101,7 @@ class MenuController(ViewWithInputState): def __init__(self, items: List[MenuItem], vm: Optional[ViewManager]) -> None: self._items = items - self._scroll_controller = GestureScrollController(2) + self._scroll_controller = ScrollController() self._scroll_controller.set_item_count(len(items)) self._view_manager = vm diff --git a/python_payload/st4m/utils.py b/python_payload/st4m/utils.py index 0123d7f7566409e46bd283f2edfddc2cd0a5f226..cdcafc4b559e8bab7236a12c8f4299f038ed209e 100644 --- a/python_payload/st4m/utils.py +++ b/python_payload/st4m/utils.py @@ -9,5 +9,8 @@ def lerp(a: float, b: float, v: float) -> float: return a + (b - a) * v -def xy_from_polar(r, phi): +def xy_from_polar(r: float, phi: float) -> tuple[float, float]: return (r * math.sin(phi), r * math.cos(phi)) # x # y + + +tau = math.pi * 2