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
[app]
name = "violin"
category = "Music"
[metadata]
author = "Flow3r Badge Authors"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
from st3m.application import Application, ApplicationContext
from st3m.input import InputState
from st3m.goose import Optional
from st3m.ui.view import ViewManager
from st3m.utils import save_file_if_changed, sd_card_plugged
import st3m.settings
import st3m.wifi
from ctx import Context
import network
import leds
import os
import json
import math
from .k3yboard import TextInputModel, KeyboardView
from .helpers import (
set_direction_leds,
copy_across_devices,
mark_unknown_characters,
)
class WifiApp(Application):
WIFI_CONFIG_FILE = "/flash/w1f1_config.json"
WIFI_CONFIG_FILE_SD = "/sd/w1f1_config.json"
_scroll_pos: float = 0.0
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self._petal_pressed = {}
self._nearby_wlans = []
self._status_text = "scanning"
self._error_text = ""
self._wifi_config = {}
self._wlan_offset = 0
self._is_connecting = False
self._waiting_for_password = False
self._password_model = TextInputModel("")
self.attempt_load_wifi_config(self.WIFI_CONFIG_FILE)
# Copy config to flash from SD card if we don't have one on flash
if (
sd_card_plugged()
and os.path.exists(self.WIFI_CONFIG_FILE_SD)
and not os.path.exists(self.WIFI_CONFIG_FILE)
):
copy_across_devices(self.WIFI_CONFIG_FILE_SD, self.WIFI_CONFIG_FILE)
self.attempt_load_wifi_config(self.WIFI_CONFIG_FILE)
if not self._wifi_config:
self._wifi_config = {
"config_version": 2,
"networks": {
"Example SSID": {"psk": "Example PSK"},
"Camp2023-open": {"psk": None},
},
}
self.save_config_json()
def attempt_load_wifi_config(self, config_path):
if os.path.exists(config_path):
try:
with open(config_path) as f:
self._wifi_config = json.load(f)
except ValueError:
broken_filename = f"{config_path}.broken"
print(
"FYI: Your wifi config file has a syntax error "
f"and has been moved to {broken_filename}."
)
if os.path.exists(broken_filename):
os.remove(broken_filename)
os.rename(config_path, broken_filename)
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
self._connection_timer = 10
self._scan_timer = 0
if st3m.wifi.iface:
self._iface = st3m.wifi.iface
else:
self._iface = network.WLAN(network.STA_IF)
st3m.wifi.iface = self._iface
self._current_ssid = None
self._current_psk = None
# TODO: big error display
if self._waiting_for_password:
ssid = self._nearby_wlans[self._wlan_offset][0].decode()
psk = self._password_model.text
print(ssid, psk)
if psk:
self.connect_wifi(ssid, psk)
self._password_model = TextInputModel("")
self._waiting_for_password = False
def draw(self, ctx: Context) -> None:
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.rgb(0, 0, 0).rectangle(-120, -90, 240, 180).fill()
ctx.rgb(0.2, 0.2, 0.2).rectangle(-120, -120, 240, 30).fill()
ctx.rgb(0.2, 0.2, 0.2).rectangle(-120, 90, 240, 30).fill()
ctx.font_size = 15
current_ssid = self._iface.config("ssid")
ctx.save()
ctx.rgb(1, 1, 1)
ctx.font = "Arimo Bold"
if self._iface.active():
ctx.rgb(0, 1, 0)
else:
ctx.rgb(1, 0, 0)
ctx.move_to(0, -110)
ctx.text("^")
ctx.move_to(0, -100)
ctx.text("toggle wlan")
ctx.restore()
ctx.rgb(1, 1, 1)
ctx.move_to(0, 100)
ctx.text(self._status_text)
wlan_draw_offset = self._wlan_offset * -20
for wlan in self._nearby_wlans:
base_ssid = wlan[0].decode()
ssid = wlan[-1]
if (
base_ssid == current_ssid
and self._iface.active()
and self._iface.isconnected()
):
ctx.rgb(0, 1, 0)
elif base_ssid == self._is_connecting:
ctx.rgb(0, 0, 1)
elif base_ssid in self._wifi_config["networks"]:
ctx.rgb(1, 1, 0)
else:
ctx.rgb(1, 1, 1)
if math.fabs(wlan_draw_offset) > 90:
wlan_draw_offset += 20
continue
selected = self._nearby_wlans[self._wlan_offset] == wlan
open_network = wlan[4] == 0
ctx.font = "Arimo Bold" if selected else "Arimo Regular"
ctx.font_size = 25 if selected else 15
ssid_width = ctx.text_width(ssid)
xpos = 0
if selected:
max_width = 220 if open_network else 200
if ssid_width > max_width:
xpos = math.sin(self._scroll_pos) * (ssid_width - max_width) / 2
if not open_network:
xpos -= 7
ctx.move_to(xpos, wlan_draw_offset)
ctx.text(ssid)
# TODO: maybe add signal indicator?
# https://fonts.google.com/icons?selected=Material+Icons+Outlined:network_wifi_1_bar:&icon.query=network+wifi&icon.set=Material+Icons
# draw a key next to wifi if it isn't open
if not open_network:
ctx.save()
ctx.font = "Material Icons"
ctx.text_align = ctx.LEFT
ctx.move_to(xpos + (ssid_width / 2) + 2, wlan_draw_offset + 2)
ctx.text("\ue897")
ctx.restore()
wlan_draw_offset += 20
def on_exit(self) -> None:
leds.set_all_rgb(0, 0, 0)
leds.update()
def scan_wifi(self):
"""
scans for nearby wifi networks, hides private ones and sorts appropriately
helpful: https://docs.micropython.org/en/latest/library/network.WLAN.html#network.WLAN.scan
"""
# skip hidden WLANs
detected_wlans = self._iface.scan()
known_wlans = []
unknown_wlans = []
for wlan in detected_wlans:
# skip hidden or invisible WLANs
if wlan[5] or not wlan[0].strip():
continue
wlan_list = list(wlan)
base_ssid = wlan[0].decode()
clean_ssid = mark_unknown_characters(base_ssid).strip()
wlan_list.append(clean_ssid)
if base_ssid in self._wifi_config["networks"]:
known_wlans.append(wlan_list)
else:
unknown_wlans.append(wlan_list)
# sort by signal strength
known_wlans.sort(key=lambda wlan: wlan[3], reverse=True)
unknown_wlans.sort(key=lambda wlan: wlan[3], reverse=True)
self._nearby_wlans = known_wlans + unknown_wlans
print(self._nearby_wlans)
def update_settings(self, ssid: str, psk: str) -> None:
st3m.settings.str_wifi_ssid.set_value(ssid)
st3m.settings.str_wifi_psk.set_value(psk)
st3m.settings.save_all()
def add_wlan_to_config_json(self, ssid: str, psk: str) -> None:
self._wifi_config["networks"][ssid] = {"psk": psk}
self.save_config_json()
def save_config_json(self) -> None:
config_str = json.dumps(self._wifi_config)
save_file_if_changed(self.WIFI_CONFIG_FILE, config_str)
if sd_card_plugged():
try:
save_file_if_changed(self.WIFI_CONFIG_FILE_SD, config_str)
except OSError as e:
print("SD issue:", str(e), ":(")
def connect_wifi(self, ssid: str, psk: str = None) -> None:
if ssid in self._wifi_config["networks"]:
psk = self._wifi_config["networks"][ssid]["psk"]
self._current_ssid = ssid
self._current_psk = psk
try:
self._is_connecting = ssid
self._iface.connect(
ssid,
psk,
)
self._status_text = "connecting"
except OSError as e:
self._status_text = str(e)
self._is_connecting = False
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
self._scroll_pos += delta_ms / 1000
leds.set_all_rgb(0, 0, 0)
if self.input.buttons.app.left.pressed and self._wlan_offset > 0:
self._wlan_offset -= 1
self._scroll_pos = 0.0
elif (
self.input.buttons.app.right.pressed
and self._wlan_offset < len(self._nearby_wlans) - 1
):
self._wlan_offset += 1
self._scroll_pos = 0.0
if (
not self._nearby_wlans
and st3m.wifi.enabled()
and self._scan_timer <= 0
and not self.vm.transitioning
):
self._status_text = "scanning"
self.scan_wifi()
self._wlan_offset = 0
self._status_text = "connecting" if st3m.wifi.is_connecting() else "ready"
self._scan_timer = 1
if not self._nearby_wlans:
self._iface.disconnect()
if not self._nearby_wlans:
self._scan_timer -= delta_ms / 1000
if ins.captouch.petals[0].pressed:
if not self._petal_pressed.get(0, False):
if st3m.wifi.enabled():
st3m.wifi.disable()
st3m.wifi.iface = self._iface
else:
st3m.wifi.setup_wifi()
self._scan_timer = 1
if not self._iface.active():
self._nearby_wlans = []
else:
self._status_text = "scanning"
self._petal_pressed[0] = True
else:
self._petal_pressed[0] = False
if self._iface.active():
set_direction_leds(0, 0, 1, 0)
else:
set_direction_leds(0, 1, 0, 0)
self._status_text = "wlan off"
if (
self.input.buttons.app.middle.pressed
and self._iface.active()
and self._nearby_wlans
):
hovered_network = self._nearby_wlans[self._wlan_offset]
ssid = hovered_network[0].decode()
if self._iface.isconnected():
self._iface.disconnect()
# network[4] = security level, 0 = open
if ssid in self._wifi_config["networks"] or hovered_network[4] == 0:
self.connect_wifi(ssid)
else:
self._waiting_for_password = True
self.vm.push(KeyboardView(self._password_model))
if self._is_connecting:
self._connection_timer -= delta_ms / 1000
if self._iface.isconnected():
self._connection_timer = 10
self._is_connecting = False
if self._current_ssid:
self.update_settings(self._current_ssid, self._current_psk)
if self._current_ssid not in self._wifi_config["networks"]:
self.add_wlan_to_config_json(
self._current_ssid, self._current_psk
)
elif self._connection_timer <= 0:
self._iface.disconnect()
self._status_text = "conn timed out"
self._is_connecting = False
if self._iface.isconnected():
self._status_text = "connected"
leds.update()
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(WifiApp)
[app]
name = "WiFi"
category = "Hidden"
[entry]
class = "WifiApp"
[metadata]
author = "ave"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
description = "Lets you use multiple wireless networks."
version = 9
import leds
def copy_across_devices(src: str, dst: str):
"""
copy a file across devices (flash->SD etc).
does the whole file at once, should only be used on small files.
"""
with open(src, "rb") as srcf:
with open(dst, "wb") as dstf:
dstf.write(srcf.read())
def set_direction_leds(direction, r, g, b):
if direction == 0:
leds.set_rgb(39, r, g, b)
else:
leds.set_rgb((direction * 4) - 1, r, g, b)
leds.set_rgb(direction * 4, r, g, b)
leds.set_rgb((direction * 4) + 1, r, g, b)
def mark_unknown_characters(text: str) -> str:
glyph_index = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿŁπ“”•…€™←↑→↓−≈▼♠♣♥♦fiflffiffl"
result_text = ""
for char in text:
result_text += char if char in glyph_index else "?"
return result_text
# code from https://git.flow3r.garden/baldo/k3yboard
# LGPL-v3-only, by baldo, 2023
from ctx import Context
import st3m.run
from st3m import Responder, InputState
from st3m.application import Application, ApplicationContext
from st3m.goose import ABCBase, Enum, Optional
from st3m.ui.view import BaseView, ViewManager
from st3m.utils import tau
class Model(ABCBase):
"""
Common base-class for models holding state that can be used for rendering views.
"""
def __init__(self):
pass
class TextInputModel(Model):
"""
Model used for rendering the TextInputView. Holds the current input.
The input is split into the part left and the part right of the cursor. The input candidate is a character that
might be added to the input next and is displayed at the position of the cursor. This is used to give viusal
feedback while toggling through multiple characters inside a input group (associated with a petal).
"""
class CursorDirection(Enum):
LEFT = -1
RIGHT = 1
def __init__(
self, input_left: str = "", input_right: str = "", input_candidate: str = ""
) -> None:
super().__init__()
self._input_left = input_left
self._input_right = input_right
self.input_candidate = input_candidate
@property
def text(self) -> str:
"""
The complete input string in its current state.
"""
return self._input_left + self._input_right
@property
def input_left(self) -> str:
return self._input_left
@property
def input_right(self) -> str:
return self._input_right
def move_cursor(self, direction: "TextInputModel.CursorDirection") -> None:
"""
Moves the cursor one step in specified direction. Any pending input will be committed beforehand.
"""
self.commit_input()
if direction == self.CursorDirection.LEFT:
self._input_right = self._input_left[-1:] + self._input_right
self._input_left = self._input_left[:-1]
elif direction == self.CursorDirection.RIGHT:
self._input_left = self._input_left + self._input_right[0:1]
self._input_right = self._input_right[1:]
def add_input_character(self, char: str) -> None:
"""
Adds an input character at the current cursor position.
"""
self._input_left += char
self.input_candidate = ""
def delete_input_character(self) -> None:
"""
Deletes the character left to the cursor (if any).
If an input candidate is pending, it will be removed instead.
"""
if self.input_candidate == "":
self._input_left = self._input_left[:-1]
self.input_candidate = ""
def commit_input(self) -> None:
"""
Adds the pending input candidate (if any) to input left of the cursor.
"""
self.add_input_character(self.input_candidate)
class TextInputFieldView(Responder):
"""
Displays the current text input and cursor of a keyboard.
"""
def __init__(
self, model: TextInputModel, cursor_blink_duration_ms: float = 500
) -> None:
super().__init__()
self._model = model
self._cursor_blink_duration_ms = cursor_blink_duration_ms
self._cursor_timer_ms = 0
def draw_cursor(self, ctx: Context) -> None:
if self._cursor_blink_duration_ms < self._cursor_timer_ms:
return
ctx.begin_path()
ctx.rgb(0.0, 0.2, 1.0).rectangle(-1.0, -15.0, 2.0, 28.0).fill()
ctx.close_path()
def draw_text_input(self, ctx: Context) -> None:
ctx.begin_path()
cursor_offset = 1.5
ctx.text_baseline = ctx.MIDDLE
ctx.font_size = 24
left_input_width = ctx.text_width(self._model.input_left)
input_candidate_width = ctx.text_width(self._model.input_candidate)
right_input_width = ctx.text_width(self._model.input_right)
ctx.gray(0.2).rectangle(
-1.0 - cursor_offset - input_candidate_width - left_input_width,
ctx.font_size / 2.4,
left_input_width
+ input_candidate_width
+ right_input_width
+ 2 * cursor_offset
+ 2 * 1.0,
2,
).fill()
ctx.text_align = ctx.END
ctx.gray(1.0).move_to(-cursor_offset - input_candidate_width, 0).text(
self._model.input_left
)
ctx.text_align = ctx.END
ctx.rgb(0.0, 0.2, 1.0).move_to(-cursor_offset, 0).text(
self._model.input_candidate
)
ctx.text_align = ctx.START
ctx.gray(1.0).move_to(cursor_offset, 0).text(self._model.input_right)
ctx.close_path()
def draw(self, ctx: Context) -> None:
ctx.begin_path()
self.draw_text_input(ctx)
self.draw_cursor(ctx)
ctx.close_path()
def think(self, ins: InputState, delta_ms: int) -> None:
self._cursor_timer_ms = (self._cursor_timer_ms + delta_ms) % (
2 * self._cursor_blink_duration_ms
)
class InputControlsModel(Model):
"""
Model used for rendering the InputControlsView.
Holds the current active input and control groups (icons displayed in a ring around the edge of the screen).
Input groups are groups of characters to toggle through to select the character to input.
Control groups are groups of icons to control the behaviour of the keyboard.
"""
def __init__(
self,
input_groups: list[list[str]],
control_groups: list[list[str]],
active_input_group: int = 0,
) -> None:
super().__init__()
self._input_groups = input_groups
self._control_groups = control_groups
self._active_input_control_group = active_input_group
@property
def active_input_control_group(self):
return self._active_input_control_group
def select_input_control_group(self, group: int) -> None:
self._active_input_control_group = group
@property
def active_input_groups(self) -> list[str]:
return self._input_groups[self._active_input_control_group]
@property
def active_control_groups(self) -> list[str]:
return self._control_groups[self._active_input_control_group]
class InputControlsView(Responder):
"""
Shows a ring of controls and input characters to choose from around the edge.
"""
def __init__(self, model: InputControlsModel) -> None:
super().__init__()
self._model = model
def draw_input_group(self, ctx: Context, group: int) -> None:
inputs = self._model.active_input_groups[group]
ctx.begin_path()
bottom_input_group = 2 <= group <= 3
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.font_size = 18.0
ctx.gray(0.6)
angle_offset = tau / 5.0 / 10
angle = group * tau / 5.0 - angle_offset * (len(inputs) - 1) / 2.0
for input in reversed(inputs) if bottom_input_group else inputs:
ctx.save()
ctx.rotate(angle)
ctx.translate(0, -109)
if bottom_input_group:
ctx.rotate(tau / 2)
ctx.move_to(0, 0)
ctx.text(input)
ctx.restore()
angle += angle_offset
ctx.close_path()
def draw_control_group(self, ctx, group: int) -> None:
controls = self._model.active_control_groups[group]
ctx.begin_path()
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.font_size = 18.0
ctx.gray(0.6)
ctx.font = "Material Icons"
angle_offset = tau / 5.0 / 10
angle = (group + 0.5) * tau / 5.0 - angle_offset * (len(controls) - 1) / 2.0
for control in controls:
ctx.save()
ctx.rotate(angle)
ctx.translate(0, -109)
ctx.rotate(-angle)
ctx.move_to(0, 0)
ctx.text(control)
ctx.restore()
angle += angle_offset
ctx.close_path()
def draw(self, ctx: Context) -> None:
ctx.begin_path()
ctx.line_width = 4.0
ctx.gray(0).arc(0, 0, 98, 0, tau, 0).stroke()
ctx.line_width = 24.0
ctx.gray(0.1).arc(0, 0, 110, 0, tau, 0).stroke()
ctx.line_width = 2.0
ctx.gray(0.3).arc(0, 0, 99, 0, tau, 0).stroke()
for i in range(0, len(self._model.active_input_groups)):
self.draw_input_group(ctx, i)
for i in range(0, len(self._model.active_control_groups)):
self.draw_control_group(ctx, i)
ctx.close_path()
def think(self, ins: InputState, delta_ms: int) -> None:
pass
class KeyboardView(BaseView):
class ControlPetal(Enum):
SPECIAL_CHARACTERS = 1
BACKSPACE = 3
SPACE = 5
CAPSLOCK = 7
NUMLOCK = 9
class InputControlGroup(Enum):
LOWERCASE_LETTERS = 0
UPPERCASE_LETTERS = 1
NUMBERS = 2
SPECIAL_CHARACTERS = 3
def __init__(self, model: TextInputModel) -> None:
super().__init__()
self._last_input_group_press = -1
self._last_input_group_character = -1
self._time_since_last_input_group_press = 0
self._input_group_timeout = 1000
self._text_input_model = model
self._text_input_view = TextInputFieldView(self._text_input_model)
self._input_controls_model = InputControlsModel(
[
[
"fghij",
"klmno",
"uvwxyz",
"pqrst",
"abcde",
],
[
"FGHIJ",
"KLMNO",
"UVWXYZ",
"PQRST",
"ABCDE",
],
[
"34",
"56",
"90",
"78",
"12",
],
[
".,!?:;",
"'\"@#~^",
"%$&<>\\|",
"+-_*/=`",
"()[]{}",
],
],
[
[
"\ue9ef", # Special characters
"\ue14a", # Backspace
"\ue256", # Space
"\ue5ce", # Shift
"\ue400", # Num
],
[
"\ue9ef", # Special characters
"\ue14a", # Backspace
"\ue256", # Space
"\ue5cf", # Shift active
"\ue400", # Num
],
[
"\ue9ef", # Special characters
"\ue14a", # Backspace
"\ue256", # Space
"\ue264", # Text
"",
],
[
"",
"\ue14a", # Backspace
"\ue256", # Space
"\ue264", # Text
"\ue400", # Num
],
],
)
self._input_controls_view = InputControlsView(self._input_controls_model)
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
def on_exit(self) -> None:
self.reset_input_state()
def draw(self, ctx: Context) -> None:
ctx.begin_path()
ctx.gray(0).rectangle(-120, -120, 240, 240).fill()
self._text_input_view.draw(ctx)
self._input_controls_view.draw(ctx)
ctx.close_path()
def reset_input_state(self) -> None:
self._time_since_last_input_group_press = 0
self._last_input_group_press = -1
self._last_input_group_character = -1
self._text_input_model.input_candidate = ""
def add_input_character(self, char: str) -> None:
self._text_input_model.commit_input()
self._text_input_model.add_input_character(char)
self.reset_input_state()
def delete_input_character(self) -> None:
self._text_input_model.commit_input()
self._text_input_model.delete_input_character()
self.reset_input_state()
def commit_input(self) -> None:
self._text_input_model.commit_input()
self.reset_input_state()
def select_input_group(self, input_group) -> None:
self.commit_input()
self._input_controls_model.select_input_control_group(input_group)
self.reset_input_state()
def handle_shoulder_buttons(self) -> None:
if self.input.buttons.app.middle.pressed:
self.commit_input()
if self.vm:
self.vm.pop()
if self.input.buttons.app.left.pressed:
self._text_input_model.move_cursor(TextInputModel.CursorDirection.LEFT)
if self.input.buttons.app.right.pressed:
self._text_input_model.move_cursor(TextInputModel.CursorDirection.RIGHT)
def handle_control_inputs(self) -> None:
if self.input.captouch.petals[
self.ControlPetal.SPECIAL_CHARACTERS
].whole.pressed:
self.select_input_group(self.InputControlGroup.SPECIAL_CHARACTERS)
if self.input.captouch.petals[self.ControlPetal.BACKSPACE].whole.pressed:
self.delete_input_character()
if self.input.captouch.petals[self.ControlPetal.SPACE].whole.pressed:
self.add_input_character(" ")
if self.input.captouch.petals[self.ControlPetal.CAPSLOCK].whole.pressed:
self.select_input_group(
self.InputControlGroup.UPPERCASE_LETTERS
if self._input_controls_model.active_input_control_group
== self.InputControlGroup.LOWERCASE_LETTERS
else self.InputControlGroup.LOWERCASE_LETTERS
)
if self.input.captouch.petals[self.ControlPetal.NUMLOCK].whole.pressed:
self.select_input_group(self.InputControlGroup.NUMBERS)
def handle_input_groups(self, delta_ms: int) -> bool:
for i in range(0, 5):
if self.input.captouch.petals[i * 2].whole.pressed:
if (
self._last_input_group_press >= 0
and self._last_input_group_press != i
):
self.commit_input()
self._last_input_group_press = i
self._last_input_group_character = (
self._last_input_group_character + 1
) % len(self._input_controls_model.active_input_groups[i])
self._time_since_last_input_group_press = 0
self._text_input_model.input_candidate = (
self._input_controls_model.active_input_groups[i][
self._last_input_group_character
]
)
return
self._time_since_last_input_group_press += delta_ms
if (
self._last_input_group_press >= 0
and self._time_since_last_input_group_press > self._input_group_timeout
):
self.commit_input()
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
self._text_input_view.think(ins, delta_ms)
self._input_controls_view.think(ins, delta_ms)
self.handle_shoulder_buttons()
self.handle_control_inputs()
self.handle_input_groups(delta_ms)
class KeyboardDemoApp(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self._model = TextInputModel("Hello world!")
def draw(self, ctx: Context) -> None:
ctx.gray(0).rectangle(-120, -120, 240, 240).fill()
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.font_size = 24
ctx.gray(1).move_to(0, -50).text("Keyboard Demo")
ctx.font_size = 16
ctx.gray(1).move_to(0, -20).text("Press left button to edit")
ctx.font_size = 24
ctx.gray(1).move_to(0, 20).text("Current input:")
ctx.font_size = 16
ctx.gray(1).move_to(0, 50).text(self._model.text)
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms) # Let Application do its thing
if self.input.buttons.app.middle.pressed:
self.vm.push(KeyboardView(self._model))
if __name__ == "__main__":
st3m.run.run_app(KeyboardDemoApp)
from st3m.application import Application
import math, cmath
import bl00mbox
import captouch
import leds
from st3m.ui import widgets, colours
class Wobbler(Application):
def __init__(self, app_ctx):
super().__init__(app_ctx)
self.blm = None
self.any_playing = False
self.tilt_ref = None
self.tilt = None
self.pitch = -36
def on_enter(self, vm):
super().on_enter(vm)
if self.blm is not None:
try:
self.blm.foreground = True
except ReferenceError:
self.blm = None
if self.blm is None:
self.build_synth()
leds.set_slew_rate(min(160, leds.get_slew_rate()))
def on_exit(self):
super().on_exit()
if self.any_playing:
self.blm.background_mute_override = True
else:
self.blm.delete()
self.blm = None
def build_synth(self):
self.blm = bl00mbox.Channel("wobbler")
self.blm.gain_dB = 0
self.mixer = self.blm.new(bl00mbox.plugins.mixer, 2)
self.mixer.signals.gain.mult = 8
self.filter = self.blm.new(bl00mbox.plugins.filter)
self.filter.signals.gain.mult = 0.2
self.filter.signals.reso.value = 22000
self.env = self.blm.new(bl00mbox.plugins.env_adsr)
self.filter.signals.input << self.mixer.signals.output
self.env.signals.input << self.filter.signals.output
self.blm.signals.line_out << self.env.signals.output
self.oscs = [self.blm.new(bl00mbox.plugins.osc) for x in range(2)]
for x, osc in enumerate(self.oscs):
osc.signals.output >> self.mixer.signals.input[x]
osc.signals.waveform.switch.SAW = True
# background widget, doesn't do normal think/on_enter/on_exit
self.tilt_widget = widgets.Inclinometer(buffer_len=2)
self.tilt_widget.on_enter()
def synth_callback(ins, delta_ms):
self.tilt_widget.think(ins, delta_ms)
roll = self.tilt_widget.roll
if roll is not None:
tilt = complex(roll, self.tilt_widget.pitch)
if self.tilt_ref is None:
self.tilt_ref = tilt
else:
tilt = tilt - self.tilt_ref
tilt *= 1.6
abs_tilt = abs(tilt)
if abs_tilt > 1:
tilt /= abs_tilt
self.tilt = tilt
self.filter.signals.cutoff.tone = self.pitch + (2 - tilt.imag) * 20
self.oscs[0].signals.pitch.tone = self.pitch + tilt.real * 2
self.oscs[1].signals.pitch.tone = self.pitch - tilt.real * 2
self.blm.callback = synth_callback
def think(self, ins, delta_ms):
super().think(ins, delta_ms)
if self.input.buttons.app.middle.pressed:
roll = self.tilt_widget.roll
if roll is not None:
tilt = complex(roll, self.tilt_widget.pitch)
self.tilt_ref = tilt
any_playing = False
pos = 0
pos_div = 0
for x in range(0, 10, 2):
if pressed := ins.captouch.petals[x].pressed:
any_playing = True
pos += ins.captouch.petals[x].pos.real
pos_div += 1
"""
if x == 2:
for x, osc in enumerate(self.oscs):
if pressed:
osc.signals.waveform.switch.SAW = True
else:
osc.signals.waveform.switch.TRI = True
elif x == 8:
self.mixer.signals.gain.mult = 8 if pressed else 2
"""
if pos_div:
self.pitch = -44 + (pos / pos_div + 1) * 6
if self.any_playing != any_playing:
if any_playing:
self.env.signals.trigger.start()
else:
self.env.signals.trigger.stop()
self.any_playing = any_playing
def get_help(self):
ret = (
"Press any top petal to play the note. How far away from "
"the center you press controls pitch.\n\n"
"Tilt forward/backward to change filter cutoff and left/right "
"to detune the oscillators. Press app button down to zero the "
"tilt reference.\n\n"
"If you exit while playing the note it will continue playing "
"in the background while still responding to tilt."
)
return ret
def draw(self, ctx):
ctx.gray(0).rectangle(-120, -120, 240, 240).fill()
col = (1, 1, 1)
eye_pos = complex(0, 0)
hue = 0
tilt = self.tilt_widget.pitch
if self.tilt is not None:
eye_pos = self.tilt * 10
hue = (eye_pos.real + eye_pos.imag) % math.tau
eye_pos *= 5
ctx.radial_gradient(
eye_pos.real, eye_pos.imag, 0, eye_pos.real, eye_pos.imag, 100
)
for x in range(5):
rel = x / 4
nval = rel
if self.any_playing:
nval /= 3
col = colours.hsv_to_rgb(hue + math.tau * rel, 1 - rel / 2, 1 - nval)
ctx.add_stop(rel, col, 1)
for y in range(8):
leds.set_rgb(y * 5 + x, *col)
leds.update()
length = 100
openness = 60
ctx.line_width = 1
ctx.move_to(length, 0).quad_to(0, openness, -length, 0).stroke()
ctx.move_to(length, 0).quad_to(0, -openness, -length, 0).stroke()
for x in range(3):
lash_length = 0.3 if x == 1 else 0.2
t = (x + 1) / 4
nt = 1 - t
# nt^2 * p0 + 2 * nt * t * p1 + t^2 * P2
lash_start_x = (nt * nt - t * t) * length
lash_start_y = t * nt * openness * 2
# 2 * (nt - t) * p1 - 2 * nt * p0 + 2 * t * p2
lash_angle_x = 2 * length
lash_angle_y = 2 * (nt - t) * openness
ctx.move_to(lash_start_x, lash_start_y)
ctx.rel_line_to(
lash_angle_y * lash_length, lash_angle_x * lash_length
).stroke()
ctx.arc(eye_pos.real, eye_pos.imag, 10, 0, math.tau, 0).stroke()
ctx.arc(eye_pos.real, eye_pos.imag, 20, 0, math.tau, 0).stroke()
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(Wobbler)
[app]
name = "wobbler"
category = "Music"
[entry]
class = "Wobbler"
[metadata]
author = "Flow3r Badge Authors"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
import st3m.run, st3m.wifi, media, uos, ctx
from st3m.application import Application
from st3m.utils import sd_card_plugged
RADIOSTATIONS = [
"http://radio-paralax.de:8000/",
"http://stream6.jungletrain.net:8000/",
"http://air.doscast.com:8054/livehitradio",
"http://lyd.nrk.no/nrk_radio_jazz_mp3_l",
"http://lyd.nrk.no/nrk_radio_mp3_mp3_l",
# "http://lyd.nrk.no/nrk_radio_alltid_nyheter_mp3_l",
# "http://pippin.gimp.org/tmp/b71207f10d522d354a001768e21a78fe"
]
class App(Application):
def __init__(self, app_ctx):
super().__init__(app_ctx)
self._streams = RADIOSTATIONS.copy()
if sd_card_plugged():
for entry in uos.ilistdir("/sd/"):
if entry[1] == 0x8000:
if (
entry[0].endswith(".mp3")
or entry[0].endswith(".mod")
or entry[0].endswith(".mpg")
):
self._streams.append("/sd/" + entry[0])
if len(self._streams) > len(RADIOSTATIONS):
# skip radio stations, they are available by going back
self._stream_no = len(RADIOSTATIONS)
else:
self._stream_no = 0
self._streaming = False
self._playing = False
self._connecting = False
self._connected = False
def show_icons(self) -> bool:
return not media.is_visual() or not media.is_playing()
def load_stream(self):
media.stop()
self._playing = self._streaming = False
self._filename = self._streams[self._stream_no]
print("loading " + self._filename)
if self._filename.startswith("http"):
self._streaming = True
if not st3m.wifi.is_connected():
return
media.load(self._filename)
self._playing = True
def next_source(self):
self._stream_no += 1
if self._stream_no >= len(self._streams):
self._stream_no = len(self._streams) - 1
self.load_stream()
def previous_source(self):
self._stream_no -= 1
if self._stream_no < 0:
self._stream_no = 0
self.load_stream()
def think(self, ins, delta_ms):
super().think(ins, delta_ms)
self._connecting = st3m.wifi.is_connecting()
self._connected = st3m.wifi.is_connected()
if self._connected and not self._playing:
self.load_stream()
if media.get_position() >= media.get_duration() and media.get_duration() > 0:
self.next_source()
if self.input.buttons.app.right.pressed:
self.right_down_time = 0
if self.input.buttons.app.right.released:
if self.right_down_time < 300:
self.next_source()
if self.input.buttons.app.right.down:
self.right_down_time += delta_ms
if self.right_down_time > 600:
dur = media.get_duration()
pos = media.get_position()
if dur > 1.0:
if dur < 300:
media.seek((pos / dur) + 0.1)
else:
media.seek((pos / dur) + 0.05)
else:
media.seek(pos + 0.1)
self.right_down_time = 300
if self.input.buttons.app.left.pressed:
self.left_down_time = 0
if self.input.buttons.app.left.released:
if self.left_down_time < 300:
self.previous_source()
if self.input.buttons.app.left.down:
self.left_down_time += delta_ms
if self.left_down_time > 600:
dur = media.get_duration()
pos = media.get_position()
if dur > 1.0:
if dur < 300:
media.seek((pos / dur) - 0.1)
else:
media.seek((pos / dur) - 0.05)
else:
media.seek(pos - 0.1)
self.left_down_time = 300
if self.input.buttons.app.middle.pressed:
if self._streaming and not self._connected:
st3m.wifi.run_wifi_settings(self.vm)
elif media.is_playing():
media.pause()
else:
media.play()
media.think(delta_ms)
def draw(self, ctx):
if self._streaming and not self._connected:
ctx.gray(0)
ctx.rectangle(-120, -120, 240, 240).fill()
ctx.gray(1)
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.font_size = 18
if st3m.wifi.is_connecting():
ctx.move_to(0, 0)
ctx.text("Connecting...")
return
ctx.move_to(0, -10)
ctx.text("Press the app button to")
ctx.move_to(0, 10)
ctx.text("enter Wi-Fi settings")
return
media.draw(ctx)
def on_enter(self, vm):
super().on_enter(vm)
if not media.is_playing():
self.load_stream()
def on_exit(self):
if media.is_visual() or not media.is_playing():
media.stop()
def on_exit_done(self):
if not st3m.wifi.enabled() and self._streaming:
media.stop()
if __name__ == "__main__":
st3m.run.run_app(App)
[app]
name = "Wurzelitzer"
category = "Media"
wifi_preference = true
[metadata]
author = "Flow3r Badge Authors"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
../components/bl00mbox/micropython/bl00mbox
\ No newline at end of file