From b80ed6eab16086694cccbdbf6dea8dbfb1128071 Mon Sep 17 00:00:00 2001 From: skjdghsdjgsdj <37231318+skjdghsdjgsdj@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:56:18 -0600 Subject: [PATCH 1/8] Added proper block char instead of guessing at one --- lcd.py | 4 +++- ui_components.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lcd.py b/lcd.py index d8345d0..1b7d3a1 100644 --- a/lcd.py +++ b/lcd.py @@ -99,6 +99,7 @@ class LCD: RIGHT = 4 LEFT = 5 CENTER = 6 + BLOCK = 7 COLUMNS = 20 LINES = 4 @@ -190,7 +191,8 @@ def get_instance(i2c: I2C): LCD.CHARGING: [0x4, 0xe, 0x1b, 0x0, 0xe, 0xa, 0xe, 0xe], LCD.RIGHT: [0x10, 0x18, 0x1c, 0x1e, 0x1c, 0x18, 0x10, 0x0], LCD.LEFT: [0x2, 0x6, 0xe, 0x1e, 0xe, 0x6, 0x2, 0x0], - LCD.CENTER: [0x0, 0xe, 0x11, 0x15, 0x11, 0xe, 0x0, 0x0] + LCD.CENTER: [0x0, 0xe, 0x11, 0x15, 0x11, 0xe, 0x0, 0x0], + LCD.BLOCK: [0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f] } class SparkfunSerLCD(LCD): diff --git a/ui_components.py b/ui_components.py index 48f5fcf..f1c955a 100644 --- a/ui_components.py +++ b/ui_components.py @@ -96,11 +96,11 @@ def render_progress(self) -> None: block_count = math.ceil(((self.index + 1) / self.count) * self.max_block_count) + 1 if self.last_block_count == -1: - self.devices.lcd.write("-" * block_count, (0, 2)) + self.devices.lcd.write(self.devices.lcd[LCD.BLOCK] * block_count, (0, 2)) self.last_block_count = block_count elif block_count != self.last_block_count: extra_blocks = block_count - self.last_block_count - self.devices.lcd.write("-" * extra_blocks, (self.last_block_count - 1, 2)) + self.devices.lcd.write(self.devices.lcd[LCD.BLOCK] * extra_blocks, (self.last_block_count - 1, 2)) def render_and_wait(self) -> None: super().render_and_wait() From 291a601e6369ecf83ff0548f3c6f4d7617150f3f Mon Sep 17 00:00:00 2001 From: skjdghsdjgsdj <37231318+skjdghsdjgsdj@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:31:47 -0600 Subject: [PATCH 2/8] Explicitly disconnect from Wi-Fi when going offline then reconnect before replaying stuff when going back online --- api.py | 5 +++++ flow.py | 47 ++++++++++++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/api.py b/api.py index c2f19e0..3bb36bc 100644 --- a/api.py +++ b/api.py @@ -49,6 +49,11 @@ def connect() -> adafruit_requests.Session: return ConnectionManager.requests + @staticmethod + def disconnect() -> None: + wifi.radio.enabled = False + ConnectionManager.requests = None + timeout = os.getenv("CIRCUITPY_WIFI_TIMEOUT") ConnectionManager.timeout = 10 if (timeout is None or not timeout) else int(timeout) diff --git a/flow.py b/flow.py index 47f8438..dbf1c13 100644 --- a/flow.py +++ b/flow.py @@ -3,6 +3,7 @@ import traceback import microcontroller +import wifi from adafruit_datetime import datetime from api import GetFirstChildIDAPIRequest, GetLastFeedingAPIRequest, PostChangeAPIRequest, Timer, \ @@ -190,15 +191,13 @@ def auto_connect(self) -> None: self.render_splash("Connecting...") # noinspection PyBroadException try: + print("Getting requests instance from ConnectionManager") self.requests = ConnectionManager.connect() except Exception as e: import traceback traceback.print_exception(e) if self.devices.rtc and self.devices.sdcard: - self.render_splash("Going offline") - self.devices.piezo.tone("info") - time.sleep(1) - NVRAMValues.OFFLINE.write(True) + self.offline() else: raise e # can't go offline automatically because there's no hardware support elif not self.devices.rtc: @@ -529,29 +528,39 @@ def settings(self) -> None: if has_offline_hardware: if NVRAMValues.OFFLINE and not responses[1]: # was offline, now back online self.back_online() + elif not NVRAMValues.OFFLINE and responses[1]: # was online, now offline + self.offline() - NVRAMValues.OFFLINE.write(responses[1]) + def offline(self): + self.render_splash("Going offline") + self.devices.piezo.tone("info") + time.sleep(1) + ConnectionManager.disconnect() + NVRAMValues.OFFLINE.write(True) def back_online(self) -> None: + NVRAMValues.OFFLINE.write(False) + self.auto_connect() files = self.offline_queue.get_json_files() - if len(files) == 0: - return # nothing to do - + if len(files) > 0: + print(f"Replaying offline-serialized {len(files)} requests") - print(f"Replaying offline-serialized {len(files)} requests") - - self.devices.lcd.clear() + self.devices.lcd.clear() - progress_bar = ProgressBar(devices = self.devices, count = len(files), message = "Syncing changes...") - progress_bar.render_and_wait() + progress_bar = ProgressBar(devices = self.devices, count = len(files), message = "Syncing changes...") + progress_bar.render_and_wait() - index = 0 - for filename in files: - progress_bar.set_index(index) - self.offline_queue.replay(filename) - index += 1 + index = 0 + for filename in files: + progress_bar.set_index(index) + try: + self.offline_queue.replay(filename) + except Exception as e: + NVRAMValues.OFFLINE.write(True) + raise e + index += 1 - self.render_success_splash("Change synced!" if len(files) == 1 else f"{len(files)} changes synced!") + self.render_success_splash("Change synced!" if len(files) == 1 else f"{len(files)} changes synced!") def diaper(self) -> None: self.render_header_text("How was diaper?") From 0c3aaa1fc9330631411023ad2676fb4d54837804 Mon Sep 17 00:00:00 2001 From: skjdghsdjgsdj <37231318+skjdghsdjgsdj@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:33:39 -0600 Subject: [PATCH 3/8] Dead import --- flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flow.py b/flow.py index dbf1c13..e37df25 100644 --- a/flow.py +++ b/flow.py @@ -3,7 +3,6 @@ import traceback import microcontroller -import wifi from adafruit_datetime import datetime from api import GetFirstChildIDAPIRequest, GetLastFeedingAPIRequest, PostChangeAPIRequest, Timer, \ From 3be240fc3c1c1c5f8f81d49060342fcb69721611 Mon Sep 17 00:00:00 2001 From: skjdghsdjgsdj <37231318+skjdghsdjgsdj@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:04:01 -0600 Subject: [PATCH 4/8] Recursively create missing directories when deploying and added --build-release-zip for building release zips for GitHub --- build-and-deploy.py | 51 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/build-and-deploy.py b/build-and-deploy.py index 0e4f36c..b9ea112 100755 --- a/build-and-deploy.py +++ b/build-and-deploy.py @@ -2,6 +2,8 @@ import argparse import glob import subprocess +import sys +import zipfile from typing import Final import shutil import os @@ -43,6 +45,13 @@ help = f"Output to this directory instead of {CIRCUITPY_PATH}" ) +parser.add_argument( + "--build-release-zip", + action = "store", + default = None, + help = "Builds a zip suitable for deployment to GitHub to this zip filename; overrides other arguments" +) + def get_base_path(): return os.path.abspath(os.path.dirname(__file__)) @@ -71,6 +80,7 @@ def clean(output_path: str): print("done") def build_and_deploy(source_module: str, output_path: str, compile_to_mpy: bool = True) -> str: + if source_module == "code": print("Copying code.py...", end = "", flush = True) dst = f"{output_path}/code.py" @@ -82,16 +92,23 @@ def build_and_deploy(source_module: str, output_path: str, compile_to_mpy: bool raise ValueError(f"File doesn't exist: {full_src}") if compile_to_mpy: - temp_mpy = tempfile.NamedTemporaryFile(suffix = ".mpy") + temp_mpy = tempfile.NamedTemporaryFile(suffix = ".mpy", delete = False, delete_on_close = False) print(f"Compiling {source_module}.py...", end = "", flush = True) result = subprocess.run(["mpy-cross", full_src, "-O9", "-o", temp_mpy.name]) if result.returncode != 0: raise ValueError(f"mpy-cross failed with status {result.returncode}") + if not os.path.isfile(temp_mpy.name): + raise ValueError(f"mpy-cross didn't actually output a file to {temp_mpy.name}") + dst = f"{output_path}/lib/{source_module}.mpy" + output_directory = pathlib.Path(dst).parent + output_directory.mkdir(parents = True, exist_ok = True) + print("deploying...", end = "", flush = True) - shutil.move(temp_mpy.name, dst) + shutil.copy(temp_mpy.name, dst) + os.remove(temp_mpy.name) print("done") else: dst = f"{output_path}/lib/{source_module}.py" @@ -103,11 +120,33 @@ def build_and_deploy(source_module: str, output_path: str, compile_to_mpy: bool args = parser.parse_args() +if args.build_release_zip: + if args.output: + print("Warning: --output has no effect when specifying --build-release-zip; a temp path will be used", file = sys.stderr) + if args.modules: + print("Warning: --modules has no effect when specifying --build-release-zip; all modules will be built", file = sys.stderr) + if args.no_reboot: + print("Warning: --no-reboot has no effect when specifying --build-release-zip; deployment won't go to device", file = sys.stderr) + if args.clean: + print("Warning: --clean has no effect when specifying --build-release-zip; deployment won't go to device", file = sys.stderr) + if args.no_compile: + print("Warning: --no-compile has no effect when specifying --build-release-zip; releases are always compiled", file = sys.stderr) + + args.output = tempfile.gettempdir() + args.modules = None + args.no_reboot = True + args.clean = False + args.no_compile = False + output_path = args.output or CIRCUITPY_PATH if args.clean: clean(output_path) +zip_file = None +if args.build_release_zip: + zip_file = zipfile.ZipFile(file = args.build_release_zip, mode = "w") + if args.modules: for module in args.modules: build_and_deploy(module, output_path, compile_to_mpy = not args.no_compile) @@ -116,7 +155,13 @@ def build_and_deploy(source_module: str, output_path: str, compile_to_mpy: bool for py_file in py_files: py_file = pathlib.Path(py_file).with_suffix("").name if py_file != "build-and-deploy" and not py_file.startswith("._"): - build_and_deploy(py_file, output_path, compile_to_mpy = not args.no_compile) + output = build_and_deploy(py_file, output_path, compile_to_mpy = not args.no_compile) + if zip_file is not None: + zip_file.write(filename = output, arcname = os.path.relpath(output, output_path)) + +if zip_file is not None: + zip_file.write("settings.toml.example", "settings.toml.example") + zip_file.close() if not args.no_reboot: print("Rebooting") From 55c7e3ad7b3d32ac47549c2983b2d28bdd73bc84 Mon Sep 17 00:00:00 2001 From: skjdghsdjgsdj <37231318+skjdghsdjgsdj@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:09:04 -0600 Subject: [PATCH 5/8] Docs for updated build script --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2efdccf..9b711da 100644 --- a/README.md +++ b/README.md @@ -132,10 +132,11 @@ If you update CircuitPython on the Feather, you will likely need to build a corr The build script supports several arguments: - `--no-compile`: instead of building files with `mpy-cross`, just copy the source `.py` files. This is useful for debugging so errors don't always show as line 1 of a file, but execution is slower. You should only use `--no-compile` when debugging. `code.py` doesn't get compiled regardless. -- `--modules`: only builds or copies the given files. For example, use `--modules code` to just copy `code.py`, or `--modules code sdcard` to just copy `code.py` and build/copy `sdcard.py`. +- `--modules example1 example2`: only builds or copies the given files. For example, use `--modules code` to just copy `code.py`, or `--modules code sdcard` to just copy `code.py` and build/copy `sdcard.py`. - `--clean`: deletes everything from `lib/` on the `CIRCUITPY` drive and repopulates it with the required Adafruit libraries. This is useful if using `--no-compile` after using compiled files, or vice versa, to ensure the `.py` or `.mpy` files are being used correctly without duplicates. It can take a minute or two to finish. - `--no-reboot`: don't attempt to reboot the Feather after copying files. -- `--output`: use the specified path instead of the `CIRCUITPY` drive; useful for building zip releases +- `--output /path/to/output/`: use the specified path instead of the `CIRCUITPY` drive +- `--build-release-zip filename.zip`: create a zip file with the given filename containing all compiled files, `code.py`, and `settings.toml.example`; overrides other options To set up a brand new BabyPod, all you should need to do is: 1. Erase the flash then re-flash CircuitPython. From 2014e7fd0836c840d3d471dfffcdf554dd4e5b3e Mon Sep 17 00:00:00 2001 From: skjdghsdjgsdj <37231318+skjdghsdjgsdj@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:22:37 -0600 Subject: [PATCH 6/8] Less frequent MOTD checking by default --- nvram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nvram.py b/nvram.py index 5adee6f..808294e 100644 --- a/nvram.py +++ b/nvram.py @@ -122,4 +122,4 @@ class NVRAMValues: # True if flags are configured and don't need to be reconfigured. No effect for the Adafruit LCD. HAS_CONFIGURED_SPARKFUN_LCD = NVRAMBooleanValue(9, False, "HAS_CONFIGURED_SPARKFUN_LCD") # How frequently in seconds to check for a MOTD - MOTD_CHECK_INTERVAL = NVRAMIntegerValue(10, 60 * 60 * 3, "MOTD_CHECK_INTERVAL") \ No newline at end of file + MOTD_CHECK_INTERVAL = NVRAMIntegerValue(10, 60 * 60 * 6, "MOTD_CHECK_INTERVAL") \ No newline at end of file From 18d7fd3d6b76cd853e1448476870097a45d188b4 Mon Sep 17 00:00:00 2001 From: skjdghsdjgsdj <37231318+skjdghsdjgsdj@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:24:07 -0600 Subject: [PATCH 7/8] Don't sleep before soft shutdown when waking up just to refresh battery --- power_control.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/power_control.py b/power_control.py index e78d4ec..2c14f30 100644 --- a/power_control.py +++ b/power_control.py @@ -107,8 +107,9 @@ def shutdown(self, silent: bool = False) -> None: self.init_center_button_interrupt() - print("Waiting a few seconds for deep sleep") - time.sleep(3) # give the user time to let go of the button, or it'll just wake immediately + if not silent: + print("Waiting a few seconds for deep sleep") + time.sleep(3) # give the user time to let go of the button, or it'll just wake immediately self.lcd_shutdown() self.enter_deep_sleep() \ No newline at end of file From 3b82662fb1a0cc51cc2df06184ac6924a9c6f7e5 Mon Sep 17 00:00:00 2001 From: skjdghsdjgsdj <37231318+skjdghsdjgsdj@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:13:33 -0600 Subject: [PATCH 8/8] Automatic soft shutdown after 5 minutes idle --- flow.py | 12 ++++++++++++ nvram.py | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/flow.py b/flow.py index e37df25..983cb89 100644 --- a/flow.py +++ b/flow.py @@ -119,6 +119,13 @@ def __init__(self, devices: Devices): ) ]) + idle_shutdown = NVRAMValues.IDLE_SHUTDOWN.get() + if self.devices.power_control and idle_shutdown: + self.devices.rotary_encoder.on_wait_tick_listeners.append(WaitTickListener( + on_tick = self.idle_shutdown, + seconds = NVRAMValues.IDLE_SHUTDOWN.get() + )) + if self.devices.rtc is None or self.devices.sdcard is None: self.offline_state = None self.offline_queue = None @@ -157,6 +164,11 @@ def idle_warning(self, _: float) -> None: print("Idle; warning not suppressed and is discharging") self.devices.piezo.tone("idle_warning") + def idle_shutdown(self, _: float) -> None: + if not self.suppress_idle_warning: + print("Idle; soft shutdown") + self.devices.power_control.shutdown(silent = True) + def on_user_input(self) -> None: if not self.suppress_dim_timeout: self.devices.lcd.backlight.set_color(BacklightColors.DEFAULT) diff --git a/nvram.py b/nvram.py index 808294e..31a1978 100644 --- a/nvram.py +++ b/nvram.py @@ -122,4 +122,7 @@ class NVRAMValues: # True if flags are configured and don't need to be reconfigured. No effect for the Adafruit LCD. HAS_CONFIGURED_SPARKFUN_LCD = NVRAMBooleanValue(9, False, "HAS_CONFIGURED_SPARKFUN_LCD") # How frequently in seconds to check for a MOTD - MOTD_CHECK_INTERVAL = NVRAMIntegerValue(10, 60 * 60 * 6, "MOTD_CHECK_INTERVAL") \ No newline at end of file + MOTD_CHECK_INTERVAL = NVRAMIntegerValue(10, 60 * 60 * 6, "MOTD_CHECK_INTERVAL") + # Soft shutdown after this many seconds of being idle and not in a timer; only has an effect if soft shutdown + # is enabled + IDLE_SHUTDOWN = NVRAMIntegerValue(11, 60 * 5, "IDLE_SHUTDOWN") \ No newline at end of file