From e3dcd9e76f1d3c51934d335c3c85db11411e77d9 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:30:55 -0500 Subject: [PATCH] Remove the T-Watch S3 app example (twatch) --- lib/examples/twatch/README.md | 59 ------ lib/examples/twatch/__init__.py | 4 - lib/examples/twatch/face.py | 39 ---- lib/examples/twatch/home.py | 27 --- lib/examples/twatch/lora.py | 53 ----- lib/examples/twatch/main.py | 24 --- lib/examples/twatch/power.py | 64 ------ lib/examples/twatch/remote.py | 35 ---- lib/examples/twatch/services.py | 338 -------------------------------- lib/examples/twatch/sound.py | 81 -------- lib/examples/twatch/steps.py | 52 ----- lib/examples/twatch/ui.py | 65 ------ lib/examples/twatch/wifi.py | 106 ---------- packages/examples.json | 48 ----- packages/twatch.json | 53 ----- pydevices-examples.toml | 12 -- 16 files changed, 1060 deletions(-) delete mode 100644 lib/examples/twatch/README.md delete mode 100644 lib/examples/twatch/__init__.py delete mode 100644 lib/examples/twatch/face.py delete mode 100644 lib/examples/twatch/home.py delete mode 100644 lib/examples/twatch/lora.py delete mode 100644 lib/examples/twatch/main.py delete mode 100644 lib/examples/twatch/power.py delete mode 100644 lib/examples/twatch/remote.py delete mode 100644 lib/examples/twatch/services.py delete mode 100644 lib/examples/twatch/sound.py delete mode 100644 lib/examples/twatch/steps.py delete mode 100644 lib/examples/twatch/ui.py delete mode 100644 lib/examples/twatch/wifi.py delete mode 100644 packages/twatch.json diff --git a/lib/examples/twatch/README.md b/lib/examples/twatch/README.md deleted file mode 100644 index 96dc9660..00000000 --- a/lib/examples/twatch/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# twatch: switchable apps for the T-Watch S3 - -The [wristwatch](../wristwatch.py) example split into separate apps that -appdev's launcher switches between, on the LILYGO T-Watch S3 with -MicroPython: - -- **Watch face**: the time, the date, the battery and today's steps. Tap it - for the home screen. -- **Home**: a button for each app. -- **Steps**, **TV remote**, **Sound**, **Power** and **LoRa**, as in the - wristwatch example. -- **Wi-Fi**: Wi-Fi and WebREPL on while it's open, so mpftp or Workbench - can reach the watch as `twatch.local`, and off when you leave it. It needs - pydevices' `wifi_manager` and a WebREPL password in `/webrepl_cfg.py` - (`PASS = "..."`, at most 9 characters). With no known network in range, - **Set up** asks for one from a phone. - -The crown goes back to the watch face from any app. On the face it turns -the screen off, and on a dark screen it wakes it. - -Only one app is in memory at a time. A switch closes the old app (its -screen, timers and anything else it made) and starts the next in about -0.3 s. If memory ever runs low, the launcher restarts the watch's program -straight into the app you asked for, in about 3 s, so it can't run out -however long you wear it. - -## Run it - -Copy the `twatch` folder to the watch's `/lib`, and [`main.py`](main.py) from it to -`/main.py`. The watch needs the T-Watch S3 board config and an `appdev` that -has `appdev.launcher`. If the firmware's frozen `appdev` is older, copy -pydevices' `lib/appdev` to the watch's root folder, which comes first on the -import path. Then reset it. - -At the REPL, `launcher.switch("steps")` switches apps the way the home -screen does, and `launcher.last` says how the last switch went: -`("switch", ms, free_heap)`. - -## What the apps share - -[`services.py`](services.py) sets up what every app uses and keeps it running across -switches: the display and touch, the AXP2101 power chip, the accelerometer's -step counter and gestures, the clock, the vibration motor, the IR LED, the -screen's dimming and sleep, and the crown. An app borrows them; nothing it -makes outlives it. - -The screen dims, then goes dark, when you leave it alone. Wake it with the -crown, a touch, a wrist tilt or a double tap. On battery the watch sleeps -while the screen is dark (light sleep, so it wakes in a moment). On USB it -only blanks the screen, so the REPL stays usable. - -## Writing another app - -An app is a module with `main(scope)` that does no work when it's imported. -It builds its screen with `ui.screen(scope, "Title")`, makes timers and -anything else through `scope` (see appdev's "Switchable apps"), and calls -`services.use(scope, update)` so the screen's tick calls `update()` while -the screen is on. Add its name to `APPS` in `main.py` and a button for it in -[`home.py`](home.py). diff --git a/lib/examples/twatch/__init__.py b/lib/examples/twatch/__init__.py deleted file mode 100644 index f8230190..00000000 --- a/lib/examples/twatch/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# gallery: skip -# deps: lvgl -"""Switchable apps for the LILYGO T-Watch S3 under appdev's launcher: see -README.md. Copy ``main.py`` to the watch as ``/main.py`` to boot into them.""" diff --git a/lib/examples/twatch/face.py b/lib/examples/twatch/face.py deleted file mode 100644 index a62a9510..00000000 --- a/lib/examples/twatch/face.py +++ /dev/null @@ -1,39 +0,0 @@ -"""The watch face: the time from the watch's clock, the date, the battery -and today's steps. Tap it for the other apps.""" - -import lvgl as lv - -from . import services as sv -from . import ui - -HOURS_24 = False # True for 13:05, False for 1:05 PM - - -def main(scope): - scr = ui.screen(scope, center=True) - lbl_time = ui.label(scr, "--:--", sv.huge) - lbl_secs = ui.label(scr, "", sv.font, sv.MUTED) - lbl_date = ui.label(scr, "", sv.font) - lbl_batt = ui.label(scr, "", sv.small, sv.MUTED) - lbl_steps = ui.label(scr, "", sv.small, sv.MUTED) - ui.label(scr, "tap for apps", sv.small, sv.MUTED) - - def update(): - y, mo, d, wd, h, mi, s = sv.now() - if HOURS_24: - lbl_time.set_text("%02d:%02d" % (h, mi)) - lbl_secs.set_text("%02d" % s) - else: - lbl_time.set_text("%d:%02d" % (h % 12 or 12, mi)) - lbl_secs.set_text("%02d %s" % (s, "PM" if h >= 12 else "AM")) - lbl_date.set_text("%s %d %s" % (sv.WEEKDAYS[wd % 7], d, sv.MONTHS[(mo - 1) % 12])) - lbl_batt.set_text(sv.battery_text()) - lbl_steps.set_text("%d steps" % sv.accel.steps if sv.features else "") - - def tapped(e): - if sv.awake_for() > 300: - sv.go("home") - - scr.add_event_cb(tapped, lv.EVENT.CLICKED, None) - sv.use(scope, update) - update() diff --git a/lib/examples/twatch/home.py b/lib/examples/twatch/home.py deleted file mode 100644 index 59d361da..00000000 --- a/lib/examples/twatch/home.py +++ /dev/null @@ -1,27 +0,0 @@ -"""The home screen: one button per app. The crown goes back to the face.""" - -from . import services as sv -from . import ui - -APPS = ( - ("Watch", "face"), - ("Steps", "steps"), - ("Remote", "remote"), - ("Sound", "sound"), - ("Power", "power"), - ("LoRa", "lora"), - ("Wi-Fi", "wifi"), -) - - -def main(scope): - scr = ui.screen(scope) - scr.set_style_pad_row(8, 0) - grid = ui.row(scr) - grid.set_style_pad_row(8, 0) - w = sv.W // 2 - 14 - rows = (len(APPS) + 1) // 2 - h = (sv.H - 16 - (rows - 1) * 8) // rows - for text, name in APPS: - ui.button(grid, text, (lambda n: lambda: sv.go(n))(name), width=w, height=h) - sv.use(scope) diff --git a/lib/examples/twatch/lora.py b/lib/examples/twatch/lora.py deleted file mode 100644 index 5c6c13b7..00000000 --- a/lib/examples/twatch/lora.py +++ /dev/null @@ -1,53 +0,0 @@ -"""LoRa: the radio's identity, a test packet, and listening for activity. - -Set ``LORA_MHZ`` to a frequency your region allows before sending (915 MHz -suits the Americas; Europe uses 868, much of Asia 433).""" - -import board_config as board - -from . import services as sv -from . import ui - -LORA_MHZ = 915.0 -LORA_DBM = 10 - - -def main(scope): - scr = ui.screen(scope, "LoRa") - lbl = ui.label(scr, "", sv.small) - if not sv.has_lora: - ui.missing(scr, "no LoRa radio role") - sv.use(scope) - return - state = {"radio": None, "pings": 0} - - def radio(): - if state["radio"] is None: - r = board.lora # the board keeps one radio; it outlives this app - r.configure(LORA_MHZ, sf=9, power_dbm=LORA_DBM) - state["radio"] = r - return state["radio"] - - def ping(): - try: - state["pings"] += 1 - ms = radio().send(b"twatch %d" % state["pings"]) - lbl.set_text("%s\nsent #%d, %d ms on air\nat %.1f MHz" % (radio().version, state["pings"], ms, LORA_MHZ)) - except Exception as e: - lbl.set_text("send failed: %r" % (e,)) - - def listen(): - try: - busy = radio().channel_active() - lbl.set_text("%s\nchannel %s\nat %.1f MHz" % (radio().version, "busy" if busy else "quiet", LORA_MHZ)) - except Exception as e: - lbl.set_text("CAD failed: %r" % (e,)) - - try: - lbl.set_text("%s\n%s\ntap Send or Listen" % (board.lora.version, board.lora.status[0])) - r = ui.row(scr) - ui.button(r, "Send", ping) - ui.button(r, "Listen", listen) - except Exception as e: - lbl.set_text("radio not answering: %r" % (e,)) - sv.use(scope) diff --git a/lib/examples/twatch/main.py b/lib/examples/twatch/main.py deleted file mode 100644 index 686310fa..00000000 --- a/lib/examples/twatch/main.py +++ /dev/null @@ -1,24 +0,0 @@ -"""The T-Watch S3's apps under appdev's launcher. Copy this file to the -watch as ``/main.py`` (with the ``twatch`` folder in ``/lib``) to boot into -the watch face. - -``launcher.switch("steps")`` at the REPL switches apps, as the home screen -does; ``launcher.last`` says how the last switch went. -""" - -# True or False switches the battery charger on or off at boot; None leaves -# it as the power chip has it. -CHARGER = None - -from appdev.launcher import Launcher - -from twatch import services - -if CHARGER is not None and services.battery is not None: - services.battery.charger(CHARGER) - -APPS = ("face", "home", "steps", "remote", "sound", "power", "lora", "wifi") - -launcher = Launcher(services.app, {name: "twatch." + name for name in APPS}, home="face") -services.launcher = launcher -launcher.boot() diff --git a/lib/examples/twatch/power.py b/lib/examples/twatch/power.py deleted file mode 100644 index 2fffbf19..00000000 --- a/lib/examples/twatch/power.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Power: battery voltage, charge and state, screen brightness, sleep, and -the memory the apps are using.""" - -import gc - -import lvgl as lv - -from . import services as sv -from . import ui - - -def main(scope): - scr = ui.screen(scope, "Power") - lbl = ui.label(scr, "", sv.small) - lbl_mem = ui.label(scr, "", sv.small, sv.MUTED) - ui.label(scr, "Brightness", sv.small, sv.MUTED) - slider = lv.slider(scr) - slider.set_width(lv.pct(85)) - slider.set_range(5, 100) - slider.set_value(int(sv.brightness * 100), 0) - - def on_bright(e): - sv.set_brightness(slider.get_value() / 100) - - slider.add_event_cb(on_bright, lv.EVENT.VALUE_CHANGED, None) - r = ui.row(scr) - ui.button(r, "Screen off", sv.screen_off) - if sv.sleep is not None: - ui.button(r, "Deep sleep", sv.deep_sleep) - - ticks = [0] - - def memory(): - # Once a second: a collection first, so the number is what's in use. - ticks[0] += 1 - if ticks[0] % 4 != 1 or not hasattr(gc, "mem_alloc"): - return - gc.collect() - la = sv.launcher - text = "heap in use %d KB" % (gc.mem_alloc() // 1024) - if la is not None: - text += "\n%d switches, last %s ms" % (la.switches, la.last[1] if la.last else "-") - lbl_mem.set_text(text) - - def update(): - memory() - battery = sv.battery - if battery is None: - lbl.set_text("no battery reading") - return - lines = [sv.battery_text()] - v = battery.voltage - if v is not None: - lines.append("battery %.2f V" % v) - state = getattr(battery, "charge_state", None) - if state is not None: - lines.append("charger: " + state) - vbus = getattr(battery, "vbus_voltage", None) - if vbus: - lines.append("USB %.2f V" % vbus) - lbl.set_text("\n".join(lines)) - - sv.use(scope, update) - update() diff --git a/lib/examples/twatch/remote.py b/lib/examples/twatch/remote.py deleted file mode 100644 index e1515fe7..00000000 --- a/lib/examples/twatch/remote.py +++ /dev/null @@ -1,35 +0,0 @@ -"""TV remote: Home, volume and mute for a Roku TV, sent from the watch's IR -LED. Point the watch at the TV. - -Set ``TV_*`` if your TV isn't a Roku.""" - -from . import services as sv -from . import ui - -# Roku TV infrared codes: extended NEC, address 0xEA 0xC7. -TV_ADDRESS = (0xEA, 0xC7) -TV_KEYS = (("Home", 0x03), ("Vol +", 0x0F), ("Vol -", 0x10), ("Mute", 0x20)) - - -def main(scope): - scr = ui.screen(scope, "TV remote") - lbl = ui.label(scr, "Point the watch at the TV", sv.small, sv.MUTED) - if sv.ir is None: - ui.missing(scr, "no IR LED") - sv.use(scope) - return - from ir_nec import send - - def key(name, code): - def cb(): - send(sv.ir, TV_ADDRESS[0], code, address2=TV_ADDRESS[1], repeats=1) - lbl.set_text("sent %s" % name) - sv.event("IR " + name) - sv.buzz(1) - - return cb - - r = ui.row(scr) - for name, code in TV_KEYS: - ui.button(r, name, key(name, code), width=sv.W // 2 - 16) - sv.use(scope) diff --git a/lib/examples/twatch/services.py b/lib/examples/twatch/services.py deleted file mode 100644 index d9a00e2d..00000000 --- a/lib/examples/twatch/services.py +++ /dev/null @@ -1,338 +0,0 @@ -""" -twatch.services -- what every T-Watch app shares, and what outlives them. - -The display, touch, the AXP2101 power chip, the accelerometer's step counter -and gestures, the clock, the vibration motor and the IR LED are set up once, -here, and stay up while apps come and go. So do the screen's dimming and -sleep, and the crown: - -* The screen dims, then goes dark, when you leave it alone. The crown, a - touch, a wrist tilt or a double tap wakes it. On battery the watch sleeps - while the screen is dark (light sleep, so it wakes in a moment); on USB it - only blanks the screen, so the REPL stays usable. -* The crown wakes a dark screen. In any app but the watch face it goes back - to the face; on the face it turns the screen off. - -An app borrows all of this: it imports this module, reads ``rtc``, -``battery``, ``accel`` and the rest, and calls ``use(scope, update)`` so the -screen's tick calls its ``update()`` while the screen is on. Nothing an app -makes here outlives it. -""" - -import time - -import board_config as board -from board_config import display_drv -import display_driver # noqa: F401 (wires LVGL to the display and inputs) -from display_driver import app # noqa: F401 -import lvgl as lv - -# -- settings --------------------------------------------------------------- - -DIM_AFTER_S = 12 # idle seconds before the screen dims -DARK_AFTER_S = 18 # ... and goes dark (and, on battery, the watch sleeps) -DIM_LEVEL = 0.15 -TICK_MS = 250 - -PERIPHERALS = getattr(board, "PERIPHERALS", frozenset()) -WEEKDAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") -MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") - - -def _role(name): - """A board role, or None when the board hasn't got it or it fails.""" - if name not in PERIPHERALS: - return None - try: - return getattr(board, name) - except Exception as e: # a chip that doesn't answer: apps show what's missing - print("twatch: %s unavailable: %r" % (name, e)) - return None - - -rtc = _role("rtc") -battery = _role("battery") -accel = _role("accelerometer") -haptic = _role("haptic") -ir = _role("ir") -sleep = _role("sleep") -has_lora = "lora" in PERIPHERALS -has_mic = "pcm_in" in PERIPHERALS -has_speaker = "pcm_out" in PERIPHERALS - -# The BMA423's feature engine: steps, wrist tilt, double tap. The chip keeps -# it loaded while the watch has power, so the step count survives a restart. -features = False -bma423 = None -if accel is not None and hasattr(accel, "load_features"): - try: - import bma423 - - accel.load_features() - accel.enable_features(bma423.STEP_COUNTER | bma423.ACTIVITY | bma423.WRIST_WEAR | bma423.DOUBLE_TAP) - accel.map_interrupts(bma423.INT_WRIST_WEAR | bma423.INT_DOUBLE_TAP) - features = True - except Exception as e: - print("twatch: no step counter or gestures: %r" % (e,)) -wake_on_tilt = features -wake_on_tap = features - -EVENTS = [] # the last few things that happened, newest last (read them at the REPL) - - -def event(text): - """Note an event without printing: a console nobody reads must never - hold the watch up.""" - EVENTS.append(text) - if len(EVENTS) > 40: - del EVENTS[0] - - -def ticks(): - try: - return time.ticks_ms() - except AttributeError: - return int(time.monotonic() * 1000) - - -def buzz(*effects): - if haptic is not None: - haptic.play(*effects) - - -# -- look --------------------------------------------------------------------- - -base = lv.screen_active() # shown between apps; each app loads its own screen -W, H = base.get_width(), base.get_height() -font = getattr(lv, "font_montserrat_16", None) or lv.font_default() -small = getattr(lv, "font_montserrat_14", None) or font -big = getattr(lv, "font_montserrat_40", None) or getattr(lv, "font_montserrat_32", None) or font -huge = getattr(lv, "font_montserrat_48", None) or big -try: - lv.display_get_default().set_theme( - lv.theme_default_init( - lv.display_get_default(), lv.palette_main(lv.PALETTE.TEAL), lv.palette_main(lv.PALETTE.AMBER), True, font - ) - ) -except AttributeError: - pass -base.set_style_bg_color(lv.color_hex(0x000000), 0) -MUTED = lv.palette_main(lv.PALETTE.GREY) -ACCENT = lv.palette_main(lv.PALETTE.TEAL) - - -# -- time and battery, which more than one app shows --------------------------- - - -def now(): - """``(year, month, day, weekday, hour, minute, second)`` from the RTC, - or the interpreter's clock without one.""" - if rtc is not None: - try: - return rtc.datetime()[:7] - except Exception: - pass - t = time.localtime() - return (t[0], t[1], t[2], t[6], t[3], t[4], t[5]) - - -def battery_text(): - if battery is None: - return "no battery reading" - status = getattr(battery, "battery_status", None) - if status in ("missing", "dead"): - return "battery missing or dead" - pct = battery.percent - text = "%d%%" % pct if pct is not None else "%.2f V" % battery.voltage - if status == "charging": - text += ", charging" - elif status == "full": - text += ", full" - return text - - -# -- the running app --------------------------------------------------------------- - -launcher = None # set by main.py -_update = None - - -def use(scope, update=None): - """Called by an app's ``main(scope)``: the screen's tick calls *update* - while the screen is on, until the app closes.""" - global _update - _update = update - - def _forget(): - global _update - _update = None - - scope.on_close(_forget) - - -def go(name): - """Switch to app *name* after the current callback.""" - if launcher is not None and launcher.current != name: - launcher.request(name) - - -def refresh(): - if _update is not None: - try: - _update() - except Exception as e: # a failing app must not stop the screen's tick - event("update failed: %r" % (e,)) - - -# -- the screen, the crown and sleep --------------------------------------------------- - - -screen = "on" # on, dim, dark -brightness = 1.0 -_dimmed_at = ticks() # when the screen last left "on" -woke_at = ticks() # when it last came on - - -def awake_for(): - """Milliseconds the screen has been on, or 0 when it's dim or dark: a - tap that woke the screen shouldn't also press what it landed on.""" - return ticks() - woke_at if screen == "on" else 0 - - -def set_brightness(level): - global brightness - brightness = level - if screen == "on": - display_drv.brightness = level - - -def wake_screen(): - global screen, woke_at - if screen != "on": - woke_at = ticks() - screen = "on" - # A gesture isn't input to LVGL: count it as some, or the next tick - # dims the screen again at once. - lv.display_get_default().trigger_activity() - display_drv.brightness = brightness - refresh() - - -stay_awake = False # an app holding a link (Wi-Fi) sets this: the screen goes dark but the watch doesn't doze - - -def screen_off(): - global screen, _dimmed_at - if screen == "on": - _dimmed_at = ticks() - screen = "dark" - display_drv.brightness = 0 - if features: - accel.interrupt_status() # forget gestures from while the screen was on - if sleep is not None and not stay_awake and battery is not None and not getattr(battery, "vbus_present", True): - doze() - - -def _wake_sources(): - sources = ["crown", "touch"] - if wake_on_tilt or wake_on_tap: - sources.append("motion") - return tuple(sources) - - -def _set_gestures(): - # The chip's own wrist tilt and double tap, and nothing of ours: a raise - # detector watching any motion woke the watch far too often. - if features: - accel.map_interrupts(bma423.INT_WRIST_WEAR, wake_on_tilt) - accel.map_interrupts(bma423.INT_DOUBLE_TAP, wake_on_tap) - - -def doze(): - """Light sleep until the crown, a touch or a gesture; then the screen comes back.""" - _set_gestures() - while True: - why = sleep(wake=_wake_sources()) - if why != "motion": - break - st = accel.interrupt_status() - if st & (bma423.INT_WRIST_WEAR | bma423.INT_DOUBLE_TAP): - why = "wrist tilt" if st & bma423.INT_WRIST_WEAR else "double tap" - break - event("woke from sleep by " + why) - if hasattr(battery, "key_events"): - battery.key_events() # the crown press that woke us mustn't also turn the screen off - buzz(1) - wake_screen() - - -def deep_sleep(): - """Deep sleep: the watch restarts when it wakes, into the watch face.""" - display_drv.brightness = 0 - _set_gestures() - sleep(deep=True, wake=_wake_sources()) - - -# The crown is the board's keypad (ENTER). It gets a group of its own, holding -# one invisible object on the system layer, so it never presses an app's -# buttons and works whichever app's screen is showing. -_crown_group = lv.group_create() -_crown_catcher = lv.obj(lv.layer_sys()) -_crown_catcher.remove_style_all() -_crown_catcher.set_size(1, 1) -_crown_group.add_obj(_crown_catcher) - - -def _on_crown(e): - if e.get_key() != lv.KEY.ENTER: - return - event("crown") - if screen != "on": - wake_screen() - elif launcher is not None and launcher.current != "face": - go("face") - else: - screen_off() - - -_crown_catcher.add_event_cb(_on_crown, lv.EVENT.KEY, None) -# display_driver keeps each input device's LVGL indev in its user_data, and -# put the keypad in the default group, where ENTER would press the focused -# widget. (The bindings have no lv.indev_get_next to walk the indevs with.) -for _dev in app.devices: - _indev = getattr(_dev, "user_data", None) - if _indev is not None and _indev.get_type() == lv.INDEV_TYPE.KEYPAD: - _indev.set_group(_crown_group) -_dev = _indev = None - - -def _tick(_t): - global screen, _dimmed_at - idle = lv.display_get_default().get_inactive_time() - since = ticks() - _dimmed_at - # touched since the screen dimmed (not counting the crown's own release) - if screen != "on" and since > 800 and idle + 50 < since: - event("woke by touch") - wake_screen() - return - if features and screen != "on": - # a gesture while dim or dark (on USB, where the watch doesn't sleep) - st = accel.interrupt_status() - if (st & bma423.INT_WRIST_WEAR and wake_on_tilt) or (st & bma423.INT_DOUBLE_TAP and wake_on_tap): - what = "wrist tilt" if st & bma423.INT_WRIST_WEAR else "double tap" - event("woke by " + what) - wake_screen() - return - if screen == "dark": - return - if screen == "on" and idle > DIM_AFTER_S * 1000: - screen = "dim" - _dimmed_at = ticks() - display_drv.brightness = min(brightness, DIM_LEVEL) - elif screen == "dim" and idle > DARK_AFTER_S * 1000: - screen_off() - return - refresh() - - -_timer = lv.timer_create(_tick, TICK_MS, None) diff --git a/lib/examples/twatch/sound.py b/lib/examples/twatch/sound.py deleted file mode 100644 index fb2ac9b6..00000000 --- a/lib/examples/twatch/sound.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Sound: a beep from the speaker, the microphone's level, and vibration -patterns. The speaker and microphone are opened when first used and closed -when you leave the app.""" - -import math - -import board_config as board -import lvgl as lv - -from . import services as sv -from . import ui - -# DRV2605 library effects (TI's numbering): name, sequence. A number above 128 -# is a pause of (n - 128) x 10 ms. -BUZZ_PATTERNS = (("Tap", (1,)), ("Double", (10,)), ("Buzz", (47,)), ("Alarm", (52, 128 + 20, 52, 128 + 20, 52))) - - -def main(scope): - scr = ui.screen(scope, "Sound") - state = {"out": None, "mic": None, "buf": None, "on": False} - r = ui.row(scr) - - def beep(): - if state["out"] is None: - from audiodev import AudioFormat - - state["out"] = scope.adopt(board.pcm_out(AudioFormat(16000, 1, 16))) - n = 16000 * 150 // 1000 - buf = bytearray(2 * n) - for i in range(n): - v = int(12000 * math.sin(2 * math.pi * 1000 * i / 16000)) - buf[2 * i] = v & 0xFF - buf[2 * i + 1] = (v >> 8) & 0xFF - state["out"].write(buf) - - if sv.has_speaker: - ui.button(r, "Beep", beep) - mic_bar = lbl_mic = btn_lbl = None - if sv.has_mic: - - def toggle_mic(): - state["on"] = not state["on"] - if state["on"] and state["mic"] is None: - state["mic"] = scope.adopt(board.pcm_in()) - state["buf"] = bytearray(1024) - btn_lbl.set_text("Mic off" if state["on"] else "Mic on") - - btn = ui.button(r, "Mic on", toggle_mic) - btn_lbl = btn.get_child(0) - mic_bar = lv.bar(scr) - mic_bar.set_width(lv.pct(90)) - mic_bar.set_range(0, 50) # -50 to 0 dBFS - lbl_mic = ui.label(scr, "", sv.small, sv.MUTED) - if not sv.has_speaker and not sv.has_mic: - ui.missing(scr, "no speaker or microphone role") - if sv.haptic is not None: - r = ui.row(scr) - for name, seq in BUZZ_PATTERNS: - ui.button(r, name, (lambda s: lambda: sv.buzz(*s))(seq), width=sv.W // 2 - 16) - else: - ui.missing(scr, "no vibration motor") - - def update(): - mic = state["mic"] - if not (state["on"] and mic is not None): - return - buf = state["buf"] - n = mic.readinto(buf) - acc = 0 - count = n // 2 - for i in range(0, n, 2): - v = buf[i] | (buf[i + 1] << 8) - if v & 0x8000: - v -= 0x10000 - acc += v * v - rms = math.sqrt(acc / count) if count else 0 - db = 20 * math.log10(rms / 32768) if rms > 0 else -90 - mic_bar.set_value(int(max(0, 50 + db)), 0) - lbl_mic.set_text("%.0f dBFS" % db) - - sv.use(scope, update) diff --git a/lib/examples/twatch/steps.py b/lib/examples/twatch/steps.py deleted file mode 100644 index 5bd5270f..00000000 --- a/lib/examples/twatch/steps.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Steps: the step counter, what you're doing (still, walking, running), the -accelerometer live, and whether a wrist tilt or a double tap wakes the -screen.""" - -import lvgl as lv - -from . import services as sv -from . import ui - - -def main(scope): - scr = ui.screen(scope, "Steps") - lbl_steps = ui.label(scr, "-", sv.big) - lbl_activity = ui.label(scr, "", sv.small, sv.MUTED) - lbl_accel = ui.label(scr, "", sv.small) - accel = sv.accel - if sv.features: - r = ui.row(scr) - for attr, text in (("wake_on_tilt", "Tilt wakes"), ("wake_on_tap", "2 taps wake")): - cell = lv.obj(r) - cell.remove_style_all() - cell.set_size(lv.SIZE_CONTENT, lv.SIZE_CONTENT) - cell.set_flex_flow(lv.FLEX_FLOW.COLUMN) - cell.set_flex_align(lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.CENTER) - ui.label(cell, text, sv.small) - sw = lv.switch(cell) - if getattr(sv, attr): - sw.add_state(lv.STATE.CHECKED) - sw.add_event_cb(_toggle(attr, sw), lv.EVENT.VALUE_CHANGED, None) - ui.button(scr, "Reset steps", accel.reset_steps) - elif accel is None: - ui.missing(scr, "no accelerometer") - else: - ui.missing(scr, "no step counter on this chip") - - def update(): - if sv.features: - lbl_steps.set_text("%d" % accel.steps) - lbl_activity.set_text(accel.activity) - if accel is not None: - x, y, z = accel.acceleration - lbl_accel.set_text("x %+.2f y %+.2f z %+.2f g" % (x, y, z)) - - sv.use(scope, update) - update() - - -def _toggle(attr, sw): - def cb(e): - setattr(sv, attr, sw.has_state(lv.STATE.CHECKED)) - - return cb diff --git a/lib/examples/twatch/ui.py b/lib/examples/twatch/ui.py deleted file mode 100644 index 1054ade3..00000000 --- a/lib/examples/twatch/ui.py +++ /dev/null @@ -1,65 +0,0 @@ -"""twatch.ui -- the few widgets the T-Watch apps build their screens from.""" - -import lvgl as lv - -from . import services as sv - - -def screen(scope, title=None, center=False): - """The app's own screen, a column, with *title* at the top. Deleted, with - everything on it, when the app closes.""" - scr = scope.screen() - scr.set_style_bg_color(lv.color_hex(0x000000), 0) - scr.set_style_text_font(sv.font, 0) - scr.set_flex_flow(lv.FLEX_FLOW.COLUMN) - main = lv.FLEX_ALIGN.CENTER if center else lv.FLEX_ALIGN.START - scr.set_flex_align(main, lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.CENTER) - scr.set_style_pad_all(8, 0) - scr.set_style_pad_row(6, 0) - if title: - label(scr, title, color=sv.ACCENT) - return scr - - -def label(parent, text="", font=None, color=None): - lbl = lv.label(parent) - lbl.set_text(text) - if font is not None: - lbl.set_style_text_font(font, 0) - if color is not None: - lbl.set_style_text_color(color, 0) - return lbl - - -def row(parent): - r = lv.obj(parent) - r.remove_style_all() - r.set_size(lv.pct(100), lv.SIZE_CONTENT) - r.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP) - r.set_flex_align(lv.FLEX_ALIGN.SPACE_EVENLY, lv.FLEX_ALIGN.CENTER, lv.FLEX_ALIGN.CENTER) - r.set_style_pad_row(6, 0) - return r - - -def button(parent, text, cb, width=None, height=None): - """A button that calls ``cb()``, unless the tap was the one that woke - the screen.""" - b = lv.button(parent) - if width: - b.set_width(width) - if height: - b.set_height(height) - lbl = lv.label(b) - lbl.set_text(text) - lbl.center() - - def clicked(e): - if sv.awake_for() > 300: - cb() - - b.add_event_cb(clicked, lv.EVENT.CLICKED, None) - return b - - -def missing(parent, what): - label(parent, what, sv.small, sv.MUTED) diff --git a/lib/examples/twatch/wifi.py b/lib/examples/twatch/wifi.py deleted file mode 100644 index 1683556b..00000000 --- a/lib/examples/twatch/wifi.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Wi-Fi: on only while this app is open, so mpftp and Workbench can reach the -watch over WebREPL. - -Opening it joins a known network (the one in ``secrets.py``, or one -``wifi_manager`` remembered) and starts WebREPL, whose password is the one in -``/webrepl_cfg.py``. Leaving it turns WebREPL and the radio off. With no known -network in range, "Set up" asks for one from a phone. While the app is open -the screen still goes dark, but the watch doesn't doze: that would drop the -link. - -Needs ``wifi_manager``, which is part of the pydevices package:: - - mip.install("pydevices", index="https://PyDevices.github.io/mip") -""" - -import lvgl as lv -import micropython - -from . import services as sv -from . import ui - -HOSTNAME = "twatch" # mpftp finds it as twatch.local - - -def main(scope): - scr = ui.screen(scope, "Wi-Fi") - lbl = ui.label(scr, "", sv.small) - hint = ui.label(scr, "", sv.small, sv.MUTED) - for t in (lbl, hint): - t.set_width(lv.pct(90)) - t.set_long_mode(lv.label.LONG_MODE.WRAP) - try: - import wifi_manager - except ImportError: - ui.missing(scr, "wifi_manager") - sv.use(scope) - return - import network - import webrepl - - sta = network.WLAN(network.STA_IF) - state = {"open": True, "busy": False} - - def off(): - try: - webrepl.stop() - except Exception: - pass - sta.active(False) - sv.event("Wi-Fi off") - - def show(ip): - if ip: - lbl.set_text("%s.local\n%s" % (HOSTNAME, ip)) - hint.set_text("WebREPL is on. Leave this app to turn Wi-Fi off.") - else: - lbl.set_text("No known network") - hint.set_text("Tap Set up, then follow the steps with a phone.") - - def job(setup): - # Runs from micropython.schedule, outside every timer callback, so - # wifi_manager's waits keep the screen and the crown going. - def run(_): - if not state["open"]: - return - state["busy"] = True - ip = None - try: - if setup: - ip = wifi_manager.run(hostname=HOSTNAME) - else: - ip = wifi_manager.connect(setup=False, hostname=HOSTNAME) - if ip: - webrepl.start() - except Exception as e: - print("wifi app:", e) - state["busy"] = False - if not state["open"]: - off() - return - sv.event("Wi-Fi " + (ip or "no network")) - show(ip) - - return run - - def start(setup=False): - if state["busy"]: - return - lbl.set_text("Setting up..." if setup else "Joining...") - hint.set_text("") - micropython.schedule(job(setup), None) - - def close(): - state["open"] = False - sv.stay_awake = False - if state["busy"]: - wifi_manager.cancel() # setup mode ends; the job turns the radio off - else: - off() - - r = ui.row(scr) - ui.button(r, "Set up", lambda: start(True)) - sv.stay_awake = True - scope.on_close(close) - start() - sv.use(scope) diff --git a/packages/examples.json b/packages/examples.json index 5eea9e58..91dc15b4 100644 --- a/packages/examples.json +++ b/packages/examples.json @@ -803,54 +803,6 @@ [ "tower_climb/tools/playtest.py", "github:PyDevices/pydevices-examples/lib/examples/tower_climb/tools/playtest.py" - ], - [ - "twatch/__init__.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/__init__.py" - ], - [ - "twatch/face.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/face.py" - ], - [ - "twatch/home.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/home.py" - ], - [ - "twatch/lora.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/lora.py" - ], - [ - "twatch/main.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/main.py" - ], - [ - "twatch/power.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/power.py" - ], - [ - "twatch/remote.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/remote.py" - ], - [ - "twatch/services.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/services.py" - ], - [ - "twatch/sound.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/sound.py" - ], - [ - "twatch/steps.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/steps.py" - ], - [ - "twatch/ui.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/ui.py" - ], - [ - "twatch/wifi.py", - "github:PyDevices/pydevices-examples/lib/examples/twatch/wifi.py" ] ], "deps": [], diff --git a/packages/twatch.json b/packages/twatch.json deleted file mode 100644 index 140228f6..00000000 --- a/packages/twatch.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "urls": [ - [ - "twatch/__init__.py", - "../lib/examples/twatch/__init__.py" - ], - [ - "twatch/face.py", - "../lib/examples/twatch/face.py" - ], - [ - "twatch/home.py", - "../lib/examples/twatch/home.py" - ], - [ - "twatch/lora.py", - "../lib/examples/twatch/lora.py" - ], - [ - "twatch/main.py", - "../lib/examples/twatch/main.py" - ], - [ - "twatch/power.py", - "../lib/examples/twatch/power.py" - ], - [ - "twatch/remote.py", - "../lib/examples/twatch/remote.py" - ], - [ - "twatch/services.py", - "../lib/examples/twatch/services.py" - ], - [ - "twatch/sound.py", - "../lib/examples/twatch/sound.py" - ], - [ - "twatch/steps.py", - "../lib/examples/twatch/steps.py" - ], - [ - "twatch/ui.py", - "../lib/examples/twatch/ui.py" - ], - [ - "twatch/wifi.py", - "../lib/examples/twatch/wifi.py" - ] - ], - "version": "0.0.1" -} diff --git a/pydevices-examples.toml b/pydevices-examples.toml index 9639a5d5..0d2c2cde 100644 --- a/pydevices-examples.toml +++ b/pydevices-examples.toml @@ -195,18 +195,6 @@ "https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/tower_climb/tower_climb.py" = "/examples/tower_climb/tower_climb.py" "https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/tower_climb/tower_climb_trace.py" = "/examples/tower_climb/tower_climb_trace.py" "https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/tv_remote_menu.py" = "/examples/tv_remote_menu.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/__init__.py" = "/examples/twatch/__init__.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/face.py" = "/examples/twatch/face.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/home.py" = "/examples/twatch/home.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/lora.py" = "/examples/twatch/lora.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/main.py" = "/examples/twatch/main.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/power.py" = "/examples/twatch/power.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/remote.py" = "/examples/twatch/remote.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/services.py" = "/examples/twatch/services.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/sound.py" = "/examples/twatch/sound.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/steps.py" = "/examples/twatch/steps.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/ui.py" = "/examples/twatch/ui.py" -"https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/twatch/wifi.py" = "/examples/twatch/wifi.py" "https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/widgets_clinic_queue.py" = "/examples/widgets_clinic_queue.py" "https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/widgets_energy_panel.py" = "/examples/widgets_energy_panel.py" "https://raw.githubusercontent.com/PyDevices/pydevices-examples/main/lib/examples/widgets_locker_kiosk.py" = "/examples/widgets_locker_kiosk.py"