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

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
Show changes
Commits on Source (18)
Showing with 404 additions and 32 deletions
......@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [1.2.0] - 2023-08-18
### Added
- Added a WiFi status indicator icon.
- Added a battery status indicator icon.
- Added support for loading apps from `/sd/apps` and `/flash/apps`.
- Added an error screen when apps fail to start.
- Added Python/st3m API to access battery charge status.
- Added the ability to always hide icons (*System**Settings**Show Icons*).
### Fixed
- File descriptor leak on app load. This would lead to the OS crashing when
too many apps are installed.
## [1.1.1] - 2023-08-17
### Fixed
- Crash on WiFi startup
## [1.1.0] - 2023-08-17
### Added
......@@ -41,7 +61,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Initial Release
[unreleased]: https://git.flow3r.garden/flow3r/flow3r-firmware/-/compare/v1.1.0...main
[unreleased]: https://git.flow3r.garden/flow3r/flow3r-firmware/-/compare/v1.2.0...main
[1.2.0]: https://git.flow3r.garden/flow3r/flow3r-firmware/-/compare/v1.1.1...v1.2.0
[1.1.1]: https://git.flow3r.garden/flow3r/flow3r-firmware/-/compare/v1.1.0...v1.1.1
[1.1.0]: https://git.flow3r.garden/flow3r/flow3r-firmware/-/compare/v1.0.0...v1.1.0
[1.0.0]: https://git.flow3r.garden/flow3r/flow3r-firmware/-/tags/v1.0.0
......@@ -8,6 +8,7 @@
#include "py/obj.h"
#include "py/runtime.h"
#include "st3m_console.h"
#include "st3m_io.h"
#include "st3m_usb.h"
#include "st3m_version.h"
......@@ -372,6 +373,13 @@ STATIC mp_obj_t mp_i2c_scan(void) {
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_i2c_scan_obj, mp_i2c_scan);
STATIC mp_obj_t mp_battery_charging(void) {
bool res = st3m_io_charger_state_get();
return mp_obj_new_bool(!res);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_battery_charging_obj, mp_battery_charging);
STATIC const mp_rom_map_elem_t globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_scheduler_snapshot),
MP_ROM_PTR(&mp_scheduler_snapshot_obj) },
......@@ -385,6 +393,8 @@ STATIC const mp_rom_map_elem_t globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_usb_console_active),
MP_ROM_PTR(&mp_usb_console_active_obj) },
{ MP_ROM_QSTR(MP_QSTR_i2c_scan), MP_ROM_PTR(&mp_i2c_scan_obj) },
{ MP_ROM_QSTR(MP_QSTR_battery_charging),
MP_ROM_PTR(&mp_battery_charging_obj) },
{ MP_ROM_QSTR(MP_QSTR_RUNNING), MP_ROM_INT(eRunning) },
{ MP_ROM_QSTR(MP_QSTR_READY), MP_ROM_INT(eReady) },
......
......@@ -58,3 +58,7 @@ uint8_t st3m_io_badge_link_disable(uint8_t pin_mask);
* a while. Warn user.
*/
uint8_t st3m_io_badge_link_enable(uint8_t pin_mask);
/* Returns true if the battery is currently being charged.
*/
bool st3m_io_charger_state_get();
``captouch`` module
===================
.. note::
Because of a driver quirk, the top pad of the petal 5 is currently not working,
resulting in that petal only reporting half of its supposed range.
.. automodule:: captouch
:members:
:undoc-members:
......
......@@ -571,6 +571,11 @@ Here is our previous code changed to use `Application` for the base of its main
# Continue to make runnable via mpremote run.
st3m.run.run_view(MyDemo(ApplicationContext()))
Using `Application` also gives you access to the `ApplicationContext`, which for example
gives you a way to find out the base path of your app, in `app_ctx.bundle_path`. For a
sample app using this, see `schneider's nyan-cat fork
<https://git.flow3r.garden/chubbson/nyan-cat/-/tree/schneider/bundle-path>`_.
To add the application to the menu we are missing one more thing: a `flow3r.toml`
file which describes the application so flow3r knows where to put it in the menu system.
Together with the Python code this file forms a so called bundle
......@@ -592,9 +597,22 @@ Together with the Python code this file forms a so called bundle
Save this as `flow3r.toml` together with the Python code as `__init__.py` in a
folder (name doesn't matter) and put that folder into the ``/flash/sys/apps``
folder on your flow3r (visible as ``sys/apps`` in `Disk Mode`_). Restart the
flow3r and it should pick up your new application.
folder (name doesn't matter) and put that folder into one of the possible
application directories (see below) using `Disk Mode`_. Restart the flow3r and
it should pick up your new application.
+--------+----------------------+---------------------+---------------------------------------+
| Medium | Path in Disk Mode | Path on Badge | Notes |
+========+======================+=====================+=======================================+
| Flash | ``sys/apps`` | ``/flash/sys/apps`` | “Default” apps. |
+--------+----------------------+---------------------+---------------------------------------+
| Flash | ``apps`` | ``/flash/apps`` | Doesn't exist by default. Split |
| | | | from ``sys`` to allow for cleaner |
| | | | updates. |
+--------+----------------------+---------------------+---------------------------------------+
| SD | ``apps`` | ``/sd/apps`` | Doesn't exist by default. Will be |
| | | | retained even across badge reflashes. |
+--------+----------------------+---------------------+---------------------------------------+
Distributing applications
-------------------------
......
......@@ -89,12 +89,12 @@ class CaptouchState(Protocol):
"""
State of individual petals.
Contains 10 elements, with the zeroth element being the pad closest to
the USB port. Then, every other pad in a clockwise direction.
Contains 10 elements, with the zeroth element being the petal closest to
the USB port. Then, every other petal in a clockwise direction.
Pads 0, 2, 4, 6, 8 are Top pads.
Petals 0, 2, 4, 6, 8 are Top petals.
Pads 1, 3, 5, 7, 9 are Bottom pads.
Petals 1, 3, 5, 7, 9 are Bottom petals.
"""
...
......
......@@ -3,11 +3,8 @@ from st3m.goose import Optional
STA_IF: int
class WLAN:
def __init__(self, mode: int) -> None:
pass
def active(self, active: bool) -> bool:
return False
def connect(self, ssid: bytes, key: Optional[bytes] = None) -> None:
pass
def isconnected(self) -> bool:
return False
def __init__(self, mode: int) -> None: ...
def active(self, active: bool) -> bool: ...
def connect(self, ssid: bytes, key: Optional[bytes] = None) -> None: ...
def isconnected(self) -> bool: ...
def status(self, s: str) -> float: ...
......@@ -71,3 +71,4 @@ def usb_console_active() -> bool: ...
def hardware_version() -> str: ...
def firmware_version() -> str: ...
def i2c_scan() -> list[int]: ...
def battery_charging() -> bool: ...
......@@ -6,14 +6,16 @@ from st3m.ui.view import (
)
from st3m.ui.menu import MenuItem
from st3m.input import InputState
from st3m.goose import Optional, List, Enum
from st3m.goose import Optional, List, Enum, Dict
from st3m.logging import Log
from ctx import Context
import toml
import os
import os.path
import stat
import sys
import random
log = Log(__name__)
......@@ -52,6 +54,7 @@ class BundleLoadException(BaseException):
res = self.MSG
if msg is not None:
res += ": " + msg
self.msg = res
super().__init__(res)
......@@ -97,7 +100,7 @@ class BundleMetadata:
This data is used to discover bundles and load them as applications.
"""
__slots__ = ["path", "name", "menu", "_t"]
__slots__ = ["path", "name", "menu", "_t", "version"]
def __init__(self, path: str) -> None:
self.path = path.rstrip("/")
......@@ -109,9 +112,12 @@ class BundleMetadata:
try:
t = toml.load(f)
except toml.TomlDecodeError as e:
f.close()
raise BundleMetadataCorrupt(str(e))
except Exception as e:
f.close()
raise BundleMetadataCorrupt(str(e))
f.close()
if "app" not in t or type(t["app"]) != dict:
raise BundleMetadataBroken("missing app section")
......@@ -126,6 +132,11 @@ class BundleMetadata:
if self.menu not in ["Apps", "Music", "Badge", "Hidden"]:
raise BundleMetadataBroken("app.menu must be either Apps, Music or Badge")
version = 0
if t.get("metadata") is not None:
version = t["metadata"].get("version", 0)
self.version = version
self._t = t
@staticmethod
......@@ -193,8 +204,74 @@ class BundleMetadata:
return []
return [MenuItemAppLaunch(self)]
@property
def source(self) -> str:
return os.path.dirname(self.path)
@property
def id(self) -> str:
return os.path.basename(self.path)
def __repr__(self) -> str:
return f"<BundleMetadata: {self.name} at {self.path}>"
return f"<BundleMetadata: {self.id} at {self.path}>"
class LoadErrorView(BaseView):
def __init__(self, e: BundleLoadException) -> None:
super().__init__()
self.e = e
self.header = "oh no"
def on_enter(self, vm: Optional[ViewManager]) -> None:
self.header = random.choice(
[
"oh no",
"aw shucks",
"whoopsie",
"ruh-roh",
"aw crud",
]
)
def think(self, ins: InputState, delta_ms: int) -> None:
pass
def draw(self, ctx: Context) -> None:
ctx.rgb(0.8, 0.1, 0.1)
ctx.rectangle(-120, -120, 240, 240)
ctx.fill()
ctx.gray(1)
ctx.font_size = 20
ctx.font = "Camp Font 1"
ctx.text_align = ctx.MIDDLE
ctx.move_to(0, -70)
ctx.text(self.header)
lines: List[List[str]] = []
msg = self.e.msg
for word in msg.split():
if len(lines) == 0:
lines.append([word])
continue
lastline = lines[-1][:]
lastline.append(word)
if sum(len(l) for l in lastline) + len(lastline) - 1 > 30:
lines.append([word])
else:
lines[-1].append(word)
ctx.gray(0)
ctx.rectangle(-120, -60, 240, 240).fill()
y = -40
ctx.gray(1)
ctx.font_size = 15
ctx.font = "Arimo Regular"
ctx.text_align = ctx.LEFT
for line in lines:
ctx.move_to(-90, y)
ctx.text(" ".join(line))
y += 15
class MenuItemAppLaunch(MenuItem):
......@@ -218,6 +295,8 @@ class MenuItemAppLaunch(MenuItem):
self._instance = self._bundle.load()
except BundleLoadException as e:
log.error(f"Could not load {self.label()}: {e}")
err = LoadErrorView(e)
vm.push(err)
return
assert self._instance is not None
vm.push(self._instance, ViewTransitionSwipeLeft())
......@@ -226,6 +305,80 @@ class MenuItemAppLaunch(MenuItem):
return self._bundle.name
class BundleManager:
"""
The BundleManager maintains information about BundleMetadata at different
locations in the badge filesystem.
It also manages updating/reloading bundles.
"""
def __init__(self) -> None:
self.bundles: Dict[str, BundleMetadata] = {}
@staticmethod
def _source_trumps(a: str, b: str) -> bool:
prios = {
"/flash/sys/apps": 200,
"/sd/apps": 120,
"/flash/apps": 100,
}
prio_a = prios.get(a, 0)
prio_b = prios.get(b, 0)
return prio_a > prio_b
def _discover_at(self, path: str) -> None:
path = path.rstrip("/")
try:
l = os.listdir(path)
except Exception as e:
log.warning(f"Could not discover bundles in {path}: {e}")
l = []
for d in l:
dirpath = path + "/" + d
st = os.stat(dirpath)
if not stat.S_ISDIR(st[0]):
continue
tomlpath = dirpath + "/flow3r.toml"
try:
st = os.stat(tomlpath)
if not stat.S_ISREG(st[0]):
continue
except Exception:
continue
try:
b = BundleMetadata(dirpath)
except BundleLoadException as e:
log.error(f"Failed to bundle from {dirpath}: {e}")
continue
id_ = b.id
if id_ not in self.bundles:
self.bundles[id_] = b
continue
ex = self.bundles[id_]
# Do we have a newer version?
if b.version > ex.version:
self.bundles[id_] = b
continue
# Do we have a higher priority source?
if self._source_trumps(b.source, ex.source):
self.bundles[id_] = b
continue
log.warning(
f"Ignoring {id_} at {b.source} as it already exists at {ex.source}"
)
def update(self) -> None:
self._discover_at("/flash/sys/apps")
self._discover_at("/flash/apps")
self._discover_at("/sd/apps")
def discover_bundles(path: str) -> List[BundleMetadata]:
"""
Discover valid bundles (directories containing flow3r.toml) inside a given
......
import machine
import time
import sys_kernel
class Power:
......@@ -29,3 +30,42 @@ class Power:
def battery_voltage(self) -> float:
self._update()
return self._battery_voltage
@property
def battery_charging(self) -> bool:
"""
True if the battery is currently being charged.
"""
return sys_kernel.battery_charging()
def approximate_battery_percentage(voltage: float) -> float:
"""
Returns approximate battery percentage ([0,100]) based on battery voltage
(in volts).
"""
if voltage > 4.20:
return 100
piecewise = [
(100, 4.20),
(90, 4.06),
(80, 3.98),
(70, 3.92),
(60, 3.87),
(50, 3.82),
(40, 3.79),
(30, 3.77),
(20, 3.74),
(10, 3.68),
(5, 3.45),
(0, 3.00),
]
for (p1, v1), (p2, v2) in zip(piecewise, piecewise[1:]):
if voltage > v1 or voltage < v2:
continue
vr = v1 - v2
pr = p1 - p2
vd = voltage - v2
p = vd / vr
return pr * p + p2
return 0
......@@ -11,7 +11,7 @@ from st3m.ui.menu import (
from st3m.ui.elements import overlays
from st3m.ui.view import View, ViewManager, ViewTransitionBlend
from st3m.ui.elements.menus import SimpleMenu, SunMenu
from st3m.application import discover_bundles, BundleMetadata, MenuItemAppLaunch
from st3m.application import BundleManager, BundleMetadata, MenuItemAppLaunch
from st3m.about import About
from st3m import settings, logging, processors, wifi
......@@ -52,9 +52,11 @@ def run_responder(r: Responder) -> None:
reactor.run()
def _make_bundle_menu(bundles: List[BundleMetadata], kind: str) -> SimpleMenu:
def _make_bundle_menu(mgr: BundleManager, kind: str) -> SimpleMenu:
entries: List[MenuItem] = [MenuItemBack()]
for bundle in bundles:
ids = sorted(mgr.bundles.keys())
for id in ids:
bundle = mgr.bundles[id]
entries += bundle.menu_entries(kind)
return SimpleMenu(entries)
......@@ -93,6 +95,15 @@ def _make_compositor(reactor: Reactor, vm: ViewManager) -> overlays.Compositor:
settings.onoff_debug_touch.subscribe(_onoff_debug_touch_update)
compositor.add_overlay(debug_touch)
# Tie compositor's icon visibility to setting.
def _onoff_show_tray_update() -> None:
compositor.enabled[
overlays.OverlayKind.Indicators
] = settings.onoff_show_tray.value
_onoff_show_tray_update()
settings.onoff_show_tray.subscribe(_onoff_show_tray_update)
# Add icon tray.
compositor.add_overlay(overlays.IconTray())
return compositor
......@@ -127,7 +138,8 @@ def run_main() -> None:
audio.set_volume_dB(-10)
leds.set_rgb(0, 255, 0, 0)
leds.update()
bundles = discover_bundles("/flash/sys/apps")
bundles = BundleManager()
bundles.update()
settings.load_all()
menu_settings = settings.build_menu()
......@@ -152,8 +164,7 @@ def run_main() -> None:
],
)
if override_main_app is not None:
requested = [b for b in bundles if b.name == override_main_app]
print([b.name for b in bundles])
requested = [b for b in bundles.bundles.values() if b.name == override_main_app]
if len(requested) > 1:
raise Exception(f"More than one bundle named {override_main_app}")
if len(requested) == 0:
......
......@@ -297,8 +297,10 @@ onoff_camp_wifi = OnOffTunable("Connect Camp WiFi", "system.camp_wifi_enabled",
onoff_button_swap = OnOffTunable("Swap Buttons", "system.swap_buttons", False)
onoff_debug = OnOffTunable("Debug Overlay", "system.debug", False)
onoff_debug_touch = OnOffTunable("Touch Overlay", "system.debug_touch", False)
onoff_show_tray = OnOffTunable("Show Icons", "system.show_icons", True)
all_settings: List[UnaryTunable] = [
onoff_camp_wifi,
onoff_show_tray,
onoff_button_swap,
onoff_debug,
onoff_debug_touch,
......
......@@ -11,11 +11,14 @@ from st3m.goose import Dict, Enum, List, ABCBase, abstractmethod, Optional
from st3m.utils import tau
from st3m.ui.view import ViewManager
from st3m.input import power
from st3m.power import approximate_battery_percentage
from ctx import Context
import st3m.wifi
import math
import audio
import sys_kernel
import network
class OverlayKind(Enum):
......@@ -345,6 +348,8 @@ class Icon(Responder):
that contains it.
"""
WIDTH: int = 25
@abstractmethod
def visible(self) -> bool:
...
......@@ -357,6 +362,8 @@ class USBIcon(Icon):
Might or might not be related to a certain serial bus.
"""
WIDTH: int = 20
def visible(self) -> bool:
return sys_kernel.usb_connected()
......@@ -375,6 +382,79 @@ class USBIcon(Icon):
pass
class WifiIcon(Icon):
WIDTH: int = 15
def __init__(self) -> None:
super().__init__()
self._rssi: float = -120
def visible(self) -> bool:
return st3m.wifi.enabled()
def draw(self, ctx: Context) -> None:
ctx.gray(1.0)
ctx.line_width = 10.0
w = 1.5
a = -w / 2 - 3.14 / 2
b = w / 2 - 3.14 / 2
r = self._rssi
ctx.gray(1.0 if r > -75 else 0.2)
ctx.arc(0, 65, 100, a, b, 0).stroke()
ctx.gray(1.0 if r > -85 else 0.2)
ctx.arc(0, 65, 70, a, b, 0).stroke()
ctx.gray(1.0 if r > -95 else 0.2)
ctx.arc(0, 65, 40, a, b, 0).stroke()
def think(self, ins: InputState, delta_ms: int) -> None:
self._rssi = st3m.wifi.rssi()
class BatteryIcon(Icon):
def __init__(self) -> None:
super().__init__()
self._percent = 100.0
self._charging = False
def visible(self) -> bool:
return True
def draw(self, ctx: Context) -> None:
if self._percent > 30:
ctx.rgb(0.17, 0.55, 0.04)
else:
ctx.rgb(0.52, 0.04, 0.17)
height = 160 * self._percent / 100
ctx.rectangle(-80, -50, height, 100)
ctx.fill()
ctx.gray(0.8)
ctx.line_width = 10.0
ctx.rectangle(80, -50, -160, 100)
ctx.stroke()
ctx.rectangle(100, -30, -20, 60)
ctx.fill()
if self._charging:
ctx.gray(1)
ctx.line_width = 20
ctx.move_to(10, -65 - 10)
ctx.line_to(-30, 20 - 10)
ctx.line_to(30, -20 - 10)
ctx.line_to(-10, 65 - 10)
ctx.line_to(-20, 35 - 10)
ctx.stroke()
ctx.move_to(-10, 65 - 10)
ctx.line_to(40, 35 - 10)
ctx.stroke()
def think(self, ins: InputState, delta_ms: int) -> None:
self._percent = approximate_battery_percentage(power.battery_voltage)
self._charging = power.battery_charging
class IconTray(Overlay):
"""
An overlay which renders Icons.
......@@ -384,7 +464,9 @@ class IconTray(Overlay):
def __init__(self) -> None:
self.icons = [
BatteryIcon(),
USBIcon(),
WifiIcon(),
]
self.visible: List[Icon] = []
......@@ -394,14 +476,16 @@ class IconTray(Overlay):
v.think(ins, delta_ms)
def draw(self, ctx: Context) -> None:
nicons = len(self.visible)
dist = 20
width = (nicons - 1) * dist
x0 = width / -2
if len(self.visible) < 1:
return
width = 0
for icon in self.visible:
width += icon.WIDTH
x0 = width / -2 + self.visible[0].WIDTH / 2
for i, v in enumerate(self.visible):
x = x0 + i * dist
ctx.save()
ctx.translate(x, -100)
ctx.translate(x0, -100)
ctx.scale(0.1, 0.1)
v.draw(ctx)
ctx.restore()
x0 = x0 + v.WIDTH
import network
from st3m import settings
from st3m.logging import Log
log = Log(__name__)
iface = None
......@@ -8,12 +11,21 @@ def setup_camp_wifi() -> None:
global iface
iface = network.WLAN(network.STA_IF)
iface.active(True)
iface.connect(b"Camp2023-open")
try:
iface.connect(b"Camp2023-open")
except OSError as e:
log.error(f"Could not connect to camp wifi: {e}")
def disable() -> None:
global iface
if iface is not None:
iface.active(False)
iface = None
def enabled() -> bool:
return iface is not None
def is_connected() -> bool:
......@@ -28,3 +40,12 @@ def _onoff_camp_wifi_update() -> None:
setup_camp_wifi()
else:
disable()
def rssi() -> float:
if iface is None:
return -120
try:
return iface.status("rssi")
except OSError:
return -120
......@@ -31,3 +31,7 @@ def freertos_sleep(ms):
def i2c_scan():
return [16, 44, 45, 85, 109, 110]
def battery_charging():
return True