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 1480 additions and 123 deletions
.. py:module:: audio
.. py:module:: sys_audio
``audio`` module
================
``sys_audio`` module
====================
This module also includes members of the ``audio`` module for convenience.
.. warning::
These functions are not to be used in applications, but only by OS settings.
Many of these functions are available in three variants: headphone volume,
speaker volume, and volume. If :code:`headphones_are_connected()` returns 1
the "headphone" variant is chosen, else the "speaker" variant is chosen.
.. py:function:: headset_is_connected() -> bool
Returns 1 if headphones with microphone were connected to the headphone
jack at the last call of audio_update_jacksense.
.. py:function:: headphones_detection_override(enable : bool, override_state : bool)
.. py:function:: headphones_are_connected() -> bool
Set to 'enable = True' if the system should ignore jacksense and use
override_state to determine whether headphones are connected (True) or not
(False).
Returns 1 if headphones with or without microphone were connected to the
headphone jack at the last call of audio_update_jacksense.
Use cases:
.. py:function:: headphones_detection_override(enable : bool)
- If a sleeve contact mic doesn't pull the detection pin low enough the
codec's built in headphone detection might fail.
- If the headset only has a mic connected but we wish to use the internal
speaker anyway
If a sleeve contact mic doesn't pull the detection pin low enough the
codec's built in headphone detection might fail. Calling this function
with 'enable = 1' overrides the detection and assumes there's headphones
plugged in. Call with 'enable = 0' to revert to automatic detection.
Call with 'enable = 0' to revert to automatic detection.
.. py:function:: headphones_set_volume_dB(vol_dB : float) -> float
.. py:function:: speaker_set_volume_dB(vol_dB : float) -> float
......@@ -97,49 +101,42 @@ the "headphone" variant is chosen, else the "speaker" variant is chosen.
:code:`audio_{headphones_/speaker_/}set_{maximum/minimum}_volume_` and 0 if
in a fake mute condition.
.. py:function:: headphones_line_in_set_hardware_thru(enable : bool)
.. py:function:: speaker_line_in_set_hardware_thru(enable : bool)
.. py:function:: line_in_set_hardware_thru(enable : bool)
.. py:function:: headset_mic_set_gain_dB(gain_dB : float)
.. py:function:: headset_mic_get_gain_dB() -> float
.. py:function:: onboard_mic_set_gain_dB(gain_dB : float)
.. py:function:: onboard_mic_get_gain_dB() -> float
.. py:function:: line_in_set_gain_dB(gain_dB : float)
.. py:function:: line_in_get_gain_dB() -> float
These route whatever is on the line in port directly to the headphones or
speaker respectively (enable = 1), or don't (enable = 0). Is affected by mute
and coarse hardware volume settings, however software fine volume is not
applied.
Set and get gain for the respective input channels.
Good for testing, might deprecate later, idk~
.. py:function:: codec_i2c_write(reg : int, data : int)
.. py:function:: input_set_source(source : int)
.. py:function:: input_get_source() -> int
Write audio codec register. Obviously very unsafe. Do not use in applications that you
distribute to users. This can fry your speakers with DC
The codec can transmit audio data from different sources. This function
enables one or no source as provided by the ``INPUT_SOURCE_*`` constants.
.. py:function:: _codec_configure_dynamic_range_control(enable : bool, attack : int, release : int, make_up_gain_dB : int, comp_ratio : int, comp_threshold_dB : int, exp_ratio : int, exp_threshold_dB : int)
Note: The onboard digital mic turns on an LED on the top board if it receives
a clock signal which is considered a good proxy for its capability of reading
data.
Configures dynamic range control (compressor/expander) in the max98091 codec.
.. py:data:: INPUT_SOURCE_NONE
.. py:data:: INPUT_SOURCE_LINE_IN
.. py:data:: INPUT_SOURCE_HEADSET_MIC
.. py:data:: INPUT_SOURCE_ONBOARD_MIC
Temporary API for compressor experimentation, to be replaced later.
.. py:function:: headset_set_gain_dB(gain_dB : int)
.. py:function:: headset_get_gain_dB() -> int
See page 134 of the datasheet for values:
Hardware preamp gain, 0dB-50dB. TODO: figure out if int/float inconsistency
is a good thing here compared to all other _dB functions.
https://www.analog.com/media/en/technical-documentation/data-sheets/MAX98091.pdf
.. py:function:: input_thru_set_volume_dB(vol_dB : float)
.. py:function:: input_thru_get_volume_dB() -> float
.. py:function:: input_thru_set_mute(mute : bool)
.. py:function:: input_thru_get_mute() -> bool
``exp_threshold_dB`` is expressed in dB from 35 to 66, not in raw values
from 0 to 31.
You can route whatever source is selected with input_set_source() to
the audio output. Use these to control volume and mute.
Example:
.. py:function:: codec_i2c_write(reg : int, data : int)
.. code-block:: python
audio._codec_configure_dynamic_range_control(True, 2, 3, 0, 4, 10, 0, 0)
Write audio codec register. Obviously very unsafe. Have fun.
Will result in enabling the compressor with attack 2 (1.25ms), release 3
(1s), no make up gain, compression ratio 4 (INF:1), compression threshold
10dB, and expander disabled (ratio 1:1)
Headphone port policy
......
File moved
File moved
Firmware Development
====================
*Note: if you just want to make apps/instruments, see the Programming section.*
*Note: we assume you've read the Firmware section before, as that contains general information about the firmware structure.*
Development
===========
Source Code
-----------
......@@ -50,15 +46,25 @@ To compile, see `Working on C st3m code`_.
For running the simulator, you'll need Python 3 with pygame and wasmer:
A::
::
$ python3 -m venv venv
$ . venv/bin/activate
$ pip install pygame wasmer wasmer-compiler-cranelift
$ venv/bin/pip install pygame requests pymad
$ venv/bin/pip install wasmer wasmer-compiler-cranelift
If using python 3.11, for some reason the wasmer developers haven't published working wheels. We have `unofficial builds here <https://flow3r.garden/tmp/wasmer-py311/>`_.
.. warning::
For Python development, you're also encouraged to use mypy for typechecks. It should be available in your distribution repositories.
The wasmer python module from PyPI `doesn't work with Python versions 3.10 or 3.11
<https://github.com/wasmerio/wasmer-python/issues/539>`_. You will get
``ImportError: Wasmer is not available on this system`` when trying to run
the simulator.
Instead, install our `rebuilt wasmer wheels <https://flow3r.garden/tmp/wasmer-py311/>`_ using
::
venv/bin/pip install https://flow3r.garden/tmp/wasmer-py311/wasmer_compiler_cranelift-1.2.0-cp311-cp311-manylinux_2_34_x86_64.whl
venv/bin/pip install https://flow3r.garden/tmp/wasmer-py311/wasmer-1.2.0-cp311-cp311-manylinux_2_34_x86_64.whl
On macOS: the above might work.
......@@ -81,12 +87,6 @@ As with application development, you can first check your changes using the simu
$ python3 sim/run.py
You should also run typechecks:
::
$ MYPYPATH=python_payload/mypystubs mypy python_payload/main.py
Working on C st3m code
----------------------
......@@ -171,13 +171,35 @@ All printf() (and other stdio) calls will be piped to the default Micropython RE
If you're debugging the USB stack, or want to see Guru Meditation crashes, connect to UART0 over the USB-C connector's sideband pins (**TODO**: link to flow3rpot).
You can also disable the USB stack and make the badge stay in UART/JTAG mode: **TODO: issue 23**. Then, you can use openocd/gdb:
gdb Debugging
-------------
You can also disable the TinyUSB stack and make the badge stay in UART/JTAG mode:
``idf.py menuconfig`` -> Component config -> debug config -> usb gdb mode
Console output (including REPL) is not currently implemented in this mode.
Do a clean build with ``rm -r build; idf.py app-flash``
In one terminal:
::
$ OPENOCD_COMMANDS="-f board/esp32s3-builtin.cfg" idf.py opencod
$ OPENOCD_COMMANDS="-f board/esp32s3-builtin.cfg" idf.py openocd
In another terminal:
::
$ idf.py gdb
If experiencing issues with ctrl-c, try calling gdb directly (reusing the ``build/gdbinit/gdbinit`` created by the above command)
::
$ xtensa-esp32s3-elf-gdb -x build/gdbinit/gdbinit build/flow3r.elf
*TODO: document how to start gdb*
Porting Doom (or other alternate firmware)
------------------------------------------
......@@ -190,11 +212,13 @@ Then, you can run your firmware by distributing the resulting ``.bin`` file and
For an example, see our doom port at **TODO**.
Rewrite it in Rust
^^^^^^^^^^^^^^^^^^
Alternative Firmware Projects
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you fancy playing with Rust on the flow3r, check out the `flow3-rs <https://git.flow3r.garden/flow3r/flow3-rs>`_ project.
A port of Open-Smartwatch is also `in the works <https://github.com/Open-Smartwatch/open-smartwatch-os/pull/368>`_.
Hardware Generations
--------------------
......
Firmware
========
Structure
=========
Introduction
------------
......@@ -75,7 +75,7 @@ This is the SPI flash partition layout we use:
| ``vfs`` | 12MiB | FAT32 filesystem (with [#WL]_ layer). |
+--------------+--------+---------------------------------------+
.. [#WL] Wear leveling, to protect internal flash from death by repeat sector write.
Accessing files from a PC
-------------------------
......@@ -203,11 +203,7 @@ Instructions on how to run ``esptool.py`` are given with every firmware update r
Updating Firmware
-----------------
Download a `release <https://git.flow3r.garden/flow3r/flow3r-firmware/-/releases>`_, extract the tarball and follow instructions in the README. There will be notes on how to perform updates through either :ref:`Disk Mode`, `Recovery Mode`_ or through a low-level flash_.
Or, if you're at CCCamp2023, visit our firmware update station, once it is availble.
.. [#WL] Wear leveling, to protect internal flash from death by repeat sector write.
(Moved to :ref:`its own section <updating_firmware>`)
Versioning
----------
......
import os
DRY_RUN = False
DOMAIN = "https://docs.flow3r.garden"
BUILD_DIR = "_build"
FILE_TEMPLATE = '<meta http-equiv="refresh" content="0; url={url}" />\n<p><a href="{url}">Redirect</a></p>'
redirects = [
# (obsolete, target)
("badge/firmware.html", "os/guide/structure.html"),
("badge/fonts.html", "app/guide/fonts.html"),
("badge/usage.html", "badge/getting_started.html"),
("badge/programming.html", "app/guide/basics.html"),
("badge/badge_link.html", "app/guide/badge_link.html"),
("badge/badge_net.html", "app/guide/badge_net.html"),
("badge/bl00mbox.html", "app/guide/bl00mbox.html"),
("badge/firmware-development.html", "os/guide/development.html"),
("api/badgenet.html","app/api/badgenet.html"),
("api/sys_display.html", "os/api/sys_display.html"),
("api/ctx.html", "app/api/ctx.html"),
("api/badgelink.html", "app/api/badgelink.html"),
("api/audio.html", "app/api/audio.html"),
("api/colours.html", "app/api/colours.html"),
("api/leds.html", "app/api/leds.html"),
("api/led_patterns.html", "app/api/led_patterns.html"),
("api/uos.html", "app/api/uos.html"),
("api/sys_kernel.html", "os/api/sys_kernel.html"),
("api/sys_buttons.html", "app/api/input.html"),
("api/captouch.html", "app/api/captouch.html"),
]
for obsolete, target in redirects:
redirect_file_content = FILE_TEMPLATE.format(url = "/".join([DOMAIN, target]))
redirect_file_path = os.path.join(BUILD_DIR, obsolete)
if not DRY_RUN:
redirect_dir_path, _ = os.path.split(redirect_file_path)
os.makedirs(redirect_dir_path, exist_ok = True)
if not os.path.isfile(redirect_file_path):
print(f"writing to {redirect_file_path}")
if not DRY_RUN:
with open(redirect_file_path, "w") as f:
f.write(redirect_file_content)
else:
print(f"{redirect_file_content}\n")
else:
print(f"file {redirect_file_path} already exists")
sphinx==5.3.0
sphinx_rtd_theme==1.2.0
sphinx-multiversion @ git+https://github.com/dequis/sphinx-multiversion@fd3f0a0a1ef90781deac2de72c5d5c102c7f66da
--- a/git.py 2024-04-30 17:43:17.242346208 +0200
+++ b/git.py 2024-04-30 17:46:54.348580864 +0200
@@ -147,17 +147,17 @@
def copy_tree(gitroot, src, dst, reference, sourcepath="."):
- with tempfile.SpooledTemporaryFile() as fp:
- cmd = (
- "git",
- "archive",
- "--format",
- "tar",
- reference.commit,
- "--",
- sourcepath,
- )
- subprocess.check_call(cmd, cwd=gitroot, stdout=fp)
- fp.seek(0)
- with tarfile.TarFile(fileobj=fp) as tarfp:
- tarfp.extractall(dst)
+ os.makedirs(dst, exist_ok=True)
+ cmd = (
+ "git",
+ "clone",
+ sourcepath,
+ dst,
+ )
+ subprocess.run(cmd, cwd=gitroot, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ cmd = (
+ "git",
+ "checkout",
+ reference.commit,
+ )
+ subprocess.run(cmd, cwd=dst, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
set(SYS_C_PATH "${CMAKE_CURRENT_BINARY_DIR}/include/sys_data.c")
if (NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/include/")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include/")
endif()
idf_component_register(
SRCS
main.c
......
#include "bl00mbox.h"
#include "flow3r_bsp.h"
#include "st3m_audio.h"
#include "st3m_badgenet.h"
......@@ -57,21 +56,7 @@ static QueueHandle_t _core1_init_done_q;
void _init_core1(void *unused) {
st3m_usb_init();
st3m_console_init();
st3m_usb_app_conf_t app = {
.fn_rx = st3m_console_cdc_on_rx,
.fn_txpoll = st3m_console_cdc_on_txpoll,
.fn_detach = st3m_console_cdc_on_detach,
};
st3m_usb_mode_t usb_mode = {
.kind = st3m_usb_mode_kind_app,
.app = &app,
};
st3m_usb_mode_switch(&usb_mode);
puts(" ___ _ ___ _ _");
puts("| _| |___ _ _ _|_ |___| |_ ___ _| |___ ___");
puts("| _| | . | | | |_ | _| . | .'| . | . | -_|");
puts("|_| |_|___|_____|___|_| |___|__,|___|_ |___|");
puts(" |___|");
st3m_usb_startup();
// Load bearing delay. USB crashes otherwise?
// TODO(q3k): debug this
......@@ -91,13 +76,10 @@ void _init_core1(void *unused) {
nvs_flash_init();
}
// Let the main startup code know that core1 init is done. Then sleep
// forever. What a waste of stack space.
// Let the main startup code know that core1 init is done.
bool done = true;
xQueueSend(_core1_init_done_q, &done, portMAX_DELAY);
for (;;) {
vTaskDelay(10000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
// Called by micropython via MICROPY_BOARD_STARTUP.
......@@ -109,7 +91,7 @@ void flow3r_startup(void) {
// Submit splash a couple of times to make sure we've fully flushed out the
// initial framebuffer (both on-ESP and in-screen) noise before we turn on
// the backlight.
for (int i = 0; i < 4; i++) {
for (int i = 0; i < 5; i++) {
st3m_gfx_splash("");
}
// Display should've flushed by now. Turn on backlight.
......@@ -130,7 +112,6 @@ void flow3r_startup(void) {
st3m_scope_init();
st3m_audio_init();
bl00mbox_init();
st3m_badgenet_init();
st3m_mode_set(st3m_mode_kind_starting, "micropython");
......
with import ./pkgs.nix;
pkgs.dockerTools.buildImage {
name = "registry.k0.hswaw.net/q3k/flow3r-build";
name = "registry.gitlab.com/flow3r-badge/flow3r-build";
copyToRoot = pkgs.buildEnv {
name = "image-root";
paths = with pkgs; [
......@@ -20,11 +20,13 @@ pkgs.dockerTools.buildImage {
(python3.withPackages (ps: with ps; [
sphinx sphinx_rtd_theme
sphinx-multiversion
black
# simulator deps
pygame wasmer
wasmer-compiler-cranelift
requests
]))
# random build tools
......
......@@ -175,6 +175,8 @@ stdenv.mkDerivation rec {
patches = [
./rack-off-me-nix-mate.patch
../../../third_party/b03c8912c73fa59061d97a2f5fd5acddcc3fa356.patch
../../../third_party/b6aa59f1626ef6b438eb15edf2391195a519cbfe.patch
../../../third_party/69047951dbbbef4930414eecd167ff394e1a4cc0.patch
];
installPhase = ''
......
......@@ -9,6 +9,25 @@ let
in with nixpkgs; rec {
# nixpkgs passthrough
inherit (nixpkgs) pkgs lib;
sphinx-multiversion =
python3Packages.buildPythonPackage rec {
pname = "sphinx-multiversion";
version = "0.2.4";
src = fetchFromGitHub {
owner = "dequis";
repo = "sphinx-multiversion";
rev = "fd3f0a0a1ef90781deac2de72c5d5c102c7f66da";
sha256 = "sha256-jW0jvLlIK1LDxPvTkeAsQnZ6JpBVQWA6IjqPKaSl8lM=";
};
doCheck = false;
propagatedBuildInputs = [
python3Packages.sphinx
];
};
# All packages require to build/lint the project.
fwbuild = [
gcc-xtensa-esp32s3-elf-bin
......@@ -32,6 +51,8 @@ in with nixpkgs; rec {
python3Packages.pygame
python3Packages.wasmer
python3Packages.wasmer-compiler-cranelift
python3Packages.pymad
python3Packages.requests
emscripten
ncurses5
esp-gdb
......
from st3m.application import Application
import math, random, sys_display
from st3m import settings
import leds
import sys_display
from st3m.ui import colours, led_patterns
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 = 0
self.font_size = 20
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
self.num_widgets = 5
self.overhang = -30
self.line_height = 24
self.input.buttons.app.left.repeat_enable(800, 300)
self.input.buttons.app.right.repeat_enable(800, 300)
self.mid_x = 55
self.led_accumulator_ms = 0
self.blueish = 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 > self.num_widgets - 1:
self.focused_widget = self.num_widgets - 1
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(*colours.PUSH_RED, 1.0)
ctx.stroke()
ctx.gray(1)
ctx.move_to(self.mid_x, self.y)
ctx.save()
ctx.rgb(0.8, 0.8, 0.8)
ctx.text_align = ctx.RIGHT
ctx.text(label + ": ")
ctx.restore()
self.y += self.line_height
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(*colours.PUSH_RED, 1.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,
on_str="on",
off_str="off",
val_col=(1, 1, 1),
on_hint=None,
off_hint=None,
):
ctx = self.ctx
if ctx is None:
return
self.draw_widget(label)
if self.widget_no == self.focused_widget and self.active:
value = not value
self.active = False
ctx.save()
ctx.rgb(*val_col)
if value:
ctx.text(on_str)
else:
ctx.text(off_str)
ctx.restore()
if self.widget_no == self.focused_widget:
if value:
hint = on_hint
else:
hint = off_hint
if hint is not None:
ctx.save()
ctx.font_size -= 4
ctx.text_align = ctx.CENTER
ctx.rgb(0.9, 0.9, 0.9)
lines = hint.split("\n")
self.y -= 3
for line in lines:
ctx.move_to(0, self.y)
ctx.text(line)
self.y += self.line_height - 5
ctx.restore()
if self.y > 115:
self.focus_widget_pos_max = self.y
return value
def draw_number(self, label, step_size, no, unit=""):
ctx = self.ctx
self.draw_widget(label)
ret = no
if self.widget_no == self.focused_widget and self.active:
if self.left_pressed:
ret -= step_size
elif self.right_pressed:
ret += step_size
elif self.select_pressed:
self.active = False
self.select_pressed = False
if self.active and self.widget_no == self.focused_widget:
ctx.save()
ctx.rgba(*colours.PUSH_RED, 1.0)
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(str(no)[:4]) + 2,
self.font_size,
).stroke()
ctx.restore()
ctx.text(str(no)[:4] + unit)
else:
ctx.text(str(no)[:4] + unit)
return ret
def draw_bg(self):
ctx = self.ctx
ctx.gray(1.0)
ctx.move_to(-100, -80)
wig = self.focused_widget - 1
if wig < 2:
wig = 2
if wig > self.num_widgets - 3:
wig = self.num_widgets - 3
focus_pos = self.overhang + (wig - 0.5) * self.line_height
if focus_pos > 40:
self.overhang -= 7
if focus_pos < -40:
self.overhang += 7
self.y = self.overhang
self.widget_no = 0
ctx.rectangle(-120, -120, 240, 240)
ctx.gray(0)
ctx.fill()
ctx.save()
ctx.translate(self.x, -100)
ctx.font_size = 20
ctx.gray(0.8)
ctx.text("audio settings")
ctx.restore()
ctx.font_size = self.font_size
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):
self.ctx = ctx
self.draw_bg()
tmp = self.draw_number(
"display brightness",
5,
int(settings.num_display_brightness.value),
unit="%",
)
if tmp < 5:
tmp = 5
if tmp > 100:
tmp = 100
if tmp != settings.num_display_brightness.value:
settings.num_display_brightness.set_value(tmp)
sys_display.set_backlight(settings.num_display_brightness.value)
tmp = self.draw_number(
"LED brightness", 5, int(settings.num_leds_brightness.value)
)
if tmp < 5:
tmp = 5
elif tmp > 255:
tmp = 255
if tmp != settings.num_leds_brightness.value:
settings.num_leds_brightness.set_value(tmp)
leds.set_brightness(settings.num_leds_brightness.value)
tmp = self.draw_number("LED speed", 5, int(settings.num_leds_speed.value))
if tmp < 0:
tmp = 0
elif tmp > 255:
tmp = 255
if tmp != settings.num_leds_speed.value:
settings.num_leds_speed.set_value(tmp)
leds.set_slew_rate(settings.num_leds_speed.value)
tmp = self.draw_boolean(
"menu LEDs",
settings.onoff_leds_random_menu.value,
on_str="rng",
off_str="user",
off_hint="set pattern with LED Painter",
)
if tmp != settings.onoff_leds_random_menu.value:
settings.onoff_leds_random_menu.set_value(tmp)
led_patterns.set_menu_colors()
leds.update()
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 self.focused_widget == 3 and leds.get_steady():
self.led_accumulator_ms += delta_ms
if self.led_accumulator_ms > 1000:
self.led_accumulator_ms = 0
led_patterns.shift_all_hsv(h=0.8)
leds.update()
def on_enter(self, vm):
super().on_enter(vm)
settings.load_all()
def on_exit(self):
settings.save_all()
super().on_exit()
if __name__ == "__main__":
from st3m.run import run_app
run_app(App)
[app]
name = "Appearance"
category = "Hidden"
[metadata]
author = "Flow3r Badge Authors"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
from st3m.application import Application
import math, random, sys_display
from st3m import settings
import sys_audio
from st3m.ui import colours
class Drawable:
def __init__(self, press):
self.x = 23
self.x_vel = 40 / 1000.0
self.y = 0
self.font_size = 20
self.active = False
self.mid_x = 30
self.num_widgets = 2
self.overhang = -70
self.line_height = 24
self.ctx = None
self.press = press
self.focus_pos_limit_min = -60
self.focus_pos_limit_max = 60
self.focus_pos_limit_first = -60
self.focus_pos_limit_last = 80
self.first_widget_pos = 0
self.last_widget_pos = 0
self.focus_widget_pos_min = 0
self.focus_widget_pos_max = 0
self._focus_widget = 2
self._focus_widget_prev = 1
@property
def focus_widget(self):
return self._focus_widget
@property
def focus_widget_prev(self):
return self._focus_widget_prev
@focus_widget.setter
def focus_widget(self, val):
if val < 2:
val = 2
if val > self.num_widgets - 1:
val = self.num_widgets - 1
self._focus_widget_prev = self._focus_widget
self._focus_widget = val
@property
def at_first_widget(self):
return self.focus_widget <= 2
@property
def at_last_widget(self):
return self.focus_widget >= (self.num_widgets - 1)
def draw_heading(
self, label, col=(0.8, 0.8, 0.8), embiggen=6, top_margin=2, bot_margin=2
):
ctx = self.ctx
if ctx is None:
return
self.widget_no += 1
self.y += embiggen + top_margin
if self.widget_no == self.focus_widget:
if self.focus_widget > self.focus_widget_prev:
self.focus_widget += 1
else:
self.focus_widget -= 1
ctx.gray(1)
ctx.move_to(self.mid_x, self.y)
ctx.save()
ctx.rgb(*col)
ctx.move_to(0, self.y)
ctx.text_align = ctx.CENTER
ctx.font_size += embiggen
ctx.text(label)
ctx.restore()
self.y += self.line_height + embiggen + bot_margin
def draw_widget(self, label):
ctx = self.ctx
if ctx is None:
return
self.widget_no += 1
if not self.active:
if self.press.select_pressed and self.focus_widget > 0:
self.active = True
self.press.select_pressed = False
elif self.press.left_pressed:
self.focus_widget -= 1
self.press.left_pressed = False
elif self.press.right_pressed:
self.focus_widget += 1
self.press.right_pressed = False
if self.widget_no == self.focus_widget:
self.focus_widget_pos_min = self.y
if 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(*colours.GO_GREEN, 1.0)
ctx.stroke()
self.focus_widget_pos_max = self.y + self.line_height
ctx.gray(1)
ctx.move_to(self.mid_x, self.y)
ctx.save()
ctx.rgb(0.8, 0.8, 0.8)
ctx.text_align = ctx.RIGHT
ctx.text(label + ": ")
ctx.restore()
self.y += self.line_height
def draw_choice(self, label, choices, no):
ctx = self.ctx
if ctx is None:
return
self.draw_widget(label)
if self.widget_no == self.focus_widget and self.active:
if self.press.left_pressed:
no -= 1
if no < 0:
no = 0
elif self.press.right_pressed:
no += 1
if no >= len(choices):
no = len(choices) - 1
elif self.press.select_pressed:
self.active = False
self.press.select_pressed = False
for a in range(len(choices)):
if a == no and self.active and self.widget_no == self.focus_widget:
ctx.save()
ctx.rgba(*colours.GO_GREEN, 1.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_number(self, label, step_size, no, unit="", val_col=(1.0, 1.0, 1.0)):
ctx = self.ctx
if ctx is None:
return
self.draw_widget(label)
ret = no
if self.widget_no == self.focus_widget and self.active:
if self.press.left_pressed:
ret -= step_size
elif self.press.right_pressed:
ret += step_size
elif self.press.select_pressed:
self.active = False
self.press.select_pressed = False
if self.active and self.widget_no == self.focus_widget:
ctx.save()
ctx.rgba(*colours.GO_GREEN, 1.0)
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(str(no)[:4]) + 2,
self.font_size,
).stroke()
ctx.restore()
ctx.save()
ctx.rgb(*val_col)
ctx.text(str(no)[:4] + unit)
ctx.restore()
return ret
def draw_boolean(
self,
label,
value,
on_str="on",
off_str="off",
val_col=(1.0, 1.0, 1.0),
on_hint=None,
off_hint=None,
):
ctx = self.ctx
if ctx is None:
return
self.draw_widget(label)
if self.widget_no == self.focus_widget and self.active:
value = not value
self.active = False
ctx.save()
ctx.rgb(*val_col)
if value:
ctx.text(on_str)
else:
ctx.text(off_str)
ctx.restore()
if self.widget_no == self.focus_widget:
if value:
hint = on_hint
else:
hint = off_hint
if hint is not None:
ctx.save()
ctx.font_size -= 4
ctx.text_align = ctx.CENTER
ctx.rgb(0.9, 0.9, 0.9)
lines = hint.split("\n")
self.y -= 3
for line in lines:
ctx.move_to(0, self.y)
ctx.text(line)
self.y += self.line_height - 5
ctx.restore()
if self.y > 115:
self.focus_widget_pos_max = self.y
return value
def draw_bg(self):
ctx = self.ctx
if ctx is None:
return
ctx.gray(1.0)
ctx.move_to(-100, -80)
scroll_val = 0
scroll_speed = 7
if self.at_last_widget:
if self.focus_widget_pos_max > self.focus_pos_limit_last:
scroll_val = -self.focus_widget_pos_max + self.focus_pos_limit_last
elif self.at_first_widget:
if self.focus_widget_pos_min < self.focus_pos_limit_first:
scroll_val = 9999
elif self.focus_widget_pos_max > self.focus_pos_limit_max:
scroll_val = -9999
elif self.focus_widget_pos_min < self.focus_pos_limit_min:
scroll_val = 9999
if scroll_val > 0:
self.overhang += min(scroll_val, scroll_speed)
else:
self.overhang += max(scroll_val, -scroll_speed)
self.y = self.overhang
self.widget_no = 0
ctx.rectangle(-120, -120, 240, 240)
ctx.gray(0)
ctx.fill()
ctx.font_size = self.font_size
class Submenu(Drawable):
def __init__(self, press):
super().__init__(press)
self.submenu_active = False
def draw(self, ctx):
if self.submenu_active:
self._draw(ctx)
def _draw(self, ctx):
# override w specific implementation!
pass
class SpeakerMenu(Submenu):
def __init__(self, press):
super().__init__(press)
self.num_widgets = 6
self.overhang = -40
self.mid_x = 50
self.focus_pos_limit_min = -100
self.focus_pos_limit_max = 100
self.focus_pos_limit_first = -100
self.focus_pos_limit_last = 100
def _draw(self, ctx):
self.ctx = ctx
self.draw_bg()
self.draw_heading("speaker")
tmp = self.draw_number(
"startup volume",
1,
int(settings.num_speaker_startup_volume_db.value),
unit="dB",
)
if tmp < -60:
tmp = -60
if tmp > 14:
tmp = 14
settings.num_speaker_startup_volume_db.set_value(tmp)
tmp = self.draw_number(
"minimum volume", 1, int(settings.num_speaker_min_db.value), unit="dB"
)
if settings.num_speaker_min_db.value != tmp:
sys_audio.speaker_set_minimum_volume_dB(tmp)
settings.num_speaker_min_db.set_value(
sys_audio.speaker_get_minimum_volume_dB()
)
tmp = self.draw_number(
"maximum volume", 1, int(settings.num_speaker_max_db.value), unit="dB"
)
if settings.num_speaker_max_db.value != tmp:
sys_audio.speaker_set_maximum_volume_dB(tmp)
settings.num_speaker_max_db.set_value(
sys_audio.speaker_get_maximum_volume_dB()
)
tmp = self.draw_boolean(
"equalizer", settings.onoff_speaker_eq_on.value, "soft", "off"
)
if settings.onoff_speaker_eq_on.value != tmp:
sys_audio.speaker_set_eq_on(tmp)
settings.onoff_speaker_eq_on.set_value(tmp)
class HeadphonesMenu(Submenu):
def __init__(self, press):
super().__init__(press)
self.num_widgets = 6
self.overhang = -80
self.mid_x = 50
self.focus_pos_limit_min = -20
self.focus_pos_limit_max = 100
self.focus_pos_limit_first = -40
self.focus_pos_limit_last = 100
def _draw(self, ctx):
self.ctx = ctx
self.draw_bg()
self.draw_heading("headphones")
tmp = self.draw_number(
"startup volume",
1,
int(settings.num_headphones_startup_volume_db.value),
unit="dB",
)
if tmp < -70:
tmp = -70
if tmp > 3:
tmp = 3
settings.num_headphones_startup_volume_db.set_value(tmp)
tmp = self.draw_number(
"minimum volume", 1, int(settings.num_headphones_min_db.value), unit="dB"
)
if settings.num_headphones_min_db.value != tmp:
sys_audio.headphones_set_minimum_volume_dB(tmp)
settings.num_headphones_min_db.set_value(
sys_audio.headphones_get_minimum_volume_dB()
)
tmp = self.draw_number(
"maximum volume", 1, int(settings.num_headphones_max_db.value), unit="dB"
)
if settings.num_headphones_max_db.value != tmp:
sys_audio.headphones_set_maximum_volume_dB(tmp)
settings.num_headphones_max_db.set_value(
sys_audio.headphones_get_maximum_volume_dB()
)
self.y += 8
# note: jack detection is the inverse of headphones detection override
# jack detection off means headphones detection override on
tmp = self.draw_boolean(
"jack detection",
settings.onoff_headphones_detection_override.value,
off_str="on", # sic
on_str="off", # sic
)
if settings.onoff_headphones_detection_override.value != tmp:
settings.onoff_headphones_detection_override.set_value(tmp)
sys_audio.headphones_detection_override(
settings.onoff_headphones_detection_override.value,
settings.onoff_headphones_detection_override_state.value,
)
if tmp:
self.num_widgets = 7
tmp = self.draw_boolean(
"headphone state",
settings.onoff_headphones_detection_override_state.value,
on_str="on",
off_str="off",
on_hint="sound will always play\nthrough headphones",
off_hint="sound will always play\nthrough speaker",
)
if settings.onoff_headphones_detection_override_state.value != tmp:
settings.onoff_headphones_detection_override_state.set_value(tmp)
sys_audio.headphones_detection_override(
settings.onoff_headphones_detection_override.value,
settings.onoff_headphones_detection_override_state.value,
)
else:
self.num_widgets = 6
self.overhang = -80
class VolumeControlMenu(Submenu):
def __init__(self, press):
super().__init__(press)
self.num_widgets = 6
self.overhang = -40
self.mid_x = 25
self.focus_pos_limit_min = -100
self.focus_pos_limit_max = 100
self.focus_pos_limit_first = -100
self.focus_pos_limit_last = 100
def _draw(self, ctx):
self.ctx = ctx
self.draw_bg()
self.draw_heading("volume control")
tmp = self.draw_number(
"step", 0.5, float(settings.num_volume_step_db.value), unit="dB"
)
if tmp < 0.5:
tmp = 0.5
if tmp > 5:
tmp = 5
settings.num_volume_step_db.set_value(tmp)
tmp = self.draw_number(
"repeat step",
0.5,
float(settings.num_volume_repeat_step_db.value),
unit="dB",
)
if tmp < 0:
tmp = 0
if tmp > 5:
tmp = 5
settings.num_volume_repeat_step_db.set_value(tmp)
tmp = self.draw_number(
"repeat wait",
50,
int(settings.num_volume_repeat_wait_ms.value),
unit="ms",
)
if tmp < 100:
tmp = 100
if tmp > 1000:
tmp = 1000
settings.num_volume_repeat_wait_ms.set_value(tmp)
tmp = self.draw_number(
"repeat", 50, int(settings.num_volume_repeat_ms.value), unit="ms"
)
if tmp < 100:
tmp = 100
if tmp > 1000:
tmp = 1000
settings.num_volume_repeat_ms.set_value(tmp)
class InputMenu(Submenu):
def __init__(self, press):
super().__init__(press)
self.num_widgets = 11
self.overhang = -89
self.mid_x = 0
self.focus_pos_limit_max = 80
self.focus_pos_limit_last = 100
def _draw(self, ctx):
self.ctx = ctx
self.draw_bg()
avail_col = (0.0, 0.9, 0.6)
warn_col = (0.9, 0.0, 0.0)
allow_col = (0.0, 0.7, 0.5)
not_allow_col = (0.8, 0.3, 0.3)
not_avail_col = (0.6, 0.6, 0.6)
self.draw_heading("line in", embiggen=5, top_margin=5, bot_margin=-4)
if sys_audio.line_in_get_allowed():
if sys_audio.input_engines_get_source_avail(sys_audio.INPUT_SOURCE_LINE_IN):
col = avail_col
else:
col = allow_col
else:
col = not_allow_col
tmp = self.draw_boolean(
"line in",
settings.onoff_line_in_allowed.value,
on_str="allowed",
off_str="blocked",
val_col=col,
)
if settings.onoff_line_in_allowed.value != tmp:
sys_audio.line_in_set_allowed(tmp)
settings.onoff_line_in_allowed.set_value(tmp)
tmp = self.draw_number(
"gain",
1.5,
float(settings.num_line_in_gain_db.value),
unit="dB",
)
if settings.num_line_in_gain_db.value != tmp:
sys_audio.line_in_set_gain_dB(tmp)
settings.num_line_in_gain_db.set_value(sys_audio.line_in_get_gain_dB())
self.draw_heading("headset mic", embiggen=5, top_margin=5, bot_margin=-4)
if sys_audio.headset_mic_get_allowed():
if sys_audio.input_engines_get_source_avail(
sys_audio.INPUT_SOURCE_HEADSET_MIC
):
col = avail_col
else:
col = allow_col
else:
col = not_allow_col
tmp = self.draw_boolean(
"access",
settings.onoff_headset_mic_allowed.value,
on_str="allowed",
off_str="blocked",
val_col=col,
)
if settings.onoff_headset_mic_allowed.value != tmp:
sys_audio.headset_mic_set_allowed(tmp)
settings.onoff_headset_mic_allowed.set_value(tmp)
tmp = self.draw_number(
"gain",
1.5,
float(settings.num_headset_mic_gain_db.value),
unit="dB",
)
if settings.num_headset_mic_gain_db.value != tmp:
tmp = sys_audio.headset_mic_set_gain_dB(tmp)
settings.num_headset_mic_gain_db.set_value(tmp)
self.draw_heading("onboard mic", embiggen=5, top_margin=5, bot_margin=-4)
if sys_audio.onboard_mic_get_allowed():
col = avail_col
else:
col = not_allow_col
tmp = self.draw_boolean(
"access",
settings.onoff_onboard_mic_allowed.value,
on_str="allowed",
off_str="blocked",
val_col=col,
)
if settings.onoff_onboard_mic_allowed.value != tmp:
sys_audio.onboard_mic_set_allowed(tmp)
settings.onoff_onboard_mic_allowed.set_value(tmp)
tmp = self.draw_number(
"gain",
1.5,
float(settings.num_onboard_mic_gain_db.value),
unit="dB",
)
if settings.num_onboard_mic_gain_db.value != tmp:
tmp = sys_audio.onboard_mic_set_gain_dB(tmp)
settings.num_onboard_mic_gain_db.set_value(tmp)
if not sys_audio.onboard_mic_to_speaker_get_allowed():
col = not_allow_col
else:
col = warn_col
tmp = self.draw_boolean(
"thru",
settings.onoff_onboard_mic_to_speaker_allowed.value,
on_str="allow",
off_str="phones",
val_col=col,
on_hint=" /!\ feedback possible /!\ ",
)
if settings.onoff_onboard_mic_to_speaker_allowed.value != tmp:
sys_audio.onboard_mic_to_speaker_set_allowed(tmp)
settings.onoff_onboard_mic_to_speaker_allowed.set_value(tmp)
class Press:
def __init__(self):
self.right_pressed = False
self.left_pressed = False
self.select_pressed = False
class App(Application):
def __init__(self, app_ctx):
super().__init__(app_ctx)
self.font_size = 20
# only the main app handles inputs
self.input.buttons.app.left.repeat_enable(800, 300)
self.input.buttons.app.right.repeat_enable(800, 300)
self.press = Press()
# submenus
self.menus = []
self.menus += [VolumeControlMenu(self.press)]
self.menus += [InputMenu(self.press)]
self.menus += [SpeakerMenu(self.press)]
self.menus += [HeadphonesMenu(self.press)]
def draw_bg(self):
ctx = self.ctx
if ctx is None:
return
ctx.rectangle(-120, -120, 240, 240)
ctx.gray(0)
ctx.fill()
ctx.font_size = self.font_size
ctx.gray(1)
def draw(self, ctx):
self.ctx = ctx
main_menu_active = True
for menu in self.menus:
menu.draw(self.ctx)
if menu.submenu_active:
main_menu_active = False
if main_menu_active:
self.draw_bg()
ctx.save()
ctx.rgb(*colours.GO_GREEN)
ctx.rotate(math.tau / 10)
for i in range(5):
if i == 2:
ctx.rotate(math.tau / 5)
continue
ctx.round_rectangle(-40, -110, 80, 45, 6).stroke()
ctx.rotate(math.tau / 5)
ctx.restore()
ctx.text_align = ctx.CENTER
ctx.rotate(math.tau / 10)
ctx.move_to(0, -91)
ctx.text("volume")
ctx.move_to(0, -74)
ctx.text("control")
ctx.move_to(0, 0)
ctx.rotate(math.tau * (1 / 5 + 1 / 2))
ctx.move_to(0, 92)
ctx.text("inputs")
ctx.move_to(0, 0)
ctx.rotate(math.tau * 2 / 5)
ctx.move_to(0, 92)
ctx.text("speaker")
ctx.move_to(0, 0)
ctx.rotate(math.tau * (1 / 5 + 1 / 2))
ctx.move_to(0, -91)
ctx.text("head")
ctx.move_to(0, -74)
ctx.text("phones")
ctx.rotate(math.tau / 10)
ctx.move_to(0, 0)
ctx.text("audio config")
ctx.rgb(0.7, 0.7, 0.7)
ctx.font_size = 18
ctx.move_to(0, 20)
ctx.text("exit to save")
self.press.select_pressed = False
self.press.left_pressed = False
self.press.right_pressed = False
def think(self, ins, delta_ms):
super().think(ins, delta_ms)
for i in range(1, 10, 2):
if self.input.captouch.petals[i].whole.pressed:
for menu in self.menus:
menu.submenu_active = False
if i < 5:
self.menus[i // 2].submenu_active = True
elif i > 5:
self.menus[(i // 2) - 1].submenu_active = True
if (
self.input.buttons.app.right.pressed
or self.input.buttons.app.right.repeated
):
self.press.right_pressed = True
if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
self.press.left_pressed = True
if self.input.buttons.app.middle.pressed:
self.press.select_pressed = True
def on_enter(self, vm):
super().on_enter(vm)
settings.load_all()
def on_exit(self):
settings.save_all()
super().on_exit()
if __name__ == "__main__":
from st3m.run import run_app
run_app(App)
[app]
name = "Audio Config"
category = "Hidden"
[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 ctx import Context
import audio
import math
# Assume this is an enum
ForceModes = ["AUTO", "FORCE_LINE_IN", "FORCE_LINE_OUT", "FORCE_MIC"]
STATE_TEXT: dict[int, str] = {
audio.INPUT_SOURCE_AUTO: "auto",
audio.INPUT_SOURCE_HEADSET_MIC: "headset mic",
audio.INPUT_SOURCE_LINE_IN: "line in",
audio.INPUT_SOURCE_ONBOARD_MIC: "onboard mic",
}
class AudioPassthrough(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self._button_0_pressed = False
self._button_5_pressed = False
self._force_mode: str = "AUTO"
self._mute = True
self._source = None
self.target_source = audio.INPUT_SOURCE_AUTO
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
self._force_mode = "AUTO"
def draw(self, ctx: Context) -> None:
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.font = ctx.get_font_name(8)
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.rgb(1, 1, 1)
# top button
ctx.move_to(105, 0)
ctx.font_size = 15
ctx.save()
ctx.rotate((math.pi / 180) * 270)
ctx.text(">")
ctx.restore()
ctx.move_to(0, -90)
ctx.text("toggle passthrough")
# middle text
ctx.font_size = 25
ctx.move_to(0, 0)
ctx.save()
if self._mute:
# 0xff4500, red
ctx.rgb(1, 0.41, 0)
else:
# 0x3cb043, green
ctx.rgb(0.24, 0.69, 0.26)
ctx.text("passthrough off" if self._mute else "passthrough on")
ctx.restore()
# bottom text
ctx.move_to(0, 25)
ctx.save()
ctx.font_size = 15
ctx.text(STATE_TEXT.get(self.target_source, ""))
ctx.move_to(0, 40)
if self.source_connected:
# 0x3cb043, green
ctx.rgb(0.24, 0.69, 0.26)
else:
# 0xff4500, red
ctx.rgb(1, 0.41, 0)
if self._mute:
ctx.text("standby")
elif self._force_mode == "AUTO":
src = audio.input_thru_get_source()
if src != audio.INPUT_SOURCE_NONE:
ctx.text("connected to")
ctx.move_to(0, 56)
ctx.text(STATE_TEXT.get(src, ""))
else:
ctx.text("waiting...")
elif self._force_mode == "FORCE_MIC":
ctx.text("connected" if self.source_connected else "(headphones only)")
else:
ctx.text("connected" if self.source_connected else "waiting...")
ctx.restore()
# bottom button
ctx.move_to(105, 0)
ctx.font_size = 15
ctx.save()
ctx.rotate((math.pi / 180) * 90)
ctx.text(">")
ctx.restore()
ctx.move_to(0, 90)
ctx.text("next source")
@property
def source_connected(self):
if self.source != audio.INPUT_SOURCE_NONE:
return self.source == audio.input_thru_get_source()
else:
return False
@property
def source(self):
if self._source is None:
self._source = audio.input_thru_get_source()
return self._source
@source.setter
def source(self, source):
audio.input_thru_set_source(source)
self._source = audio.input_thru_get_source()
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
if ins.captouch.petals[0].pressed:
if not self._button_0_pressed:
self._button_0_pressed = True
self._mute = not self._mute
else:
self._button_0_pressed = False
if ins.captouch.petals[5].pressed:
if not self._button_5_pressed:
self._button_5_pressed = True
index = ForceModes.index(self._force_mode)
index = (index + 1) % 4
self._force_mode = ForceModes[index]
else:
self._button_5_pressed = False
if self._mute:
self.source = audio.INPUT_SOURCE_NONE
else:
if self._force_mode == "FORCE_MIC":
self.target_source = audio.INPUT_SOURCE_ONBOARD_MIC
elif self._force_mode == "AUTO":
self.target_source = audio.INPUT_SOURCE_AUTO
elif self._force_mode == "FORCE_LINE_IN":
self.target_source = audio.INPUT_SOURCE_LINE_IN
elif self._force_mode == "FORCE_LINE_OUT":
self.target_source = audio.INPUT_SOURCE_HEADSET_MIC
self.source = self.target_source
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(AudioPassthrough)
[app]
name = "Audio Passthrough"
category = "Media"
[entry]
class = "AudioPassthrough"
[metadata]
author = "ave"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
description = "Allows toggling audio passthrough through line-in/mic to speaker or lineout."
version = 7
import st3m.run, random
from st3m.application import Application, ApplicationContext
from st3m.input import InputState
from ctx import Context
import random
from st3m.application import Application
import sys_display
class Cloud:
def __init__(self, x: float, y: float, z: float) -> None:
def __init__(self, path, x, y, z):
self.path = path
self.x = x
self.y = y
self.z = z
def draw(self, ctx: Context) -> None:
def draw(self, ctx) -> None:
x = self.x / self.z * 120
y = self.y / self.z * 120
width = 200.0 / self.z * 120
height = 100.0 / self.z * 120
width = 200.0 / self.z * 160
height = 100.0 / self.z * 160
ctx.image(
"/flash/sys/apps/clouds/cloud.png",
self.path,
x - width / 2,
y - height / 2,
width,
......@@ -25,28 +24,45 @@ class Cloud:
)
class Clouds(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
class App(Application):
def __init__(self, app_ctx):
super().__init__(app_ctx)
self.clouds = []
for i in range(10):
self.clouds.append(
Cloud(
app_ctx.bundle_path + "/cloud.png",
((random.getrandbits(16) - 32767) / 32767.0) * 200,
((random.getrandbits(16)) / 65535.0) * 50 - 5,
((random.getrandbits(16)) / 65535.0) * 200 + 5,
((random.getrandbits(16)) / 65535.0) * 60 - 10,
((random.getrandbits(16)) / 65535.0) * 200 + 5 + i / 10.0,
)
)
def think(self, ins: InputState, delta_ms: int) -> None:
def think(self, ins, delta_ms):
super().think(ins, delta_ms)
no = 0
for c in self.clouds:
c.z -= 40 * delta_ms / 1000.0
c.x -= (delta_ms / 1000.0) * ins.imu.acc[1] * 10
c.z -= (delta_ms / 1000.0) * (ins.imu.acc[2] - 5) * 20
# wrap x and z coordinates around
if c.z < 10:
c.z = 300
c.z = 300.0 + no / 10.0
elif c.z > 300:
c.z = 10
c.z = 10.0 + no / 10.0
if c.x < -200:
c.x = 200
c.x = 200.0
elif c.x > 200:
c.x = -200
c.x = -200.0
no = no + 1
self.clouds = sorted(self.clouds, key=lambda c: -c.z)
def draw(self, ctx: Context) -> None:
def draw(self, ctx):
# faster, and with smoothing is incorrect
ctx.image_smoothing = False
ctx.rectangle(-120, -120, 240, 120)
ctx.rgb(0, 0.34, 0.72)
......@@ -58,7 +74,12 @@ class Clouds(Application):
for c in self.clouds:
c.draw(ctx)
def on_enter(self, vm):
super().on_enter(vm)
sys_display.set_mode(24 + sys_display.osd)
# For running with `mpremote run`:
if __name__ == "__main__":
st3m.run.run_view(Clouds(ApplicationContext()))
from st3m.run import run_app
run_app(App, "/flash/sys/apps/clouds")