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
  • 9Rmain
  • anon/gpndemo
  • anon/update-sim
  • anon/webflasher
  • audio_input
  • audio_io
  • bl00mbox
  • bl00mbox_old
  • captouch-threshold
  • ch3/bl00mbox_docs
  • ci-1690580595
  • compressor
  • dev_p4
  • dev_p4-iggy
  • dev_p4-iggy-rebased
  • dos
  • dos-main-patch-50543
  • events
  • fm_fix
  • fm_fix2
  • fpletz/flake
  • history-rewrite
  • icon-flower
  • iggy/stemming
  • iggy/stemming_merge
  • json-error
  • main
  • main+schneider
  • media-buf
  • micropython_api
  • moon2_applications
  • moon2_demo_temp
  • moon2_gay_drums
  • passthrough
  • phhw
  • pippin/display-python-errors-on-display
  • pippin/make_empty_drawlists_skip_render_and_blit
  • pippin/media_framework
  • pippin/uhm_flash_access_bust
  • pressable_bugfix
  • q3k/doom-poc
  • rahix/big-flow3r
  • rahix/flow3rseeds
  • raw_captouch_new
  • raw_captouch_old
  • release/1.0.0
  • release/1.1.0
  • release/1.1.1
  • rev4_micropython
  • schneider/application-remove-name
  • schneider/bhi581
  • schneider/factory_test
  • schneider/recovery
  • scope
  • scope_hack
  • sdkconfig-spiram-tinyusb
  • sec/auto-nick
  • sec/blinky
  • simtest
  • slewtest
  • t
  • test
  • test2
  • uctx-wip
  • view-think
  • vm-pending
  • vsync
  • wave
  • 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
83 results
Show changes
Showing
with 2421 additions and 492 deletions
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,29 +36,72 @@ 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="\ue8d5", label="Scroll Y"),
Action(icon="\ue8b6", label="Zoom"),
Action(icon="\ue5c4", label="Back"),
Action(icon="\ue8d4", label="Scroll X"),
]
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)
......@@ -67,23 +113,34 @@ class Reader(ActionView):
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[8].gesture, delta_ms)
self.scroll_y.update(self.input.captouch.petals[2].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 InputController, InputState
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
......@@ -15,34 +17,40 @@ 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"]
input: InputController
background: Flow3rView
state: ViewState
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx=app_ctx)
self.input = InputController()
self.background = Flow3rView()
self._sc = ScrollController()
self._sc.set_item_count(3)
self.acceptSdCard = False
self.state = ViewState.CONTENT
def on_enter(self, vm: ViewManager | None) -> None:
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
if self.vm is None:
raise RuntimeError("vm is None")
def on_exit(self) -> bool:
# request thinks after on_exit
return True
def draw(self, ctx: Context) -> None:
if self.state == ViewState.NO_INTERNET:
ctx.move_to(0, 0)
ctx.rgb(*colours.BLACK)
ctx.rectangle(
-120.0,
......@@ -51,78 +59,52 @@ class Gr33nhouseApp(Application):
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
if self.state == ViewState.BAD_SDCARD:
ctx.font_size = 18
ctx.move_to(0, -15)
ctx.text("No internet")
ctx.move_to(0, 15)
ctx.text("Check settings")
ctx.restore()
return
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.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.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
for idx, item in enumerate(self.items):
if idx == self._sc.target_position():
ctx.gray(0.0)
else:
ctx.gray(1.0)
ctx.move_to(0, offset)
ctx.text(item)
offset += 30
ctx.move_to(0, -15)
ctx.text("Connecting..." if st3m.wifi.is_connecting() else "No internet")
ctx.restore()
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:
self.input.think(ins, delta_ms)
super().think(ins, delta_ms)
self._sc.think(ins, delta_ms)
if self.vm is None:
raise RuntimeError("vm is None")
self.background.think(ins, delta_ms)
if not network.WLAN(network.STA_IF).isconnected():
self.state = ViewState.NO_INTERNET
return
else:
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.background.think(ins, delta_ms)
if self.input.buttons.app.left.pressed:
self._sc.scroll_left()
elif self.input.buttons.app.right.pressed:
self._sc.scroll_right()
elif self.input.buttons.app.middle.pressed:
pos = self._sc.target_position()
if pos == 0:
self.vm.push(AppList())
elif pos == 1:
self.vm.push(RecordView())
elif pos == 2:
self.vm.push(ManualInputView())
self.update_state()
from st3m.goose import Optional, Enum, Any
from st3m.input import InputController, InputState
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):
......@@ -17,26 +22,407 @@ class ViewState(Enum):
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):
initial_ticks: int = 0
_scroll_pos: float = 0.0
_state: ViewState = ViewState.INITIAL
apps: list[Any] = []
items: list[Any] = ["All"]
category_order: list[Any] = ["Badge", "Music", "Games", "Media", "Apps", "Demos"]
input: InputController
background: Flow3rView
def __init__(self) -> None:
self.input = InputController()
self.vm = None
self.background = Flow3rView()
super().__init__()
self.background = Flow3rView(ColorTheme.get("SolidBlack"))
self._sc = ScrollController()
self._sc.set_item_count(len(self.items))
self.category_prev = ""
def on_enter(self, vm: Optional[ViewManager]) -> None:
self.vm = vm
self.initial_ticks = time.ticks_ms()
def draw(self, ctx: Context) -> None:
def draw(self, ctx):
ctx.move_to(0, 0)
if self._state == ViewState.INITIAL or self._state == ViewState.LOADING:
......@@ -79,6 +465,8 @@ class AppList(BaseView):
return
elif self._state == ViewState.LOADED:
ctx.move_to(0, 0)
self.background.draw(ctx)
ctx.save()
......@@ -91,7 +479,6 @@ class AppList(BaseView):
).fill()
ctx.translate(0, -30 * self._sc.current_position())
offset = 0
ctx.font = "Camp Font 3"
......@@ -100,14 +487,19 @@ class AppList(BaseView):
ctx.text_baseline = ctx.MIDDLE
ctx.move_to(0, 0)
for idx, app in enumerate(self.apps):
if idx == self._sc.target_position():
for idx, item in enumerate(self.items):
target = idx == self._sc.target_position()
if target:
ctx.gray(0.0)
else:
ctx.gray(1.0)
ctx.move_to(0, offset)
ctx.text(app["name"])
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()
......@@ -115,27 +507,41 @@ class AppList(BaseView):
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.initial_ticks == 0 or time.ticks_ms() < self.initial_ticks + 300:
return
self.input.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")
self.apps = res.json()["apps"]
if self.apps == None:
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
self._sc.set_item_count(len(self.apps))
print("App list loaded")
except Exception as e:
print(f"Load failed: {e}")
self._state = ViewState.ERROR
......@@ -146,23 +552,56 @@ class AppList(BaseView):
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:
if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
self._sc.scroll_left()
elif self.input.buttons.app.right.pressed:
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))
app = self.apps[self._sc.target_position()]
url = app["tarDownloadUrl"]
name = app["name"]
author = app["author"]
self.vm.push(
ConfirmationView(
url=url,
name=name,
author=author,
)
)
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 InputController, InputState
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),
]
class Flow3rView(BaseView):
input: InputController
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)
def __init__(self) -> None:
self.vm = None
self.input = InputController()
@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(8):
self.flowers.append(
Flower(
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 c in self.flowers:
c.y += (10 * delta_ms / 1000.0) * 200 / c.z
if c.y > 300:
c.y = -300
c.rot += float(delta_ms) * c.rot_speed
self.flowers = sorted(self.flowers, key=lambda c: -c.z)
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)
ctx.rgb(0.1, 0.4, 0.3)
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:
......@@ -43,7 +173,12 @@ class Flow3rView(BaseView):
class Flower:
def __init__(self, x: float, y: float, z: float) -> None:
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
......@@ -52,12 +187,18 @@ class Flower:
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(
......@@ -78,32 +219,11 @@ class Flower:
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().rgba(
1.0, 0.6, 0.4, 0.4
).fill()
ctx.restore()
return
ctx.move_to(116.89842, 17.221179).rel_curve_to(
6.77406, 15.003357, 9.99904, 35.088466, 0.27033, 47.639569
).curve_to(
108.38621, 76.191194, 87.783414, 86.487988, 75.460015, 75.348373
).curve_to(
64.051094, 64.686361, 61.318767, 54.582827, 67.499384, 36.894251
).curve_to(
79.03955, 16.606134, 103.60918, 15.612261, 116.89842, 17.221179
).close_path().rgb(
0.5, 0.3, 0.4
).fill()
).close_path()
if self.solid:
ctx.fill()
else:
ctx.stroke()
ctx.move_to(75.608612, 4.2453713).curve_to(
85.516707, 17.987709, 93.630911, 33.119248, 94.486497, 49.201225
).curve_to(
95.068862, 60.147617, 85.880014, 75.820834, 74.919761, 75.632395
).curve_to(
63.886159, 75.442695, 57.545631, 61.257211, 57.434286, 50.22254
).curve_to(
57.257291, 32.681814, 65.992688, 16.610811, 75.608612, 4.2453713
).close_path().rgb(
0.2, 0.5, 0.8
).fill()
ctx.restore()
from st3m.input import InputController, InputState
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
input: InputController
url: str
name: str
author: str
def __init__(self, app) -> None:
super().__init__()
self.background = Flow3rView(app.colors)
def __init__(self, url: str, name: str, author: str) -> None:
self.input = InputController()
self.vm = None
self.background = Flow3rView()
self.app = app
self.url = url
self.name = name
self.author = author
def on_enter(self, vm: ViewManager | None) -> None:
def on_enter(self, vm):
super().on_enter(vm)
if self.vm is None:
raise RuntimeError("vm is None")
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.save()
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,
-80.0,
-35.0,
240.0,
160.0,
40.0,
).fill()
ctx.rgb(*colours.BLACK)
ctx.font = "Camp Font 3"
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.font_size = 16
ctx.move_to(0, -60)
ctx.text("Install")
app = self.app
ctx.rgb(*colours.BLACK)
ctx.font_size = 24
ctx.move_to(0, -30)
ctx.text(self.name)
ctx.move_to(0, -15)
ctx.text(app.name)
ctx.font_size = 16
ctx.move_to(0, 0)
ctx.text("by")
if app.author:
ctx.font_size = 17
ctx.gray(1)
ctx.move_to(0, 21)
ctx.text("by " + app.author)
ctx.font_size = 24
ctx.move_to(0, 30)
ctx.text(self.author)
ctx.font_size = 16
ctx.move_to(0, 60)
ctx.text("(Right shoulder to abort)")
ctx.restore()
for x, button in enumerate(self.buttons):
button.draw(ctx, highlight=x == self.button_index)
def think(self, ins: InputState, delta_ms: int) -> None:
self.input.think(ins, delta_ms)
super().think(ins, delta_ms)
self.background.think(ins, delta_ms)
self.desc.think(ins, delta_ms)
if self.vm is None:
raise RuntimeError("vm is None")
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:
self.vm.replace(
button = self.buttons[self.button_index]
if isinstance(button, InstallButton):
print("Installing", self.app.name, "from", button.version.tar_url)
self.vm.push(
DownloadView(
url=self.url,
self.app,
button.version,
)
)
elif isinstance(button, DeleteButton):
self.vm.push(
DeleteView(
self.app,
)
)
else:
self.vm.pop()
from st3m.input import InputState
from st3m.goose import Optional, List
from st3m.ui import colours
from st3m.utils import sd_card_plugged
from st3m.ui.view import BaseView
from ctx import Context
import os
from st3m import application_settings
class DeleteView(BaseView):
def __init__(self, app) -> None:
super().__init__()
self._app = app
self._delete = 0
def draw(self, ctx: Context) -> None:
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.gray(1)
ctx.move_to(0, 0)
ctx.font = "Camp Font 3"
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.font_size = 24
ctx.text(self._app.name)
ctx.move_to(0, -40)
ctx.font_size = 20
ctx.text("Delete app?")
for x in range(2):
x = 1 - x
ctx.translate(0, 35)
ctx.move_to(0, 0)
ctx.gray(1)
if x == self._delete:
ctx.rectangle(-120, -15, 240, 30).fill()
ctx.gray(0)
ctx.text("yes" if x else "no")
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms) # Let BaseView do its thing
self._delete += self.input.buttons.app.right.pressed
self._delete -= self.input.buttons.app.left.pressed
self._delete %= 2
if self.input.buttons.app.middle.pressed:
if self._delete:
application_settings.delete_app(self._app.installed_path)
self._app.update_installed_version(None)
self.vm.pop()
from st3m.input import InputController, InputState
from st3m.goose import Optional
from st3m.input import InputState
from st3m.goose import Optional, List
from st3m.ui import colours
from st3m.utils import sd_card_plugged
import urequests
import gzip
from utarfile import TarFile, DIRTYPE
import io
import os
import gc
import math
from st3m.ui.view import BaseView
from ctx import Context
from st3m import application_settings
class DownloadView(BaseView):
response: Optional[urequests.Response]
response: Optional[bytes] = b""
error_message: str = ""
_download_instance: Optional["download_file"]
"""
View state
......@@ -21,108 +27,205 @@ class DownloadView(BaseView):
3 = Extracting
4 = Extracting
5 = Done
6 = Error
"""
state: int
_state: int
input: InputController
def __init__(self, url: str) -> None:
def __init__(self, app, version) -> None:
super().__init__()
self._state = 1
self._try = 1
self._url = url
self._app = app
self._version = version
self._url = version.tar_url
self.response = b""
self._download_instance = None
self.download_percentage = 0
def _get_app_folder(self, tar_size: int) -> Optional[str]:
if sd_card_plugged():
sd_statvfs = os.statvfs("/sd")
if tar_size < sd_statvfs[1] * sd_statvfs[3]:
return "/sd/apps/"
self.input = InputController()
flash_statvfs = os.statvfs("/flash")
if tar_size < flash_statvfs[1] * flash_statvfs[3]:
return "/flash/apps/"
return None
def draw(self, ctx: Context) -> None:
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.rgb(*colours.WHITE)
# Arc strokes should be drawn before move_to for some reason to look nice
if self.download_percentage and self._state == 2:
ctx.save()
ctx.line_width = 3
ctx.rotate(270 * math.pi / 180)
ctx.arc(
0, 0, 117, math.tau * (1 - self.download_percentage), math.tau, 0
).stroke()
ctx.restore()
ctx.save()
ctx.move_to(0, 0)
if self._state == 1 or self._state == 2:
# Fetching
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("Downloading...")
text_to_draw = ""
if self._state == 1 or self._state == 2:
# Fetching
text_to_draw = "Downloading..."
if self._download_instance:
text_to_draw += f"\n{len(self.response)}b"
self._state = 2
elif self._state == 3 or self._state == 4:
# Extracting
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("Extracting...")
text_to_draw = "Extracting..."
self._state = 4
elif self._state == 5:
# Done
ctx.move_to(0, -30)
ctx.text("All done...")
ctx.gray(0.75)
ctx.font_size = 22
text_to_draw = "The app will be\navailable after reboot"
elif self._state == 6:
# Errored
ctx.move_to(0, -30)
ctx.text("Oops...")
text_to_draw = self.error_message
ctx.font_size = 12
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("All done!")
y_offset = 0
for line in text_to_draw.split("\n"):
ctx.move_to(0, y_offset)
ctx.text(line)
y_offset += int(ctx.font_size * 1.25)
ctx.move_to(0, 0)
ctx.text("The app will be")
ctx.restore()
ctx.move_to(0, 30)
ctx.text("available after reboot")
def download_file(self, url: str, block_size=10240) -> List[bytes]:
gc.collect()
req = urequests.get(url)
total_size = int(req.headers["Content-Length"])
ctx.restore()
try:
while True:
new_data = req.raw.read(block_size)
yield new_data, total_size
if len(new_data) < block_size:
break
finally:
req.close()
def think(self, ins: InputState, delta_ms: int) -> None:
# super().think(ins, delta_ms) # Let BaseView do its thing
self.input.think(ins, delta_ms)
super().think(ins, delta_ms) # Let BaseView do its thing
if self.vm.transitioning:
return
if self.input.buttons.app.middle.pressed:
if self.vm is None:
raise RuntimeError("vm is None")
self.vm.pop()
if self._state == 2:
fail_reason = ""
try:
if not self._download_instance:
print("Getting it")
self.response = urequests.get(self._url)
self.response = b""
self._download_instance = self.download_file(self._url)
return
else:
try:
new_data, total_size = next(self._download_instance)
self.download_percentage = len(self.response) / total_size
if new_data:
self.response += new_data
return
except StopIteration:
self._download_instance = None
if self.response is not None and self.response.content is not None:
if self.response is not None:
print("Got something")
self._state = 3
return
print("no content...")
except:
print("Exception")
print("Next try")
fail_reason = "No content"
except MemoryError:
gc.collect()
self.response = None
self.error_message = "Out of Memory\n(app too big?)"
self._state = 6
return
except Exception as e:
fail_reason = f"Exception:\n{str(e)}"
print(fail_reason)
self._try += 1
if self._try >= 3:
self.response = None
self.error_message = fail_reason
self._state = 6
elif self._state == 4:
if self.response is None:
raise RuntimeError("response is None")
tar = gzip.decompress(self.response.content)
try:
gc.collect()
tar = gzip.decompress(self.response)
self.response = None
gc.collect()
t = TarFile(fileobj=io.BytesIO(tar))
except MemoryError:
gc.collect()
self.response = None
self.error_message = "Out of Memory\n(app too big?)"
self._state = 6
return
app_folder = self._get_app_folder(len(tar))
if not app_folder:
gc.collect()
self.response = None
self.error_message = f"Not Enough Space\nSD/flash lack:\n{len(tar)}b"
self._state = 6
return
if not os.path.exists(app_folder):
print(f"making {app_folder}")
os.mkdir(app_folder)
installed_path = app_folder + self._app.slug
if os.path.exists(installed_path):
print("removing old files at", installed_path)
application_settings.delete_app(installed_path)
for i in t:
print(i.name)
if i.type == DIRTYPE:
print("dirtype")
dirname = "/flash/sys/apps/" + i.name
dirname = app_folder + i.name
if not os.path.exists(dirname):
print("making", dirname)
os.mkdir(dirname)
else:
print("dir", dirname, "exists")
else:
filename = "/flash/sys/apps/" + i.name
filename = app_folder + i.name
print("writing to", filename)
f = t.extractfile(i)
with open(filename, "wb") as of:
of.write(f.read())
self._state = 5
while data := f.read():
of.write(data)
if self.input.buttons.app.middle.pressed:
if self.vm is None:
raise RuntimeError("vm is None")
self.vm.pop()
self._app.installed_path = installed_path
self._app.update_installed_version(self._version)
print("installed at", installed_path)
self._state = 5
[app]
name = "Get Apps"
menu = "Hidden"
category = "Hidden"
wifi_preference = true
[entry]
class = "Gr33nhouseApp"
......
from st3m.goose import Optional, Enum
from st3m.input import InputController, InputState
from st3m.input import InputState
from st3m.ui import colours
from st3m.ui.view import BaseView, ViewManager
from ctx import Context
from .confirmation import ConfirmationView
from .background import Flow3rView
from .background import seed_cols as PETAL_COLORS
import math
import urequests
import time
import gc
PETAL_COLORS = [
(0, 0, 1),
(0, 1, 1),
(1, 1, 0),
(0, 1, 0),
(1, 0, 1),
]
PETAL_MAP = [0, 2, 4, 6, 8]
ONE_FIFTH = math.pi * 2 / 5
ONE_TENTH = math.pi * 2 / 10
......@@ -33,10 +27,9 @@ class ManualInputView(BaseView):
current_petal: Optional[int]
wait_timer: Optional[int]
def __init__(self) -> None:
self.input = InputController()
self.vm = None
self.background = Flow3rView()
def __init__(self, colors=None) -> None:
super().__init__()
self.background = Flow3rView(colors)
self.flow3r_seed = ""
self.current_petal = None
......@@ -47,8 +40,10 @@ class ManualInputView(BaseView):
super().on_enter(vm)
self.flow3r_seed = ""
self.state = ViewState.ENTER_SEED
if self.vm is None:
raise RuntimeError("vm is None")
def on_exit(self) -> bool:
# request thinks after on_exit
return True
def draw(self, ctx: Context) -> None:
self.background.draw(ctx)
......@@ -109,9 +104,12 @@ class ManualInputView(BaseView):
ctx.text(f"not found!")
def think(self, ins: InputState, delta_ms: int) -> None:
self.input.think(ins, delta_ms)
super().think(ins, delta_ms)
self.background.think(ins, delta_ms)
if not self.is_active():
return
if self.state == ViewState.ENTER_SEED:
if self.current_petal is not None:
if not ins.captouch.petals[self.current_petal].pressed:
......
from st3m.input import InputController, InputState
from st3m.input import InputState
from st3m.ui import colours
from st3m.ui.view import BaseView, ViewManager
from ctx import Context
......@@ -6,19 +6,10 @@ from .background import Flow3rView
class RecordView(BaseView):
input: InputController
def __init__(self) -> None:
self.input = InputController()
self.vm = None
super().__init__()
self.background = Flow3rView()
def on_enter(self, vm: ViewManager | None) -> None:
super().on_enter(vm)
if self.vm is None:
raise RuntimeError("vm is None")
def draw(self, ctx: Context) -> None:
ctx.move_to(0, 0)
ctx.save()
......@@ -37,6 +28,3 @@ class RecordView(BaseView):
ctx.text_baseline = ctx.MIDDLE
ctx.text("Coming soon")
ctx.restore()
def think(self, ins: InputState, delta_ms: int) -> None:
self.input.think(ins, delta_ms)
from st3m.application import Application
import math, random, sys_display
from ctx import Context
class App(Application):
def __init__(self, app_ctx):
super().__init__(app_ctx)
self.x = 23
self.x_vel = 40 / 1000.0
self.y = -53
self.font_size = 16
self.delta_ms = 0
self.right_pressed = False
self.left_pressed = False
self.select_pressed = False
self.angle = 0
self.focused_widget = 1
self.active = False
def draw_widget(self, label):
ctx = self.ctx
self.widget_no += 1
if not self.active:
if self.select_pressed and self.focused_widget > 0:
self.active = True
self.select_pressed = False
elif self.left_pressed:
self.focused_widget -= 1
if self.focused_widget < 1:
self.focused_widget = 1
self.left_pressed = False
elif self.right_pressed:
self.focused_widget += 1
if self.focused_widget > 9:
self.focused_widget = 9
self.right_pressed = False
if self.widget_no == self.focused_widget and not self.active:
ctx.rectangle(-130, int(self.y - self.font_size * 0.8), 260, self.font_size)
ctx.line_width = 2.0
ctx.rgba(0.8, 0.6, 0.1, 1.0)
ctx.stroke()
ctx.gray(1)
ctx.move_to(-95, self.y)
self.y += self.font_size
ctx.text(label + ": ")
def draw_choice(self, label, choices, no):
ctx = self.ctx
self.draw_widget(label)
if self.widget_no == self.focused_widget and self.active:
if self.left_pressed:
no -= 1
if no < 0:
no = 0
elif self.right_pressed:
no += 1
if no >= len(choices):
no = len(choices) - 1
elif self.select_pressed:
self.active = False
self.select_pressed = False
for a in range(len(choices)):
if a == no and self.active and self.widget_no == self.focused_widget:
ctx.save()
ctx.rgba(0.8, 0.6, 0.1, 1.0)
ctx.line_width = 2.0
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(choices[a]) + 2,
self.font_size,
).stroke()
ctx.restore()
ctx.text(choices[a] + " ")
elif a == no:
ctx.save()
ctx.gray(1)
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(choices[a]) + 2,
self.font_size,
).fill()
ctx.gray(0)
ctx.text(choices[a] + " ")
ctx.restore()
else:
ctx.text(choices[a] + " ")
return no
def draw_boolean(self, label, value):
ctx = self.ctx
self.draw_widget(label)
if self.widget_no == self.focused_widget and self.active:
value = not value
self.active = False
if value:
ctx.text(" on")
else:
ctx.text(" off")
return value
def draw_bg(self):
ctx = self.ctx
ctx.gray(1.0)
ctx.font_size = self.font_size
ctx.move_to(-100, -50)
self.y = -50
self.widget_no = 0
ctx.rectangle(-120, -120, 240, 240)
ctx.gray(0)
ctx.fill()
ctx.save()
ctx.translate(self.x, -80)
ctx.logo(0, 0, 40)
ctx.restore()
self.x += self.delta_ms * self.x_vel
if self.x < -50 or self.x > 50:
self.x_vel *= -1
self.x += self.delta_ms * self.x_vel
def draw(self, ctx: Context):
curmode = sys_display.get_mode()
low_latency = (curmode & sys_display.low_latency) != 0
direct_ctx = (curmode & sys_display.direct_ctx) != 0
lock = (curmode & sys_display.lock) != 0
osd = (curmode & sys_display.osd) != 0
think_per_draw = (curmode & sys_display.EXPERIMENTAL_think_per_draw) != 0
smart_redraw = (curmode & sys_display.smart_redraw) != 0
scale = 0
if (curmode & sys_display.x4) == sys_display.x2:
scale = 1
elif (curmode & sys_display.x4) == sys_display.x3:
scale = 2
elif (curmode & sys_display.x4) == sys_display.x4:
scale = 3
bpp = curmode & 63
palette = 0
if bpp == 9:
palette = 0
bpp = 0
elif bpp == 8:
palette = 1
bpp = 0
elif bpp == 10:
palette = 2
bpp = 0
elif bpp == 11:
palette = 3
bpp = 0
elif bpp == 16:
bpp = 1
elif bpp == 24:
bpp = 2
elif bpp == 32:
bpp = 3
elif bpp == 1:
bpp = 4
elif bpp == 2:
bpp = 5
elif bpp == 4:
bpp = 6
else:
bpp = 0
self.ctx = ctx
self.draw_bg()
bpp = self.draw_choice("bpp", ["8", "16", "24", "32", "1", "2", "4"], bpp)
if bpp > 0:
palette = 0
palette = self.draw_choice("palette", ["RGB", "gray", "sepia", "cool"], palette)
scale = self.draw_choice("scale", ["1x", "2x", "3x", "4x"], scale)
low_latency = self.draw_boolean("low latency", low_latency)
direct_ctx = self.draw_boolean("direct ctx", direct_ctx)
think_per_draw = self.draw_boolean("think per draw", think_per_draw)
smart_redraw = self.draw_boolean("smart redraw", smart_redraw)
osd = self.draw_boolean("osd", osd)
lock = self.draw_boolean("lock", lock)
if direct_ctx:
low_latency = True
if palette != 0:
bpp = 0
if bpp == 0:
if palette == 0:
mode = 9
elif palette == 1:
mode = 8
elif palette == 2:
mode = 10
elif palette == 3:
mode = 11
elif bpp == 1:
mode = 16
elif bpp == 2:
mode = 24
elif bpp == 3:
mode = 32
elif bpp == 4:
mode = 1
elif bpp == 5:
mode = 2
elif bpp == 6:
mode = 4
mode += osd * sys_display.osd
mode += low_latency * sys_display.low_latency
mode += direct_ctx * sys_display.direct_ctx
mode += lock * sys_display.lock
mode += think_per_draw * sys_display.EXPERIMENTAL_think_per_draw
mode += smart_redraw * sys_display.smart_redraw
if scale == 1:
mode += sys_display.x2
elif scale == 2:
mode += sys_display.x3
elif scale == 3:
mode += sys_display.x4
if mode != curmode:
sys_display.set_default_mode(mode)
################################################################
self.delta_ms = 0
self.select_pressed = False
self.left_pressed = False
self.right_pressed = False
def think(self, ins, delta_ms):
super().think(ins, delta_ms)
self.delta_ms += delta_ms
if (
self.input.buttons.app.right.pressed
or self.input.buttons.app.right.repeated
):
self.right_pressed = True
if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
self.left_pressed = True
if self.input.buttons.app.middle.pressed:
self.select_pressed = True
if __name__ == "__main__":
from st3m.run import run_app
run_app(App)
[app]
name = "Graphics Mode"
category = "Hidden"
[metadata]
author = "Flow3r Badge Authors"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
# micropython imports
from machine import I2C, Pin
# flow3r imports
from st3m.application import Application, ApplicationContext
from ctx import Context
from st3m.input import InputController, InputState
class I2CScanner(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self.input = InputController()
self.qwiic = I2C(1, freq=400000)
self._pending_scan = False
def scan(self):
self._devices = self.qwiic.scan()
print("Found Devices:")
print(self._devices)
def on_enter(self, vm):
super().on_enter(vm)
self._devices = None
def draw(self, ctx: Context) -> None:
# Get the default font
ctx.font = ctx.get_font_name(1)
# Draw a black background
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.text_align = ctx.MIDDLE
ctx.font_size = 16
ctx.rgb(1, 1, 1)
ctx.move_to(0, -60).text("I2C Device Scanner")
ctx.rgb(0.7, 0.7, 0.7)
ctx.font_size = 14
yOffset = 18
yStart = -30
ctx.move_to(0, yStart).text("Attach a device to the Qwiic port")
ctx.font_size = 16
ctx.move_to(0, yStart + yOffset * 1).text("Press OK to Re-Scan")
if self._devices is None:
ctx.move_to(0, yStart + (3 * yOffset)).text("Scanning...")
self._pending_scan = True
elif len(self._devices) == 0:
ctx.rgb(0.7, 0.0, 0.0)
ctx.move_to(0, yStart + (3 * yOffset)).text("No Devices Found")
else:
ctx.rgb(0.0, 0.7, 0.0)
ctx.move_to(0, yStart + (3 * yOffset)).text(
"Found %d Devices: " % len(self._devices)
)
devices_str = ""
for d in self._devices:
devices_str += hex(d) + " "
ctx.move_to(0, yStart + (4 * yOffset)).text(devices_str)
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
if self.input.buttons.app.middle.pressed:
self._devices = None
if self._pending_scan and not self.vm.transitioning:
self.scan()
self._pending_scan = False
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(I2CScanner)
[app]
name = "I2C/Qwiic Scanner"
category = "Demos"
[entry]
class = "I2CScanner"
[metadata]
author = "Flow3r Badge Authors"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
......@@ -2,6 +2,8 @@
import random
import time
import math
import json
import errno
# flow3r imports
from st3m.application import Application, ApplicationContext
......@@ -24,12 +26,21 @@ log.info("hello led painter")
class LEDPainter(Application):
def get_help(self):
help_text = (
"use petals 0 to 5 to set rgb values or petals "
"6 or 7 for shortcuts to black and white.\n\n"
"use the app button to move cw/ccw through "
"the LEDs by pressing left/right and enable "
"or disable drawing to LEDs by pressing down.\n\n"
"your pattern is saved when you exit and can be "
'set as a "LED wallpaper" in settings->appearance.'
)
return help_text
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self.input = InputController()
# self.scroll_R = CapScrollController()
# self.scroll_G = CapScrollController()
# self.scroll_B = CapScrollController()
self._cursor = 0
self._draw = True
self.STEPS = 30
......@@ -58,65 +69,152 @@ class LEDPainter(Application):
(39, 94),
(70, 51),
]
# self.PETAL_POS.reverse()
self.PETAL_POS = [
tuple([(k - 120) * 1.1 + 120 for k in g]) for g in self.PETAL_POS
]
def _try_load_settings(self, path):
try:
with open(path, "r") as f:
return json.load(f)
except OSError as e:
if e.errno != errno.ENOENT:
raise # ignore file not found
def _try_save_settings(self, path, settings):
try:
with open(path, "w+") as f:
f.write(json.dumps(settings))
f.close()
except OSError as e:
if e.errno != errno.ENOENT:
raise # ignore file not found
def _load_settings(self):
settings_path = "/flash/menu_leds.json"
settings = self._try_load_settings(settings_path)
if settings is None:
return
self.LEDS = settings["leds"]
for i in range(40):
col = settings["leds"][i]
leds.set_rgb(i, col[0], col[1], col[2])
def _save_settings(self):
settings_path = "/flash/menu_leds.json"
old_settings = self._try_load_settings(settings_path)
file_is_different = False
if old_settings is None:
file_is_different = True
else:
try:
ref_LEDS = old_settings["leds"]
for l in range(40):
if file_is_different:
break
for c in range(3):
if self.LEDS[l][c] != ref_LEDS[l][c]:
file_is_different = True
except:
file_is_different = True
if file_is_different:
settings = {}
settings["leds"] = self.LEDS
self._try_save_settings(settings_path, settings)
def on_enter(self, vm):
super().on_enter(vm)
self._load_settings()
leds.set_slew_rate(max(leds.get_slew_rate(), 220))
def on_exit(self):
self._save_settings()
def draw(self, ctx: Context) -> None:
ctx.font = ctx.get_font_name(1)
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.rgb(1, 1, 1).rectangle(-31, -31, 62, 62).fill()
ctx.rgb(self.r / 255, self.g / 255, self.b / 255).rectangle(
-30, -30, 60, 60
).fill()
if (self.r == 0) and (self.g == 0) and (self.b == 0):
ctx.font_size = 20
ctx.move_to(-30, 0).rgb(255, 255, 255).text("BLACK")
# if (self.r == 0) and (self.g == 0) and (self.b == 0):
# ctx.move_to(0, 0).rgb(255, 255, 255).text("BLACK")
ctx.font_size = 16
ctx.text_align = ctx.LEFT
ctx.move_to(39, 5 - 17)
ctx.rgb(1, 0, 0).text(str(self.r))
ctx.move_to(39, 5)
ctx.rgb(0, 1, 0).text(str(self.g))
ctx.move_to(39, 5 + 17)
ctx.rgb(0, 0, 1).text(str(self.b))
ctx.rgb(0.7, 0.7, 0.7)
text_shift = -20
ctx.text_align = ctx.RIGHT
ctx.move_to(text_shift - 5, -62).text("brush:")
ctx.move_to(text_shift - 5, -42).text("<-/->:")
ctx.text_align = ctx.LEFT
if self._draw:
self.LEDS[self._cursor] = [self.r, self.g, self.b]
ctx.font_size = 14
ctx.move_to(-80, -40).rgb(255, 255, 255).text("(Center L) Brush Down")
ctx.move_to(text_shift, -60).text("down")
ctx.move_to(text_shift, -44).text("draw")
for i in range(len(self.LEDS)):
leds.set_rgb(i, self.LEDS[i][0], self.LEDS[i][1], self.LEDS[i][2])
else:
ctx.font_size = 14
ctx.move_to(-80, -40).rgb(255, 255, 255).text("(Center L) Brush Up")
ctx.move_to(text_shift, -60).text("up")
ctx.move_to(text_shift, -44).text("move")
for i in range(len(self.LEDS)):
leds.set_rgb(i, self.LEDS[i][0], self.LEDS[i][1], self.LEDS[i][2])
leds.set_rgb(self._cursor, 255, 255, 255)
if (self.r == 255) and (self.g == 255) and (self.b == 255):
leds.set_rgb(self._cursor, 0, 0, 255)
leds.update()
off_x = 130
off_y = 110
ctx.text_align = ctx.CENTER
ctx.font_size = 20
leds.update()
off_x = 120
off_y = 120
ctx.move_to(self.PETAL_POS[0][0] - off_x, self.PETAL_POS[0][1] - off_y).rgb(
255, 255, 255
1, 0, 0
).text("R+")
ctx.move_to(self.PETAL_POS[1][0] - off_x, self.PETAL_POS[1][1] - off_y).rgb(
255, 255, 255
1, 0, 0
).text("R-")
ctx.move_to(self.PETAL_POS[2][0] - off_x, self.PETAL_POS[2][1] - off_y).rgb(
255, 255, 255
0, 1, 0
).text("G+")
ctx.move_to(self.PETAL_POS[3][0] - off_x, self.PETAL_POS[3][1] - off_y).rgb(
255, 255, 255
0, 1, 0
).text("G-")
ctx.move_to(self.PETAL_POS[4][0] - off_x, self.PETAL_POS[4][1] - off_y).rgb(
255, 255, 255
0, 0, 1
).text("B+")
ctx.move_to(self.PETAL_POS[5][0] - off_x, self.PETAL_POS[5][1] - off_y).rgb(
255, 255, 255
0, 0, 1
).text("B-")
ctx.move_to(self.PETAL_POS[6][0] - off_x, self.PETAL_POS[6][1] - off_y).rgb(
255, 255, 255
).text("B")
ctx.move_to(self.PETAL_POS[7][0] - off_x, self.PETAL_POS[7][1] - off_y).rgb(
255, 255, 255
).text("W")
ctx.font_size = 16
pos_x = self.PETAL_POS[6][0] - off_x
pos_y = self.PETAL_POS[6][1] - off_y
ctx.move_to(pos_x, pos_y).rgb(255, 255, 255)
ctx.rectangle(pos_x - 19, pos_y - 18, 38, 26).stroke()
ctx.text("BLK")
pos_x = self.PETAL_POS[7][0] - off_x
pos_y = self.PETAL_POS[7][1] - off_y
ctx.move_to(pos_x, pos_y).rgb(255, 255, 255)
ctx.rectangle(pos_x - 19, pos_y - 18, 38, 26).fill()
ctx.rgb(0, 0, 0)
ctx.text("WHT")
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
......@@ -193,9 +291,19 @@ class LEDPainter(Application):
self.g = 255
self.b = 255
def get_help(self):
ret = (
"Use the petals to set a color and the application button to move and "
"lift the brush to paint on the LEDs! On exit the LED data is stored "
"at /flash/menu_leds.json. This file sets the default LED color "
"pattern for the main menus and is not specific to LED Painter, other "
"applications may read and write to it."
)
return ret
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_view(LEDPainter(ApplicationContext()))
st3m.run.run_app(LEDPainter)
[app]
name = "LED Painter"
menu = "Apps"
category = "Apps"
[entry]
class = "LEDPainter"
......
from st3m.application import Application
import sys_display, math, random
class App(Application):
def __init__(self, app_ctx):
super().__init__(app_ctx)
def on_enter(self, vm):
super().on_enter(vm)
sys_display.set_mode(1)
pal = bytearray(256 * 3)
modr = random.getrandbits(7) + 1
modg = random.getrandbits(7) + 1
modb = random.getrandbits(7) + 1
pal[0 * 3] = 0
pal[0 * 3 + 1] = 0
pal[0 * 3 + 2] = 0
pal[1 * 3] = 255
pal[1 * 3 + 1] = 255
pal[1 * 3 + 2] = 255
sys_display.set_palette(pal)
self.y = 0
self.xa = -1.5
self.xb = 1.5
self.ya = -2.0
self.yb = 1.0
def draw(self, ctx: Context):
fb_info = sys_display.fb(0)
fb = fb_info[0]
max_iterations = 30
width = fb_info[1]
height = fb_info[2]
stride = fb_info[3]
for chunk in range(3): # do 3 scanlines at a time
y = self.y
if y < height:
zy = y * (self.yb - self.ya) / (height - 1) + self.ya
inners = 0
for x in range(width):
zx = x * (self.xb - self.xa) / (width - 1) + self.xa
z = zy + zx * 1j
c = z
reached = 0
for i in range(max_iterations):
if abs(z) > 2.0:
break
z = z * z + c
reached = i
val = reached * 255 / max_iterations
val = int(math.sqrt(val / 255) * 16) & 1
fb[int((y * stride * 8 + x) / 8)] |= int(val) << (x & 7)
self.y += 1
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(App)
from st3m.application import Application
import sys_display, math, random
class App(Application):
def __init__(self, app_ctx):
super().__init__(app_ctx)
def on_enter(self, vm):
super().on_enter(vm)
sys_display.set_mode(4)
self.y = 0
self.xa = -1.5
self.xb = 1.5
self.ya = -2.0
self.yb = 1.0
def draw(self, ctx: Context):
fb_info = sys_display.fb(0)
fb = fb_info[0]
max_iterations = 30
width = fb_info[1]
height = fb_info[2]
stride = fb_info[3]
for chunk in range(3): # do 3 scanlines at a time
y = self.y
if y < height:
zy = y * (self.yb - self.ya) / (height - 1) + self.ya
inners = 0
for x in range(width):
zx = x * (self.xb - self.xa) / (width - 1) + self.xa
z = zy + zx * 1j
c = z
reached = 0
for i in range(max_iterations):
if abs(z) > 2.0:
break
z = z * z + c
reached = i
val = reached * 255 / max_iterations
val = math.sqrt(val / 255) * 16
if val > 15:
val = 15
fb[int((y * stride * 2 + x) / 2)] |= int(val) << ((x & 1) * 4)
self.y += 1
def think(self, ins, delta_ms):
super().think(ins, delta_ms)
if self.input.buttons.app.right.pressed:
pal = bytearray(256 * 3)
modr = random.getrandbits(7) + 1
modg = random.getrandbits(7) + 1
modb = random.getrandbits(7) + 1
for i in range(256):
pal[i * 3] = int((i % modr) * (255 / modr))
pal[i * 3 + 1] = int((i % modg) * (255 / modg))
pal[i * 3 + 2] = int((i % modb) * (255 / modb))
sys_display.set_palette(pal)
if self.input.buttons.app.left.pressed:
pal = bytearray(256 * 3)
for i in range(256):
pal[i * 3] = i
pal[i * 3 + 1] = i
pal[i * 3 + 2] = i
sys_display.set_palette(pal)
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(App)