Skip to content

Commit 1785236

Browse files
committed
Add OTA support.
1 parent 925e611 commit 1785236

File tree

4 files changed

+106
-3
lines changed

4 files changed

+106
-3
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "simplyprint-duet3d"
3-
version = "1.3.10"
3+
version = "1.3.11"
44
description = "SimplyPrint integration with any Duet3D powered RepRapFirmware printers "
55
readme = "README.rst"
66
license-files = ["LICENSE"]

simplyprint_duet3d/ota.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Over-the-air update utilities."""
2+
import os
3+
import subprocess
4+
import sys
5+
from importlib import metadata
6+
7+
8+
def in_virtual_env() -> bool:
9+
"""Detect venvs (venv/virtualenv), pipx venvs, and conda envs."""
10+
# venv/virtualenv
11+
if getattr(sys, "real_prefix", None): # virtualenv sets this
12+
return True
13+
if sys.prefix != getattr(sys, "base_prefix", sys.prefix): # python -m venv
14+
return True
15+
# conda
16+
if os.environ.get("CONDA_PREFIX"):
17+
return True
18+
# pipx typically sets these and runs inside a venv too
19+
if os.environ.get("PIPX_BIN_DIR") or os.environ.get("PIPX_HOME"):
20+
return True
21+
# fallback: VIRTUAL_ENV env var
22+
return bool(os.environ.get("VIRTUAL_ENV"))
23+
24+
25+
def _dist_for_import_name(import_name: str) -> str:
26+
"""Map a top-level import name (module) to its distribution name for pip.
27+
28+
Falls back to the import name if we can't find a better match.
29+
"""
30+
try:
31+
# e.g. {"requests": ["requests"]} or {"Pillow": ["PIL"]}
32+
mapping = metadata.packages_distributions()
33+
top = import_name.split(".")[0]
34+
dists = mapping.get(top, [])
35+
return dists[0] if dists else top
36+
except Exception:
37+
return import_name.split(".")[0]
38+
39+
40+
def self_update(
41+
import_name: str,
42+
version_spec: str | None = None,
43+
allow_system: bool = False,
44+
pre: bool = False,
45+
index_url: str | None = None,
46+
extra_index_url: str | None = None,
47+
) -> int:
48+
"""Update the installed package that provides `import_name` using pip.
49+
50+
Returns the pip exit code. Re-raises CalledProcessError on failure.
51+
52+
- If not in a venv/conda and `allow_system` is False, install with --user.
53+
- Use `version_spec` (e.g., '==2.1.0' or '>=2.1,<3') to pin.
54+
- Set `pre=True` to allow pre-releases.
55+
"""
56+
dist = _dist_for_import_name(import_name)
57+
requirement = dist + (version_spec or "")
58+
59+
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", requirement, "--upgrade-strategy", "only-if-needed"]
60+
if pre:
61+
cmd.append("--pre")
62+
if index_url:
63+
cmd += ["--index-url", index_url]
64+
if extra_index_url:
65+
cmd += ["--extra-index-url", extra_index_url]
66+
67+
if not in_virtual_env() and not allow_system:
68+
# Avoid modifying a global Python; prefer per-user install.
69+
cmd.append("--user")
70+
71+
# On some locked-down images pip might be missing; ensurepip can help.
72+
try:
73+
return subprocess.call(cmd)
74+
except FileNotFoundError:
75+
# Try bootstrapping pip, then retry once.
76+
import ensurepip
77+
ensurepip.bootstrap()
78+
return subprocess.call(cmd)

simplyprint_duet3d/printer.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
FileDemandData,
3030
GcodeDemandData,
3131
MeshDataMsg,
32+
PluginInstallDemandData,
3233
PrinterSettingsMsg,
3334
)
3435
from simplyprint_ws_client.shared.camera.mixin import ClientCameraMixin
@@ -37,7 +38,7 @@
3738

3839
from yarl import URL
3940

40-
from . import __version__
41+
from . import __version__, ota
4142
from .duet.api import RepRapFirmware
4243
from .duet.model import DuetPrinterModel
4344
from .gcode import GCodeBlock
@@ -790,3 +791,27 @@ async def on_resolve_notification(self, data: ResolveNotificationDemandData):
790791
if heater_idx in tool['heaters']:
791792
# Make tool active.
792793
await self.duet.gcode(f"M568 P{tool_idx} A2")
794+
795+
async def on_plugin_install(self, event: PluginInstallDemandData) -> None:
796+
"""Handle plugin installation demand event."""
797+
# XXX: Least thread-safe code on the planet.
798+
plugin = event.plugins.pop()
799+
800+
if plugin.get("type") != "install" and plugin.get("name") != "simplyprint-duet3d":
801+
self.logger.warning(f"Plugin install demand received for {plugin}, but it is not supported.")
802+
return
803+
804+
ret = ota.self_update("simplyprint_duet3d", extra_index_url="https://www.piwheels.org/simple")
805+
806+
if ret == 0:
807+
self.logger.info("Plugin updated successfully, restarting API.")
808+
await self.on_api_restart()
809+
return
810+
811+
await self.push_notification(
812+
severity=NotificationEventSeverity.WARNING,
813+
payload=NotificationEventPayload(
814+
title="Failed to update plugin",
815+
message="An error occurred while updating the SimplyPrint Duet3D plugin. Please check the logs.",
816+
),
817+
)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)