Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 89-apps-should-be-able-to-specify-if-they-want-wifi-to-be-disabled-when-entering-them
  • 9Rmain
  • allow-reloading-sunmenu
  • always-have-a-wifi-instance
  • anon/gpndemo
  • anon/update-sim
  • anon/webflasher
  • app_text_viewer
  • audio_input
  • audio_io
  • blm_dev_chan
  • ch3/bl00mbox_docs
  • ci-1690580595
  • dev_p4
  • dev_p4-iggy
  • dev_p4-iggy-rebased
  • dx/dldldld
  • dx/fb-save-restore
  • dx/hint-hint
  • dx/jacksense-headset-mic-only
  • events
  • fil3s-limit-filesize
  • fil3s-media
  • fpletz/flake
  • gr33nhouse-improvements
  • history-rewrite
  • icon-flower
  • iggy/stemming
  • iggy/stemming_merge
  • led_fix_fix
  • main
  • main+schneider
  • media_has_video_has_audio
  • micropython_api
  • mixer2
  • moon2_demo_temp
  • moon2_migrate_apps
  • more-accurate-battery
  • pippin/ctx_sprite_sheet_support
  • pippin/display-python-errors-on-display
  • pippin/make_empty_drawlists_skip_render_and_blit
  • pippin/more-accurate-battery
  • pippin/tcp_redirect_hack
  • pippin/tune_ctx_config_update_from_upstream
  • pippin/uhm_flash_access_bust
  • pressable_bugfix
  • py_only_update_fps_overlay_when_changing
  • q3k/doom-poc
  • q3k/render-to-texture
  • rahix/flow3rseeds
  • raw_captouch_new
  • raw_captouch_old
  • release/1.0.0
  • release/1.1.0
  • release/1.1.1
  • release/1.2.0
  • release/1.3.0
  • release/1.4.0
  • restore_blit
  • return_of_melodic_demo
  • rev4_micropython
  • schneider/application-remove-name
  • schneider/bhi581
  • schneider/factory_test
  • schneider/recovery
  • scope_hack
  • sdkconfig-spiram-tinyusb
  • sec/auto-nick
  • sec/blinky
  • sector_size_512
  • shoegaze-fps
  • smaller_gradient_lut
  • store_delta_ms_and_ins_as_class_members
  • task_cleanup
  • uctx-wip
  • w1f1-in-sim
  • widgets_draw
  • wifi-json-error-handling
  • wip-docs
  • wip-tinyusb
  • v1.0.0
  • v1.0.0+rc1
  • v1.0.0+rc2
  • v1.0.0+rc3
  • v1.0.0+rc4
  • v1.0.0+rc5
  • v1.0.0+rc6
  • v1.1.0
  • v1.1.0+rc1
  • v1.1.1
  • v1.2.0
  • v1.2.0+rc1
  • v1.3.0
  • v1.4.0
94 results

Target

Select target project
  • flow3r/flow3r-firmware
  • Vespasian/flow3r-firmware
  • alxndr42/flow3r-firmware
  • pl/flow3r-firmware
  • Kari/flow3r-firmware
  • raimue/flow3r-firmware
  • grandchild/flow3r-firmware
  • mu5tach3/flow3r-firmware
  • Nervengift/flow3r-firmware
  • arachnist/flow3r-firmware
  • TheNewCivilian/flow3r-firmware
  • alibi/flow3r-firmware
  • manuel_v/flow3r-firmware
  • xeniter/flow3r-firmware
  • maxbachmann/flow3r-firmware
  • yGifoom/flow3r-firmware
  • istobic/flow3r-firmware
  • EiNSTeiN_/flow3r-firmware
  • gnudalf/flow3r-firmware
  • 999eagle/flow3r-firmware
  • toerb/flow3r-firmware
  • pandark/flow3r-firmware
  • teal/flow3r-firmware
  • x42/flow3r-firmware
  • alufers/flow3r-firmware
  • dos/flow3r-firmware
  • yrlf/flow3r-firmware
  • LuKaRo/flow3r-firmware
  • ThomasElRubio/flow3r-firmware
  • ai/flow3r-firmware
  • T_X/flow3r-firmware
  • highTower/flow3r-firmware
  • beanieboi/flow3r-firmware
  • Woazboat/flow3r-firmware
  • gooniesbro/flow3r-firmware
  • marvino/flow3r-firmware
  • kressnerd/flow3r-firmware
  • quazgar/flow3r-firmware
  • aoid/flow3r-firmware
  • jkj/flow3r-firmware
  • naomi/flow3r-firmware
41 results
Select Git revision
  • 89-apps-should-be-able-to-specify-if-they-want-wifi-to-be-disabled-when-entering-them
  • 9Rmain
  • allow-reloading-sunmenu
  • always-have-a-wifi-instance
  • anon/gpndemo
  • anon/update-sim
  • anon/webflasher
  • app_text_viewer
  • audio_input
  • audio_io
  • blm_dev_chan
  • ch3/bl00mbox_docs
  • ci-1690580595
  • dev_p4
  • dev_p4-iggy
  • dev_p4-iggy-rebased
  • dx/dldldld
  • dx/fb-save-restore
  • dx/hint-hint
  • dx/jacksense-headset-mic-only
  • events
  • fil3s-limit-filesize
  • fil3s-media
  • fpletz/flake
  • gr33nhouse-improvements
  • history-rewrite
  • icon-flower
  • iggy/stemming
  • iggy/stemming_merge
  • led_fix_fix
  • main
  • main+schneider
  • media_has_video_has_audio
  • micropython_api
  • mixer2
  • moon2_demo_temp
  • moon2_migrate_apps
  • more-accurate-battery
  • pippin/ctx_sprite_sheet_support
  • pippin/display-python-errors-on-display
  • pippin/make_empty_drawlists_skip_render_and_blit
  • pippin/more-accurate-battery
  • pippin/tcp_redirect_hack
  • pippin/tune_ctx_config_update_from_upstream
  • pippin/uhm_flash_access_bust
  • pressable_bugfix
  • py_only_update_fps_overlay_when_changing
  • q3k/doom-poc
  • q3k/render-to-texture
  • rahix/flow3rseeds
  • raw_captouch_new
  • raw_captouch_old
  • release/1.0.0
  • release/1.1.0
  • release/1.1.1
  • release/1.2.0
  • release/1.3.0
  • release/1.4.0
  • restore_blit
  • return_of_melodic_demo
  • rev4_micropython
  • schneider/application-remove-name
  • schneider/bhi581
  • schneider/factory_test
  • schneider/recovery
  • scope_hack
  • sdkconfig-spiram-tinyusb
  • sec/auto-nick
  • sec/blinky
  • sector_size_512
  • shoegaze-fps
  • smaller_gradient_lut
  • store_delta_ms_and_ins_as_class_members
  • task_cleanup
  • uctx-wip
  • w1f1-in-sim
  • widgets_draw
  • wifi-json-error-handling
  • wip-docs
  • wip-tinyusb
  • v1.0.0
  • v1.0.0+rc1
  • v1.0.0+rc2
  • v1.0.0+rc3
  • v1.0.0+rc4
  • v1.0.0+rc5
  • v1.0.0+rc6
  • v1.1.0
  • v1.1.0+rc1
  • v1.1.1
  • v1.2.0
  • v1.2.0+rc1
  • v1.3.0
  • v1.4.0
94 results
Show changes
Showing
with 2259 additions and 329 deletions
......@@ -37,3 +37,10 @@ class IMUDemo(Application):
else:
self.v_x = 0
self.v_y = 0
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(IMUDemo)
[app]
name = "IMU Demo"
menu = "Apps"
category = "Demos"
[entry]
class = "IMUDemo"
......
import bl00mbox
blm = bl00mbox.Channel("Melodic Demo")
import captouch
import leds
from st3m.goose import List, Optional
from st3m.input import InputState
from st3m.input import InputState, InputController
from st3m.ui.view import ViewManager
from ctx import Context
octave = 0
synths: List[bl00mbox.patches.tinysynth_fm] = []
scale = [0, 2, 4, 5, 7, 9, 11]
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: 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)
highlight_bottom_petal(4, r, g, b)
highlight_bottom_petal(2, 55, 0, 55)
leds.set_rgb(18, 55, 0, 55)
leds.set_rgb(19, 55, 0, 55)
leds.set_rgb(27, 55, 0, 55)
leds.set_rgb(28, 55, 0, 55)
leds.update()
def adjust_playing_field_to_octave() -> None:
global octave
if octave == -1:
change_playing_field_color(0, 0, 55)
elif octave == 0:
change_playing_field_color(0, 27, 27)
elif octave == 1:
change_playing_field_color(0, 55, 0)
def run(ins: InputState) -> None:
global scale
global octave
global synths
for i in range(10):
petal = ins.captouch.petals[i]
if petal.pressed:
if i == 6:
octave = -1
adjust_playing_field_to_octave()
elif i == 5:
octave = 0
adjust_playing_field_to_octave()
elif i == 4:
octave = 1
adjust_playing_field_to_octave()
else:
k = 10 - i
if k > 3:
k -= 10
k = 3 - k
note = scale[k] + 12 * octave
synths[0].signals.pitch.tone = note
synths[0].signals.trigger.start()
def init() -> None:
global synths
for i in range(1):
synth = blm.new(bl00mbox.patches.tinysynth_fm)
synth.signals.output = blm.mixer
synths += [synth]
for synth in synths:
synth.signals.decay = 100
def foreground() -> None:
adjust_playing_field_to_octave()
from st3m.application import Application, ApplicationContext
from ctx import Context
import bl00mbox
import leds
import math
# TODO(q3k): properly port this app
class MelodicApp(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
init()
self.synths: List[bl00mbox.patches.tinysynth] = []
self.base_scale = [0, 2, 4, 5, 7, 9, 11]
self.mid_point = 7
self.mid_point_petal = 0
self.mid_point_lock = False
self.mid_point_petal_hyst = 3
self.min_note = -36
self.max_note = +30
self.at_min_note = False
self.at_max_note = False
self.auto_color = (1, 0.5, 1)
self.min_hue = 0
self.max_hue = 0
self.scale = [0] * 10
self.prev_note = None
self.legato = False
self.blm = None
def base_scale_get_val_from_mod_index(self, index):
o = index // len(self.base_scale)
i = index % len(self.base_scale)
return 12 * o + self.base_scale[i]
def base_scale_get_mod_index_from_val(self, val):
val = int(val)
index = val
while True:
try:
i = self.base_scale.index(index % 12)
break
except:
index -= 1
o = val // 12
return i + len(self.base_scale) * o
def make_scale(self):
i = self.base_scale_get_mod_index_from_val(self.mid_point)
for j in range(-5, 5):
tone = self.base_scale_get_val_from_mod_index(i + j)
self.scale[(self.mid_point_petal + j) % 10] = tone
def draw(self, ctx: Context) -> None:
ctx.rgb(1, 1, 1).rectangle(-120, -120, 240, 240).fill()
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.text_align = ctx.CENTER
ctx.get_font_name(4)
ctx.font_size = 20
ctx.rgb(0.8, 0.8, 0.8)
ctx.move_to(0, -15)
ctx.text("note shift:")
ctx.font_size = 25
ctx.move_to(0, 15)
if self.mid_point_lock:
ctx.rgb(0, 0.5, 1)
ctx.text("manual")
else:
ctx.rgb(*self.auto_color)
ctx.text("auto")
ctx.rgb(0, 0.5, 1)
ctx.rotate(math.tau * ((self.mid_point_petal / 10) - 0.05))
tmp = ctx.line_width
ctx.line_width = 30
ctx.arc(0, 0, 105, -math.tau * 0.7, math.tau * 0.2, 0).stroke()
if not self.mid_point_lock:
ctx.rgb(*self.auto_color)
if not self.at_max_note:
ctx.arc(0, 0, 105, math.tau * 0.05, math.tau * 0.2, 0).stroke()
if not self.at_min_note:
ctx.arc(0, 0, 105, -math.tau * 0.7, -math.tau * 0.55, 0).stroke()
ctx.line_width = tmp
if self.mid_point_lock:
ctx.rgb(1, 1, 1)
else:
ctx.rgb(0, 0, 0)
ctx.scope()
ctx.fill()
ctx.rotate(-math.tau / 10)
if not self.at_max_note:
ctx.move_to(0, 110)
ctx.text("<<<")
ctx.move_to(0, 0)
if not self.at_min_note:
ctx.rotate(math.tau / 5)
ctx.move_to(0, 110)
ctx.text(">>>")
def _build_synth(self):
if self.blm is None:
self.blm = bl00mbox.Channel("melodic demo")
self.synths = []
for i in range(1):
synth = self.blm.new(bl00mbox.patches.tinysynth)
synth.signals.output = self.blm.mixer
self.synths += [synth]
for synth in self.synths:
synth.signals.decay = 100
def update_leds(self):
for i in range(40):
hue_deg = ((i * 90 / 40) + (self.mid_point * 270 / 60) + 180) % 360
if i == 0:
self.hue_min = hue_deg
if i == 39:
self.hue_max = hue_deg
index = i + (self.mid_point_petal - 5) * 4
leds.set_hsv(index % 40, hue_deg, 1, 1)
leds.update()
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
foreground()
if self.blm is None:
self._build_synth()
self.blm.foreground = True
self.make_scale()
def on_exit(self):
if self.blm is not None:
self.blm.free = True
self.blm = None
def shift_playing_field_by_num_petals(self, num):
num_positive = True
if num < 0:
num_positive = False
self.at_max_note = False
elif num > 0:
self.at_min_note = False
num = abs(num)
while num != 0:
if num > 3:
num_part = 3
num -= 3
else:
num_part = num
num = 0
if num_positive:
self.mid_point_petal += num_part
self.mid_point_petal = self.mid_point_petal % 10
else:
self.mid_point_petal -= num_part
self.mid_point_petal = self.mid_point_petal % 10
self.mid_point = self.scale[self.mid_point_petal]
self.make_scale()
# make sure things stay in bounds
while max(self.scale) > self.max_note:
self.mid_point_petal -= 1
self.mid_point_petal = self.mid_point_petal % 10
self.mid_point = self.scale[self.mid_point_petal]
self.make_scale()
self.at_max_note = True
while min(self.scale) < self.min_note:
self.mid_point_petal += 1
self.mid_point_petal = self.mid_point_petal % 10
self.mid_point = self.scale[self.mid_point_petal]
self.make_scale()
self.at_min_note = True
self.make_scale()
if max(self.scale) == self.max_note:
self.at_max_note = True
if min(self.scale) == self.min_note:
self.at_min_note = True
def think(self, ins: InputState, delta_ms: int) -> None:
if self.blm is None:
return
super().think(ins, delta_ms)
run(ins)
petals = []
if self.input.buttons.app.middle.pressed:
self.mid_point_lock = not self.mid_point_lock
if self.input.buttons.app.right.pressed:
self.shift_playing_field_by_num_petals(4)
if self.input.buttons.app.left.pressed:
self.shift_playing_field_by_num_petals(-4)
for i in range(10):
if ins.captouch.petals[i].pressed:
petals += [i]
if len(petals) == 0:
self.synths[0].signals.trigger.stop()
self.prev_note = None
else:
if (len(petals) == 1) and (not self.mid_point_lock):
delta = petals[0] - self.mid_point_petal
if delta > 4:
delta -= 10
if delta < -5:
delta += 10
if delta > 2:
self.shift_playing_field_by_num_petals(delta - 2)
if delta < -3:
self.shift_playing_field_by_num_petals(delta + 3)
avg = 0
for petal in petals:
avg += self.scale[petal]
avg /= len(petals)
self.synths[0].signals.pitch.tone = avg
if (self.legato and self.prev_note is None) or (
(not self.legato) and self.prev_note != avg
):
self.synths[0].signals.trigger.start()
self.prev_note = avg
self.update_leds()
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(MelodicApp)
[app]
name = "Melodic"
menu = "Music"
name = "melodic demo"
category = "Hidden"
[entry]
class = "MelodicApp"
......
......@@ -18,6 +18,8 @@ class ScrollDemo(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
# this class is deprecated, please don't use it for new apps
# check out st3m.ui.widgets.Scroller instead!
self.scroll = CapScrollController()
def draw(self, ctx: Context) -> None:
......@@ -70,3 +72,10 @@ class ScrollDemo(Application):
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
self.scroll.update(self.input.captouch.petals[self.PETAL_NO].gesture, delta_ms)
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(ScrollDemo)
[app]
name = "Scroll Demo"
menu = "Apps"
category = "Demos"
[entry]
class = "ScrollDemo"
......
......@@ -21,55 +21,40 @@ class AppWorms(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
# HACK: we work against double buffering by keeping note of how many
# times on_draw got called.
#
# We know there's two buffers, so if we render the same state twice in a
# row we'll be effectively able to keep a persistent framebuffer, like
# with the old API.
#
# When bufn is in [0, 1] we render the background image.
# When bufn is in [2, ...) we render the worms.
# When bufn is > 3, we enable updating the worm state.
#
# TODO(q3k): allow apps to request single-buffered graphics for
# persistent framebuffers.
self.bufn = 0
self.worms = []
for i in range(0):
self.worms.append(Worm())
self.just_shown = True
self.just_shown = False
def on_enter(self, vm: Optional[ViewManager]) -> None:
# print("on foreground")
super().on_enter(vm)
self.just_shown = True
self.just_shown = False
self.worms = [] # reset worms
def draw(self, ctx: Context) -> None:
if self.bufn <= 5:
def draw_background(self, ctx):
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(*WHITE).text("touch me :)")
self.bufn += 1
return
def draw(self, ctx: Context) -> None:
if self.vm.transitioning and self.is_active():
self.draw_background(ctx)
else:
for w in self.worms:
w.draw(ctx)
self.bufn += 1
self.just_shown = True
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
# Simulation is currently locked to FPS.
if self.bufn > 7:
if self.just_shown:
for w in self.worms:
w.move()
self.bufn = 6
self.just_shown = False
for index, petal in enumerate(self.input.captouch.petals):
if petal.whole.pressed or petal.whole.repeated:
self.worms.append(Worm(-tau * index / 10 + math.pi))
......@@ -126,6 +111,7 @@ class Worm:
self.size += 1
self.speed = self.size / 5
self.speed /= 2 # temporary hack bc framerate doubling
self.direction += (random.random() - 0.5) * math.pi / 4
......@@ -140,3 +126,10 @@ class Worm:
self.direction = -math.atan2(dy, dx)
self.mutate()
self._lastdist = dist
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(AppWorms)
[app]
name = "Worms"
menu = "Apps"
category = "Games"
[entry]
class = "AppWorms"
......
from st3m import logging
from st3m.application import Application, ApplicationContext
from st3m.goose import Optional
from st3m.input import InputState
from st3m.ui.view import View, ViewManager
from ctx import Context
......@@ -12,9 +13,11 @@ class Fil3sApp(Application):
log = logging.Log(__name__, level=logging.INFO)
path: str = "/"
selected: Optional[str] = None
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx=app_ctx)
self.view = "browser"
def on_enter(self, vm: ViewManager | None) -> None:
super().on_enter(vm)
......@@ -22,19 +25,29 @@ class Fil3sApp(Application):
if self.vm is None:
raise RuntimeError("vm is None")
self.vm.replace(Browser(self.path, self.on_navigate, self.on_update_path))
if self.view == "browser":
self.vm.replace(
Browser(self.path, self.on_navigate, self.on_update_path, self.selected)
)
elif self.view == "reader":
self.vm.replace(Reader(self.path, self.on_navigate, self.on_update_path))
def on_navigate(self, view: str) -> None:
if self.vm is None:
raise RuntimeError("vm is None")
self.view = view
if view == "browser":
self.vm.replace(Browser(self.path, self.on_navigate, self.on_update_path))
self.vm.replace(
Browser(self.path, self.on_navigate, self.on_update_path, self.selected)
)
elif view == "reader":
self.vm.replace(Reader(self.path, self.on_navigate, self.on_update_path))
def on_update_path(self, path: str) -> None:
def on_update_path(self, path: str, selected: str = None) -> None:
self.path = path
self.selected = selected
def draw(self, ctx: Context) -> None:
pass
......
import os
import uos
import stat
from st3m.goose import Callable, Generator
import math
from st3m.goose import Callable, Generator, Optional
from st3m.input import InputState
from ctx import Context
......@@ -20,6 +21,8 @@ class Browser(ActionView):
up_enabled: bool = False
prev_enabled: bool = False
next_enabled: bool = False
delete_enabled: bool = True
select_enabled: bool = True
current_pos = 0
current_entry: tuple[str, str]
......@@ -28,10 +31,18 @@ class Browser(ActionView):
path: str,
navigate: Callable[[str], None],
update_path: Callable[[str], None],
selected: Optional[str] = None,
) -> None:
super().__init__()
self._delete_held_for = 0.0
self._delete_hold_time = 1.5
self._delete_require_release = False
self._scroll_pos = 0.0
self.path = path
self.selected = selected
self.navigate = navigate
self.update_path = update_path
......@@ -39,18 +50,20 @@ class Browser(ActionView):
self._scan_path()
def _on_action(self, index: int) -> None:
if index == 1:
if index == 4:
if self.current_pos > 0:
self.current_pos -= 1
self._update_position()
if index == 2:
elif index == 3:
self._up()
if index == 3:
elif index == 2:
self._select()
elif index == 4:
elif index == 1:
if self.current_pos < len(self.dir_entries) - 1:
self.current_pos += 1
self._update_position()
elif index == 0:
self._delete()
def draw(self, ctx: Context) -> None:
utils.fill_screen(ctx, theme.BACKGROUND)
......@@ -65,22 +78,36 @@ class Browser(ActionView):
ctx.text(self.current_entry[1])
ctx.font_size = 24
ctx.move_to(0, 20)
ctx.font = "Camp Font 3"
xpos = 0.0
if (width := ctx.text_width(self.current_entry[0])) > 220:
xpos = math.sin(self._scroll_pos) * (width - 220) / 2
ctx.move_to(xpos, 20)
ctx.text(self.current_entry[0])
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
for i in range(0, 5):
self._scroll_pos += delta_ms / 1000
# Handle delete petal being held down
if ins.captouch.petals[0].pressed:
if not self._delete_require_release:
self._delete_held_for += delta_ms / 1000
if self._delete_held_for > self._delete_hold_time:
self._delete_held_for = self._delete_hold_time
self._delete_require_release = True
self._on_action(0)
else:
self._delete_held_for = 0.0
self._delete_require_release = False
self.actions[0].progress = self._delete_held_for / self._delete_hold_time
for i in range(1, 5):
if self.input.captouch.petals[i * 2].whole.pressed:
self._on_action(i)
return
def _is_dir(self, path: str) -> bool:
st_mode = uos.stat(path)[0] # Can fail with OSError
return stat.S_ISDIR(st_mode)
def _get_dir_entry(
self, names: list[str]
) -> Generator[tuple[str, str], None, None]:
......@@ -88,7 +115,7 @@ class Browser(ActionView):
try:
if self.path + name == "/flash/sys/st3m":
yield (name, "\ue545")
elif self._is_dir(self.path + name):
elif utils.is_dir(self.path + name):
yield (name, "\ue2c7")
else:
yield (name, "\ue873")
......@@ -97,14 +124,19 @@ class Browser(ActionView):
print(f"Failed to create entry for {name}: {e}")
def _scan_path(self) -> None:
dir = os.listdir(self.path)
self.current_pos = 0
if self.selected and self.selected in dir:
self.current_pos = dir.index(self.selected)
self.dir_entries = list(self._get_dir_entry(os.listdir(self.path)))
self.dir_entries = list(self._get_dir_entry(dir))
self._update_position()
def _change_path(self, path: str) -> None:
def _change_path(self, path: str, selected: Optional[str] = None) -> None:
self.path = path
self.selected = selected
self._scan_path()
self.up_enabled = self.path != "/"
......@@ -112,15 +144,19 @@ class Browser(ActionView):
if up_action is not None:
up_action.enabled = self.up_enabled
self.update_path(self.path)
self.update_path(self.path, selected)
self._update_actions()
def _select(self) -> None:
if not self.select_enabled:
return
name = self.dir_entries[self.current_pos][0]
old_path = self.path
new_path = self.path + name
try:
if self._is_dir(new_path):
if utils.is_dir(new_path):
self._change_path(new_path + "/")
else:
self.update_path(self.path + name)
......@@ -130,32 +166,70 @@ class Browser(ActionView):
print(f"Failed to open {new_path}: {e}")
self._change_path(old_path)
def _delete(self) -> None:
if not self.delete_enabled:
return
name = self.dir_entries[self.current_pos][0]
path = self.path + name
try:
if utils.is_dir(path):
utils.rmdirs(path)
else:
os.remove(path)
print(f"deleted file: {path}")
# refresh dir listing
self.current_pos = max(self.current_pos - 1, 0)
self._scan_path()
except Exception as e:
# TODO: Create error view
print(f"Failed to delete {path}: {e}")
def _up(self) -> None:
if not self.up_enabled or len(self.path) <= 1:
return
segments = self.path[1:-1].split("/")
if len(segments) == 1:
self._change_path("/")
self._change_path("/", segments[-1])
else:
segments.pop()
self._change_path("/" + "/".join(segments) + "/")
selected = segments.pop()
self._change_path("/" + "/".join(segments) + "/", selected)
def _update_actions(self) -> None:
self.actions = [
Action(icon="\ue3e3", label="Menu", enabled=False),
Action(icon="\ue5cb", label="Prev", enabled=self.prev_enabled),
Action(icon="\ue5c4", label="Back", enabled=self.up_enabled),
Action(icon="\ue876", label="Select"),
Action(icon="\ue92b", label="Delete", enabled=self.delete_enabled),
Action(icon="\ue409", label="Next", enabled=self.next_enabled),
Action(icon="\ue876", label="Select", enabled=self.select_enabled),
Action(icon="\ue5c4", label="Back", enabled=self.up_enabled),
Action(icon="\ue5cb", label="Prev", enabled=self.prev_enabled),
]
def _update_position(self) -> None:
try:
self.current_entry = self.dir_entries[self.current_pos]
except:
self.select_enabled = True
except Exception:
self.current_entry = ("\ue002", "No files")
self.select_enabled = False
self.up_enabled = self.path != "/"
self.prev_enabled = self.current_pos > 0
self.next_enabled = self.current_pos < len(self.dir_entries) - 1
# disallow deleting st3m folder
name = self.current_entry[0]
self.delete_enabled = (
self.path + name
not in [
"/flash",
"/flash/sys",
"/flash/sys/st3m",
"/sd",
]
and self.select_enabled
)
self._scroll_pos = math.pi / 2
self._update_actions()
......@@ -13,11 +13,15 @@ class Action:
icon: str
label: str
enabled: bool
progress: float
def __init__(self, icon: str, label: str, enabled: bool = True) -> None:
def __init__(
self, icon: str, label: str, enabled: bool = True, progress: float = 0.0
) -> None:
self.icon = icon
self.label = label
self.enabled = enabled
self.progress = progress
class ActionView(BaseView):
......@@ -47,7 +51,7 @@ class ActionView(BaseView):
self.input = InputController()
for i in range(0, 5):
petal_angle = 2.0 * pi / 5.0
petal_angle = 2.0 * -pi / 5.0
self.action_x[i] = int(cos(-petal_angle * float(i) - pi / 2.0) * 100.0)
self.action_y[i] = int(sin(-petal_angle * float(i) - pi / 2.0) * 100.0)
......@@ -66,7 +70,12 @@ class ActionView(BaseView):
if action.enabled:
utils.draw_circle(
ctx, theme.PRIMARY, self.action_x[i], self.action_y[i], 18
ctx,
theme.PRIMARY,
self.action_x[i],
self.action_y[i],
18,
action.progress,
)
else:
utils.draw_circle(
......
from math import pi
from math import tau
from ctx import Context
import uos
import stat
def fill_screen(ctx: Context, color: tuple[float, float, float]) -> None:
......@@ -13,9 +15,31 @@ def fill_screen(ctx: Context, color: tuple[float, float, float]) -> None:
def draw_circle(
ctx: Context, color: tuple[float, float, float], x: int, y: int, radius: int
ctx: Context,
color: tuple[float, float, float],
x: int,
y: int,
radius: int,
progress: float = 0.0,
) -> None:
ctx.move_to(x, y)
ctx.rgb(*color)
ctx.arc(x, y, radius, -pi, pi, True)
ctx.arc(x, y, radius, tau * progress, tau, 0)
ctx.fill()
def is_dir(path: str) -> bool:
st_mode = uos.stat(path)[0] # Can fail with OSError
return stat.S_ISDIR(st_mode)
def rmdirs(base_path):
for entry in uos.listdir(base_path):
path = f"{base_path}/{entry}"
if is_dir(path):
rmdirs(path)
else:
uos.remove(path)
print(f"deleted file: {path}")
uos.rmdir(base_path)
print(f"deleted folder: {base_path}")
[app]
name = "Files"
menu = "Apps"
category = "Apps"
[entry]
class = "Fil3sApp"
......
import os
import media
from ctx import Context
from st3m.goose import Callable
......@@ -10,6 +11,9 @@ from .common.action_view import ActionView
from .common import utils
from .common import theme
import captouch
from st3m.ui import widgets
class Reader(ActionView):
path: str
......@@ -18,12 +22,11 @@ class Reader(ActionView):
is_loading: bool = True
has_error: bool = False
is_media: bool = False
content: str
viewport_offset = (0.0, 0.0)
zoom_enabled = False
scroll_x: CapScrollController
scroll_y: CapScrollController
scroller: widgets.Scroller
def __init__(
self,
......@@ -33,57 +36,111 @@ class Reader(ActionView):
) -> None:
super().__init__()
self.scroll_x = CapScrollController()
self.scroll_y = CapScrollController()
self.path = path
self.navigate = navigate
self.update_path = update_path
self.actions = [
None,
Action(icon="\ue8d4", label="Scroll X"),
Action(icon="\ue5c4", label="Back"),
Action(icon="\ue8b6", label="Zoom"),
Action(icon="\ue8d5", label="Scroll Y"),
]
self.padding = 80
# TODO: Buffered reading?
if self._check_file():
if self._is_media():
self.is_loading = False
self.is_media = True
media.load(self.path)
if (
not media.has_video()
and not media.has_audio()
and not media.is_visual()
):
self.has_error = True
elif self._check_file():
self._read_file()
self.is_loading = False
else:
self.has_error = True
self.is_loading = False
self._update_actions()
self.captouch_config = captouch.Config.empty()
for x in range(2, 10, 2):
self.captouch_config.petals[x].mode = 3
self.scroller = widgets.Scroller(
self.captouch_config,
2,
gain=35 * captouch.PETAL_ROTORS[2],
bounce=0.1,
constraint=widgets.constraints.Rectangle(),
)
def _update_actions(self):
controls = not self.has_error
if self.is_media:
self.actions = [
None,
Action(
icon="\ue034" if media.is_playing() else "\ue037",
label="Pause",
enabled=controls,
),
Action(icon="\ue8b6", label="Zoom", enabled=False),
Action(icon="\ue5c4", label="Back"),
Action(icon="\ue042", label="Rewind", enabled=controls),
]
return
self.actions = [
None,
Action(icon="\ue56b", label="Scroll", enabled=controls),
Action(icon="\ue8b6", label="Zoom", enabled=controls),
Action(icon="\ue5c4", label="Back"),
Action(icon="\ue5d6", label="Break", enabled=controls),
]
def on_enter(self, vm):
super().on_enter(vm)
self.captouch_config.apply()
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
if self.is_loading:
return
if self.input.captouch.petals[4].whole.pressed:
if self.input.captouch.petals[6].whole.pressed:
self._back()
elif self.input.captouch.petals[6].whole.pressed:
elif self.input.captouch.petals[4].whole.pressed:
self.zoom_enabled = not self.zoom_enabled
# TODO: Use "joystick-style" input for scrolling
self.scroll_x.update(self.input.captouch.petals[2].gesture, delta_ms)
self.scroll_y.update(self.input.captouch.petals[8].gesture, delta_ms)
x = self.scroll_x.position[0] * 0.2
y = self.scroll_y.position[0] * 0.2
self.viewport_offset = (x - 80, y - 80)
if self.is_media:
if self.input.captouch.petals[2].whole.pressed:
if media.is_playing():
media.pause()
else:
media.play()
self._update_actions()
elif self.input.captouch.petals[8].whole.pressed:
media.seek(0)
else:
self.scroller.friction = 1 if ins.captouch.petals[8].pressed else 0.9
self.scroller.think(ins, delta_ms)
def draw(self, ctx: Context) -> None:
utils.fill_screen(ctx, theme.BACKGROUND)
if self.is_loading:
self._draw_loading(ctx)
return
elif self.has_error:
self._draw_not_supported(ctx)
elif self.is_media:
self._draw_media(ctx)
else:
self._draw_content(ctx)
super().draw(ctx)
def _draw_loading(self, ctx: Context) -> None:
ctx.save()
ctx.text_align = ctx.CENTER
......@@ -111,6 +168,7 @@ class Reader(ActionView):
ctx.move_to(0, -10)
ctx.text("Can't read file")
if not self.is_media:
ctx.move_to(0, 20)
ctx.text("(Not UTF-8?)")
......@@ -126,21 +184,66 @@ class Reader(ActionView):
ctx.font_size = 32
else:
ctx.font_size = 16
ctx.move_to(self.viewport_offset[0], self.viewport_offset[1])
ctx.text(f"{self.content}")
line_height = ctx.font_size
x_offset = self.scroller.pos.real - self.padding
y_offset = self.scroller.pos.imag - self.padding
x_size = x_offset
for i, line in enumerate(self.content):
x, y = x_offset, y_offset + i * line_height
if y < -120 - line_height:
continue
if y > 120 + line_height:
break
ctx.move_to(x, y)
if len(line) > 10240:
ctx.text(line[:10240])
else:
ctx.text(line)
if ctx.x > x_size:
x_size = ctx.x
x_size -= x_offset
y_size = len(self.content) * line_height
padding = 30
x_size -= 2 * padding
y_size -= 2 * padding
if x_size <= 0:
x_size = 0.00001
if y_size <= 0:
y_size = 0.00001
size = complex(x_size, y_size)
constraint = self.scroller.constraint
constraint.size = size
center_diff = self.padding - padding
constraint.center = -size / 2 + complex(center_diff, center_diff)
# "bad bounce" workaround
self.scroller.pos = constraint.apply_hold(self.scroller.pos)
ctx.restore()
super().draw(ctx)
def _draw_media(self, ctx: Context) -> None:
if media.is_visual():
media.draw(ctx)
elif media.has_audio():
ctx.gray(1.0)
ctx.scope()
def _back(self) -> None:
if self.is_media:
media.stop()
dir = os.path.dirname(self.path) + "/"
self.update_path(dir)
self.update_path(dir, os.path.basename(self.path))
self.navigate("browser")
def _read_file(self) -> None:
try:
with open(self.path, "r", encoding="utf-8") as f:
self.content = f.read()
self.content = [line.rstrip("\n") for line in f.readlines()]
except:
self.has_error = True
......@@ -152,3 +255,14 @@ class Reader(ActionView):
return True
except UnicodeError:
return False
def _is_media(self) -> bool:
for ext in [".mp3", ".mod", ".mpg", ".gif"]:
if self.path.lower().endswith(ext):
return True
return False
def on_exit(self):
if self.is_media:
media.stop()
self.captouch_config.apply_default()
import bl00mbox
import captouch
import leds
import captouch
from st3m.application import Application, ApplicationContext
from st3m.input import InputState
......@@ -8,6 +8,9 @@ from st3m.goose import Tuple, Iterator, Optional, Callable, List, Any, TYPE_CHEC
from ctx import Context
from st3m.ui.view import View, ViewManager
import json
import errno
if TYPE_CHECKING:
Number = float | int
Color = Tuple[Number, Number, Number]
......@@ -20,11 +23,9 @@ else:
class GayDrums(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self.blm = bl00mbox.Channel("gay drums")
self.blm = None
self.num_samplers = 6
self.seq = self.blm.new(bl00mbox.patches.sequencer, num_tracks=8, num_steps=32)
self.bar = self.seq.signals.step.value // 16
self.set_bar_mode(0)
self.seq = None
self.kick: Optional[bl00mbox.patches._Patch] = None
self.hat: Optional[bl00mbox.patches._Patch] = None
......@@ -33,77 +34,205 @@ class GayDrums(Application):
self.crash: Optional[bl00mbox.patches._Patch] = None
self.snare: Optional[bl00mbox.patches._Patch] = None
self.seq.signals.bpm.value = 80
self.track_names = ["kick", "hihat", "snare", "crash", "nya", "woof"]
self.ct_prev = captouch.read()
self.ct_prev: Optional[captouch.CaptouchState] = None
self.track = 1
self.blm.background_mute_override = True
self.tap_tempo_press_counter = 0
self.track_back_press_counter = 0
self.delta_acc = 0
self.stopped = False
self.stopped = True
self.tapping = False
self.tap = {
"sum_y": float(0),
"sum_x": float(1),
"sum_xy": float(0),
"sum_xx": float(1),
"last": float(0),
"time_ms": 0,
"count": 0,
}
self.track_back = False
self.bpm = self.seq.signals.bpm.value
self.bpm = 0
self.samples_loaded = 0
self.samples_total = len(self.track_names)
self.loading_text = ""
self.init_complete = False
self.load_iter = self.iterate_loading()
self._render_list_2: List[Tuple[Rendee, Any]] = []
self._render_list_1: List[Tuple[Rendee, Any]] = []
self._render_list: List[Tuple[Rendee, Any]] = []
self._group_highlight_on = [False] * 4
self._group_highlight_redraw = [False] * 4
self._redraw_background = 2
self._group_gap = 5
self.background_col: Color = (0, 0, 0)
self.highlight_col: Color = (0.15, 0.15, 0.15)
self.bar_col = (0.5, 0.5, 0.5)
self.highlight_col: Color = self.bar_col
self.highlight_outline: bool = True
def set_bar_mode(self, mode: int) -> None:
# TODO: figure out how to speed up re-render
if mode == 0:
self.seq.signals.step_start = 0
self.seq.signals.step_end = 15
if mode == 1:
self.seq.signals.step_start = 16
self.seq.signals.step_end = 31
if mode == 2:
self.bar = 0
self._tracks_empty: bool = True
self._bpm_saved = None
self._steps_saved = None
self._seq_beat_saved = None
self._file_settings = None
self.settings_path = "/flash/gay_drums.json"
self.try_loading = True
def _try_load_settings(self, path):
try:
with open(path, "r") as f:
settings = json.load(f)
self._file_settings = settings
return settings
except OSError as e:
pass
def _try_save_settings(self, path, settings):
try:
with open(path, "w+") as f:
f.write(json.dumps(settings))
f.close()
self._file_settings = settings
except OSError as e:
pass
def _save_settings(self):
if not self.init_complete:
return
file_difference = False
settings = {}
beat = {}
settings["beats"] = [beat]
beat["bpm"] = self.bpm
beat["steps"] = self.steps
beat["pattern"] = self.tracks_dump_pattern()
if self._file_settings is None:
file_difference = True
else:
file_beat = self._file_settings["beats"][0]
for i in beat:
if beat.get(i) != file_beat.get(i):
file_difference = True
break
if file_difference:
self._try_save_settings(self.settings_path, settings)
def _load_settings(self):
settings = self._try_load_settings(self.settings_path)
if settings is None:
self.stopped = False
elif self.blm is not None:
beat = settings["beats"][0]
if "sequencer_table" in beat:
# legacy support
a = beat["sequencer_table"]
pattern = {}
tracks = []
for i in range(8):
track = {}
track["type"] = "trigger"
track["steps"] = a[(33 * i) + 1 : 33 * (i + 1)]
tracks += [track]
pattern["tracks"] = tracks
self.seq.load_pattern(pattern)
else:
self.tracks_load_pattern(beat["pattern"])
self.bpm = beat["bpm"]
if not self.stopped:
self.seq.signals.bpm.value = self.bpm
self.steps = beat["steps"]
self._tracks_empty = self.tracks_are_empty()
@property
def steps(self):
if self.blm is not None:
return self.seq.signals.step_end.value + 1
return 0
@steps.setter
def steps(self, val):
if self.blm is not None:
self.seq.signals.step_start = 0
self.seq.signals.step_end = 31
self.seq.signals.step_end = val - 1
def iterate_loading(self) -> Iterator[Tuple[int, str]]:
if self.blm is None:
self.blm = bl00mbox.Channel("gay drums")
self.seq = self.blm.new(bl00mbox.plugins.sequencer, 8, 32)
if self.try_loading:
self._load_settings()
self.try_loading = False
if self._bpm_saved is None:
bpm = 80
else:
bpm = self._bpm_saved
if self._steps_saved is None:
steps = 16
else:
steps = self._steps_saved
self.steps = steps
self.seq.signals.bpm = bpm
self.bpm = self.seq.signals.bpm.value
if self._seq_beat_saved is not None:
self.tracks_load_pattern(self._seq_beat_saved)
self._tracks_empty = self.tracks_are_empty()
if self.stopped:
self.seq.signals.bpm = 0
self.seq.signals.sync_in.start()
self._render_list += [(self.draw_bpm, None)]
self.blm.foreground = False
yield 0, "kick.wav"
self.nya = self.blm.new(bl00mbox.patches.sampler, "nya.wav")
self.nya.signals.output = self.blm.mixer
self.nya.signals.trigger = self.seq.plugins.seq.signals.track6
self.kick = self.blm.new(bl00mbox.patches.sampler, "kick.wav")
self.kick.signals.output = self.blm.mixer
self.kick.signals.trigger = self.seq.plugins.seq.signals.track0
self.nya = self.blm.new(bl00mbox.plugins.sampler, "/flash/sys/samples/nya.wav")
self.nya.signals.playback_output = self.blm.mixer
self.nya.signals.playback_trigger = self.seq.signals.track[6]
self.kick = self.blm.new(
bl00mbox.plugins.sampler, "/flash/sys/samples/kick.wav"
)
self.kick.signals.playback_output = self.blm.mixer
self.kick.signals.playback_trigger = self.seq.signals.track[0]
yield 1, "hihat.wav"
self.woof = self.blm.new(bl00mbox.patches.sampler, "bark.wav")
self.woof.signals.output = self.blm.mixer
self.woof.signals.trigger = self.seq.plugins.seq.signals.track7
self.hat = self.blm.new(bl00mbox.patches.sampler, "hihat.wav")
self.hat.signals.output = self.blm.mixer
self.hat.signals.trigger = self.seq.plugins.seq.signals.track1
self.woof = self.blm.new(
bl00mbox.plugins.sampler, "/flash/sys/samples/bark.wav"
)
self.woof.signals.playback_output = self.blm.mixer
self.woof.signals.playback_trigger = self.seq.signals.track[7]
self.hat = self.blm.new(
bl00mbox.plugins.sampler, "/flash/sys/samples/hihat.wav"
)
self.hat.signals.playback_output = self.blm.mixer
self.hat.signals.playback_trigger = self.seq.signals.track[1]
yield 2, "close.wav"
self.close = self.blm.new(bl00mbox.patches.sampler, "close.wav")
self.close.signals.output = self.blm.mixer
self.close.signals.trigger = self.seq.plugins.seq.signals.track2
self.close = self.blm.new(
bl00mbox.plugins.sampler, "/flash/sys/samples/close.wav"
)
self.close.signals.playback_output = self.blm.mixer
self.close.signals.playback_trigger = self.seq.signals.track[2]
yield 3, "open.wav"
self.open = self.blm.new(bl00mbox.patches.sampler, "open.wav")
self.open.signals.output = self.blm.mixer
self.open.signals.trigger = self.seq.plugins.seq.signals.track3
self.open = self.blm.new(
bl00mbox.plugins.sampler, "/flash/sys/samples/open.wav"
)
self.open.signals.playback_output = self.blm.mixer
self.open.signals.playback_trigger = self.seq.signals.track[3]
yield 4, "snare.wav"
self.snare = self.blm.new(bl00mbox.patches.sampler, "snare.wav")
self.snare.signals.output = self.blm.mixer
self.snare.signals.trigger = self.seq.plugins.seq.signals.track4
self.snare = self.blm.new(
bl00mbox.plugins.sampler, "/flash/sys/samples/snare.wav"
)
self.snare.signals.playback_output = self.blm.mixer
self.snare.signals.playback_trigger = self.seq.signals.track[4]
yield 5, "crash.wav"
self.crash = self.blm.new(bl00mbox.patches.sampler, "crash.wav")
self.crash.signals.output = self.blm.mixer
self.crash.signals.trigger = self.seq.plugins.seq.signals.track5
self.crash = self.blm.new(
bl00mbox.plugins.sampler, "/flash/sys/samples/crash.wav"
)
self.crash.signals.playback_output = self.blm.mixer
self.crash.signals.playback_speed.tone = 2
self.crash.signals.playback_trigger = self.seq.signals.track[5]
yield 6, ""
def _highlight_petal(self, num: int, r: int, g: int, b: int) -> None:
......@@ -128,23 +257,17 @@ class GayDrums(Application):
def draw_background(self, ctx: Context, data: None) -> None:
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
# group bars
bar_len = 10 * (1 + self.num_samplers)
bar_pos = 4 * 12 + self._group_gap
self.ctx_draw_centered_rect(ctx, 0, 0, 1, bar_len, (0.5, 0.5, 0.5))
self.ctx_draw_centered_rect(ctx, bar_pos, 0, 1, bar_len, (0.5, 0.5, 0.5))
self.ctx_draw_centered_rect(ctx, -bar_pos, 0, 1, bar_len, (0.5, 0.5, 0.5))
self.draw_bars(ctx, None)
ctx.font = ctx.get_font_name(1)
ctx.font_size = 15
ctx.rgb(0.6, 0.6, 0.6)
ctx.move_to(0, -85)
ctx.move_to(0, -80)
ctx.text("(hold) stop")
ctx.move_to(0, -100)
ctx.move_to(0, -95)
ctx.text("tap tempo")
self.draw_track_name(ctx, None)
......@@ -154,76 +277,134 @@ class GayDrums(Application):
for step in range(16):
self.draw_track_step_marker(ctx, (track, step))
def tracks_are_empty(self):
for track in range(6):
for step in range(16):
if self.track_get_state(track, step):
return False
return True
def tracks_dump_pattern(self):
tracks = [None] * 6
for t in range(6):
track = {}
steps = [0] * 16
for s in range(16):
steps[s] = self.track_get_state(t, s)
track["steps"] = steps
track["name"] = self.track_names[t]
tracks[t] = track
beat = {}
beat["tracks"] = tracks
return beat
def tracks_load_pattern(self, beat):
for track in range(6):
steps = beat["tracks"][track]["steps"]
for step in range(16):
self.track_set_state(track, step, steps[step])
def track_get_state(self, track: int, step: int) -> int:
sequencer_track = track
if track > 1:
sequencer_track += 2
if track == 1:
if self.seq.trigger_state(1, step):
if self.seq.trigger_state(1, step) > 0:
return 1
elif self.seq.trigger_state(2, step):
elif self.seq.trigger_state(2, step) > 0:
return 2
elif self.seq.trigger_state(3, step):
elif self.seq.trigger_state(3, step) > 0:
return 3
else:
return 0
else:
state = self.seq.trigger_state(sequencer_track, step)
if state == 0:
if state <= 0:
return 0
elif state == 32767:
return 3
elif state < 16384:
elif state < 17000:
return 1
else:
return 2
return 3
def track_set_state(self, track, step, state):
# lol
last = self.track_get_state(track, step)
while self.track_get_state(track, step) != state:
self.track_incr_state(track, step)
if last == self.track_get_state(track, step):
break
def track_incr_state(self, track: int, step: int) -> None:
sequencer_track = track
step = step % 16
if track > 1:
sequencer_track += 2
if track == 1:
state = self.track_get_state(track, step)
if state == 0:
while step < 32:
self.seq.trigger_start(1, step)
self.seq.trigger_clear(2, step)
self.seq.trigger_clear(3, step)
self.seq.trigger_stop(2, step)
self.seq.trigger_stop(3, step)
step += 16
if state == 1:
self.seq.trigger_clear(1, step)
while step < 32:
self.seq.trigger_stop(1, step)
self.seq.trigger_start(2, step)
self.seq.trigger_clear(3, step)
self.seq.trigger_stop(3, step)
step += 16
if state == 2:
self.seq.trigger_clear(1, step)
self.seq.trigger_clear(2, step)
while step < 32:
self.seq.trigger_stop(1, step)
self.seq.trigger_stop(2, step)
self.seq.trigger_start(3, step)
step += 16
if state == 3:
while step < 32:
self.seq.trigger_clear(1, step)
self.seq.trigger_clear(2, step)
self.seq.trigger_clear(3, step)
step += 16
else:
state = self.seq.trigger_state(sequencer_track, step)
state = self.track_get_state(track, step)
if track == 3:
if state == 0:
new_state = 16000
elif state == 32767:
new_state = 10000
elif state == 1:
new_state = 20000
else:
new_state = 0
else:
if state == 0:
new_state = 16000
elif state == 1:
new_state = 32767
else:
new_state = 0
if new_state <= 0:
self.seq.trigger_clear(sequencer_track, step)
self.seq.trigger_clear(sequencer_track, step + 16)
else:
self.seq.trigger_start(sequencer_track, step, new_state)
self.seq.trigger_start(sequencer_track, step + 16, new_state)
def draw_track_step_marker(self, ctx: Context, data: Tuple[int, int]) -> None:
track, step = data
self._group_gap = 4
rgb = self._track_col(track)
rgbf = (rgb[0] / 256, rgb[1] / 256, rgb[2] / 256)
y = -int(12 * (track - (self.num_samplers - 1) / 2))
trigger_state = self.track_get_state(track, step)
empty = self._tracks_empty
# retrieving the state is super slow, so don't do it when transitioning
if self.vm.transitioning:
empty = True
trigger_state = self.track_get_state(track, step) if not empty else 0
size = 2
x = 12 * (7.5 - step)
x += self._group_gap * (1.5 - (step // 4))
x = int(-x)
group = step // 4
bg = self.background_col
if self._group_highlight_on[group]:
if self._group_highlight_on[group] and not self.highlight_outline:
bg = self.highlight_col
if trigger_state == 3:
self.ctx_draw_centered_rect(ctx, x, y, 8, 8, rgbf)
......@@ -245,89 +426,177 @@ class GayDrums(Application):
nosy = int(posy - (sizey / 2))
ctx.rectangle(nosx, nosy, int(sizex), int(sizey)).fill()
def draw_bars(self, ctx: Context, data: None) -> None:
ctx.move_to(0, 0)
bar_len = 10 * (1 + self.num_samplers)
bar_pos = 4 * 12 + self._group_gap
self.ctx_draw_centered_rect(ctx, 0, 0, 1, bar_len, self.bar_col)
self.ctx_draw_centered_rect(ctx, bar_pos, 0, 1, bar_len, self.bar_col)
self.ctx_draw_centered_rect(ctx, -bar_pos, 0, 1, bar_len, self.bar_col)
def draw_group_highlight(self, ctx: Context, data: int) -> None:
i = data
col = self.background_col
if self._group_highlight_on[i]:
col = self.highlight_col
sizex = 48 + self._group_gap - 2
sizey = 10 * (1 + self.num_samplers)
posx = -int((12 * 4 + 1 + self._group_gap) * (1.5 - i))
bar_len = 10 * (1 + self.num_samplers)
bar_pos = 4 * 12 + self._group_gap
sizex = 4 * 12
sizey = bar_len
posx = int(bar_pos * (i - 1.5))
posy = 0
self.ctx_draw_centered_rect(ctx, posx, posy, sizex, sizey, col)
if self.highlight_outline:
self.ctx_draw_centered_rect(
ctx, posx, int(sizey / 2) + 2, sizex - 2, 1, col
)
self.ctx_draw_centered_rect(
ctx, posx, int(-sizey / 2) - 2, sizex - 2, 1, col
)
if i == 0:
self.ctx_draw_centered_rect(ctx, -2 * bar_pos, 0, 1, bar_len, col)
if i == 3:
self.ctx_draw_centered_rect(ctx, 2 * bar_pos, 0, 1, bar_len, col)
else:
self.ctx_draw_centered_rect(ctx, posx, posy, sizex, sizey, col)
for x in range(self.num_samplers):
for y in range(4):
self.draw_track_step_marker(ctx, (x, y + 4 * i))
def draw_bpm(self, ctx: Context, data: None) -> None:
self.ctx_draw_centered_rect(ctx, 0, -65, 200, 22, (0, 0, 0))
ctx.text_align = ctx.MIDDLE
ctx.move_to(0, 0)
self.ctx_draw_centered_rect(ctx, 0, -60, 200, 26, (0, 0, 0))
bpm = self.seq.signals.bpm.value
ctx.font = ctx.get_font_name(1)
ctx.font_size = 20
ctx.move_to(0, -65)
ctx.move_to(0, -60)
ctx.rgb(255, 255, 255)
ctx.text(str(bpm) + " bpm")
def draw(self, ctx: Context) -> None:
ctx.text_align = ctx.MIDDLE
if not self.init_complete:
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.font = ctx.get_font_name(0)
ctx.text_align = ctx.MIDDLE
ctx.font_size = 24
ctx.move_to(0, -10)
ctx.move_to(0, -40)
ctx.rgb(0.8, 0.8, 0.8)
if self.samples_loaded == self.samples_total:
ctx.text("Loading complete")
self.loading_text = ""
else:
ctx.text("Loading samples...")
ctx.font_size = 16
ctx.move_to(0, 10)
ctx.move_to(0, -20)
ctx.text(self.loading_text)
tutorial = (
"how to:\n"
"press one of the 4 left petals\n"
"and one of the 4 right petals\n"
"at the same time to toggle\n"
"an event in the grid"
)
ctx.font_size = 16
for x, line in enumerate(tutorial.split("\n")):
if not x:
continue
ctx.move_to(0, 5 + 19 * x)
ctx.text(line)
progress = self.samples_loaded / self.samples_total
bar_len = 120 / self.samples_total
for x in range(self.samples_loaded):
rgb = self._track_col(x)
rgbf = (rgb[0] / 256, rgb[1] / 256, rgb[2] / 256)
ctx.rgb(*rgbf)
ctx.rectangle(x * bar_len - 60, 30, bar_len, 10).fill()
ctx.rectangle(x * bar_len - 60, -8, bar_len, 10).fill()
return
if self.vm.transitioning:
self._render_list += [(self.draw_background, None)]
for i in range(4):
if self._group_highlight_redraw[i]:
self._group_highlight_redraw[i] = False
self._render_list_1 += [(self.draw_group_highlight, i)]
self._render_list += [(self.draw_group_highlight, i)]
for rendee in self._render_list_1:
fun, param = rendee
fun(ctx, param)
for rendee in self._render_list_2:
for rendee in self._render_list:
fun, param = rendee
fun(ctx, param)
self._render_list_2 = self._render_list_1.copy()
self._render_list_1 = []
self._render_list = []
size = 4
st = self.seq.signals.step.value
stepx = -12 * (7.5 - st)
stepx -= self._group_gap * (1.5 - (st // 4))
stepy = -12 - 5 * self.num_samplers
stepx = -12 * (7.5 - (st % 16))
stepx -= self._group_gap * (1.5 - ((st % 16) // 4))
stepy = -13 - 5 * self.num_samplers
trigger_state = self.track_get_state(self.track, st)
dotsize = 1
if trigger_state:
dotsize = 4
self.ctx_draw_centered_rect(ctx, 0, stepy, 200, 4, self.background_col)
self.ctx_draw_centered_rect(ctx, 0, stepy, 240, 8, self.background_col)
self.ctx_draw_centered_rect(ctx, stepx, stepy, dotsize, dotsize, (1, 1, 1))
st_old = st
st = self.steps - 1
st_mod = st % 16
repeater_col = (0.5, 0.5, 0.5)
if st != 15:
stepx = -12 * (7.5 - st_mod)
stepx -= self._group_gap * (1.5 - (st_mod // 4))
if st_mod == 3 or st_mod == 11:
stepx += self._group_gap / 2
elif st_mod == 7:
stepx += (self._group_gap + 1) / 2
if st == st_mod:
stepx += 6
self.ctx_draw_centered_rect(ctx, stepx, stepy, 1, 6, repeater_col)
else:
stepx += 5
if st_old < 16:
self.ctx_draw_centered_rect(
ctx, stepx, stepy + 2.5, 1, 3, repeater_col
)
self.ctx_draw_centered_rect(
ctx, stepx, stepy - 2.5, 1, 3, repeater_col
)
stepx += 2
self.ctx_draw_centered_rect(
ctx, stepx, stepy + 2.5, 1, 3, repeater_col
)
self.ctx_draw_centered_rect(
ctx, stepx, stepy - 2.5, 1, 3, repeater_col
)
else:
self.ctx_draw_centered_rect(ctx, stepx, stepy, 1, 6, repeater_col)
stepx += 2
self.ctx_draw_centered_rect(ctx, stepx, stepy, 1, 6, repeater_col)
def draw_steps(self, ctx, data):
self.ctx_draw_centered_rect(ctx, 0, 60, 200, 40, self.background_col)
ctx.font = ctx.get_font_name(1)
ctx.text_align = ctx.MIDDLE
ctx.font_size = 20
ctx.rgb(1, 1, 1)
ctx.move_to(120, -120)
ctx.text(str(data))
def draw_track_name(self, ctx: Context, data: None) -> None:
self.ctx_draw_centered_rect(ctx, 0, 60, 200, 30, self.background_col)
self.ctx_draw_centered_rect(ctx, 0, 60, 200, 40, self.background_col)
ctx.font = ctx.get_font_name(1)
ctx.text_align = ctx.MIDDLE
......@@ -371,14 +640,28 @@ class GayDrums(Application):
super().think(ins, delta_ms)
if not self.init_complete:
if self.vm.transitioning:
return
try:
self.samples_loaded, self.loading_text = next(self.load_iter)
except StopIteration:
self.init_complete = True
self._render_list_1 += [(self.draw_background, None)]
self._render_list += [(self.draw_background, None)]
return
st = self.seq.signals.step.value
if self.input.buttons.app.left.pressed:
if self.steps > 3:
self.steps -= 1
if self.input.buttons.app.right.pressed:
if self.steps < 31:
self.steps += 1
if self.ct_prev is None:
self.ct_prev = ins.captouch
return
st = self.seq.signals.step.value % 16
rgb = self._track_col(self.track)
leds.set_all_rgb(0, 0, 0)
self._highlight_petal(10 - (4 - (st // 4)), *rgb)
......@@ -387,9 +670,9 @@ class GayDrums(Application):
if self.bar != (st // 16):
self.bar = st // 16
self._group_highlight_redraw = [True] * 4
# self._group_highlight_redraw = [True] * 4
ct = captouch.read()
ct = ins.captouch
for i in range(4):
if ct.petals[6 + i].pressed:
if not self._group_highlight_on[i]:
......@@ -399,9 +682,10 @@ class GayDrums(Application):
if ct.petals[4 - j].pressed and not (
self.ct_prev.petals[4 - j].pressed
):
self._tracks_empty = False
self.track_incr_state(self.track, self.bar * 16 + i * 4 + j)
self._render_list_1 += [
self._render_list += [
(self.draw_track_step_marker, (self.track, i * 4 + j))
]
else:
......@@ -414,28 +698,58 @@ class GayDrums(Application):
self.track_back = False
else:
self.track = (self.track - 1) % self.num_samplers
self._render_list_1 += [(self.draw_track_name, None)]
self._render_list += [(self.draw_track_name, None)]
if ct.petals[0].pressed and not (self.ct_prev.petals[0].pressed):
if self.stopped:
self.seq.signals.bpm = self.bpm
self._render_list_1 += [(self.draw_bpm, None)]
self.blm.background_mute_override = True
self._render_list += [(self.draw_bpm, None)]
self.stopped = False
elif self.delta_acc < 3000 and self.delta_acc > 10:
bpm = int(60000 / self.delta_acc)
if bpm > 40 and bpm < 500:
self.blm.foreground = True
elif self.tapping:
t = self.tap["time_ms"] * 0.001
n = self.tap["count"]
l = self.tap["last"]
self.tap["sum_y"] += t
self.tap["sum_x"] += n
self.tap["sum_xy"] += n * t
self.tap["sum_xx"] += n * n
self.tap["last"] = t
T = (
self.tap["sum_xy"] / n
- self.tap["sum_x"] / n * self.tap["sum_y"] / n
) / (
self.tap["sum_xx"] / n
- self.tap["sum_x"] / n * self.tap["sum_x"] / n
)
if t - l < T / 1.2 or t - l > T * 1.2 or T <= 0.12 or T > 1.5:
self.tapping = False
else:
bpm = int(60 / T)
self.seq.signals.bpm = bpm
self._render_list_1 += [(self.draw_bpm, None)]
self._render_list += [(self.draw_bpm, None)]
self.bpm = bpm
self.delta_acc = 0
if not self.tapping:
self.tap["sum_y"] = 0
self.tap["sum_x"] = 1
self.tap["sum_xy"] = 0
self.tap["sum_xx"] = 1
self.tap["count"] = 1
self.tap["last"] = 0
self.tap["time_ms"] = 0
self.tapping = True
self.tap["count"] += 1
if ct.petals[0].pressed:
if self.tap_tempo_press_counter > 500:
self.seq.signals.bpm = 0
self._render_list_1 += [(self.draw_bpm, None)]
self.seq.signals.sync_in.start()
self._render_list += [(self.draw_bpm, None)]
self.stopped = True
self.blm.background_mute_override = False
self.blm.foreground = False
else:
self.tap_tempo_press_counter += delta_ms
else:
......@@ -444,17 +758,67 @@ class GayDrums(Application):
if ct.petals[5].pressed:
if (self.track_back_press_counter > 500) and not self.track_back:
self.track = (self.track + 1) % self.num_samplers
self._render_list_1 += [(self.draw_track_name, None)]
self._render_list += [(self.draw_track_name, None)]
self.track_back = True
else:
self.track_back_press_counter += delta_ms
else:
self.track_back_press_counter = 0
if self.delta_acc < 3000:
self.delta_acc += delta_ms
self.tap["time_ms"] += delta_ms
self.ct_prev = ct
def on_enter(self, vm: Optional[ViewManager]) -> None:
self._render_list_1 += [(self.draw_background, None)]
self._render_list_1 += [(self.draw_background, None)] # nice
super().on_enter(vm)
self.ct_prev = None
def on_enter_done(self) -> None:
# schedule one more redraw so draw_track_step_marker draws the real state
self._render_list += [(self.draw_background, None)]
def on_exit_done(self):
if not self.blm:
return
self._bpm_saved = self.bpm
self._steps_saved = self.steps
self._save_settings()
self._seq_beat_saved = self.tracks_dump_pattern()
if self.tracks_are_empty() or self.stopped:
self.blm.background_mute_override = False
self.blm.clear()
self.blm.free = True
self.blm = None
self.init_complete = False
self.samples_loaded = 0
self.load_iter = self.iterate_loading()
else:
self.blm.background_mute_override = True
def get_help(self):
ret = (
"Toggle an event on the selected track by pressing one"
"or more of the left (6-9) and one or more of the right "
"(1-4) petals at the same time. the left petals select "
"one of 4 group of 4 events each, the right petals "
"select the event within that group.\n\n"
"Select the track with petal 5 (short press: forward, "
"long press: backward).\n\n"
"Start and set the tempo by repeatedly tapping on petal "
"0 or long press it to stop the playback.\n\n"
"If you exit the app while playback is active it will "
"continue playing in the background. Stop the playback "
"before exiting to avoid this.\n\n"
"Left/right on the track button change sequence length.\n\n"
"The beat is saved when exiting the app and loaded "
"when entering it at /flash/sys/gay_drums.json. "
"Note for developers: Saving on flash is bad practice, do not replicate "
"(see Documentation.) This will be moved in the future."
)
return ret
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(GayDrums, "/flash/sys/apps/gay_drums")
[app]
name = "gay drums"
menu = "Music"
category = "Music"
[entry]
class = "GayDrums"
......
from st3m.goose import Enum
from st3m.application import Application, ApplicationContext
from st3m.input import InputState
from st3m.ui.interactions import ScrollController
from st3m.ui import colours
from st3m.ui.view import ViewManager
from st3m.utils import sd_card_unreliable
import st3m.wifi
from ctx import Context
import network
from .applist import AppList
from .background import Flow3rView
from .record import RecordView
from .manual import ManualInputView
class ViewState(Enum):
CONTENT = 1
NO_INTERNET = 2
BAD_SDCARD = 3
class Gr33nhouseApp(Application):
items = ["Browse apps", "Record flow3r seed", "Enter flow3r seed"]
background: Flow3rView
state: ViewState
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx=app_ctx)
self.background = Flow3rView()
self._sc = ScrollController()
self._sc.set_item_count(3)
self.acceptSdCard = False
def on_enter(self, vm):
super().on_enter(vm)
self.update_state()
def update_state(self):
if not self.acceptSdCard and sd_card_unreliable():
self.state = ViewState.BAD_SDCARD
elif not st3m.wifi.is_connected():
self.state = ViewState.NO_INTERNET
else:
self.state = ViewState.CONTENT
def on_exit(self) -> bool:
# request thinks after on_exit
return True
def draw(self, ctx: Context) -> None:
ctx.rgb(*colours.BLACK)
ctx.rectangle(
-120.0,
-120.0,
240.0,
240.0,
).fill()
ctx.rgb(*colours.WHITE)
ctx.font = "Camp Font 3"
ctx.font_size = 24
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
if self.state == ViewState.BAD_SDCARD:
ctx.font_size = 18
ctx.move_to(0, -15)
ctx.text("Unreliable SD card detected!")
ctx.move_to(0, 5)
ctx.text("Please replace it.")
ctx.gray(0.75)
ctx.move_to(0, 40)
ctx.font_size = 16
ctx.text("Press the app button to")
ctx.move_to(0, 55)
ctx.text("continue anyway.")
elif self.state == ViewState.NO_INTERNET:
ctx.font_size = 24
ctx.move_to(0, -15)
ctx.text("Connecting..." if st3m.wifi.is_connecting() else "No internet")
ctx.gray(0.75)
ctx.move_to(0, 40)
ctx.font_size = 16
ctx.text("Press the app button to")
ctx.move_to(0, 55)
ctx.text("enter Wi-Fi settings.")
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
self._sc.think(ins, delta_ms)
self.background.think(ins, delta_ms)
if self.state == ViewState.BAD_SDCARD:
if self.input.buttons.app.middle.pressed:
self.state = ViewState.CONTENT
self.acceptSdCard = True
elif self.state == ViewState.NO_INTERNET:
if self.input.buttons.app.middle.pressed:
st3m.wifi.run_wifi_settings(self.vm)
else:
self.vm.replace(AppList())
self.update_state()
from st3m.goose import Optional, Enum, Any
from st3m.input import InputState
from st3m.ui import colours
from st3m.ui.view import BaseView, ViewManager
from st3m.ui.interactions import ScrollController
from ctx import Context
from math import sin
import urequests
import time
import os
import json
from .background import Flow3rView
from .confirmation import ConfirmationView
from .background import ColorTheme, broken_col, update_col, installed_col, color_themes
from .manual import ManualInputView
class ViewState(Enum):
INITIAL = 1
LOADING = 2
ERROR = 3
LOADED = 4
class AppDB:
class App:
class Version:
def __init__(self, version, url_dict=None):
# None indicates unknown value for most fields
# version of the app as in its flow3r.toml
self.version = version
# urls
if url_dict is not None:
self.zip_url = url_dict["downloadUrl"]
self.tar_url = url_dict["tarDownloadUrl"]
# if this version is installed
self.installed = None
# if this version is considered an update
self.update = None
# this version of the app was tested by flow3r team
self.tested = False
# which version was tested
self.tested_version = None
# test result: is app broken?
self.broken = None
# true if app is patch by flow3r team
self.patch = None
# which original version this patch forks
self.patch_base_version = None
def __init__(self, raw_app):
self.raw_app = raw_app
self.processed = False
def process(self, toml_cache):
if self.processed:
return
raw_app = self.raw_app
self.category = raw_app.get("menu")
for field in ["name", "author", "description", "stars", "featured"]:
setattr(self, field, raw_app.get(field))
slug_bits = raw_app.get("repoUrl").split("/")
self.slug = "-".join([slug_bits[-2], slug_bits[-1].lower()])
# get latest original version
orig = self.Version(self.raw_app["version"], url_dict=self.raw_app)
self.available_versions = [orig]
self.orig = orig
# check what version is installed
installed = None
self.installed_version = None
self.installed = False
self.installed_path = None
for app_dir in ["/sd/apps/", "/sys/flash/apps/"]:
path = app_dir + self.slug
app_installed = toml_cache.get(path)
if not app_installed:
continue
if not os.path.exists(path):
print(f"app in database but not in filesystem: {path}")
continue
try:
installed = self.Version(app_installed["metadata"]["version"])
except:
print("parsing installed version in toml.cache failed")
installed.patch = "patch_source" in app_installed
self.installed_path = path
self.installed_version = installed
self.installed = True
break
# check for flow3r team status flags/patched version
patch = None
self.broken = None
if (status := raw_app.get("status")) is not None:
if (tested_version := status.get("tested_version")) is not None:
orig.tested_version = tested_version
orig.tested = orig.tested_version == orig.version
if (broken := status.get("broken")) is not None:
# if we specify a tested version, a new version shouldn't
# be flagged as broken.
if orig.tested_version is None or orig.tested:
orig.broken = broken
if (app_patch := status.get("patch")) is not None:
version = app_patch.get("version")
patch = self.Version(version, url_dict=app_patch)
patch.patch = True
patch.patch_base_version = orig.tested_version
# order of list is display order, so if the original
# isn't broken we wanna recommend it frist, else
# we default to the patched version
self.broken = False
if orig.broken:
self.available_versions.insert(0, patch)
else:
self.available_versions.append(patch)
else:
self.broken = orig.broken
self.patch = patch
# check for style
self.colors = ColorTheme.from_style(raw_app["style"], self.category)
# check if updates are available
self.processed = True
self._update_versions()
def _update_versions(self):
installed = self.installed_version
patch = self.patch
orig = self.orig
orig.installed = False
orig.update = False
if patch:
patch.installed = False
patch.update = False
if not installed:
self.installed = False
self.update_available = None
return
if patch is None:
orig.installed = orig.version == installed.version
if orig.version > installed.version:
self.update_available = True
else:
if installed.patch:
orig.installed = False
patch.installed = patch.version == installed.version
# option 1: new version of original came out
orig.update = orig.version > patch.patch_base_version
# option 2: new version of patch came out
patch.update = patch.version > installed.version
else:
orig.installed = orig.version == installed.version
patch.installed = False
# option 1: new version of original came out
orig.update = orig.version > installed.version
# option 2: patch for installed version came out
patch.update = patch.patch_base_version == installed.version
self.installed = True
self.update_available = any(
[version.update for version in self.available_versions]
)
def update_installed_version(self, version):
if False:
print(f"updating {self.slug}:")
print(f" previous at {self.installed_path}:")
for v in self.available_versions:
print(
f' {"patch" if v.patch else "orig"} v{v.version} {"installed" if v.installed else ""}'
)
if self.installed_version is not None:
v = self.installed_version
print(
f' installed: {"patch" if v.patch else "orig"} v{v.version}'
)
self.installed_version = version
self._update_versions()
if False:
print(f" now at {self.installed_path}:")
for v in self.available_versions:
print(
f' {"patch" if v.patch else "orig"} v{v.version} {"installed" if v.installed else ""}'
)
if self.installed_version is not None:
v = self.installed_version
print(
f' installed: {"patch" if v.patch else "orig"} v{v.version}'
)
class Category:
def __init__(self, name, db):
self.name = name
self._db = db
self.apps = []
def add_app(self, app):
if self.name != app.category:
print("app seems to be in wrong category")
self.apps.append(app)
def scan_all(self):
db = self._db
for x in range(db._process_index, len(db._raw_apps)):
raw_app = db._unprocessed_apps[x]
if raw_app.get("menu") == self.name:
db.add_raw_app(raw_app)
db.applist_sort(self.apps)
@staticmethod
def applist_sort_key(app):
if app.update_available:
return -1
elif app.installed:
return 1
return 0
@classmethod
def applist_sort(cls, apps):
apps.sort(key=cls.applist_sort_key)
def __init__(self, raw_apps):
self._raw_apps = raw_apps
self._process_index = 0
self.apps = []
try:
with open("/sd/apps/toml_cache.json") as f:
self._toml_cache = json.load(f)
except:
self._toml_cache = {}
self._done = False
self.categories = {}
def scan_incremental(self, increment=8):
if self._done:
return
while True:
if self._process_index >= len(self._raw_apps):
self.applist_sort(self.apps)
self._done = True
print("Database finished")
return
raw_app = self._raw_apps[self._process_index]
self._process_index += 1
self.add_raw_app(raw_app)
if increment is not None:
increment -= 1
if not increment:
break
def scan_all_category_names(self):
for x in range(self._process_index, len(self._raw_apps)):
raw_app = self._raw_apps[x]
if (cat := raw_app.get("menu")) not in self.categories:
self.categories[cat] = None
def scan_all(self):
self.scan_incremental(None)
def add_raw_app(self, raw_app):
app = self.App(raw_app)
app.process(self._toml_cache)
self.apps.append(app)
# write into category list
cat = app.category
# may exist as key but contain None, see scan_all_categories
if (category := self.categories.get(cat)) is None:
category = self.Category(cat, self)
self.categories[cat] = category
category.add_app(app)
class AppSubList(BaseView):
_scroll_pos: float = 0.0
background: Flow3rView
def __init__(self, apps, colors, hide_tags=tuple(), app_filter=None) -> None:
super().__init__()
self.background = Flow3rView(colors)
self.colors = colors
self._sc = ScrollController()
self.hide_tags = tuple(hide_tags)
self.app_filter = app_filter
if self.app_filter:
self.unfiltered_apps = apps
else:
self.apps = apps
self._sc.set_item_count(len(apps))
def on_enter(self, vm):
super().on_enter(vm)
if self.app_filter:
self.apps = [x for x in self.unfiltered_apps if self.app_filter(x)]
self._sc.set_item_count(len(self.apps))
def on_exit(self) -> bool:
# request thinks after on_exit
return True
def draw(self, ctx: Context) -> None:
ctx.move_to(0, 0)
self.background.draw(ctx)
ctx.save()
ctx.rgb(*self.colors.text_bg)
ctx.rectangle(
-120.0,
-15.0,
240.0,
30.0,
).fill()
ctx.translate(0, -30 * self._sc.current_position())
offset = 0
ctx.font = "Camp Font 3"
ctx.font_size = 24
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.move_to(0, 0)
for idx, app in enumerate(self.apps):
target = idx == self._sc.target_position()
if target:
ctx.rgb(*self.colors.text_fg)
else:
ctx.rgb(*self.colors.fg)
if abs(self._sc.current_position() - idx) <= 5:
xpos = 0.0
if target and (width := ctx.text_width(app.name)) > 220:
xpos = sin(self._scroll_pos) * (width - 220) / 2
ctx.move_to(xpos, offset)
ctx.text(app.name)
if target:
text = None
col = (0, 0, 0)
if app.update_available:
col = update_col
text = "update"
elif app.installed:
col = installed_col
text = "installed"
elif app.broken:
col = broken_col
text = "broken"
if text and not (text in self.hide_tags):
ctx.save()
ctx.rgb(*col)
ctx.font_size = 16
ctx.text_align = ctx.LEFT
ctx.rel_move_to(0, -5)
ctx.text(text)
ctx.restore()
offset += 30
if not self.apps:
ctx.rgb(*self.colors.text_fg)
ctx.text("(empty)")
ctx.restore()
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
self._sc.think(ins, delta_ms)
self.background.think(ins, delta_ms)
self._scroll_pos += delta_ms / 1000
if not self.is_active():
return
if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
self._sc.scroll_left()
self._scroll_pos = 0.0
elif (
self.input.buttons.app.right.pressed
or self.input.buttons.app.right.repeated
):
self._sc.scroll_right()
self._scroll_pos = 0.0
elif self.input.buttons.app.middle.pressed:
if self.vm is None:
raise RuntimeError("vm is None")
if self.apps:
app = self.apps[self._sc.target_position()]
self.vm.push(ConfirmationView(app))
class AppList(BaseView):
_scroll_pos: float = 0.0
_state: ViewState = ViewState.INITIAL
items: list[Any] = ["All"]
category_order: list[Any] = ["Badge", "Music", "Games", "Media", "Apps", "Demos"]
background: Flow3rView
def __init__(self) -> None:
super().__init__()
self.background = Flow3rView(ColorTheme.get("SolidBlack"))
self._sc = ScrollController()
self._sc.set_item_count(len(self.items))
self.category_prev = ""
def draw(self, ctx):
ctx.move_to(0, 0)
if self._state == ViewState.INITIAL or self._state == ViewState.LOADING:
ctx.rgb(*colours.BLACK)
ctx.rectangle(
-120.0,
-120.0,
240.0,
240.0,
).fill()
ctx.save()
ctx.rgb(*colours.WHITE)
ctx.font = "Camp Font 3"
ctx.font_size = 24
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.text("Collecting seeds...")
ctx.restore()
return
elif self._state == ViewState.ERROR:
ctx.rgb(*colours.BLACK)
ctx.rectangle(
-120.0,
-120.0,
240.0,
240.0,
).fill()
ctx.save()
ctx.rgb(*colours.WHITE)
ctx.gray(1.0)
ctx.font = "Camp Font 3"
ctx.font_size = 24
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.text("Something went wrong")
ctx.restore()
return
elif self._state == ViewState.LOADED:
ctx.move_to(0, 0)
self.background.draw(ctx)
ctx.save()
ctx.gray(1.0)
ctx.rectangle(
-120.0,
-15.0,
240.0,
30.0,
).fill()
ctx.translate(0, -30 * self._sc.current_position())
offset = 0
ctx.font = "Camp Font 3"
ctx.font_size = 24
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.move_to(0, 0)
for idx, item in enumerate(self.items):
target = idx == self._sc.target_position()
if target:
ctx.gray(0.0)
else:
ctx.gray(1.0)
if abs(self._sc.current_position() - idx) <= 5:
xpos = 0.0
if target and (width := ctx.text_width(item)) > 220:
xpos = sin(self._scroll_pos) * (width - 220) / 2
ctx.move_to(xpos, offset)
ctx.text(item)
offset += 30
ctx.restore()
else:
raise RuntimeError(f"Invalid view state {self._state}")
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
self._sc.think(ins, delta_ms)
if self._state == ViewState.INITIAL:
if self.vm.transitioning:
return
try:
self._state = ViewState.LOADING
print("Loading app list...")
res = urequests.get("https://flow3r.garden/api/apps.json")
raw_apps = res.json()["apps"]
if raw_apps == None:
print(f"Invalid JSON or no apps: {res.json()}")
self._state = ViewState.ERROR
return
print("Initializing database...")
self.db = AppDB(raw_apps)
self.apps = self.db.apps
self.db.scan_all_category_names()
categories = list(self.db.categories.keys())
def sortkey(obj):
try:
return self.category_order.index(obj)
except ValueError:
return len(self.category_order)
categories.sort(key=sortkey)
print("Found categories:", categories)
self.items = ["All"] + categories + ["Updates", "Installed", "Seeds"]
self._sc.set_item_count(len(self.items))
self._state = ViewState.LOADED
except Exception as e:
print(f"Load failed: {e}")
self._state = ViewState.ERROR
return
elif self._state == ViewState.LOADING:
raise RuntimeError(f"Invalid view state {self._state}")
elif self._state == ViewState.ERROR:
return
self.background.think(ins, delta_ms)
self._scroll_pos += delta_ms / 1000
if not self.is_active():
return
self.db.scan_incremental()
if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
self._sc.scroll_left()
self._scroll_pos = 0.0
elif (
self.input.buttons.app.right.pressed
or self.input.buttons.app.right.repeated
):
self._sc.scroll_right()
self._scroll_pos = 0.0
elif self.input.buttons.app.middle.pressed:
if self.vm is None:
raise RuntimeError("vm is None")
category = self.items[self._sc.target_position()]
colors = ColorTheme.get(category)
hide_tags = []
apps = None
app_filter = None
if category == "All":
self.db.scan_all()
apps = self.apps
elif category == "Updates":
self.db.scan_all()
app_filter = lambda x: x.update_available
apps = self.apps
elif category == "Installed":
self.db.scan_all()
app_filter = lambda x: x.installed
apps = self.apps
hide_tags = ["installed"]
elif category == "Featured":
self.db.scan_all()
app_filter = lambda x: x.featured
apps = self.apps
elif category == "Seeds":
self.vm.push(ManualInputView(colors=colors))
else:
category = self.db.categories[category]
category.scan_all()
apps = category.apps
if apps is not None:
self.vm.push(AppSubList(apps, colors, hide_tags, app_filter))
category = self.items[self._sc.target_position()]
if category != self.category_prev:
self.category_prev = category
self.background.update_colors(ColorTheme.get(category))
import random
from st3m.input import InputState
from st3m.ui.view import BaseView
from ctx import Context
seed_cols = [
(0, 0, 1),
(0, 1, 1),
(1, 1, 0),
(0, 1, 0),
(1, 0, 1),
]
broken_col = (0, 0.4, 1)
update_col = (0.9, 0.7, 0)
installed_col = (0.5, 0.5, 0.5)
def color_blend(lo, hi, blend):
if blend >= 1:
return hi
elif blend <= 0:
return lo
num_x = 3
if len(lo) > 3 or len(hi) > 3:
lo = list(lo) + [1]
hi = list(hi) + [1]
num_x = 4
return tuple([lo[x] * (1 - blend) + hi[x] * blend for x in range(num_x)])
class FlowerTheme:
def __init__(self, fg, solid=False):
self.fg = fg
self.solid = solid
class ColorTheme:
def __init__(self, bg, flowers, num_stars=8):
self.bg = bg
self.fg = (1, 1, 1)
self.text_bg = (1, 1, 1)
self.text_fg = (0, 0, 0)
self.flowers = flowers
self.num_stars = num_stars
@classmethod
def from_style(cls, style, category):
def color_from_style(color):
try:
ret = []
if color.startswith("rgb"):
ret = color[color.find("(") + 1 : color.find(")")].split(",")
ret = [float(x) for x in ret]
if color.startswith("#"):
ret = [
int(color[x : x + 2], 16) / 255 for x in range(1, len(color), 2)
]
if 2 < len(ret) < 5:
return tuple(ret)
except:
pass
fallback = cls.get(category)
if style:
bg = color_from_style(style.get("background"))
color = color_from_style(style.get("color"))
else:
bg = None
color = None
if bg is None:
bg = fallback.bg
if color is None:
color = fallback.flowers[0].fg
flower_colors = [FlowerTheme(color)]
return cls(bg, flower_colors)
@classmethod
def get(cls, key):
if key in color_themes:
return color_themes[key]
else:
return default_color_theme
color_themes = {
"Classic": ColorTheme((0.1, 0.4, 0.3), [FlowerTheme((1.0, 0.6, 0.4, 0.4), True)]),
"SolidBlack": ColorTheme((0, 0, 0), [FlowerTheme((0, 0, 0), True)]),
"Updates": ColorTheme((0.9, 0.8, 0), [FlowerTheme((0.9, 0.6, 0))]),
"Installed": ColorTheme(installed_col, [FlowerTheme((0, 0.8, 0.8))]),
"Music": ColorTheme((1, 0.4, 0.7), [FlowerTheme((1.0, 0.8, 0))]),
"Games": ColorTheme((0.9, 0.5, 0.1), [FlowerTheme((0.5, 0.3, 0.1))]),
"Badge": ColorTheme((0.3, 0.4, 0.7), [FlowerTheme((0, 0.8, 0.8))]),
"Apps": ColorTheme((0.7, 0, 0.7), [FlowerTheme((0, 0, 0))]),
"Demos": ColorTheme((0, 0, 0), [FlowerTheme((0.5, 0.5, 0.5))]),
"Media": ColorTheme((0, 0, 0), [FlowerTheme((0.5, 0.5, 0.5))]),
"All": ColorTheme((0.18, 0.81, 0.36), [FlowerTheme((0, 0.3, 0))]),
"Seeds": ColorTheme(
(0.1, 0.1, 0.1), [FlowerTheme([x * 0.7 for x in col]) for col in seed_cols]
),
"Featured": ColorTheme(
(0.2, 0.0, 0.3),
[FlowerTheme((0.9, 0.6, 0)), FlowerTheme((1.0, 0.4, 0.7), True)],
),
}
default_color_theme = color_themes["Classic"]
class Flow3rView(BaseView):
def __init__(self, colors=None) -> None:
super().__init__()
self.colors = default_color_theme if colors is None else colors
self.flowers = []
for i in range(self.colors.num_stars):
flower_color = random.choice(self.colors.flowers)
flower = Flower(
((random.getrandbits(16) - 32767) / 32767.0) * 200,
((random.getrandbits(16)) / 65535.0) * 240 - 120,
((random.getrandbits(16)) / 65535.0) * 400 + 25,
colors=flower_color,
)
flower.fg_col_prev = flower.fg_col
flower.fg_col_target = flower.fg_col
self.flowers.append(flower)
self.bg_col = self.colors.bg
self.bg_col_prev = self.bg_col
self.bg_col_target = self.bg_col
self.color_blend = None
def update_colors(self, colors):
self.colors = colors
self.bg_col_prev = self.bg_col
self.bg_col_target = self.colors.bg
for f in self.flowers:
flower_color = random.choice(self.colors.flowers)
f.fg_col_prev = f.fg_col
f.fg_col_target = flower_color.fg
self.color_blend = 0
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
for f in self.flowers:
f.y += (10 * delta_ms / 1000.0) * 200 / f.z
if f.y > 300:
f.y = -300
f.rot += float(delta_ms) * f.rot_speed
self.flowers = sorted(self.flowers, key=lambda f: -f.z)
if self.color_blend is not None:
self.color_blend += delta_ms / 160
def draw(self, ctx: Context) -> None:
ctx.save()
ctx.rectangle(-120, -120, 240, 240)
if self.color_blend is not None:
self.bg_col = color_blend(
self.bg_col_prev, self.bg_col_target, self.color_blend
)
for f in self.flowers:
f.fg_col = color_blend(f.fg_col_prev, f.fg_col_target, self.color_blend)
if self.color_blend >= 1:
self.color_blend = None
ctx.rgb(*self.bg_col)
ctx.fill()
for f in self.flowers:
f.draw(ctx)
ctx.restore()
class Flower:
def __init__(self, x: float, y: float, z: float, colors=None) -> None:
self.fg_col = (1.0, 0.6, 0.4, 0.4)
self.solid = True
if colors is not None:
self.fg_col = colors.fg
self.solid = colors.solid
self.x = x
self.y = y
self.z = z
self.rot = 0.0
self.rot_speed = (((random.getrandbits(16) - 32767) / 32767.0) - 0.5) / 800
def draw(self, ctx: Context) -> None:
ctx.save()
ctx.line_width = 4
ctx.translate(-78 + self.x, -70 + self.y)
ctx.translate(50, 40)
ctx.rotate(self.rot)
ctx.translate(-50, -40)
ctx.scale(100 / self.z, 100.0 / self.z)
if len(self.fg_col) == 4:
ctx.rgba(*self.fg_col)
else:
ctx.rgb(*self.fg_col)
ctx.move_to(76.221727, 3.9788409).curve_to(
94.027758, 31.627675, 91.038918, 37.561293, 94.653428, 48.340473
).rel_curve_to(
25.783102, -3.90214, 30.783332, -1.52811, 47.230192, 4.252451
).rel_curve_to(
-11.30184, 19.609496, -21.35729, 20.701768, -35.31018, 32.087063
).rel_curve_to(
5.56219, 12.080061, 12.91196, 25.953973, 9.98735, 45.917643
).rel_curve_to(
-19.768963, -4.59388, -22.879866, -10.12216, -40.896842, -23.93099
).rel_curve_to(
-11.463256, 10.23025, -17.377386, 18.2378, -41.515124, 25.03533
).rel_curve_to(
0.05756, -29.49286, 4.71903, -31.931936, 10.342734, -46.700913
).curve_to(
33.174997, 77.048676, 19.482194, 71.413009, 8.8631648, 52.420793
).curve_to(
27.471602, 45.126773, 38.877997, 45.9184, 56.349456, 48.518302
).curve_to(
59.03275, 31.351935, 64.893201, 16.103886, 76.221727, 3.9788409
).close_path()
if self.solid:
ctx.fill()
else:
ctx.stroke()
ctx.restore()
from st3m.input import InputState
from st3m.ui import colours
from st3m.ui.view import BaseView, ViewManager
from ctx import Context
from .background import Flow3rView
from .background import broken_col, update_col, installed_col
from .download import DownloadView
from .delete import DeleteView
from st3m.utils import wrap_text
import math
class Button:
def __init__(self, pos, size):
self.len = list(size)
self.mid = list(pos)
self.min = [pos[x] - size[x] / 2 for x in range(2)]
self.shift = 0
self.top_tags = list()
self.bot_tags = list()
self.text = "back"
def draw(self, ctx, highlight=False):
ctx.font_size = 18
ctx.gray(1)
if not highlight:
ctx.move_to(*self.mid)
ctx.text(self.text)
return
ctx.rectangle(*self.min, *self.len).fill()
ctx.fill()
ctx.save()
ctx.gray(0)
ctx.move_to(self.shift + self.mid[0], self.mid[1])
ctx.text_align = ctx.LEFT
ctx.text(self.text)
ctx.font_size = 14
ctx.rel_move_to(0, -6)
x_start = ctx.x
for text, col in self.top_tags:
ctx.rgb(*col)
ctx.text(" " + text)
x_stop = ctx.x
ctx.rel_move_to(x_start - x_stop, 12)
for text, col in self.bot_tags:
ctx.rgb(*col)
ctx.text(" " + text)
x_stop = max(x_stop, ctx.x)
ctx.restore()
if abs(x_asym := self.shift + x_stop) > 0.1:
self.shift -= x_asym / 2
self.draw(ctx, True)
class InstallButton(Button):
def __init__(self, pos, size, version):
super().__init__(pos, size)
self.version = version
self.text = "install"
if version.patch:
self.text += " patch"
tags = self.top_tags
if version.broken:
tags.append(("broken", broken_col))
tags = self.bot_tags
if version.update:
tags.append(("update", update_col))
tags = self.bot_tags
if version.installed:
tags.append(("installed", installed_col))
tags = self.bot_tags
class DeleteButton(Button):
def __init__(self, pos, size):
super().__init__(pos, size)
self.text = "delete"
class ScrollBlock:
def __init__(self, raw_text):
self.num_lines = 2
self.line_height = 18
self.start = -87
self.width = 175
self.grad_len = 0
self.end = self.start + self.num_lines * self.line_height
self.raw_text = raw_text
self.speed = 0
self.pos = self.start + self.line_height / 2
self.grad = self.grad_len / (self.end - self.start + 2 * self.grad_len)
self.clip_min = self.start - self.grad_len
self.clip_max = self.end + self.grad_len
def get_lines(self, ctx):
self.lines = wrap_text(self.raw_text, self.width, ctx)
if not self.lines:
return
if len(self.lines) > self.num_lines:
self.speed = 1 / 160
self.lines.append("")
else:
self.pos += (self.num_lines - len(self.lines)) * self.line_height / 2
self.overflow_pos = len(self.lines) * self.line_height
def draw(self, ctx):
if not hasattr(self, "lines"):
self.get_lines(ctx)
if not self.lines:
return
ctx.save()
ctx.rectangle(
-self.width / 2, self.clip_min, self.width, self.clip_max - self.clip_min
).clip()
if not self.speed:
for x, line in enumerate(self.lines):
pos = x * self.line_height + self.pos
ctx.move_to(0, pos)
ctx.text(line)
else:
self.pos %= self.overflow_pos
x = int(
(self.clip_min - self.line_height / 2 - self.pos) // self.line_height
)
while True:
line = self.lines[x % len(self.lines)]
pos = x * self.line_height + self.pos
if pos > self.clip_max + self.line_height / 2:
break
if pos > self.end - self.line_height / 2:
ctx.linear_gradient(0, self.clip_min, 0, self.clip_max)
ctx.add_stop(0, (1.0, 1.0, 1.0), 1)
ctx.add_stop(1 - self.grad, (1.0, 1.0, 1.0), 1)
ctx.add_stop(1, (1.0, 1.0, 1.0), 0)
elif pos < self.start + self.line_height / 2:
ctx.linear_gradient(0, self.clip_min, 0, self.clip_max)
ctx.add_stop(0, (1.0, 1.0, 1.0), 0)
ctx.add_stop(self.grad, (1.0, 1.0, 1.0), 1)
ctx.add_stop(1, (1.0, 1.0, 1.0), 1)
else:
ctx.gray(1)
ctx.move_to(0, pos)
ctx.text(line)
x += 1
ctx.restore()
def think(self, ins, delta_ms):
self.pos -= delta_ms * self.speed
class ConfirmationView(BaseView):
background: Flow3rView
def __init__(self, app) -> None:
super().__init__()
self.background = Flow3rView(app.colors)
self.app = app
def on_enter(self, vm):
super().on_enter(vm)
self.buttons = []
self.button_index = 0
x = 0
size = [240, 25]
for version in self.app.available_versions:
pos = [0, 48 + 25 * x]
self.buttons.append(InstallButton(pos, size, version))
x += 1
if self.app.installed:
pos = [0, 48 + 25 * x]
self.buttons.append(DeleteButton(pos, size))
x += 1
self.desc = ScrollBlock(self.app.description)
def on_exit(self) -> bool:
# request thinks after on_exit
return True
def draw(self, ctx: Context) -> None:
ctx.move_to(0, 0)
self.background.draw(ctx)
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.font_size = 17
self.desc.draw(ctx)
ctx.rgb(*colours.WHITE)
ctx.rectangle(
-120.0,
-35.0,
240.0,
40.0,
).fill()
ctx.font = "Camp Font 3"
app = self.app
ctx.rgb(*colours.BLACK)
ctx.font_size = 24
ctx.move_to(0, -15)
ctx.text(app.name)
if app.author:
ctx.font_size = 17
ctx.gray(1)
ctx.move_to(0, 21)
ctx.text("by " + app.author)
for x, button in enumerate(self.buttons):
button.draw(ctx, highlight=x == self.button_index)
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
self.background.think(ins, delta_ms)
self.desc.think(ins, delta_ms)
if self.is_active():
self.button_index += self.input.buttons.app.right.pressed
self.button_index -= self.input.buttons.app.left.pressed
self.button_index %= len(self.buttons)
if self.input.buttons.app.middle.pressed:
button = self.buttons[self.button_index]
if isinstance(button, InstallButton):
print("Installing", self.app.name, "from", button.version.tar_url)
self.vm.push(
DownloadView(
self.app,
button.version,
)
)
elif isinstance(button, DeleteButton):
self.vm.push(
DeleteView(
self.app,
)
)
else:
self.vm.pop()