git.s-ol.nu hw/0x33.board/firmware / 41bc53a
initial commit s-ol 4 months ago
11 changed file(s) with 1004 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 boot_out.txt
1 hex.config.json
2
3 __pycache__
4 lib
5
6 .Trashes
7 .fseventsd
8 .metadata_never_index
9 System Volume Information
0 import supervisor
1 import board
2 import digitalio
3 import storage
4
5 supervisor.set_usb_identification("s-ol", "0x33.board", 0x732D, 0x3362)
6
7 col = digitalio.DigitalInOut(board.GP8)
8 row = digitalio.DigitalInOut(board.GP10)
9 col.switch_to_output(value=True)
10 row.switch_to_input(pull=digitalio.Pull.DOWN)
11 if row.value:
12 storage.disable_usb_drive()
13 storage.remount("/", False)
14 print("Mounting read-write")
15 else:
16 print("Mounting readonly")
0 # import test
1 from hex33board import Keyboard
2
3 k = Keyboard()
4 k.run()
Binary diff not shown
0 from __future__ import annotations
1
2 import board
3 from busio import I2C, UART
4 from adafruit_displayio_ssd1306 import SSD1306
5 from adafruit_display_text import label
6 from displayio import I2CDisplay
7 from neopixel import NeoPixel
8 from audiopwmio import PWMAudioOut
9 from audiocore import WaveFile
10 import displayio
11 import supervisor
12
13 import usb_midi
14 from adafruit_midi import MIDI
15
16 from .matrix import Matrix
17 from .util import ticks_diff, FONT_10, led_map
18 from .base import Key, Note, Scale, Layout, WickiHaydenLayout, HarmonicLayout, GerhardLayout, Mode
19 from .menu import Settings, SliderSetting, ChoiceSetting, MenuMode, _color_offor
20
21
22 class Keyboard:
23 scale: Scale
24 layout: Layout
25 mode: Mode
26
27 keys: list[Key]
28 notes: dict[int, Note]
29
30 matrix: Matrix
31 pixels: NeoPixel
32 display: SSD1306
33
34 midi_usb: MIDI
35 midi_din: MIDI
36
37 audio_out: PWMAudioOut
38
39 def __init__(self):
40 self.matrix = Matrix(
41 [board.GP8, board.GP4, board.GP0, board.GP6, board.GP7, board.GP9],
42 [board.GP5, board.GP1, board.GP2, board.GP3, board.GP10],
43 )
44
45 self.pixels = NeoPixel(board.GP11, 48 + 4, auto_write=False)
46
47 displayio.release_displays()
48 i2c = I2C(sda=board.GP14, scl=board.GP15)
49 bus = I2CDisplay(i2c, device_address=0x3C)
50 self.display = SSD1306(bus, width=128, height=32, rotation=180, auto_refresh=False)
51
52 din = UART(tx=board.GP16, rx=None, baudrate=31250)
53 self.midi_din = MIDI(None, din)
54
55 midi_in = next(p for p in usb_midi.ports if isinstance(p, usb_midi.PortIn))
56 midi_out = next(p for p in usb_midi.ports if isinstance(p, usb_midi.PortOut))
57 self.midi_usb = MIDI(midi_in, midi_out)
58
59 self.audio_out = PWMAudioOut(left_channel=board.GP12, right_channel=board.GP13)
60 self.test_file = WaveFile(open("test.wav", "rb"))
61
62 self.keys = [Key(self, i) for i in range(48)]
63 self.notes = {}
64 self.notes_expiring = {}
65
66 self.settings = Settings({
67 "midi_ch_usb": SliderSetting("MIDI out chan (USB)", 15, fmt="CH{}", thresh = lambda v, t: v == t),
68 "midi_ch_din": SliderSetting("MIDI out chan (Jack)", 15, fmt="CH{}", thresh = lambda v, t: v == t),
69 "midi_vel": SliderSetting("MIDI velocity", 127, default=64),
70 "rgb_bright": SliderSetting("LED brightness", 100, default=100, fmt="{}%", color=_color_offor),
71 "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),
72 "layout_name": ChoiceSetting("Keyboard Layout", ['wicki/hayden', 'harmonic table', 'gerhard'], default='wicki/hayden'),
73
74 "layout_offset": SliderSetting("Layout start note", 127, default=24),
75 "scale_name": ChoiceSetting("Highlight Scale", ['major', 'minor nat', 'minor harm', 'minor mel', 'minor hung', 'whole', 'penta'], default='major'),
76 "scale_root": SliderSetting("Scale root note", 127, default=43),
77 })
78
79 self.layout = WickiHaydenLayout()
80 self.scale = Scale(0, Scale.STEPS["major"], "major")
81
82 self.settings.on('midi_ch_usb', self.on_midi_ch_usb)
83 self.settings.on('midi_ch_din', self.on_midi_ch_din)
84 self.settings.on('midi_vel', self.on_midi_vel)
85 self.settings.on('rgb_bright', self.on_rgb_bright)
86 self.settings.on('jam_timeout', self.on_jam_timeout)
87 self.settings.on('layout_name', self.on_layout)
88 self.settings.on('layout_offset', self.on_layout)
89 self.settings.on('scale_name', self.on_scale)
90 self.settings.on('scale_root', self.on_scale)
91
92 self.modes = {
93 "base": BaseMode(self),
94 "base_shift": BaseShiftMode(self),
95 "menu": MenuMode(self, settings=self.settings, ui_settings_ids=[
96 "midi_ch_usb", "midi_ch_din", "midi_vel", "rgb_bright", "jam_timeout", "layout_name"
97 ]),
98 }
99 self.mode = self.modes["base"]
100
101 self.settings.load()
102
103 def on_midi_ch_usb(self, ch): self.midi_usb.out_channel = ch
104 def on_midi_ch_din(self, ch): self.midi_din.out_channel = ch
105 def on_midi_vel(self, vel): self.velocity = vel
106 def on_rgb_bright(self, b): self.pixels.brightness = b/100
107 def on_jam_timeout(self, t): self.jam_timeout = t * 1000
108 def on_layout(self, v):
109 name = self.settings.get('layout_name').value
110 offset = self.settings.get('layout_offset').value
111 if name == 'wicki/hayden':
112 self.layout = WickiHaydenLayout(offset)
113 elif name == 'harmonic table':
114 self.layout = HarmonicLayout(offset)
115 elif name == 'gerhard':
116 self.layout = GerhardLayout(offset)
117 self.update_scale()
118 self.audio_out.play(self.test_file)
119 def on_scale(self, v):
120 name = self.settings.get('scale_name').value
121 root = self.settings.get('scale_root').value
122 self.scale = Scale(root, Scale.STEPS[name], name)
123 self.update_scale()
124
125 @property
126 def mode(self) -> Mode:
127 return self._mode
128
129 @mode.setter
130 def mode(self, mode: Mode):
131 if hasattr(self, '_mode'):
132 if self._mode == mode:
133 return
134 self._mode.exit()
135
136 self._mode = mode
137 self._mode.enter()
138
139 def update_scale(self):
140 for key in self.keys:
141 key.update_scale()
142
143 def send_midi(self, message):
144 self.midi_usb.send(message)
145 self.midi_din.send(message)
146
147 def run(self):
148 while True:
149 ticks_ms = supervisor.ticks_ms()
150 for note in self.notes_expiring.values():
151 note.update_expiry(ticks_ms)
152
153 for (i, pressed) in self.matrix.scan_for_changes():
154 if self.mode:
155 handled = self.mode.key_event(i, pressed)
156 if handled:
157 continue
158
159 if i < len(self.keys):
160 key = self.keys[i]
161 if pressed:
162 key.on_press()
163 else:
164 key.on_release()
165
166 for key in self.keys:
167 self.pixels[led_map[key.i]] = key.update_color()
168 self.mode.update_pixels(self.pixels)
169
170 active = [pitch for pitch in self.notes]
171 active.sort()
172 self.modes["base"].notes_label.text = ' '.join(self.scale.label(p) for p in active)
173
174 self.pixels.show()
175 self.display.refresh()
176
177
178 class BaseShiftMode(Mode):
179 color = (0.1, 1.0, 1.0)
180
181 def enter(self):
182 self.entered = supervisor.ticks_ms()
183 self.pressed_something = False
184
185 def key_event(self, i: int, pressed: bool) -> bool:
186 if not pressed and i == Key.MENU_I:
187 held = ticks_diff(supervisor.ticks_ms(), self.entered)
188 if not self.pressed_something and held <= 800:
189 # tap: enter menu
190 self.keyboard.mode = self.keyboard.modes["menu"]
191 return True
192
193 self.keyboard.mode = self.keyboard.modes["base"]
194 return True
195
196 if not pressed:
197 return False
198
199 if i < len(self.keyboard.keys):
200 key = self.keyboard.keys[i]
201 self.keyboard.settings.get("scale_root").value = key._pitch
202 self.keyboard.settings.dispatch("scale_root")
203 self.keyboard.modes["base"].update_display()
204 self.pressed_something = True
205 return True
206
207 if i == Key.PREV_I:
208 self.keyboard.settings.get("scale_name").press_prev()
209 self.keyboard.settings.dispatch("scale_name")
210 self.keyboard.modes["base"].update_display()
211 self.pressed_something = True
212 return True
213
214 if i == Key.NEXT_I:
215 self.keyboard.settings.get("scale_name").press_next()
216 self.keyboard.settings.dispatch("scale_name")
217 self.keyboard.modes["base"].update_display()
218 self.pressed_something = True
219 return True
220
221
222 class BaseMode(Mode):
223 label: label.Label
224
225 def __init__(self, *args):
226 super().__init__(*args)
227
228 self.scale_label = label.Label(FONT_10, text="", color=0xffffff, x=0, y=3)
229 self.notes_label = label.Label(FONT_10, text="", color=0xffffff, x=2, y=18)
230 self.group.append(self.scale_label)
231 self.group.append(self.notes_label)
232
233 def enter(self):
234 super().enter()
235 self.update_display()
236
237 def update_display(self):
238 self.scale_label.text = self.keyboard.scale.format()
239
240 def key_event(self, i: int, pressed: bool) -> bool:
241 if not pressed:
242 return False
243
244 if i == Key.MENU_I:
245 self.keyboard.mode = self.keyboard.modes["base_shift"]
246 return True
247
248 if i == Key.PREV_I:
249 self.keyboard.layout.offset -= self.keyboard.scale.size
250 self.keyboard.update_scale()
251
252 layout_offset = self.keyboard.settings.get("layout_offset")
253 layout_offset.value -= self.keyboard.scale.size
254 self.keyboard.settings.dispatch("layout_offset")
255 return True
256
257 if i == Key.NEXT_I:
258 layout_offset = self.keyboard.settings.get("layout_offset")
259 layout_offset.value += self.keyboard.scale.size
260 self.keyboard.settings.dispatch("layout_offset")
261 return True
0 from __future__ import annotations
1
2 import displayio
3 import supervisor
4 from micropython import const
5 from adafruit_midi.note_on import NoteOn
6 from adafruit_midi.note_off import NoteOff
7 from adafruit_fancyled.adafruit_fancyled import CHSV, clamp_norm
8
9 from .util import ticks_diff
10
11
12 class Scale:
13 STEPS = {
14 "major": [2, 2, 1, 2, 2, 2, 1],
15 "minor nat": [2, 1, 2, 2, 1, 2, 2],
16 "minor harm": [2, 1, 2, 2, 1, 3, 1],
17 "minor mel": [2, 1, 2, 2, 2, 2, 1],
18 "minor hung": [2, 1, 3, 1, 1, 3, 1],
19 "whole": [2, 2, 2, 2, 2, 2],
20 "penta": [2, 2, 3, 2, 3],
21 }
22
23 # LABELS = {
24 # 'english': 'C C# D D# E F F# G G# A A# B'.split(' '),
25 # 'german': 'C C# D D# E F F# G G# A A# H'.split(' '),
26 # 'sol': 'do do# re re# mi fa fa# sol sol# la la# si'.split(' '),
27 # }
28 LABELS = 'C C# D D# E F F# G G# A A# B'.split(' ')
29
30 root: int
31 name: str
32 notes: list[int]
33 size: int
34
35 def __init__(self, root: int, steps: list[int], name: str):
36 self.root = root
37 self.name = name
38 self.notes = [sum(steps[:i]) for i in range(len(steps))]
39 self.size = sum(steps)
40
41 def get_hsv(self, pitch: int) -> tuple[float, float, float]:
42 relative_pitch = (pitch - self.root) % self.size
43
44 if relative_pitch not in self.notes:
45 # out of scale
46 return (.3, 0.8, 0.0)
47
48 if self.root <= pitch < self.root + self.size:
49 # in core scale
50 return (.1, 1.0, 0.2)
51
52 # in scale
53 return (.7, 0.9, 0.15)
54
55 def label(self, pitch):
56 return self.LABELS[pitch % self.size]
57
58 def format(self):
59 oct = self.root // self.size
60 off = self.root % self.size
61
62 return "scale: {} {} {}".format(
63 self.LABELS[off],
64 oct,
65 self.name,
66 )
67
68
69 class Layout:
70 offset: int
71
72 def __init__(self, offset: int = 24):
73 self.offset = offset
74
75 def get_pitch(self, key: Key) -> int:
76 pass
77
78
79 class WickiHaydenLayout(Layout):
80 def get_pitch(self, key: Key) -> int:
81 x, y = key.pos
82 return int(self.offset + 2*x + 6*y)
83
84
85 class HarmonicLayout(Layout):
86 def get_pitch(self, key: Key) -> int:
87 x, y = key.pos
88 return int(self.offset + 4*x + 5*y)
89
90
91 class GerhardLayout(Layout):
92 def get_pitch(self, key: Key) -> int:
93 x, y = key.pos
94 return int(self.offset + 3*x + 2.5*y)
95
96
97 class Note:
98 keyboard: Keyboard
99 pitch: int
100
101 end_tick: None | int
102 expiry: float
103
104 def __init__(self, keyboard: Keyboard, pitch: int):
105 self.keyboard = keyboard
106 self.pitch = pitch
107 self.end_tick = None
108 self.expiry = 1.0
109
110 def on(self):
111 self.keyboard.notes[self.pitch] = self
112 self.keyboard.send_midi(NoteOn(self.pitch, self.keyboard.velocity))
113
114 if self.pitch in self.keyboard.notes_expiring:
115 del self.keyboard.notes_expiring[self.pitch]
116
117 def off(self):
118 if self.keyboard.notes.get(self.pitch) is self:
119 self.keyboard.notes_expiring[self.pitch] = self
120 del self.keyboard.notes[self.pitch]
121
122 self.keyboard.send_midi(NoteOff(self.pitch))
123 self.end_tick = supervisor.ticks_ms()
124
125 def update_expiry(self, ticks_ms_now):
126 delta = ticks_diff(ticks_ms_now, self.end_tick)
127
128 if delta > self.keyboard.jam_timeout:
129 del self.keyboard.notes_expiring[self.pitch]
130 self.expiry = 0
131 return
132
133 self.expiry = clamp_norm(1 - delta / self.keyboard.jam_timeout)
134
135
136 class Key:
137 MENU_I = const(48+0)
138 PREV_I = const(48+4)
139 NEXT_I = const(48+5)
140
141 keyboard: Keyboard
142
143 i: int
144 '''matrix index for this key.'''
145
146 pos: tuple[float, int]
147 '''logical coordinates for this key.'''
148
149 note: None | Note
150
151 _pitch: int
152 _color: tuple[float, float, float]
153
154 def __init__(self, keyboard: Keyboard, i: int):
155 x = i % 12
156 y = i // 12
157
158 y = 4 - y
159 if y % 2 == 0:
160 x += 0.5
161
162 self.keyboard = keyboard
163 self.i = i
164 self.pos = (x, y)
165 self.note = None
166
167 def update_scale(self):
168 self._pitch = self.keyboard.layout.get_pitch(self)
169 self._hsv = self.keyboard.scale.get_hsv(self._pitch)
170
171 def update_color(self):
172 if self._pitch < 0 or self._pitch > 127:
173 return 0
174
175 hue, sat, key_val = self._hsv
176
177 note_for_pitch = self.keyboard.notes.get(self._pitch)
178 note_for_pitch = note_for_pitch or self.keyboard.notes_expiring.get(self._pitch)
179
180 if self.note:
181 value = 1.0
182 elif note_for_pitch:
183 value = note_for_pitch.expiry * 0.8
184 else:
185 value = 0.0
186
187 rest = 1.0 - key_val
188 value = key_val + value * rest
189 return CHSV(hue, sat, value).pack()
190
191 def on_press(self):
192 if self.note:
193 self.note.off()
194
195 if self._pitch < 0 or self._pitch > 127:
196 return
197
198 self.note = Note(self.keyboard, self._pitch)
199 self.note.on()
200
201 def on_release(self):
202 if not self.note:
203 return
204
205 self.note.off()
206 self.note = None
207
208
209 class Mode:
210 keyboard: Keyboard
211 group: displayio.Group = displayio.Group()
212 color: tuple[float, float, float] = (0.5, 1.0, 1.0)
213
214 def __init__(self, keyboard: Keyboard):
215 self.keyboard = keyboard
216
217 def enter(self):
218 self.keyboard.display.show(self.group)
219
220 def exit(self):
221 pass
222
223 def key_event(self, i: int, pressed: bool) -> bool:
224 return False
225
226 def update_pixels(self, pixels: NeoPixel):
227 pixels[0] = CHSV(*self.color).pack()
0 from __future__ import annotations
1
2 from digitalio import DigitalInOut, Pull
3
4
5 def range_rev(start, stop):
6 return range(stop - 1, start - 1, -1)
7
8
9 def mapping_up_down(cols, rows):
10 '''
11 Maps keys according to the following diagram:
12
13 1 2 3
14 +------
15 1| A A A
16 2| A A A
17 |
18 1| B B B
19 2| B B B
20 '''
21
22 coord_mapping = list(range(2 * cols * rows))
23 return coord_mapping
24
25
26 def mapping_up_down_mirrored(cols, rows):
27 '''
28 Maps keys according to the following diagram:
29
30 1 2 3
31 +------
32 1| A A A
33 2| A A A
34 |
35 2| B B B
36 1| B B B
37 '''
38
39 coord_mapping = []
40 coord_mapping.extend(range(cols * rows))
41 coord_mapping.extend(range_rev(cols * rows, 2 * cols * rows))
42 return coord_mapping
43
44
45 def mapping_left_right(cols, rows):
46 '''
47 Maps keys according to the following diagram:
48
49 1 2 3 1 2 3
50 +-------------
51 1| A A A B B B
52 2| A A A B B B
53 '''
54
55 size = cols * rows
56 coord_mapping = []
57 for y in range(rows):
58 yy = y * cols
59 coord_mapping.extend(range(yy, yy + cols))
60 coord_mapping.extend(range(yy + size, yy + cols + size))
61 return coord_mapping
62
63
64 def mapping_left_right_mirrored(cols, rows):
65 '''
66 Maps keys according to the following diagram:
67
68 1 2 3 3 2 1
69 +-------------
70 1| A A A B B B
71 2| A A A B B B
72 '''
73
74 size = cols * rows
75 coord_mapping = []
76 for y in range(rows):
77 yy = y * 2 * cols
78 coord_mapping.extend(range(yy, yy + cols))
79 coord_mapping.extend(range_rev(yy + size, yy + cols + size))
80 return coord_mapping
81
82
83 def mapping_interleave_cols(cols, rows):
84 '''
85 Maps keys according to the following diagram:
86
87 1 1 2 2 3 3
88 +-------------
89 1| A B A B A B
90 2| A B A B A B
91 '''
92
93 coord_mapping = []
94 mat = cols * rows
95 for i in range(mat):
96 coord_mapping.append(i)
97 coord_mapping.append(i + mat)
98 return coord_mapping
99
100
101 # these two are actually equivalent
102 mapping_interleave_rows = mapping_left_right
103
104
105 class Matrix:
106 '''
107 A Scanner for Keyboard Matrices with Diodes in both directions.
108
109 In a bidirectional matrix, each (col, row) crossing can be used twice -
110 once with a ROW2COL diode ("A"), and once with a COL2ROW diode ("B").
111
112 The raw key numbers returned by this scanner are based on this layout ("up_down"):
113
114 C1 C2 C3
115 +-----------
116 R1| A0 A1 A2
117 R2| A3 A4 A5
118 +-----------
119 R1| B6 B7 B8
120 R1| B9 B10 B11
121
122 If the physical layout of the matrix is different, you can pass a function
123 for `mapping`. The function is passed `len_cols` and `len_rows` and should
124 return a `coord_mapping` list.
125 Various common mappings are provided in this module, see:
126 - `kmk.scanners.bidirectional.mapping_left_right`
127 - `kmk.scanners.bidirectional.mapping_left_right_mirrored`
128 - `kmk.scanners.bidirectional.mapping_up_down`
129 - `kmk.scanners.bidirectional.mapping_up_down_mirrored`
130 - `kmk.scanners.bidirectional.mapping_interleave_rows`
131 - `kmk.scanners.bidirectional.mapping_interleave_cols`
132
133 :param cols: A sequence of pins that are the columns for matrix A.
134 :param rows: A sequence of pins that are the rows for matrix A.
135 :param mapping: A coord_mapping generator function, see above.
136 '''
137
138 def __init__(self, cols, rows, mapping=mapping_left_right):
139 self.len_cols = len(cols)
140 self.len_rows = len(rows)
141 self.half_size = self.len_cols * self.len_rows
142 self.keys = self.half_size * 2
143
144 self.coord_mapping = mapping(self.len_cols, self.len_rows)
145
146 # A pin cannot be both a row and column, detect this by combining the
147 # two tuples into a set and validating that the length did not drop
148 #
149 # repr() hackery is because CircuitPython Pin objects are not hashable
150 unique_pins = {repr(c) for c in cols} | {repr(r) for r in rows}
151 assert (
152 len(unique_pins) == self.len_cols + self.len_rows
153 ), 'Cannot use a pin as both a column and row'
154 del unique_pins
155
156 # __class__.__name__ is used instead of isinstance as the MCP230xx lib
157 # does not use the digitalio.DigitalInOut, but rather a self defined one:
158 # https://github.com/adafruit/Adafruit_CircuitPython_MCP230xx/blob/3f04abbd65ba5fa938fcb04b99e92ae48a8c9406/adafruit_mcp230xx/digital_inout.py#L33
159
160 self.cols = [
161 x if x.__class__.__name__ == 'DigitalInOut' else DigitalInOut(x)
162 for x in cols
163 ]
164 self.rows = [
165 x if x.__class__.__name__ == 'DigitalInOut' else DigitalInOut(x)
166 for x in rows
167 ]
168
169 self.state = bytearray(self.keys)
170
171 def scan_for_changes(self):
172 for (inputs, outputs, flip) in [
173 (self.rows, self.cols, False),
174 (self.cols, self.rows, True),
175 ]:
176 for pin in outputs:
177 pin.switch_to_input()
178
179 for pin in inputs:
180 pin.switch_to_input(pull=Pull.DOWN)
181
182 for oidx, opin in enumerate(outputs):
183 opin.switch_to_output(value=True)
184
185 for iidx, ipin in enumerate(inputs):
186 if flip:
187 ba_idx = oidx * len(inputs) + iidx + self.half_size
188 else:
189 ba_idx = iidx * len(outputs) + oidx
190
191 # cast to int to avoid
192 #
193 # >>> xyz = bytearray(3)
194 # >>> xyz[2] = True
195 # Traceback (most recent call last):
196 # File "<stdin>", line 1, in <module>
197 # OverflowError: value would overflow a 1 byte buffer
198 #
199 # I haven't dived too far into what causes this, but it's
200 # almost certainly because bool types in Python aren't just
201 # aliases to int values, but are proper pseudo-types
202 new_val = int(ipin.value)
203 old_val = self.state[ba_idx]
204
205 if old_val != new_val:
206 self.state[ba_idx] = new_val
207 yield self.coord_mapping.index(ba_idx), new_val
208
209 opin.switch_to_input()
0 from __future__ import annotations
1
2 from adafruit_display_text import label
3 from adafruit_fancyled.adafruit_fancyled import CHSV
4 import displayio
5 import json
6
7 from .util import FONT_10, led_map
8 from .base import Mode, Key
9
10 BLUE = CHSV(0.48, 1.0, 1.0).pack()
11 BLUE_DIM = CHSV(0.48, 1.0, 0.15).pack()
12
13 CHOICE_NEUTRAL = CHSV(0.0, 0.0, 0.0).pack()
14 RED = CHSV(0.95, 1.0, 1.0).pack()
15 RED_DIM = CHSV(0.95, 1.0, 0.15).pack()
16 GREEN = CHSV(0.27, 1.0, 1.0).pack()
17 GREEN_DIM = CHSV(0.27, 1.0, 0.15).pack()
18
19
20 def _thresh(val, t):
21 val >= t
22
23
24 def _color(val, active: bool) -> tuple[float, float, float]:
25 return BLUE if active else BLUE_DIM
26
27
28 def _color_offor(val, active: bool) -> tuple[float, float, float]:
29 if val:
30 return BLUE if active else BLUE_DIM
31 else:
32 return RED if active else RED_DIM
33
34
35 class Setting:
36 name: str
37 _value: any
38
39 def __init__(self, name: str, default: any):
40 self.name = name
41 self._value = default
42
43 @property
44 def value(self):
45 return self._value
46
47 @value.setter
48 def value(self, val):
49 self._value = val
50
51
52 class SliderSetting(Setting):
53 name: str
54 max: int
55 _value: int
56 fmt: str | None
57
58 def __init__(self, name: str, max: int, default: int = 0, fmt = None, thresh = _thresh, color = _color):
59 super().__init__(name, default)
60
61 self.max = max
62 self.fmt = fmt
63 self.width = min(23, max)
64
65 self.thresh = thresh
66 self.color = color
67
68 def press_prev(self):
69 self._value = (self._value - 1) % (self.max + 1)
70
71 def press_next(self):
72 self._value = (self._value + 1) % (self.max + 1)
73
74 def press_key(self, i):
75 if i > self.width:
76 return
77
78 self._value = int(self.max * i / self.width)
79
80 def get_colors(self) -> list[int]:
81 colors = []
82 for i in range(self.width + 1):
83 thresh = int(self.max * i / self.width)
84 active = self.thresh(self.value, thresh)
85 colors.append(self.color(thresh, active))
86
87 return colors
88
89 def format(self) -> str:
90 if self.fmt:
91 return self.fmt.format(self.value)
92 return str(self.value)
93
94
95 class ChoiceSetting(SliderSetting):
96 def __init__(self, name: str, values: list, default=0, **kwargs):
97 default = values.index(default)
98 super().__init__(name, len(values) - 1, default=default, thresh=lambda v, t: v == t, **kwargs)
99 self.values = values
100
101 @property
102 def value(self):
103 return self.values[self._value]
104
105 def get_colors(self) -> list[int]:
106 colors = []
107 value = self.value
108 for i in range(self.width + 1):
109 thresh = self.values[int(self.max * i / self.width)]
110 active = self.thresh(value, thresh)
111 colors.append(self.color(thresh, active))
112 return colors
113
114
115 class Settings:
116 settings: dict[str, Setting]
117
118 def __init__(self, settings: dict[str, Setting]):
119 self.settings = settings
120 self.change_handlers = {}
121
122 def on(self, id: str, fn):
123 if id in self.change_handlers:
124 raise ValueError("already have a handler for {}".format(id))
125 self.change_handlers[id] = fn
126
127 def dispatch(self, id: str):
128 self.change_handlers[id](self.settings[id].value)
129
130 def get(self, id: str) -> Setting:
131 return self.settings[id]
132
133 def load(self):
134 try:
135 with open('hex.config.json', 'r') as f:
136 data = json.load(f)
137 for id in self.settings:
138 if id in data:
139 self.settings[id].value = data[id]
140
141 self.dispatch(id)
142 except OSError:
143 pass
144
145 def store(self):
146 data = {}
147 for id in self.settings:
148 data[id] = self.settings[id].value
149
150 try:
151 with open('hex.config.json', 'w') as f:
152 json.dump(data, f)
153 except OSError:
154 pass
155 pass
156
157
158 class MenuMode(Mode):
159 color = (1.0, 1.0, 1.0)
160
161 settings: Settings
162 ui_settings: list[Setting]
163
164 SETTING_ACTIVE = CHSV(0.12, 1.0, 1.0).pack()
165 SETTING_ACTIVE_DIM = CHSV(0.12, 1.0, 0.15).pack()
166
167 def __init__(self, *args, settings: Settings, ui_settings_ids: list[str]):
168 super().__init__(*args)
169
170 self.settings = settings
171 self.ui_settings_ids = ui_settings_ids
172 self.ui_settings = [settings.get(id) for id in ui_settings_ids]
173 self.settings_i = 0
174
175 self.group = displayio.Group()
176 self.label_settings = label.Label(FONT_10, text="volume", color=0xffffff, anchor_point=(0, -1), anchored_position=(0, 6))
177 self.label_value = label.Label(FONT_10, text="100%", color=0xffffff, anchor_point=(1, -1), anchored_position=(128, 22))
178 self.group.append(self.label_settings)
179 self.group.append(self.label_value)
180
181 def exit(self):
182 super().exit()
183 self.settings.store()
184
185 def enter(self):
186 super().enter()
187 self.update_display(True)
188
189 @property
190 def setting_id(self) -> str:
191 return self.ui_settings_ids[self.settings_i]
192
193 @property
194 def setting(self) -> Setting:
195 return self.ui_settings[self.settings_i]
196
197 def update_pixels(self, pixels: NeoPixel):
198 pixels.fill(0)
199
200 super().update_pixels(pixels)
201
202 for i, setting in enumerate(self.ui_settings):
203 pixels[led_map[i]] = self.SETTING_ACTIVE_DIM
204 pixels[led_map[self.settings_i]] = self.SETTING_ACTIVE
205
206 for i, color in enumerate(self.setting.get_colors()):
207 pixels[led_map[24+i]] = color
208
209 def update_display(self, update_name=False):
210 if update_name:
211 self.label_settings.text = self.setting.name
212 self.label_value.text = str(self.setting.format())
213
214 def key_event(self, i: int, pressed: bool) -> bool:
215 if pressed and i == Key.MENU_I:
216 self.keyboard.mode = self.keyboard.modes["base"]
217 return True
218
219 if not pressed:
220 return False
221
222 if i < len(self.ui_settings):
223 self.settings_i = i
224 self.update_display(True)
225 return True
226
227 if 23 < i < 48:
228 self.setting.press_key(i - 24)
229 self.update_display()
230 self.settings.dispatch(self.setting_id)
231 return True
232
233 if i == Key.PREV_I:
234 self.setting.press_prev()
235 self.update_display()
236 self.settings.dispatch(self.setting_id)
237 return True
238
239 if i == Key.NEXT_I:
240 self.setting.press_next()
241 self.update_display()
242 self.settings.dispatch(self.setting_id)
243 return True
0 from __future__ import annotations
1
2 from adafruit_bitmap_font import bitmap_font
3 from micropython import const
4
5 FONT_10 = bitmap_font.load_font("fonts/bitbuntu.pcf")
6
7 _TICKS_PERIOD = const(1 << 29)
8 _TICKS_MAX = const(_TICKS_PERIOD - 1)
9 _TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2)
10
11
12 def ticks_diff(ticks1, ticks2):
13 ''' Compute the signed difference between two ticks values,
14 assuming that they are within 2**28 ticks
15 '''
16
17 diff = (ticks1 - ticks2) & _TICKS_MAX
18 diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
19 return diff
20
21
22 led_map = []
23 led_map.extend(range(15, 3, -1))
24 led_map.extend(range(16, 28))
25 led_map.extend(range(39, 27, -1))
26 led_map.extend(range(40, 52))
Binary diff not shown