From 186e1eb31dfa742cee74459676c47cb1eda70d16 Mon Sep 17 00:00:00 2001 From: Kyle Krenzer Date: Fri, 4 Sep 2026 12:56:19 -0700 Subject: [PATCH 1/3] Run COSMOS updates through Moonraker's update_manager Register a "cosmos" entry with Moonraker's update_manager, so every client that speaks that API (Mainsail and Fluidd today) shows the installed and latest COSMOS version, offers the Update button, and follows the update in the standard update dialog. The update itself runs under Moonraker, outside Klipper's command queue, so it can no longer be cut off by the shell command timeout. - cosmos_update.py (Moonraker component): looks up the installed version and channel, checks GitHub for the latest stable release (or commits behind main on the nightly channel), and runs update-cosmos with its output streamed to the clients. No change to update_manager itself. - update-cosmos prints what it is doing (download progress in 20% steps, install, reboot, failures) and sends the curl/swupdate chatter to /board-resource/update-cosmos.log. - cosmos-update-start asks Moonraker to run the update and returns at once; the "Update Now" prompt button and the screen's Update button both use it, so all entry points share the same path and the same progress. Co-Authored-By: Claude Fable 5.1 --- .../grumyscreen/files/grumpyscreen.cfg | 6 +- .../grumyscreen/grumpyscreen_20260518.bb | 2 +- .../recipes-apps/klipper/files/shell.cfg | 4 +- .../moonraker/files/cosmos_update.py | 252 ++++++++++++++++++ .../moonraker/files/moonraker-readonly.conf | 9 + .../moonraker/moonraker_0.10.0.bb | 5 +- .../update-scripts/files/cosmos-update-start | 6 + .../update-scripts/files/update-cosmos | 41 ++- .../update-scripts/update-scripts_0.1.0.bb | 3 + 9 files changed, 320 insertions(+), 8 deletions(-) create mode 100644 meta-opencentauri/recipes-apps/moonraker/files/cosmos_update.py create mode 100755 meta-opencentauri/recipes-data/update-scripts/files/cosmos-update-start diff --git a/meta-opencentauri/recipes-apps/grumyscreen/files/grumpyscreen.cfg b/meta-opencentauri/recipes-apps/grumyscreen/files/grumpyscreen.cfg index bb9e2566..bbc5f2dd 100644 --- a/meta-opencentauri/recipes-apps/grumyscreen/files/grumpyscreen.cfg +++ b/meta-opencentauri/recipes-apps/grumyscreen/files/grumpyscreen.cfg @@ -44,10 +44,14 @@ port: 80 # set this if host is not 127.0.0.1 / localhost or you want to download thumbnails thumbnail_path: +# The update button drives the cosmos entry of Moonraker's update_manager +[update_manager] +application: cosmos + [commands] factory_reset_cmd: /usr/bin/factory-reset gui_restart_cmd: /etc/init.d/gui-switcher restart -cosmos_update_cmd: /usr/bin/update-cosmos +cosmos_update_cmd: /usr/bin/cosmos-update-start switch_to_stock_cmd: /usr/bin/switch-to-oc-patched support_zip_cmd: /usr/bin/generate-support-zip restart_klipper_cmd: /etc/init.d/klipper restart diff --git a/meta-opencentauri/recipes-apps/grumyscreen/grumpyscreen_20260518.bb b/meta-opencentauri/recipes-apps/grumyscreen/grumpyscreen_20260518.bb index 47044064..343185a3 100644 --- a/meta-opencentauri/recipes-apps/grumyscreen/grumpyscreen_20260518.bb +++ b/meta-opencentauri/recipes-apps/grumyscreen/grumpyscreen_20260518.bb @@ -64,7 +64,7 @@ do_compile() { UPDATE_CMD=cosmos_update_cmd \ UPDATE_TEXT="Update\nCOSMOS" \ UPDATE_PROMPT="Are you sure you want to update COSMOS?\n\nThis will download and update to the latest version of COSMOS!" \ - UPDATE_SUCCESS="Your printer will restart shortly!" \ + UPDATE_SUCCESS="Update started. Progress is shown in Mainsail, the printer restarts when it is done." \ UPDATE_FAILURE="Failed to initiate update COSMOS!" \ SWITCH_TO_STOCK_TEXT="Switch to OC\nPatched" \ SWITCH_TO_STOCK_PROMPT="Are you sure you want to switch to OpenCentauri patched firmware?\n\nThis will take some time, **DO NOT TURN OFF YOUR PRINTER**, just wait for it to reboot." \ diff --git a/meta-opencentauri/recipes-apps/klipper/files/shell.cfg b/meta-opencentauri/recipes-apps/klipper/files/shell.cfg index 9aa5cefc..4722ffe4 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/shell.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/shell.cfg @@ -93,8 +93,8 @@ timeout: 5 verbose: False [gcode_shell_command UPDATE_COSMOS] -command: update-cosmos -timeout: 500 +command: cosmos-update-start +timeout: 10 verbose: False [gcode_macro _UPDATE_COSMOS] diff --git a/meta-opencentauri/recipes-apps/moonraker/files/cosmos_update.py b/meta-opencentauri/recipes-apps/moonraker/files/cosmos_update.py new file mode 100644 index 00000000..0cbc9e9b --- /dev/null +++ b/meta-opencentauri/recipes-apps/moonraker/files/cosmos_update.py @@ -0,0 +1,252 @@ +# COSMOS firmware updater for Moonraker's update_manager +# +# Registers a "cosmos" entry with update_manager so Mainsail, Fluidd and any +# other client that speaks the update_manager API can see the installed and +# latest COSMOS version, start an update, and follow its progress through the +# standard notify_update_response stream. No changes to update_manager itself +# are needed: the updater is added to its table from this component. +# +# Configuration (moonraker.conf): +# +# [update_manager] +# enable_system_updates: False +# +# [cosmos_update] +# update_command: /usr/bin/update-cosmos # command whose stdout is shown as progress +# update_timeout: 3600 # seconds before the update is aborted +# refresh_interval: 24 # hours between version checks +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +from __future__ import annotations +import configparser +import logging +import pathlib +import time +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from .update_manager.base_deploy import BaseDeploy + +if TYPE_CHECKING: + from ..confighelper import ConfigHelper + from .update_manager.update_manager import UpdateManager + +ISSUE_FILE = pathlib.Path("/etc/issue") +COSMOS_CONF = pathlib.Path("/etc/klipper/config/cosmos.conf") +GITHUB_OWNER = "OpenCentauri" +GITHUB_REPO = "cosmos" +MAX_COMMITS = 30 + + +class CosmosDeploy(BaseDeploy): + def __init__(self, config: ConfigHelper) -> None: + super().__init__(config, name="cosmos") + self.update_cmd: str = config.get("update_command", "/usr/bin/update-cosmos") + self.update_timeout: float = config.getfloat("update_timeout", 3600.) + self.version: str = "?" + self.remote_version: str = "?" + self.channel: str = "stable" + self.commits_behind: List[Dict[str, Any]] = [] + self.last_error: str = "" + + async def initialize(self) -> Dict[str, Any]: + storage = await super().initialize() + self.remote_version = storage.get("remote_version", "?") + self.commits_behind = storage.get("commits_behind", []) + self.last_error = storage.get("last_error", "") + await self._read_local_state() + return storage + + async def _read_local_state(self) -> None: + # Installed version, e.g. "OpenCentauri Cosmos 26.08.0 \n \l" + try: + parts = ISSUE_FILE.read_text().split() + self.version = parts[2] if len(parts) > 2 else "?" + except Exception: + self.log_exc("Unable to read the installed version", traceback=False) + self.version = "?" + # Update channel from cosmos.conf, via config-manager if available + channel = "" + try: + scmd = self.cmd_helper.get_shell_command() + channel = await scmd.exec_cmd("config-manager update release", timeout=10.) + channel = channel.strip() + except Exception: + channel = "" + if not channel: + try: + parser = configparser.ConfigParser() + parser.read(COSMOS_CONF) + channel = parser.get("update", "release", fallback="stable") + except Exception: + channel = "stable" + self.channel = channel or "stable" + + def _api(self): + return self.cmd_helper.get_http_client() + + async def refresh(self) -> None: + await self._read_local_state() + self.last_error = "" + try: + if self.channel == "nightly": + await self._refresh_nightly() + else: + await self._refresh_stable() + except Exception as e: + self.last_error = str(e) + self.log_exc(f"Version check failed: {e}", traceback=False) + self._save_state() + + async def _refresh_stable(self) -> None: + resp = await self._api().github_api_request( + f"repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases?per_page=10" + ) + if resp.has_error(): + raise self.server.error(f"GitHub request failed: {resp.error}") + releases = resp.json() + latest = None + for release in releases: + if release.get("prerelease") or release.get("draft"): + continue + latest = release.get("tag_name") + break + if not latest: + raise self.server.error("No stable release found on GitHub") + self.remote_version = str(latest) + self.commits_behind = [] + self.log_info(f"installed {self.version}, latest {self.remote_version}") + + async def _refresh_nightly(self) -> None: + # Nightly builds are identified by a short commit hash + resp = await self._api().github_api_request( + f"repos/{GITHUB_OWNER}/{GITHUB_REPO}/compare/{self.version}...main" + ) + if resp.has_error(): + raise self.server.error(f"GitHub request failed: {resp.error}") + data = resp.json() + commits = data.get("commits", [])[-MAX_COMMITS:] + behind: List[Dict[str, Any]] = [] + for c in commits: + commit = c.get("commit", {}) + author = commit.get("author", {}) + msg = commit.get("message", "") + try: + date = time.mktime(time.strptime( + author.get("date", ""), "%Y-%m-%dT%H:%M:%SZ")) - time.timezone + except Exception: + date = 0 + behind.append({ + "sha": c.get("sha", ""), + "author": author.get("name", ""), + "date": int(date), + "subject": msg.split("\n", 1)[0], + "message": msg, + "tag": None, + }) + self.commits_behind = behind + head = data.get("commits", []) + self.remote_version = head[-1]["sha"][:10] if head else self.version + self.log_info( + f"installed {self.version}, {data.get('ahead_by', 0)} commit(s) behind main" + ) + + async def update(self) -> bool: + if self.remote_version in ("?", self.version): + self.notify_status(f"Reinstalling COSMOS {self.version}...") + else: + self.notify_status(f"Updating COSMOS {self.version} to {self.remote_version}...") + self.notify_status( + "The printer reboots by itself when the update is installed. " + "Do not power it off." + ) + try: + await self.cmd_helper.run_cmd( + self.update_cmd, timeout=self.update_timeout, notify=True, + log_stderr=True + ) + except Exception as e: + self.last_error = str(e) + self._save_state() + raise self.log_exc(f"COSMOS update failed: {e}", traceback=False) + # update-cosmos issues the reboot itself; this is the last thing the + # clients hear before the connection drops. + self.notify_status("COSMOS update installed, the printer is rebooting", is_complete=True) + return True + + def get_update_status(self) -> Dict[str, Any]: + status = super().get_update_status() + nightly = self.channel == "nightly" + status.update({ + "name": self.name, + "configured_type": "git_repo" if nightly else "zip", + "detected_type": "git_repo" if nightly else "zip", + "channel": self.channel, + "owner": GITHUB_OWNER, + "repo_name": GITHUB_REPO, + "version": self.version, + "remote_version": self.remote_version, + "current_hash": self.version if nightly else "", + "remote_hash": self.remote_version if nightly else "", + "commits_behind": self.commits_behind, + "is_valid": True, + "is_dirty": False, + "detached": False, + "corrupt": False, + "branch": "main", + "remote_alias": "origin", + "last_error": self.last_error, + "warnings": [], + "anomalies": [], + "info_tags": ["desc=COSMOS firmware"], + }) + return status + + def get_persistent_data(self) -> Dict[str, Any]: + data = super().get_persistent_data() + data.update({ + "remote_version": self.remote_version, + "commits_behind": self.commits_behind, + "last_error": self.last_error, + }) + return data + + +class CosmosUpdate: + def __init__(self, config: ConfigHelper) -> None: + self.server = config.get_server() + um: UpdateManager = self.server.load_component(config, "update_manager") + updaters = um.get_updaters() + # Neither Klipper nor Moonraker is a git checkout on this image, so + # update_manager holds placeholder entries for them that show up as + # empty rows in the UIs. Drop them; COSMOS updates both anyway. + for name in ("klipper", "moonraker"): + if type(updaters.get(name)) is BaseDeploy: + updaters.pop(name, None) + if "cosmos" in updaters: + raise config.error("update_manager already has a 'cosmos' entry") + self.deploy = CosmosDeploy(config) + updaters["cosmos"] = self.deploy + # update_manager re-creates its klipper entry (as a background task) + # every time Klippy connects, so prune the placeholder again shortly + # after that happens. + self.server.register_event_handler( + "server:klippy_identified", self._schedule_prune + ) + logging.info("cosmos_update: registered COSMOS updater with update_manager") + + def _schedule_prune(self) -> None: + loop = self.server.get_event_loop() + loop.delay_callback(2., self._prune_placeholders) + loop.delay_callback(15., self._prune_placeholders) + + def _prune_placeholders(self, eventtime: float = 0.) -> None: + um: UpdateManager = self.server.lookup_component("update_manager") + updaters = um.get_updaters() + for name in ("klipper", "moonraker"): + if type(updaters.get(name)) is BaseDeploy: + updaters.pop(name, None) + + +def load_component(config: ConfigHelper) -> CosmosUpdate: + return CosmosUpdate(config) diff --git a/meta-opencentauri/recipes-apps/moonraker/files/moonraker-readonly.conf b/meta-opencentauri/recipes-apps/moonraker/files/moonraker-readonly.conf index bad6c275..7f32be0b 100644 --- a/meta-opencentauri/recipes-apps/moonraker/files/moonraker-readonly.conf +++ b/meta-opencentauri/recipes-apps/moonraker/files/moonraker-readonly.conf @@ -42,3 +42,12 @@ subscriptions: service: mjpegstreamer-adaptive stream_url: /webcam/?action=stream snapshot_url: /webcam/?action=snapshot + +[update_manager] +enable_auto_refresh: True +enable_system_updates: False +refresh_interval: 24 + +# COSMOS firmware updates through update_manager (see cosmos_update.py) +[cosmos_update] +refresh_interval: 24 diff --git a/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb b/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb index 5770b60c..eab569c2 100644 --- a/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb +++ b/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb @@ -13,6 +13,7 @@ SRC_URI = " \ file://moonraker-init-d \ file://moonraker.conf \ file://moonraker-readonly.conf \ + file://cosmos_update.py \ file://0001-Serve-static-files.patch \ file://0001-Reduce-log-rotate-threshold.patch \ " @@ -21,7 +22,7 @@ SRCREV = "16e530eb663218faa6ccd97ffb0583f1880e2983" S = "${WORKDIR}/git" -PR = "r1" +PR = "r2" inherit python3-dir update-rc.d @@ -70,6 +71,8 @@ do_install() { # Install moonraker python package install -d ${D}${datadir}/moonraker cp -r ${S}/moonraker ${D}${datadir}/moonraker/ + # COSMOS updater for update_manager + install -m 0644 ${WORKDIR}/cosmos_update.py ${D}${datadir}/moonraker/moonraker/components/ # Install default moonraker config install -d ${D}${sysconfdir}/klipper diff --git a/meta-opencentauri/recipes-data/update-scripts/files/cosmos-update-start b/meta-opencentauri/recipes-data/update-scripts/files/cosmos-update-start new file mode 100755 index 00000000..390afefb --- /dev/null +++ b/meta-opencentauri/recipes-data/update-scripts/files/cosmos-update-start @@ -0,0 +1,6 @@ +#!/bin/sh +# Start a COSMOS update through Moonraker's update_manager and return at once. +# Every UI that speaks the update_manager API then shows the progress; the +# update itself runs under Moonraker, outside Klipper's command queue. +curl -s -X POST "http://localhost/machine/update/client?name=cosmos" >/dev/null 2>&1 & +exit 0 diff --git a/meta-opencentauri/recipes-data/update-scripts/files/update-cosmos b/meta-opencentauri/recipes-data/update-scripts/files/update-cosmos index 62b535dd..ddc35748 100644 --- a/meta-opencentauri/recipes-data/update-scripts/files/update-cosmos +++ b/meta-opencentauri/recipes-data/update-scripts/files/update-cosmos @@ -13,14 +13,49 @@ case "$RELEASE" in esac SWUFILE="/user-resource/update.swu" +LOG="/board-resource/update-cosmos.log" -# Download firmware -curl -k -f -S -o "$SWUFILE" -L "$FW_URL" +# Everything printed here is shown to the user by Moonraker's update_manager +# (Mainsail, Fluidd, ...). The chatter from curl and swupdate goes to $LOG. +: > "$LOG" + +# Size of the asset after redirects, so the download can report progress. If +# this fails the download still runs, just without percentages. +TOTAL=$(curl -k -sIL "$FW_URL" 2>>"$LOG" \ + | awk '/^[Cc]ontent-[Ll]ength:/ {v=$2} END {gsub(/\r/,"",v); print v+0}') + +echo "Downloading COSMOS update..." +curl -k -f -sS -o "$SWUFILE" -L "$FW_URL" 2>>"$LOG" & +CURL_PID=$! + +LAST=0 +while kill -0 "$CURL_PID" 2>/dev/null; do + if [ "$TOTAL" -gt 0 ] && [ -f "$SWUFILE" ]; then + CUR=$(wc -c < "$SWUFILE" || echo 0) + STEP=$(( CUR * 100 / TOTAL / 20 * 20 )) + if [ "$STEP" -gt "$LAST" ]; then + LAST=$STEP + echo "Downloading COSMOS update... ${STEP}%" + fi + fi + sleep 1 +done + +if ! wait "$CURL_PID"; then + echo "Download failed, see $LOG" + exit 1 +fi +[ "$LAST" -lt 100 ] && echo "Downloading COSMOS update... 100%" # Install firmware -flash "$SWUFILE" +echo "Installing the update, do not power off the printer!" +if ! flash "$SWUFILE" >>"$LOG" 2>&1; then + echo "Installing the update failed, see $LOG" + exit 1 +fi # Clean up firmware file rm "$SWUFILE" +echo "Update installed, rebooting..." reboot diff --git a/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb b/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb index 56e4b53b..d5cfff69 100644 --- a/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb +++ b/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb @@ -5,6 +5,7 @@ LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda SRC_URI = " \ file://factory-reset \ file://update-cosmos \ + file://cosmos-update-start \ file://switch-to-stock \ file://switch-to-oc-patched \ file://swu-decrypt.py \ @@ -26,6 +27,7 @@ do_install() { install -d ${D}${bindir} install -m 0755 ${WORKDIR}/factory-reset ${D}${bindir}/ install -m 0755 ${WORKDIR}/update-cosmos ${D}${bindir}/ + install -m 0755 ${WORKDIR}/cosmos-update-start ${D}${bindir}/ install -m 0755 ${WORKDIR}/switch-to-stock ${D}${bindir}/ install -m 0755 ${WORKDIR}/switch-to-oc-patched ${D}${bindir}/ install -m 0755 ${WORKDIR}/swu-decrypt.py ${D}${bindir}/ @@ -39,6 +41,7 @@ do_install() { FILES_${PN} += " \ ${bindir}/factory-reset \ ${bindir}/update-cosmos \ + ${bindir}/cosmos-update-start \ ${bindir}/switch-to-stock \ ${bindir}/switch-to-oc-patched \ ${bindir}/swu-decrypt.py \ From fe0996019efa1bdbdb42bbb96efa1831a4aaaf3c Mon Sep 17 00:00:00 2001 From: Kyle Krenzer Date: Sat, 5 Sep 2026 00:33:35 -0700 Subject: [PATCH 2/3] Move all update handling into Moonraker, drop the Klipper-side pieces Review follow-up. The cosmos_update.py component is now placed into the Moonraker source tree with the subdir fetcher option instead of being copied in do_install. The cosmos-update-start helper, the UPDATE_COSMOS and CHECK_FOR_UPDATES shell commands, the _UPDATE_COSMOS macro, the startup update check, the check-update script and the check_for_updates option are removed: update_manager refreshes on its own schedule and the web UIs show what it finds, and grumpyscreen starts the update over its own Moonraker connection (pellcorp/grumpyscreen#289) using the [update_manager] application key in grumpyscreen.cfg. The screen's cosmos_update_cmd and the recipe wording are back to what main ships. Co-Authored-By: Claude Fable 5.1 --- .../grumyscreen/files/grumpyscreen.cfg | 2 +- .../grumyscreen/grumpyscreen_20260518.bb | 2 +- .../recipes-apps/klipper/files/macros.cfg | 5 -- .../recipes-apps/klipper/files/shell.cfg | 22 ------ .../recipes-apps/klipper/kalico_2026.02.00.bb | 1 - .../moonraker/moonraker_0.10.0.bb | 5 +- .../check-update/check-update_0.1.bb | 24 ------ .../check-update/files/check-update.py | 78 ------------------- .../config-manager/files/config_manager.py | 1 - .../config-manager/files/default.conf | 2 - .../update-scripts/files/cosmos-update-start | 6 -- .../update-scripts/update-scripts_0.1.0.bb | 3 - 12 files changed, 3 insertions(+), 148 deletions(-) delete mode 100644 meta-opencentauri/recipes-data/check-update/check-update_0.1.bb delete mode 100644 meta-opencentauri/recipes-data/check-update/files/check-update.py delete mode 100755 meta-opencentauri/recipes-data/update-scripts/files/cosmos-update-start diff --git a/meta-opencentauri/recipes-apps/grumyscreen/files/grumpyscreen.cfg b/meta-opencentauri/recipes-apps/grumyscreen/files/grumpyscreen.cfg index bbc5f2dd..d299d3a6 100644 --- a/meta-opencentauri/recipes-apps/grumyscreen/files/grumpyscreen.cfg +++ b/meta-opencentauri/recipes-apps/grumyscreen/files/grumpyscreen.cfg @@ -51,7 +51,7 @@ application: cosmos [commands] factory_reset_cmd: /usr/bin/factory-reset gui_restart_cmd: /etc/init.d/gui-switcher restart -cosmos_update_cmd: /usr/bin/cosmos-update-start +cosmos_update_cmd: /usr/bin/update-cosmos switch_to_stock_cmd: /usr/bin/switch-to-oc-patched support_zip_cmd: /usr/bin/generate-support-zip restart_klipper_cmd: /etc/init.d/klipper restart diff --git a/meta-opencentauri/recipes-apps/grumyscreen/grumpyscreen_20260518.bb b/meta-opencentauri/recipes-apps/grumyscreen/grumpyscreen_20260518.bb index 343185a3..47044064 100644 --- a/meta-opencentauri/recipes-apps/grumyscreen/grumpyscreen_20260518.bb +++ b/meta-opencentauri/recipes-apps/grumyscreen/grumpyscreen_20260518.bb @@ -64,7 +64,7 @@ do_compile() { UPDATE_CMD=cosmos_update_cmd \ UPDATE_TEXT="Update\nCOSMOS" \ UPDATE_PROMPT="Are you sure you want to update COSMOS?\n\nThis will download and update to the latest version of COSMOS!" \ - UPDATE_SUCCESS="Update started. Progress is shown in Mainsail, the printer restarts when it is done." \ + UPDATE_SUCCESS="Your printer will restart shortly!" \ UPDATE_FAILURE="Failed to initiate update COSMOS!" \ SWITCH_TO_STOCK_TEXT="Switch to OC\nPatched" \ SWITCH_TO_STOCK_PROMPT="Are you sure you want to switch to OpenCentauri patched firmware?\n\nThis will take some time, **DO NOT TURN OFF YOUR PRINTER**, just wait for it to reboot." \ diff --git a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg index 35a4c9df..b6386677 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg @@ -882,11 +882,6 @@ gcode: RESPOND TYPE=command MSG="action:prompt_begin {esc_title}" RESPOND TYPE=command MSG="action:prompt_text {esc_message}" - # Yes this is a hack. I don't want to hear about it. - {% if title == "Update Available" %} - RESPOND TYPE=command MSG="action:prompt_footer_button Update Now|_UPDATE_COSMOS|warning" - {% endif %} - RESPOND TYPE=command MSG="action:prompt_footer_button Dismiss|RESPOND TYPE=command MSG=action:prompt_end" RESPOND TYPE=command MSG="action:prompt_show" diff --git a/meta-opencentauri/recipes-apps/klipper/files/shell.cfg b/meta-opencentauri/recipes-apps/klipper/files/shell.cfg index 4722ffe4..2bd50c00 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/shell.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/shell.cfg @@ -86,25 +86,3 @@ verbose: False [gcode_macro REBOOT_MACHINE] gcode: RUN_SHELL_COMMAND CMD=REBOOT_SYSTEM - -[gcode_shell_command CHECK_FOR_UPDATES] -command: check-update -timeout: 5 -verbose: False - -[gcode_shell_command UPDATE_COSMOS] -command: cosmos-update-start -timeout: 10 -verbose: False - -[gcode_macro _UPDATE_COSMOS] -gcode: - _SHOW_PROMPT TITLE="Updating..." MESSAGE="Please wait while COSMOS is updated. This may take a few minutes." - RUN_SHELL_COMMAND CMD=UPDATE_COSMOS - -[delayed_gcode check_for_updates_startup] -initial_duration: 60 -gcode: - {% if printer.idle_timeout.state in ["Idle", "Ready"] and printer.print_stats.state == "standby" %} - RUN_SHELL_COMMAND CMD=CHECK_FOR_UPDATES - {% endif %} \ No newline at end of file diff --git a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb index 7d11153b..417cda3b 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb @@ -33,7 +33,6 @@ RDEPENDS:${PN} = " \ kalico-firmware-dsp \ kalico-firmware-toolhead \ kalico-firmware-bed \ - check-update \ kalico-firmware-canvas \ " diff --git a/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb b/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb index eab569c2..c4caabd8 100644 --- a/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb +++ b/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb @@ -13,7 +13,7 @@ SRC_URI = " \ file://moonraker-init-d \ file://moonraker.conf \ file://moonraker-readonly.conf \ - file://cosmos_update.py \ + file://cosmos_update.py;subdir=git/moonraker/components \ file://0001-Serve-static-files.patch \ file://0001-Reduce-log-rotate-threshold.patch \ " @@ -71,9 +71,6 @@ do_install() { # Install moonraker python package install -d ${D}${datadir}/moonraker cp -r ${S}/moonraker ${D}${datadir}/moonraker/ - # COSMOS updater for update_manager - install -m 0644 ${WORKDIR}/cosmos_update.py ${D}${datadir}/moonraker/moonraker/components/ - # Install default moonraker config install -d ${D}${sysconfdir}/klipper install -d ${D}${sysconfdir}/klipper/config diff --git a/meta-opencentauri/recipes-data/check-update/check-update_0.1.bb b/meta-opencentauri/recipes-data/check-update/check-update_0.1.bb deleted file mode 100644 index 404597fa..00000000 --- a/meta-opencentauri/recipes-data/check-update/check-update_0.1.bb +++ /dev/null @@ -1,24 +0,0 @@ -DESCRIPTION = "COSMOS update availability checker" -LICENSE = "GPL-3.0-only" -LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-3.0-only;md5=c79ff39f19dfec6d293b95dea7b07891" - -SRC_URI = "file://check-update.py" - -inherit allarch - -RDEPENDS:${PN} = " \ - config-manager \ - curl \ - python3-core \ - python3-json \ - screen-actions \ -" - -do_install[vardeps] += "DISTRO_VERSION" - -do_install() { - install -d ${D}${bindir} - install -m 0755 ${WORKDIR}/check-update.py ${D}${bindir}/check-update -} - -FILES:${PN} = "${bindir}/check-update" diff --git a/meta-opencentauri/recipes-data/check-update/files/check-update.py b/meta-opencentauri/recipes-data/check-update/files/check-update.py deleted file mode 100644 index 228f4a7d..00000000 --- a/meta-opencentauri/recipes-data/check-update/files/check-update.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -import subprocess, json, sys - -def get_current_version() -> str: - with open("/etc/issue") as f: - return f.read().strip().split(" ")[2] - -def get_version_branch() -> str: - return subprocess.run(["config-manager", "update", "release"], capture_output=True, text=True, check=True).stdout - -def is_check_for_updates_enabled() -> bool: - result = subprocess.run(["config-manager", "update", "check_for_updates"], capture_output=True, text=True, check=True) - return result.stdout.strip().lower() == "true" - -def make_request(url: str) -> list|None: - try: - result = subprocess.run(["curl", "-sf", url], capture_output=True, text=True, check=True) - return json.loads(result.stdout) - except (subprocess.CalledProcessError, json.JSONDecodeError): - return None - -def get_latest_release() -> str|None: - data = make_request("https://api.github.com/repos/OpenCentauri/cosmos/releases?per_page=5") - - if data is None: - return None - - for release in data: - if release.get("prerelease", False): - continue - return release.get("tag_name", None) - - return None - -def get_latest_commit() -> str|None: - data = make_request("https://api.github.com/repos/OpenCentauri/cosmos/commits?per_page=1") - - if data is None: - return None - - if len(data) == 0: - return None - - return data[0].get("sha", None) - -def notify_update_availabe(new_version : str, current_version : str): - title = "Update Available" - message = f"An upgrade from COSMOS version {current_version} to {new_version} is available. Upgrade to the latest version for new features and bugfixes." - subprocess.run(["uiprompt", title, message]) - print(title) - print(message) - -def main(): - if not is_check_for_updates_enabled(): - print("Update check is disabled. Skipping update check.") - return - - version = sys.argv[1] if len(sys.argv) > 1 else get_current_version() - - if "PR" in version: - print("Running a PR build. Skipping update check.") - return - - is_stable = get_version_branch() == "stable" - remote_version = get_latest_release() if is_stable else get_latest_commit() - - if remote_version is None: - print("Failed to fetch latest version information.") - return - - is_match = remote_version == version if is_stable else remote_version.startswith(version) - if not is_match: - notify_update_availabe(remote_version, version) - else: - print("No updates available.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/meta-opencentauri/recipes-data/config-manager/files/config_manager.py b/meta-opencentauri/recipes-data/config-manager/files/config_manager.py index 8378fc2e..6588459c 100644 --- a/meta-opencentauri/recipes-data/config-manager/files/config_manager.py +++ b/meta-opencentauri/recipes-data/config-manager/files/config_manager.py @@ -9,7 +9,6 @@ }, 'update': { 'release': ['stable', 'nightly'], - 'check_for_updates': ['True', 'False'], }, 'extras': { 'elegoo_canvas': ['True', 'False'], diff --git a/meta-opencentauri/recipes-data/config-manager/files/default.conf b/meta-opencentauri/recipes-data/config-manager/files/default.conf index dabb12d7..dfae0aef 100644 --- a/meta-opencentauri/recipes-data/config-manager/files/default.conf +++ b/meta-opencentauri/recipes-data/config-manager/files/default.conf @@ -13,8 +13,6 @@ screen_brightness = 100 [update] # Update channel used by the COSMOS updater. Options: stable, nightly. release = stable -# Check for updates automatically on startup. Options: True, False. -check_for_updates = True [extras] # Enable support for the CANVAS unit. Also enables the AFC subsystem. diff --git a/meta-opencentauri/recipes-data/update-scripts/files/cosmos-update-start b/meta-opencentauri/recipes-data/update-scripts/files/cosmos-update-start deleted file mode 100755 index 390afefb..00000000 --- a/meta-opencentauri/recipes-data/update-scripts/files/cosmos-update-start +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -# Start a COSMOS update through Moonraker's update_manager and return at once. -# Every UI that speaks the update_manager API then shows the progress; the -# update itself runs under Moonraker, outside Klipper's command queue. -curl -s -X POST "http://localhost/machine/update/client?name=cosmos" >/dev/null 2>&1 & -exit 0 diff --git a/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb b/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb index d5cfff69..56e4b53b 100644 --- a/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb +++ b/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb @@ -5,7 +5,6 @@ LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda SRC_URI = " \ file://factory-reset \ file://update-cosmos \ - file://cosmos-update-start \ file://switch-to-stock \ file://switch-to-oc-patched \ file://swu-decrypt.py \ @@ -27,7 +26,6 @@ do_install() { install -d ${D}${bindir} install -m 0755 ${WORKDIR}/factory-reset ${D}${bindir}/ install -m 0755 ${WORKDIR}/update-cosmos ${D}${bindir}/ - install -m 0755 ${WORKDIR}/cosmos-update-start ${D}${bindir}/ install -m 0755 ${WORKDIR}/switch-to-stock ${D}${bindir}/ install -m 0755 ${WORKDIR}/switch-to-oc-patched ${D}${bindir}/ install -m 0755 ${WORKDIR}/swu-decrypt.py ${D}${bindir}/ @@ -41,7 +39,6 @@ do_install() { FILES_${PN} += " \ ${bindir}/factory-reset \ ${bindir}/update-cosmos \ - ${bindir}/cosmos-update-start \ ${bindir}/switch-to-stock \ ${bindir}/switch-to-oc-patched \ ${bindir}/swu-decrypt.py \ From ca903e3e36ffe7c28065a59409014a6f1892266d Mon Sep 17 00:00:00 2001 From: Kyle Krenzer Date: Sat, 5 Sep 2026 00:45:59 -0700 Subject: [PATCH 3/3] Hide the klipper/moonraker placeholders with a Moonraker patch, not from the component update_manager always creates klipper and moonraker entries; on this image neither is a git checkout, so they are bare BaseDeploy placeholders whose status is an empty dict and the UIs show them as empty rows. The component used to pop them from update_manager's table and re-pop them on timers after every Klippy connect. Replace that with a two-hunk patch to update_manager that leaves entries without a status out of the status response and the refreshed notification. The component now only registers the cosmos updater. Also use calendar.timegm for the commit dates and drop the duplicate refresh_interval from the cosmos_update section. Co-Authored-By: Claude Fable 5.1 --- ...anager-skip-placeholder-applications.patch | 48 +++++++++++++++++++ .../moonraker/files/cosmos_update.py | 45 ++++------------- .../moonraker/files/moonraker-readonly.conf | 3 +- .../moonraker/moonraker_0.10.0.bb | 1 + 4 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 meta-opencentauri/recipes-apps/moonraker/files/0001-update_manager-skip-placeholder-applications.patch diff --git a/meta-opencentauri/recipes-apps/moonraker/files/0001-update_manager-skip-placeholder-applications.patch b/meta-opencentauri/recipes-apps/moonraker/files/0001-update_manager-skip-placeholder-applications.patch new file mode 100644 index 00000000..fee13ca4 --- /dev/null +++ b/meta-opencentauri/recipes-apps/moonraker/files/0001-update_manager-skip-placeholder-applications.patch @@ -0,0 +1,48 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Kyle +Date: Sat, 5 Sep 2026 00:45:11 -0700 +Subject: [PATCH] update_manager: leave placeholder applications out of the + status + +The klipper and moonraker entries are created even when the installed +copies are not git repositories, zips or python packages, in which case +they are bare BaseDeploy placeholders whose status is an empty dict. The +UIs render those as empty rows. Skip entries with no status in the status +response and in the refreshed notification. +--- + .../components/update_manager/update_manager.py | 12 ++++++++++-- + 1 file changed, 10 insertions(+), 2 deletions(-) + +diff --git a/moonraker/components/update_manager/update_manager.py b/moonraker/components/update_manager/update_manager.py +index cd580c9..1093349 100644 +--- a/moonraker/components/update_manager/update_manager.py ++++ b/moonraker/components/update_manager/update_manager.py +@@ -422,7 +422,12 @@ class UpdateManager: + for name, updater in list(self.updaters.items()): + if check_refresh: + await updater.refresh() +- vinfo[name] = updater.get_update_status() ++ status = updater.get_update_status() ++ if not status: ++ # Placeholder for an application that cannot be ++ # updated from here, nothing to report ++ continue ++ vinfo[name] = status + except Exception: + raise + finally: +@@ -636,7 +641,10 @@ class CommandHelper: + def notify_update_refreshed(self) -> None: + vinfo: Dict[str, Any] = {} + for name, updater in self.get_updaters().items(): +- vinfo[name] = updater.get_update_status() ++ status = updater.get_update_status() ++ if not status: ++ continue ++ vinfo[name] = status + uinfo = self.get_rate_limit_stats() + uinfo['version_info'] = vinfo + uinfo['busy'] = self.is_update_busy() +-- +2.50.1 (Apple Git-155) + diff --git a/meta-opencentauri/recipes-apps/moonraker/files/cosmos_update.py b/meta-opencentauri/recipes-apps/moonraker/files/cosmos_update.py index 0cbc9e9b..1d2ca144 100644 --- a/meta-opencentauri/recipes-apps/moonraker/files/cosmos_update.py +++ b/meta-opencentauri/recipes-apps/moonraker/files/cosmos_update.py @@ -3,8 +3,8 @@ # Registers a "cosmos" entry with update_manager so Mainsail, Fluidd and any # other client that speaks the update_manager API can see the installed and # latest COSMOS version, start an update, and follow its progress through the -# standard notify_update_response stream. No changes to update_manager itself -# are needed: the updater is added to its table from this component. +# standard notify_update_response stream. The updater is added to +# update_manager's table from this component. # # Configuration (moonraker.conf): # @@ -19,11 +19,11 @@ # This file may be distributed under the terms of the GNU GPLv3 license. from __future__ import annotations +import calendar import configparser -import logging import pathlib import time -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List from .update_manager.base_deploy import BaseDeploy @@ -82,9 +82,6 @@ async def _read_local_state(self) -> None: channel = "stable" self.channel = channel or "stable" - def _api(self): - return self.cmd_helper.get_http_client() - async def refresh(self) -> None: await self._read_local_state() self.last_error = "" @@ -99,7 +96,7 @@ async def refresh(self) -> None: self._save_state() async def _refresh_stable(self) -> None: - resp = await self._api().github_api_request( + resp = await self.cmd_helper.get_http_client().github_api_request( f"repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases?per_page=10" ) if resp.has_error(): @@ -119,7 +116,7 @@ async def _refresh_stable(self) -> None: async def _refresh_nightly(self) -> None: # Nightly builds are identified by a short commit hash - resp = await self._api().github_api_request( + resp = await self.cmd_helper.get_http_client().github_api_request( f"repos/{GITHUB_OWNER}/{GITHUB_REPO}/compare/{self.version}...main" ) if resp.has_error(): @@ -132,8 +129,9 @@ async def _refresh_nightly(self) -> None: author = commit.get("author", {}) msg = commit.get("message", "") try: - date = time.mktime(time.strptime( - author.get("date", ""), "%Y-%m-%dT%H:%M:%SZ")) - time.timezone + date = calendar.timegm( + time.strptime(author.get("date", ""), "%Y-%m-%dT%H:%M:%SZ") + ) except Exception: date = 0 behind.append({ @@ -217,35 +215,10 @@ def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() um: UpdateManager = self.server.load_component(config, "update_manager") updaters = um.get_updaters() - # Neither Klipper nor Moonraker is a git checkout on this image, so - # update_manager holds placeholder entries for them that show up as - # empty rows in the UIs. Drop them; COSMOS updates both anyway. - for name in ("klipper", "moonraker"): - if type(updaters.get(name)) is BaseDeploy: - updaters.pop(name, None) if "cosmos" in updaters: raise config.error("update_manager already has a 'cosmos' entry") self.deploy = CosmosDeploy(config) updaters["cosmos"] = self.deploy - # update_manager re-creates its klipper entry (as a background task) - # every time Klippy connects, so prune the placeholder again shortly - # after that happens. - self.server.register_event_handler( - "server:klippy_identified", self._schedule_prune - ) - logging.info("cosmos_update: registered COSMOS updater with update_manager") - - def _schedule_prune(self) -> None: - loop = self.server.get_event_loop() - loop.delay_callback(2., self._prune_placeholders) - loop.delay_callback(15., self._prune_placeholders) - - def _prune_placeholders(self, eventtime: float = 0.) -> None: - um: UpdateManager = self.server.lookup_component("update_manager") - updaters = um.get_updaters() - for name in ("klipper", "moonraker"): - if type(updaters.get(name)) is BaseDeploy: - updaters.pop(name, None) def load_component(config: ConfigHelper) -> CosmosUpdate: diff --git a/meta-opencentauri/recipes-apps/moonraker/files/moonraker-readonly.conf b/meta-opencentauri/recipes-apps/moonraker/files/moonraker-readonly.conf index 7f32be0b..28a57819 100644 --- a/meta-opencentauri/recipes-apps/moonraker/files/moonraker-readonly.conf +++ b/meta-opencentauri/recipes-apps/moonraker/files/moonraker-readonly.conf @@ -48,6 +48,5 @@ enable_auto_refresh: True enable_system_updates: False refresh_interval: 24 -# COSMOS firmware updates through update_manager (see cosmos_update.py) +# COSMOS firmware updates through update_manager (cosmos_update.py) [cosmos_update] -refresh_interval: 24 diff --git a/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb b/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb index c4caabd8..1532b44e 100644 --- a/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb +++ b/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb @@ -16,6 +16,7 @@ SRC_URI = " \ file://cosmos_update.py;subdir=git/moonraker/components \ file://0001-Serve-static-files.patch \ file://0001-Reduce-log-rotate-threshold.patch \ + file://0001-update_manager-skip-placeholder-applications.patch \ " SRCREV = "16e530eb663218faa6ccd97ffb0583f1880e2983"