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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ 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
Expand Down
5 changes: 0 additions & 5 deletions meta-opencentauri/recipes-apps/klipper/files/macros.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
22 changes: 0 additions & 22 deletions meta-opencentauri/recipes-apps/klipper/files/shell.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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: update-cosmos
timeout: 500
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 %}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ RDEPENDS:${PN} = " \
kalico-firmware-dsp \
kalico-firmware-toolhead \
kalico-firmware-bed \
check-update \
kalico-firmware-canvas \
"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Kyle <kyle@phiplant.com>
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)

225 changes: 225 additions & 0 deletions meta-opencentauri/recipes-apps/moonraker/files/cosmos_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# 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. The updater is added to
# update_manager's 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 calendar
import configparser
import pathlib
import time
from typing import TYPE_CHECKING, Any, Dict, List

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"

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.cmd_helper.get_http_client().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.cmd_helper.get_http_client().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 = calendar.timegm(
time.strptime(author.get("date", ""), "%Y-%m-%dT%H:%M:%SZ")
)
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()
if "cosmos" in updaters:
raise config.error("update_manager already has a 'cosmos' entry")
self.deploy = CosmosDeploy(config)
updaters["cosmos"] = self.deploy


def load_component(config: ConfigHelper) -> CosmosUpdate:
return CosmosUpdate(config)
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,11 @@ 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 (cosmos_update.py)
[cosmos_update]
5 changes: 3 additions & 2 deletions meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ SRC_URI = " \
file://moonraker-init-d \
file://moonraker.conf \
file://moonraker-readonly.conf \
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"

S = "${WORKDIR}/git"

PR = "r1"
PR = "r2"

inherit python3-dir update-rc.d

Expand Down Expand Up @@ -70,7 +72,6 @@ do_install() {
# Install moonraker python package
install -d ${D}${datadir}/moonraker
cp -r ${S}/moonraker ${D}${datadir}/moonraker/

# Install default moonraker config
install -d ${D}${sysconfdir}/klipper
install -d ${D}${sysconfdir}/klipper/config
Expand Down
24 changes: 0 additions & 24 deletions meta-opencentauri/recipes-data/check-update/check-update_0.1.bb

This file was deleted.

Loading