diff options
| author | s-ol <s+removethis@s-ol.nu> | 2022-05-19 19:15:59 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2022-05-21 16:51:01 +0000 |
| commit | 41bc53a7ac16fe96c9cd9de2ef930b945f60b981 (patch) | |
| tree | 28bb59e92bd957b7a1ed929a972bfbb003c131a2 | |
| parent | initial commit (diff) | |
| download | firmware-41bc53a7ac16fe96c9cd9de2ef930b945f60b981.tar.gz firmware-41bc53a7ac16fe96c9cd9de2ef930b945f60b981.zip | |
initial commit
| -rw-r--r-- | .gitignore | 10 | ||||
| -rw-r--r-- | boot.py | 17 | ||||
| -rw-r--r-- | code.py | 5 | ||||
| -rw-r--r-- | fonts/bitbuntu.pcf | bin | 0 -> 10880 bytes | |||
| -rw-r--r-- | hex33board/__init__.py | 262 | ||||
| -rw-r--r-- | hex33board/base.py | 228 | ||||
| -rw-r--r-- | hex33board/layout.py | 1 | ||||
| -rw-r--r-- | hex33board/matrix.py | 210 | ||||
| -rw-r--r-- | hex33board/menu.py | 244 | ||||
| -rw-r--r-- | hex33board/util.py | 27 | ||||
| -rw-r--r-- | test.wav | bin | 0 -> 221596 bytes |
11 files changed, 1004 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf6c02b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +boot_out.txt +hex.config.json + +__pycache__ +lib + +.Trashes +.fseventsd +.metadata_never_index +System Volume Information @@ -0,0 +1,17 @@ +import supervisor +import board +import digitalio +import storage + +supervisor.set_usb_identification("s-ol", "0x33.board", 0x732D, 0x3362) + +col = digitalio.DigitalInOut(board.GP8) +row = digitalio.DigitalInOut(board.GP10) +col.switch_to_output(value=True) +row.switch_to_input(pull=digitalio.Pull.DOWN) +if row.value: + storage.disable_usb_drive() + storage.remount("/", False) + print("Mounting read-write") +else: + print("Mounting readonly") @@ -0,0 +1,5 @@ +# import test +from hex33board import Keyboard + +k = Keyboard() +k.run() diff --git a/fonts/bitbuntu.pcf b/fonts/bitbuntu.pcf Binary files differnew file mode 100644 index 0000000..cd6495e --- /dev/null +++ b/fonts/bitbuntu.pcf diff --git a/hex33board/__init__.py b/hex33board/__init__.py new file mode 100644 index 0000000..de2619f --- /dev/null +++ b/hex33board/__init__.py @@ -0,0 +1,262 @@ +from __future__ import annotations + +import board +from busio import I2C, UART +from adafruit_displayio_ssd1306 import SSD1306 +from adafruit_display_text import label +from displayio import I2CDisplay +from neopixel import NeoPixel +from audiopwmio import PWMAudioOut +from audiocore import WaveFile +import displayio +import supervisor + +import usb_midi +from adafruit_midi import MIDI + +from .matrix import Matrix +from .util import ticks_diff, FONT_10, led_map +from .base import Key, Note, Scale, Layout, WickiHaydenLayout, HarmonicLayout, GerhardLayout, Mode +from .menu import Settings, SliderSetting, ChoiceSetting, MenuMode, _color_offor + + +class Keyboard: + scale: Scale + layout: Layout + mode: Mode + + keys: list[Key] + notes: dict[int, Note] + + matrix: Matrix + pixels: NeoPixel + display: SSD1306 + + midi_usb: MIDI + midi_din: MIDI + + audio_out: PWMAudioOut + + def __init__(self): + self.matrix = Matrix( + [board.GP8, board.GP4, board.GP0, board.GP6, board.GP7, board.GP9], + [board.GP5, board.GP1, board.GP2, board.GP3, board.GP10], + ) + + self.pixels = NeoPixel(board.GP11, 48 + 4, auto_write=False) + + displayio.release_displays() + i2c = I2C(sda=board.GP14, scl=board.GP15) + bus = I2CDisplay(i2c, device_address=0x3C) + self.display = SSD1306(bus, width=128, height=32, rotation=180, auto_refresh=False) + + din = UART(tx=board.GP16, rx=None, baudrate=31250) + self.midi_din = MIDI(None, din) + + midi_in = next(p for p in usb_midi.ports if isinstance(p, usb_midi.PortIn)) + midi_out = next(p for p in usb_midi.ports if isinstance(p, usb_midi.PortOut)) + self.midi_usb = MIDI(midi_in, midi_out) + + self.audio_out = PWMAudioOut(left_channel=board.GP12, right_channel=board.GP13) + self.test_file = WaveFile(open("test.wav", "rb")) + + self.keys = [Key(self, i) for i in range(48)] + self.notes = {} + self.notes_expiring = {} + + self.settings = Settings({ + "midi_ch_usb": SliderSetting("MIDI out chan (USB)", 15, fmt="CH{}", thresh = lambda v, t: v == t), + "midi_ch_din": SliderSetting("MIDI out chan (Jack)", 15, fmt="CH{}", thresh = lambda v, t: v == t), + "midi_vel": SliderSetting("MIDI velocity", 127, default=64), + "rgb_bright": SliderSetting("LED brightness", 100, default=100, fmt="{}%", color=_color_offor), + "jam_timeout": ChoiceSetting("Jam Mode fade time", [0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 10, 12, 20], default=6, fmt="{:.2f}s", color=_color_offor), + "layout_name": ChoiceSetting("Keyboard Layout", ['wicki/hayden', 'harmonic table', 'gerhard'], default='wicki/hayden'), + + "layout_offset": SliderSetting("Layout start note", 127, default=24), + "scale_name": ChoiceSetting("Highlight Scale", ['major', 'minor nat', 'minor harm', 'minor mel', 'minor hung', 'whole', 'penta'], default='major'), + "scale_root": SliderSetting("Scale root note", 127, default=43), + }) + + self.layout = WickiHaydenLayout() + self.scale = Scale(0, Scale.STEPS["major"], "major") + + self.settings.on('midi_ch_usb', self.on_midi_ch_usb) + self.settings.on('midi_ch_din', self.on_midi_ch_din) + self.settings.on('midi_vel', self.on_midi_vel) + self.settings.on('rgb_bright', self.on_rgb_bright) + self.settings.on('jam_timeout', self.on_jam_timeout) + self.settings.on('layout_name', self.on_layout) + self.settings.on('layout_offset', self.on_layout) + self.settings.on('scale_name', self.on_scale) + self.settings.on('scale_root', self.on_scale) + + self.modes = { + "base": BaseMode(self), + "base_shift": BaseShiftMode(self), + "menu": MenuMode(self, settings=self.settings, ui_settings_ids=[ + "midi_ch_usb", "midi_ch_din", "midi_vel", "rgb_bright", "jam_timeout", "layout_name" + ]), + } + self.mode = self.modes["base"] + + self.settings.load() + + def on_midi_ch_usb(self, ch): self.midi_usb.out_channel = ch + def on_midi_ch_din(self, ch): self.midi_din.out_channel = ch + def on_midi_vel(self, vel): self.velocity = vel + def on_rgb_bright(self, b): self.pixels.brightness = b/100 + def on_jam_timeout(self, t): self.jam_timeout = t * 1000 + def on_layout(self, v): + name = self.settings.get('layout_name').value + offset = self.settings.get('layout_offset').value + if name == 'wicki/hayden': + self.layout = WickiHaydenLayout(offset) + elif name == 'harmonic table': + self.layout = HarmonicLayout(offset) + elif name == 'gerhard': + self.layout = GerhardLayout(offset) + self.update_scale() + self.audio_out.play(self.test_file) + def on_scale(self, v): + name = self.settings.get('scale_name').value + root = self.settings.get('scale_root').value + self.scale = Scale(root, Scale.STEPS[name], name) + self.update_scale() + + @property + def mode(self) -> Mode: + return self._mode + + @mode.setter + def mode(self, mode: Mode): + if hasattr(self, '_mode'): + if self._mode == mode: + return + self._mode.exit() + + self._mode = mode + self._mode.enter() + + def update_scale(self): + for key in self.keys: + key.update_scale() + + def send_midi(self, message): + self.midi_usb.send(message) + self.midi_din.send(message) + + def run(self): + while True: + ticks_ms = supervisor.ticks_ms() + for note in self.notes_expiring.values(): + note.update_expiry(ticks_ms) + + for (i, pressed) in self.matrix.scan_for_changes(): + if self.mode: + handled = self.mode.key_event(i, pressed) + if handled: + continue + + if i < len(self.keys): + key = self.keys[i] + if pressed: + key.on_press() + else: + key.on_release() + + for key in self.keys: + self.pixels[led_map[key.i]] = key.update_color() + self.mode.update_pixels(self.pixels) + + active = [pitch for pitch in self.notes] + active.sort() + self.modes["base"].notes_label.text = ' '.join(self.scale.label(p) for p in active) + + self.pixels.show() + self.display.refresh() + + +class BaseShiftMode(Mode): + color = (0.1, 1.0, 1.0) + + def enter(self): + self.entered = supervisor.ticks_ms() + self.pressed_something = False + + def key_event(self, i: int, pressed: bool) -> bool: + if not pressed and i == Key.MENU_I: + held = ticks_diff(supervisor.ticks_ms(), self.entered) + if not self.pressed_something and held <= 800: + # tap: enter menu + self.keyboard.mode = self.keyboard.modes["menu"] + return True + + self.keyboard.mode = self.keyboard.modes["base"] + return True + + if not pressed: + return False + + if i < len(self.keyboard.keys): + key = self.keyboard.keys[i] + self.keyboard.settings.get("scale_root").value = key._pitch + self.keyboard.settings.dispatch("scale_root") + self.keyboard.modes["base"].update_display() + self.pressed_something = True + return True + + if i == Key.PREV_I: + self.keyboard.settings.get("scale_name").press_prev() + self.keyboard.settings.dispatch("scale_name") + self.keyboard.modes["base"].update_display() + self.pressed_something = True + return True + + if i == Key.NEXT_I: + self.keyboard.settings.get("scale_name").press_next() + self.keyboard.settings.dispatch("scale_name") + self.keyboard.modes["base"].update_display() + self.pressed_something = True + return True + + +class BaseMode(Mode): + label: label.Label + + def __init__(self, *args): + super().__init__(*args) + + self.scale_label = label.Label(FONT_10, text="", color=0xffffff, x=0, y=3) + self.notes_label = label.Label(FONT_10, text="", color=0xffffff, x=2, y=18) + self.group.append(self.scale_label) + self.group.append(self.notes_label) + + def enter(self): + super().enter() + self.update_display() + + def update_display(self): + self.scale_label.text = self.keyboard.scale.format() + + def key_event(self, i: int, pressed: bool) -> bool: + if not pressed: + return False + + if i == Key.MENU_I: + self.keyboard.mode = self.keyboard.modes["base_shift"] + return True + + if i == Key.PREV_I: + self.keyboard.layout.offset -= self.keyboard.scale.size + self.keyboard.update_scale() + + layout_offset = self.keyboard.settings.get("layout_offset") + layout_offset.value -= self.keyboard.scale.size + self.keyboard.settings.dispatch("layout_offset") + return True + + if i == Key.NEXT_I: + layout_offset = self.keyboard.settings.get("layout_offset") + layout_offset.value += self.keyboard.scale.size + self.keyboard.settings.dispatch("layout_offset") + return True diff --git a/hex33board/base.py b/hex33board/base.py new file mode 100644 index 0000000..28e6dfb --- /dev/null +++ b/hex33board/base.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +import displayio +import supervisor +from micropython import const +from adafruit_midi.note_on import NoteOn +from adafruit_midi.note_off import NoteOff +from adafruit_fancyled.adafruit_fancyled import CHSV, clamp_norm + +from .util import ticks_diff + + +class Scale: + STEPS = { + "major": [2, 2, 1, 2, 2, 2, 1], + "minor nat": [2, 1, 2, 2, 1, 2, 2], + "minor harm": [2, 1, 2, 2, 1, 3, 1], + "minor mel": [2, 1, 2, 2, 2, 2, 1], + "minor hung": [2, 1, 3, 1, 1, 3, 1], + "whole": [2, 2, 2, 2, 2, 2], + "penta": [2, 2, 3, 2, 3], + } + + # LABELS = { + # 'english': 'C C# D D# E F F# G G# A A# B'.split(' '), + # 'german': 'C C# D D# E F F# G G# A A# H'.split(' '), + # 'sol': 'do do# re re# mi fa fa# sol sol# la la# si'.split(' '), + # } + LABELS = 'C C# D D# E F F# G G# A A# B'.split(' ') + + root: int + name: str + notes: list[int] + size: int + + def __init__(self, root: int, steps: list[int], name: str): + self.root = root + self.name = name + self.notes = [sum(steps[:i]) for i in range(len(steps))] + self.size = sum(steps) + + def get_hsv(self, pitch: int) -> tuple[float, float, float]: + relative_pitch = (pitch - self.root) % self.size + + if relative_pitch not in self.notes: + # out of scale + return (.3, 0.8, 0.0) + + if self.root <= pitch < self.root + self.size: + # in core scale + return (.1, 1.0, 0.2) + + # in scale + return (.7, 0.9, 0.15) + + def label(self, pitch): + return self.LABELS[pitch % self.size] + + def format(self): + oct = self.root // self.size + off = self.root % self.size + + return "scale: {} {} {}".format( + self.LABELS[off], + oct, + self.name, + ) + + +class Layout: + offset: int + + def __init__(self, offset: int = 24): + self.offset = offset + + def get_pitch(self, key: Key) -> int: + pass + + +class WickiHaydenLayout(Layout): + def get_pitch(self, key: Key) -> int: + x, y = key.pos + return int(self.offset + 2*x + 6*y) + + +class HarmonicLayout(Layout): + def get_pitch(self, key: Key) -> int: + x, y = key.pos + return int(self.offset + 4*x + 5*y) + + +class GerhardLayout(Layout): + def get_pitch(self, key: Key) -> int: + x, y = key.pos + return int(self.offset + 3*x + 2.5*y) + + +class Note: + keyboard: Keyboard + pitch: int + + end_tick: None | int + expiry: float + + def __init__(self, keyboard: Keyboard, pitch: int): + self.keyboard = keyboard + self.pitch = pitch + self.end_tick = None + self.expiry = 1.0 + + def on(self): + self.keyboard.notes[self.pitch] = self + self.keyboard.send_midi(NoteOn(self.pitch, self.keyboard.velocity)) + + if self.pitch in self.keyboard.notes_expiring: + del self.keyboard.notes_expiring[self.pitch] + + def off(self): + if self.keyboard.notes.get(self.pitch) is self: + self.keyboard.notes_expiring[self.pitch] = self + del self.keyboard.notes[self.pitch] + + self.keyboard.send_midi(NoteOff(self.pitch)) + self.end_tick = supervisor.ticks_ms() + + def update_expiry(self, ticks_ms_now): + delta = ticks_diff(ticks_ms_now, self.end_tick) + + if delta > self.keyboard.jam_timeout: + del self.keyboard.notes_expiring[self.pitch] + self.expiry = 0 + return + + self.expiry = clamp_norm(1 - delta / self.keyboard.jam_timeout) + + +class Key: + MENU_I = const(48+0) + PREV_I = const(48+4) + NEXT_I = const(48+5) + + keyboard: Keyboard + + i: int + '''matrix index for this key.''' + + pos: tuple[float, int] + '''logical coordinates for this key.''' + + note: None | Note + + _pitch: int + _color: tuple[float, float, float] + + def __init__(self, keyboard: Keyboard, i: int): + x = i % 12 + y = i // 12 + + y = 4 - y + if y % 2 == 0: + x += 0.5 + + self.keyboard = keyboard + self.i = i + self.pos = (x, y) + self.note = None + + def update_scale(self): + self._pitch = self.keyboard.layout.get_pitch(self) + self._hsv = self.keyboard.scale.get_hsv(self._pitch) + + def update_color(self): + if self._pitch < 0 or self._pitch > 127: + return 0 + + hue, sat, key_val = self._hsv + + note_for_pitch = self.keyboard.notes.get(self._pitch) + note_for_pitch = note_for_pitch or self.keyboard.notes_expiring.get(self._pitch) + + if self.note: + value = 1.0 + elif note_for_pitch: + value = note_for_pitch.expiry * 0.8 + else: + value = 0.0 + + rest = 1.0 - key_val + value = key_val + value * rest + return CHSV(hue, sat, value).pack() + + def on_press(self): + if self.note: + self.note.off() + + if self._pitch < 0 or self._pitch > 127: + return + + self.note = Note(self.keyboard, self._pitch) + self.note.on() + + def on_release(self): + if not self.note: + return + + self.note.off() + self.note = None + + +class Mode: + keyboard: Keyboard + group: displayio.Group = displayio.Group() + color: tuple[float, float, float] = (0.5, 1.0, 1.0) + + def __init__(self, keyboard: Keyboard): + self.keyboard = keyboard + + def enter(self): + self.keyboard.display.show(self.group) + + def exit(self): + pass + + def key_event(self, i: int, pressed: bool) -> bool: + return False + + def update_pixels(self, pixels: NeoPixel): + pixels[0] = CHSV(*self.color).pack() diff --git a/hex33board/layout.py b/hex33board/layout.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/hex33board/layout.py @@ -0,0 +1 @@ + diff --git a/hex33board/matrix.py b/hex33board/matrix.py new file mode 100644 index 0000000..0fb6796 --- /dev/null +++ b/hex33board/matrix.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +from digitalio import DigitalInOut, Pull + + +def range_rev(start, stop): + return range(stop - 1, start - 1, -1) + + +def mapping_up_down(cols, rows): + ''' + Maps keys according to the following diagram: + + 1 2 3 + +------ + 1| A A A + 2| A A A + | + 1| B B B + 2| B B B + ''' + + coord_mapping = list(range(2 * cols * rows)) + return coord_mapping + + +def mapping_up_down_mirrored(cols, rows): + ''' + Maps keys according to the following diagram: + + 1 2 3 + +------ + 1| A A A + 2| A A A + | + 2| B B B + 1| B B B + ''' + + coord_mapping = [] + coord_mapping.extend(range(cols * rows)) + coord_mapping.extend(range_rev(cols * rows, 2 * cols * rows)) + return coord_mapping + + +def mapping_left_right(cols, rows): + ''' + Maps keys according to the following diagram: + + 1 2 3 1 2 3 + +------------- + 1| A A A B B B + 2| A A A B B B + ''' + + size = cols * rows + coord_mapping = [] + for y in range(rows): + yy = y * cols + coord_mapping.extend(range(yy, yy + cols)) + coord_mapping.extend(range(yy + size, yy + cols + size)) + return coord_mapping + + +def mapping_left_right_mirrored(cols, rows): + ''' + Maps keys according to the following diagram: + + 1 2 3 3 2 1 + +------------- + 1| A A A B B B + 2| A A A B B B + ''' + + size = cols * rows + coord_mapping = [] + for y in range(rows): + yy = y * 2 * cols + coord_mapping.extend(range(yy, yy + cols)) + coord_mapping.extend(range_rev(yy + size, yy + cols + size)) + return coord_mapping + + +def mapping_interleave_cols(cols, rows): + ''' + Maps keys according to the following diagram: + + 1 1 2 2 3 3 + +------------- + 1| A B A B A B + 2| A B A B A B + ''' + + coord_mapping = [] + mat = cols * rows + for i in range(mat): + coord_mapping.append(i) + coord_mapping.append(i + mat) + return coord_mapping + + +# these two are actually equivalent +mapping_interleave_rows = mapping_left_right + + +class Matrix: + ''' + A Scanner for Keyboard Matrices with Diodes in both directions. + + In a bidirectional matrix, each (col, row) crossing can be used twice - + once with a ROW2COL diode ("A"), and once with a COL2ROW diode ("B"). + + The raw key numbers returned by this scanner are based on this layout ("up_down"): + + C1 C2 C3 + +----------- + R1| A0 A1 A2 + R2| A3 A4 A5 + +----------- + R1| B6 B7 B8 + R1| B9 B10 B11 + + If the physical layout of the matrix is different, you can pass a function + for `mapping`. The function is passed `len_cols` and `len_rows` and should + return a `coord_mapping` list. + Various common mappings are provided in this module, see: + - `kmk.scanners.bidirectional.mapping_left_right` + - `kmk.scanners.bidirectional.mapping_left_right_mirrored` + - `kmk.scanners.bidirectional.mapping_up_down` + - `kmk.scanners.bidirectional.mapping_up_down_mirrored` + - `kmk.scanners.bidirectional.mapping_interleave_rows` + - `kmk.scanners.bidirectional.mapping_interleave_cols` + + :param cols: A sequence of pins that are the columns for matrix A. + :param rows: A sequence of pins that are the rows for matrix A. + :param mapping: A coord_mapping generator function, see above. + ''' + + def __init__(self, cols, rows, mapping=mapping_left_right): + self.len_cols = len(cols) + self.len_rows = len(rows) + self.half_size = self.len_cols * self.len_rows + self.keys = self.half_size * 2 + + self.coord_mapping = mapping(self.len_cols, self.len_rows) + + # A pin cannot be both a row and column, detect this by combining the + # two tuples into a set and validating that the length did not drop + # + # repr() hackery is because CircuitPython Pin objects are not hashable + unique_pins = {repr(c) for c in cols} | {repr(r) for r in rows} + assert ( + len(unique_pins) == self.len_cols + self.len_rows + ), 'Cannot use a pin as both a column and row' + del unique_pins + + # __class__.__name__ is used instead of isinstance as the MCP230xx lib + # does not use the digitalio.DigitalInOut, but rather a self defined one: + # https://github.com/adafruit/Adafruit_CircuitPython_MCP230xx/blob/3f04abbd65ba5fa938fcb04b99e92ae48a8c9406/adafruit_mcp230xx/digital_inout.py#L33 + + self.cols = [ + x if x.__class__.__name__ == 'DigitalInOut' else DigitalInOut(x) + for x in cols + ] + self.rows = [ + x if x.__class__.__name__ == 'DigitalInOut' else DigitalInOut(x) + for x in rows + ] + + self.state = bytearray(self.keys) + + def scan_for_changes(self): + for (inputs, outputs, flip) in [ + (self.rows, self.cols, False), + (self.cols, self.rows, True), + ]: + for pin in outputs: + pin.switch_to_input() + + for pin in inputs: + pin.switch_to_input(pull=Pull.DOWN) + + for oidx, opin in enumerate(outputs): + opin.switch_to_output(value=True) + + for iidx, ipin in enumerate(inputs): + if flip: + ba_idx = oidx * len(inputs) + iidx + self.half_size + else: + ba_idx = iidx * len(outputs) + oidx + + # cast to int to avoid + # + # >>> xyz = bytearray(3) + # >>> xyz[2] = True + # Traceback (most recent call last): + # File "<stdin>", line 1, in <module> + # OverflowError: value would overflow a 1 byte buffer + # + # I haven't dived too far into what causes this, but it's + # almost certainly because bool types in Python aren't just + # aliases to int values, but are proper pseudo-types + new_val = int(ipin.value) + old_val = self.state[ba_idx] + + if old_val != new_val: + self.state[ba_idx] = new_val + yield self.coord_mapping.index(ba_idx), new_val + + opin.switch_to_input() diff --git a/hex33board/menu.py b/hex33board/menu.py new file mode 100644 index 0000000..1254543 --- /dev/null +++ b/hex33board/menu.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +from adafruit_display_text import label +from adafruit_fancyled.adafruit_fancyled import CHSV +import displayio +import json + +from .util import FONT_10, led_map +from .base import Mode, Key + +BLUE = CHSV(0.48, 1.0, 1.0).pack() +BLUE_DIM = CHSV(0.48, 1.0, 0.15).pack() + +CHOICE_NEUTRAL = CHSV(0.0, 0.0, 0.0).pack() +RED = CHSV(0.95, 1.0, 1.0).pack() +RED_DIM = CHSV(0.95, 1.0, 0.15).pack() +GREEN = CHSV(0.27, 1.0, 1.0).pack() +GREEN_DIM = CHSV(0.27, 1.0, 0.15).pack() + + +def _thresh(val, t): + val >= t + + +def _color(val, active: bool) -> tuple[float, float, float]: + return BLUE if active else BLUE_DIM + + +def _color_offor(val, active: bool) -> tuple[float, float, float]: + if val: + return BLUE if active else BLUE_DIM + else: + return RED if active else RED_DIM + + +class Setting: + name: str + _value: any + + def __init__(self, name: str, default: any): + self.name = name + self._value = default + + @property + def value(self): + return self._value + + @value.setter + def value(self, val): + self._value = val + + +class SliderSetting(Setting): + name: str + max: int + _value: int + fmt: str | None + + def __init__(self, name: str, max: int, default: int = 0, fmt = None, thresh = _thresh, color = _color): + super().__init__(name, default) + + self.max = max + self.fmt = fmt + self.width = min(23, max) + + self.thresh = thresh + self.color = color + + def press_prev(self): + self._value = (self._value - 1) % (self.max + 1) + + def press_next(self): + self._value = (self._value + 1) % (self.max + 1) + + def press_key(self, i): + if i > self.width: + return + + self._value = int(self.max * i / self.width) + + def get_colors(self) -> list[int]: + colors = [] + for i in range(self.width + 1): + thresh = int(self.max * i / self.width) + active = self.thresh(self.value, thresh) + colors.append(self.color(thresh, active)) + + return colors + + def format(self) -> str: + if self.fmt: + return self.fmt.format(self.value) + return str(self.value) + + +class ChoiceSetting(SliderSetting): + def __init__(self, name: str, values: list, default=0, **kwargs): + default = values.index(default) + super().__init__(name, len(values) - 1, default=default, thresh=lambda v, t: v == t, **kwargs) + self.values = values + + @property + def value(self): + return self.values[self._value] + + def get_colors(self) -> list[int]: + colors = [] + value = self.value + for i in range(self.width + 1): + thresh = self.values[int(self.max * i / self.width)] + active = self.thresh(value, thresh) + colors.append(self.color(thresh, active)) + return colors + + +class Settings: + settings: dict[str, Setting] + + def __init__(self, settings: dict[str, Setting]): + self.settings = settings + self.change_handlers = {} + + def on(self, id: str, fn): + if id in self.change_handlers: + raise ValueError("already have a handler for {}".format(id)) + self.change_handlers[id] = fn + + def dispatch(self, id: str): + self.change_handlers[id](self.settings[id].value) + + def get(self, id: str) -> Setting: + return self.settings[id] + + def load(self): + try: + with open('hex.config.json', 'r') as f: + data = json.load(f) + for id in self.settings: + if id in data: + self.settings[id].value = data[id] + + self.dispatch(id) + except OSError: + pass + + def store(self): + data = {} + for id in self.settings: + data[id] = self.settings[id].value + + try: + with open('hex.config.json', 'w') as f: + json.dump(data, f) + except OSError: + pass + pass + + +class MenuMode(Mode): + color = (1.0, 1.0, 1.0) + + settings: Settings + ui_settings: list[Setting] + + SETTING_ACTIVE = CHSV(0.12, 1.0, 1.0).pack() + SETTING_ACTIVE_DIM = CHSV(0.12, 1.0, 0.15).pack() + + def __init__(self, *args, settings: Settings, ui_settings_ids: list[str]): + super().__init__(*args) + + self.settings = settings + self.ui_settings_ids = ui_settings_ids + self.ui_settings = [settings.get(id) for id in ui_settings_ids] + self.settings_i = 0 + + self.group = displayio.Group() + self.label_settings = label.Label(FONT_10, text="volume", color=0xffffff, anchor_point=(0, -1), anchored_position=(0, 6)) + self.label_value = label.Label(FONT_10, text="100%", color=0xffffff, anchor_point=(1, -1), anchored_position=(128, 22)) + self.group.append(self.label_settings) + self.group.append(self.label_value) + + def exit(self): + super().exit() + self.settings.store() + + def enter(self): + super().enter() + self.update_display(True) + + @property + def setting_id(self) -> str: + return self.ui_settings_ids[self.settings_i] + + @property + def setting(self) -> Setting: + return self.ui_settings[self.settings_i] + + def update_pixels(self, pixels: NeoPixel): + pixels.fill(0) + + super().update_pixels(pixels) + + for i, setting in enumerate(self.ui_settings): + pixels[led_map[i]] = self.SETTING_ACTIVE_DIM + pixels[led_map[self.settings_i]] = self.SETTING_ACTIVE + + for i, color in enumerate(self.setting.get_colors()): + pixels[led_map[24+i]] = color + + def update_display(self, update_name=False): + if update_name: + self.label_settings.text = self.setting.name + self.label_value.text = str(self.setting.format()) + + def key_event(self, i: int, pressed: bool) -> bool: + if pressed and i == Key.MENU_I: + self.keyboard.mode = self.keyboard.modes["base"] + return True + + if not pressed: + return False + + if i < len(self.ui_settings): + self.settings_i = i + self.update_display(True) + return True + + if 23 < i < 48: + self.setting.press_key(i - 24) + self.update_display() + self.settings.dispatch(self.setting_id) + return True + + if i == Key.PREV_I: + self.setting.press_prev() + self.update_display() + self.settings.dispatch(self.setting_id) + return True + + if i == Key.NEXT_I: + self.setting.press_next() + self.update_display() + self.settings.dispatch(self.setting_id) + return True diff --git a/hex33board/util.py b/hex33board/util.py new file mode 100644 index 0000000..52bf10d --- /dev/null +++ b/hex33board/util.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from adafruit_bitmap_font import bitmap_font +from micropython import const + +FONT_10 = bitmap_font.load_font("fonts/bitbuntu.pcf") + +_TICKS_PERIOD = const(1 << 29) +_TICKS_MAX = const(_TICKS_PERIOD - 1) +_TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2) + + +def ticks_diff(ticks1, ticks2): + ''' Compute the signed difference between two ticks values, + assuming that they are within 2**28 ticks + ''' + + diff = (ticks1 - ticks2) & _TICKS_MAX + diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD + return diff + + +led_map = [] +led_map.extend(range(15, 3, -1)) +led_map.extend(range(16, 28)) +led_map.extend(range(39, 27, -1)) +led_map.extend(range(40, 52)) diff --git a/test.wav b/test.wav Binary files differnew file mode 100644 index 0000000..7541aa5 --- /dev/null +++ b/test.wav |
