Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/system/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ def _interpreter():
name = {"micropython": "MicroPython", "circuitpython": "CircuitPython", "cpython": "CPython"}.get(
impl.name, impl.name
)
if impl.name == "circuitpython":
# Its version tuple leaves out the pre-release, which tells one
# alpha from the next: "11.0.0-alpha.2 on 2026-11-05".
try:
return "%s %s" % (name, os.uname().version.split(" on ")[0])
except Exception:
pass
v = impl.version
return "%s %d.%d.%d" % (name, v[0], v[1], v[2])

Expand Down
12 changes: 10 additions & 2 deletions lib/appkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ timer still running, and `appkit.serve()` picks the loop up again. A Ctrl-C
that lands inside a timer's callback is held until the callback ends, so
it sometimes needs pressing twice.

On a CircuitPython board Ctrl-C ends `code.py`, display and all, so there's
no REPL to go to while the apps run. The loop reads the console instead:
type a line and it runs in `code.py`'s namespace, with its value printed.
`launcher.current` or `import apps.system; apps.system.stats()` answer
while the apps keep running.

## An app set

Put each app in its own folder under `apps/`, with an `app.json` naming it:
Expand Down Expand Up @@ -229,8 +235,10 @@ import board_config
...
```

On CircuitPython boards a launcher can't run scripts yet: there's no
restart to go through.
On a CircuitPython board the launcher keeps the next app's name in
`/data/.next_app`, so CIRCUITPY has to be writable from code. While a
computer has the drive mounted it isn't: a script then doesn't start, and a
restart for memory carries on in-process instead.

## A frozen build

Expand Down
2 changes: 0 additions & 2 deletions lib/appkit/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Roadmap

- **Scripts on CircuitPython boards.** Run a plain script from the home
screen there too, through CircuitPython's own way to start a file next.
- **Publishing.** appkit in the mip index with the other packages here, and
later a pip package.
- **A model for a front panel.** An app can expose a model (describe, apply,
Expand Down
58 changes: 58 additions & 0 deletions lib/appkit/foreground.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ def frame():
that lands inside a timer's callback is kept by the timers until they're
done, so it sometimes needs pressing twice. ``appkit.serve()`` resumes the
loop.

CircuitPython has no REPL to go to with the program still running: Ctrl-C
ends ``code.py``, and the display with it. There `serve` reads lines typed
on the console instead, runs each in ``code.py``'s namespace and prints what
it returns, so ``apps.system.stats()`` answers while the apps keep running.
"""

import sys
Expand All @@ -59,6 +64,10 @@ def frame():

def _report(exc):
print("appkit.foreground: the foreground function failed:")
_print_exc(exc)


def _print_exc(exc):
pe = getattr(sys, "print_exception", None)
if pe is not None:
pe(exc)
Expand Down Expand Up @@ -214,6 +223,52 @@ def _done(app):
return app is not None and app.quit_requested


def _console():
"""A function that runs the lines typed on CircuitPython's console so
far, or None on any other host (or a CircuitPython with no console)."""
if sys.implementation.name != "circuitpython":
return None
try:
import supervisor

rt = supervisor.runtime
rt.serial_bytes_available
# code.py's globals; CircuitPython keeps __main__ out of sys.modules.
namespace = __import__("__main__").__dict__
except Exception:
return None
buf = [""]

def poll():
n = rt.serial_bytes_available
if not n:
return
text = buf[0] + sys.stdin.read(n).replace("\r", "\n")
while "\n" in text:
line, text = text.split("\n", 1)
line = line.strip()
if line:
print(">>> " + line)
_run_line(line, namespace)
buf[0] = text

return poll


def _run_line(src, namespace):
try:
try:
code = compile(src, "<console>", "eval")
except SyntaxError:
exec(compile(src, "<console>", "exec"), namespace)
else:
value = eval(code, namespace)
if value is not None:
print(repr(value))
except Exception as exc:
_print_exc(exc)


def serve(app=None):
"""Run the foreground function over and over until the program quits.

Expand All @@ -233,9 +288,12 @@ def serve(app=None):
# A stop() asked for before the loop started still counts: a restart
# from inside boot(), say.
_stop_timer()
console = _console()
_serving = True
try:
while not _done(app):
if console is not None:
console()
fn = _fn
if fn is None:
multimer.sleep_ms(IDLE_MS)
Expand Down
22 changes: 17 additions & 5 deletions lib/appkit/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@
ESP32 (MicroPython) ``machine.RTC().memory()`` ``machine.soft_reset()``, or
``machine.reset()`` from a callback;
main.py calls ``boot()`` again
Other boards a file, ``/data/.next_app`` the same
CircuitPython a file, ``/data/.next_app`` ``supervisor.reload()``, or
``microcontroller.reset()`` from a
callback; code.py calls ``boot()``
again
Other boards a file, ``/data/.next_app`` the same as ESP32
Desktop MicroPython ``<data root>/.next_app`` exit with code 75; run the program
under ``python -m appkit.launcher``
to have it started again
Expand Down Expand Up @@ -356,8 +360,7 @@ def restart_into(name, count=1, path=None):
With *name* None the program restarts with nothing pending, so it starts
as it does from cold: on its home screen. Doesn't return where the host
can restart (it exits, execs or resets). In a browser it returns after
asking for the reload: stop work and let the page go. Raises
``NotImplementedError`` where there is no restart.
asking for the reload: stop work and let the page go.
"""
h = host()
if h == "browser":
Expand All @@ -371,8 +374,6 @@ def restart_into(name, count=1, path=None):
params.set("app_path", path)
js.window.location.search = params.toString()
return
if h == "circuitpython":
raise NotImplementedError("no restart fallback on CircuitPython boards yet")
if name is not None:
save_next(name, count, path)
else:
Expand All @@ -395,6 +396,17 @@ def restart_into(name, count=1, path=None):
# A real soft reset stops it with every other timer.
multimer.after(500, lambda t: machine.reset(), name="launcher.reset")
machine.soft_reset()
elif h == "circuitpython":
import microcontroller
import supervisor

# The same as a board's soft reset: reload() raises an exception that
# a callback's caller catches, so from a callback reset the chip; the
# file in /data survives either.
if _in_callback():
microcontroller.reset()
multimer.after(500, lambda t: microcontroller.reset(), name="launcher.reset")
supervisor.reload()
elif h == "desktop":
import os

Expand Down
7 changes: 4 additions & 3 deletions lib/appkit/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,12 @@ def mem_free():
def _lvgl():
"""The lvgl module when it is loaded and initialised, else None.

MicroPython keeps built-in modules out of ``sys.modules``, so there an
import (free for a built-in, an ImportError without one) is the test.
MicroPython and CircuitPython keep built-in modules out of
``sys.modules``, so there an import (free for a built-in, an ImportError
without one) is the test.
"""
lv = sys.modules.get("lvgl")
if lv is None and sys.implementation.name == "micropython":
if lv is None and sys.implementation.name in ("micropython", "circuitpython"):
try:
import lvgl as lv
except ImportError:
Expand Down
35 changes: 31 additions & 4 deletions lib/touchkit/net.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

A desktop has no ``wifi`` or ``network`` module and is on its network
already: `up` is then True once the computer has an address, and
`ip` and `hostname` are the computer's own.
`ip` and `hostname` are the computer's own. CircuitPython has no
``network`` module either: there `ip`, `rssi` and `hostname` read
``wifi.radio``.

The board joins a known network at boot (`boot`) and stays joined
while apps come and go, like the display. An app that needs the network
Expand All @@ -26,7 +28,10 @@ def _host_ip():
For a host with no ``network`` module (a desktop), which is on its LAN
already: a UDP socket "connected" to a documentation address (TEST-NET-1,
RFC 5737) sends nothing and names the interface the OS would use."""
import socket
try:
import socket
except ImportError: # CircuitPython names it socketpool
return None

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
Expand All @@ -41,6 +46,16 @@ def _host_ip():
s.close()


def _radio():
"""CircuitPython's ``wifi.radio``, where there is no ``network`` module; else None."""
try:
import wifi

return wifi.radio
except (ImportError, AttributeError):
return None


def hostname():
"""The board's name on the network, without ``.local``, or None."""
name = settings.get("hostname")
Expand All @@ -50,7 +65,10 @@ def hostname():
import network

return network.hostname()
except ImportError: # a desktop: the computer's own name
except ImportError: # CircuitPython's radio, or a desktop: the computer's own name
r = _radio()
if r is not None:
return r.hostname
try:
import socket

Expand Down Expand Up @@ -152,10 +170,15 @@ def setup_job():


def rssi():
"""The signal from the access point in dBm, or None off the network."""
try:
import network

return network.WLAN(network.STA_IF).status("rssi")
except ImportError:
r = _radio()
info = r.ap_info if r is not None else None
return info.rssi if info is not None else None
except Exception:
return None

Expand All @@ -169,7 +192,11 @@ def ip():
if wl.isconnected():
return wl.ifconfig()[0]
except ImportError:
return _host_ip()
r = _radio()
if r is None:
return _host_ip()
addr = r.ipv4_address
return str(addr) if addr else None
except Exception:
pass
return None
Expand Down
3 changes: 2 additions & 1 deletion lib/touchkit/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ def make_home_button(self):
def _boot_button(self):
try:
import board_config as board
from machine import Pin

pin = board.boot_button # a lazy role: reading it may hand back a factory
from machine import Pin

if not hasattr(pin, "irq"):
pin = pin()
pin.irq(trigger=Pin.IRQ_FALLING, handler=_boot_irq)
Expand Down
Loading