实现下位机版本检测升级功能以及优化断电续打

# Conflicts:
#	config/moonraker.conf   resolved by f5461563b8a7a5a25470a8763b8513779dbff253 version
#	scripts/sync_dependencies.py   resolved by f5461563b8a7a5a25470a8763b8513779dbff253 version
This commit is contained in:
张开科 2025-05-10 16:36:55 +08:00
parent 8633a9e349
commit 08648da7a4
6 changed files with 1466 additions and 34 deletions

View File

@ -23,6 +23,8 @@ cors_domains:
[octoprint_compat]
[firmware_manager]
[history]
[power Printer]
@ -58,41 +60,34 @@ pin: ^!gpiochip4/gpio19
debounce_period: .01
minimum_event_time: 0
on_press:
{% set server_info = call_method("server.info") %}
{% set server_info_data = server_info | tojson | fromjson %}
{% if server_info_data['klippy_state'] == "ready" %}
{% set query_objs = {"print_stats": ["state"], "toolhead": ["extruder"], "gcode_move": ["gcode_position"], "virtual_sdcard": ["file_path"]} %}
{% do call_method("printer.gcode.script", script="TURN_OFF_HEATERS") %}
{% do call_method("printer.gcode.script", script="GET_TASKLINE") %}
{% set query_objs = {"print_stats": ["state"], "toolhead": ["extruder"], "virtual_sdcard": ["file_path", "file_position", "file_line"]} %}
{% set status = call_method("printer.objects.query", objects=query_objs) %}
{% do call_method("printer.emergency_stop") %}
{% set data = status | tojson | fromjson %}
{% set print_state = data['status']['print_stats']['state'] %}
# Judging the printer status
{% if print_state | string == 'printing' or print_state | string == 'paused' %}
{% set x_position = data['status']['gcode_move']['gcode_position'][0] | round(4) %}
{% set y_position = data['status']['gcode_move']['gcode_position'][1] | round(4) %}
{% set z_position = data['status']['gcode_move']['gcode_position'][2] | round(4) %}
{% set hotend = data['status']['toolhead']['extruder'] %}
{% set position = data['status']['virtual_sdcard']['file_position'] %}
{% set line = data['status']['virtual_sdcard']['file_line'] %}
{% set filepath = data['status']['virtual_sdcard']['file_path'] %}
{% set filename = filepath.split('/')[-1] %}
# save position
{% if print_state | string == 'printing' %}
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=power_resume_x VALUE=" + x_position | string) %}
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=power_resume_y VALUE=" + y_position | string) %}
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=power_resume_z VALUE=" + z_position | string) %}
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=power_loss_paused VALUE=False") %}
{% elif print_state | string == 'paused' %}
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=power_loss_paused VALUE=True") %}
{% endif%}
# save file position and line
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=power_resume_position VALUE=" + position | string) %}
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=power_resume_line VALUE=" + line | string) %}
{% set script = "SAVE_VARIABLE VARIABLE=power_loss_paused VALUE=" ~ ("False" if print_state | string == 'printing' else "True") %}
{% if print_state | string in ['printing', 'paused'] %}
{% do call_method("printer.gcode.script", script=script) %}
{% endif %}
# save extruder
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=power_resume_extruder VALUE=\"'" + hotend | string + "'\"") %}
# save file
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=filepath VALUE=\"'" + filepath | string + "'\"") %}
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=last_file VALUE=\"'" + filename | string + "'\"") %}
# save interrupt
save interrupt
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=was_interrupted VALUE=True") %}
{% else %}
{% do call_method("printer.gcode.script", script="SAVE_VARIABLE VARIABLE=was_interrupted VALUE=False") %}
{% endif %}
{% endif %}
# shutdown
{% do call_method("machine.shutdown") %}

View File

@ -0,0 +1,206 @@
import os
import logging
import asyncio
from .flash_tool import FlashTool
from ..confighelper import ConfigHelper
from ..common import KlippyState
from .klippy_apis import KlippyAPI
from typing import (
TYPE_CHECKING,
Any,
Dict,
)
if TYPE_CHECKING:
from .machine import Machine
from .klippy_connection import KlippyConnection as Klippy
HOME = os.path.expanduser("~")
KLIPPER_DIR = os.path.join(HOME, "klipper/firmware")
MAIN_DEV = "/dev/ttyACM0"
SYSTEM_PYTHON = "python3"
class FirmwareUpdate:
def __init__(self, config: ConfigHelper) -> None:
self.server = config.get_server()
self.mcu_info: Dict[str, Dict[str, Any]] = {}
self._service_info: Dict[str, Any] = {}
self.min_version: str = ""
self.klipper_version: str = ""
self.current_progress = 0
self.updating = False
self.need_check_update = True
self.server.register_event_handler(
"server:klippy_started", self._on_klippy_startup)
@property
def klippy_apis(self) -> KlippyAPI:
return self.server.lookup_component("klippy_apis")
def is_updating(self) -> bool:
return self.updating
async def _on_klippy_startup(self, state: KlippyState) -> None:
if not self.need_check_update or not self._is_firmware_dir_exists():
return
try:
await self.build_mcu_info()
logging.info(f"{self.mcu_info}")
except Exception as e:
logging.exception(f"An error occurred during the building mcu info process: {e}")
for mcu_data in self.mcu_info.values():
if mcu_data.get('need_update', False):
await self.start_update()
break
async def _do_klipper_action(self, action: str) -> None:
try:
machine: Machine = self.server.lookup_component("machine")
await machine.do_service_action(action, "klipper")
logging.info(f"Klipper service {action}.")
except self.server.error:
pass
def _is_firmware_dir_exists(self):
return os.path.exists(KLIPPER_DIR)
async def start_update(self):
try:
self.updating = True
await self._do_klipper_action("stop")
await self.upgrade_needed_tool_mcus()
await self.upgrade_mcu()
await self._do_klipper_action("start")
self.updating = False
except Exception as e:
logging.exception(f"An error occurred during the update process: {e}")
self.updating = False
async def build_mcu_info(self) -> None:
printer_info: Dict[str, Any] = {}
cfg_status: Dict[str, Any] = {}
try:
printer_info = await self.klippy_apis.get_klippy_info()
cfg_status = await self.klippy_apis.query_objects({'configfile': None})
except self.server.error:
logging.exception("PanelDue initialization request failed")
config = cfg_status.get('configfile', {}).get('config', {})
self.klipper_version = printer_info.get("software_version", "").split('-')[0]
try:
self._build_basic_mcu_info(config)
await self._update_mcu_versions()
self._check_mcu_update_needed()
except Exception as e:
logging.exception(f"An error occurred while building MCU info: {e}")
def _build_basic_mcu_info(self, config: Dict[str, Any]) -> None:
for mcu, value in config.items():
if mcu.startswith("mcu") and "canbus_uuid" in value:
self.mcu_info[mcu] = {"canbus_uuid": value["canbus_uuid"]}
async def _update_mcu_versions(self) -> None:
for mcu in self.mcu_info:
try:
response = await self.klippy_apis.query_objects({mcu: None})
mcu_data = response.get(mcu, {})
mcu_version: str = mcu_data.get('mcu_version', '')
if mcu == "mcu":
self.min_version = mcu_data.get('min_firmware_version', "")
if mcu_version:
self.need_check_update = False
short_version: str = mcu_version.split('-')[0]
if short_version.lower().startswith('v'):
short_version = short_version[1:]
self.mcu_info[mcu]['mcu_version'] = short_version
except Exception as e:
logging.error(f"Error querying {mcu}: {e}")
def _compare_versions(self, version1, version2):
v1_parts = [int(part) for part in version1.split('.') if part]
v2_parts = [int(part) for part in version2.split('.') if part]
max_length = max(len(v1_parts), len(v2_parts))
v1_parts.extend([0] * (max_length - len(v1_parts)))
v2_parts.extend([0] * (max_length - len(v2_parts)))
for i in range(max_length):
if v1_parts[i] < v2_parts[i]:
return -1
elif v1_parts[i] > v2_parts[i]:
return 1
return 0
def _check_mcu_update_needed(self) -> None:
if self.klipper_version.lower().startswith('v'):
self.klipper_version = self.klipper_version[1:]
logging.info(f"min version: {self.min_version}")
logging.info(f"klipper version: {self.klipper_version}")
for mcu in self.mcu_info:
mcu_version = self.mcu_info[mcu].get('mcu_version', "")
mcu_vs_min = self._compare_versions(mcu_version, self.min_version)
mcu_vs_klipper = self._compare_versions(mcu_version, self.klipper_version)
if mcu_vs_min < 0 or mcu_vs_klipper > 0:
self.mcu_info[mcu]['need_update'] = True
else:
self.mcu_info[mcu]['need_update'] = False
async def upgrade_needed_tool_mcus(self):
firmware_mapping = {
"mcu L_tool": os.path.join(KLIPPER_DIR, "F072_L.bin"),
"mcu R_tool": os.path.join(KLIPPER_DIR, "F072_R.bin"),
"mcu tool": os.path.join(KLIPPER_DIR, "F072_L.bin")
}
for mcu_name, mcu_data in self.mcu_info.items():
if mcu_data.get('need_update', False) and mcu_name != "mcu":
firmware_path = firmware_mapping.get(mcu_name)
if not firmware_path:
logging.warning(f"No firmware specified for {mcu_name}, skipping upgrade.")
continue
try:
flash_tool = FlashTool(
self.server,
uuid = mcu_data['canbus_uuid'],
firmware = firmware_path
)
exit_code = await flash_tool.run()
await asyncio.sleep(1)
if exit_code == 0:
logging.info(f"Upgrade operation for {mcu_name} succeeded, exit code: {exit_code}")
else:
logging.error(f"Upgrade operation for {mcu_name} failed, exit code: {exit_code}")
except Exception as e:
logging.exception(f"An exception occurred during the upgrade process for {mcu_name}: {e}")
async def upgrade_mcu(self):
for mcu_name, mcu_data in self.mcu_info.items():
if mcu_name == "mcu" and mcu_data.get('need_update', False):
try:
flash_tool = FlashTool(
self.server,
uuid=mcu_data['canbus_uuid'],
request_bootloader=True
)
exit_code = await flash_tool.run()
if exit_code != 0:
logging.error(f"Failed to request bootloader for {mcu_name}, exit code: {exit_code}")
continue
await asyncio.sleep(2)
flash_tool = FlashTool(
self.server,
device=MAIN_DEV,
firmware=os.path.join(KLIPPER_DIR, "F446.bin")
)
exit_code = await flash_tool.run()
if exit_code == 0:
logging.info(f"Upgrade operation for {mcu_name} succeeded, exit code: {exit_code}")
else:
logging.error(f"Upgrade operation for {mcu_name} failed, exit code: {exit_code}")
except Exception as e:
logging.exception(f"An exception occurred during the upgrade process for {mcu_name}: {e}")
def load_component(config: ConfigHelper) -> FirmwareUpdate:
return FirmwareUpdate(config)

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,7 @@ if TYPE_CHECKING:
from .http_client import HttpClient
from .klippy_connection import KlippyConnection
from .shell_command import ShellCommandFactory as ShellCommand
from .firmware_manager import FirmwareUpdate
class PrinterPower:
def __init__(self, config: ConfigHelper) -> None:
@ -411,6 +412,12 @@ class PowerDevice:
raise self.server.error(
f"Unable to change power for {self.name} "
"while printing")
updata: FirmwareUpdate
updata = self.server.lookup_component("firmware_manager")
if updata.is_updating():
raise self.server.error(
f"Unable to change power for {self.name} "
"while updating")
ret = self.set_power(req)
if ret is not None:
await ret

View File

@ -258,6 +258,9 @@ class AppDeploy(BaseDeploy):
if svc == "klipper":
kconn: Klippy = self.server.lookup_component("klippy_connection")
svc = kconn.unit_name
firmware_manager = self.server.lookup_component("firmware_manager", None)
if firmware_manager is not None:
firmware_manager.need_check_update = True
await machine.do_service_action("restart", svc)
async def _read_system_dependencies(self) -> List[str]:

View File

@ -101,6 +101,7 @@ class ZeroconfRegistrar:
else:
# Use the UUID. First 8 hex digits should be unique enough
instance_name = f"Moonraker-{instance_uuid[:8]}"
instance_name = "CreatBot"
hi = self.server.get_host_info()
host = self.mdns_name
zc_service_props = {
@ -167,9 +168,9 @@ class ZeroconfRegistrar:
SSDP_ADDR = ("239.255.255.250", 1900)
SSDP_SERVER_ID = "Moonraker SSDP/UPNP Server"
SSDP_SERVER_ID = "CreatBot SSDP/UPNP Server"
SSDP_MAX_AGE = 1800
SSDP_DEVICE_TYPE = "urn:arksine.github.io:device:Moonraker:1"
SSDP_DEVICE_TYPE = "urn:creatbot-com:device:3dprinter:2"
SSDP_DEVICE_XML = """
<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0" configId="{config_id}">
@ -180,12 +181,12 @@ SSDP_DEVICE_XML = """
<device>
<deviceType>{device_type}</deviceType>
<friendlyName>{friendly_name}</friendlyName>
<manufacturer>Arksine</manufacturer>
<manufacturerURL>https://github.com/Arksine/moonraker</manufacturerURL>
<manufacturer>CreatBot</manufacturer>
<manufacturerURL>https://www.creatbot.com</manufacturerURL>
<modelDescription>API Server for Klipper</modelDescription>
<modelName>Moonraker</modelName>
<modelName>CreatBot</modelName>
<modelNumber>{model_number}</modelNumber>
<modelURL>https://github.com/Arksine/moonraker</modelURL>
<modelURL>https://github.com/CreatBotOfficail</modelURL>
<serialNumber>{serial_number}</serialNumber>
<UDN>uuid:{device_uuid}</UDN>
<presentationURL>{presentation_url}</presentationURL>
@ -197,7 +198,10 @@ class SSDPServer(asyncio.protocols.DatagramProtocol):
def __init__(self, config: ConfigHelper) -> None:
self.server = config.get_server()
self.unique_id = uuid.UUID(self.server.get_app_args()["instance_uuid"])
self.name: str = "Moonraker"
self.name: str = "CreatBot"
machine: Machine = self.server.lookup_component("machine")
self.serial_number = machine.get_system_info().get("cpu_info", {}).get("serial_number", "N/A")
self.serial_number = self.serial_number[1:].upper()
self.base_url: str = ""
self.response_headers: List[str] = []
self.registered: bool = False
@ -280,10 +284,18 @@ class SSDPServer(asyncio.protocols.DatagramProtocol):
if len(name) > 64:
name = name[:64]
self.name = name
model = self.name
device_name = self.name
if "(" in self.name:
model = self.name.split("(", 1)[-1].rsplit("-", 1)[0].rstrip(")")
elif "-" in self.name:
model = self.name.rsplit("-", 1)[0]
if "(" in self.name and ")" in self.name:
device_name = self.name.split("(", 1)[1].split(")", 1)[0]
app: MoonrakerApp = self.server.lookup_component("application")
self.base_url = f"http://{host_name_or_ip}:{port}{app.route_prefix}"
self.base_url = f"http://{host_name_or_ip}"
self.response_headers = [
f"USN: uuid:{self.unique_id}::upnp:rootdevice",
f"USN: uuid:{self.serial_number}::upnp:rootdevice::urn:creatbot-com:device:3dprinter:2",
f"LOCATION: {self.base_url}/server/zeroconf/ssdp",
"ST: upnp:rootdevice",
"EXT:",
@ -291,6 +303,8 @@ class SSDPServer(asyncio.protocols.DatagramProtocol):
f"CACHE-CONTROL: max-age={SSDP_MAX_AGE}",
f"BOOTID.UPNP.ORG: {self.boot_id}",
f"CONFIGID.UPNP.ORG: {self.config_id}",
f"DEVICE-MODEL: {model}",
f"DEVICE-NAME: {device_name}",
]
self.registered = True
advertisements = self._build_notifications("ssdp:alive")
@ -302,14 +316,20 @@ class SSDPServer(asyncio.protocols.DatagramProtocol):
async def _handle_xml_request(self, web_request: WebRequest) -> str:
if not self.registered:
raise self.server.error("Moonraker SSDP Device not registered", 404)
raise self.server.error("CreatBot SSDP Device not registered", 404)
app_args = self.server.get_app_args()
if "(" in self.name:
model = self.name.split("(", 1)[-1].rsplit("-", 1)[0].rstrip(")")
elif "-" in self.name:
model = self.name.rsplit("-", 1)[0]
else:
model = self.name
return SSDP_DEVICE_XML.format(
device_type=SSDP_DEVICE_TYPE,
config_id=str(self.config_id),
friendly_name=self.name,
model_number=app_args["software_version"],
serial_number=self.unique_id.hex,
model_number=model,
serial_number=self.serial_number,
device_uuid=str(self.unique_id),
presentation_url=self.base_url
)
@ -360,7 +380,7 @@ class SSDPServer(asyncio.protocols.DatagramProtocol):
):
# Not a discovery request
return
if headers.get("ST") not in ["upnp:rootdevice", "ssdp:all"]:
if headers.get("ST") not in ["upnp:rootdevice", "ssdp:all", "urn:creatbot-com:device:3dprinter:2",]:
# Service Type doesn't apply
return
if self.response_handle is not None:
@ -390,7 +410,7 @@ class SSDPServer(asyncio.protocols.DatagramProtocol):
notify_types = [
("upnp:rootdevice", f"uuid:{self.unique_id}::upnp:rootdevice"),
(f"uuid:{self.unique_id}", f"uuid:{self.unique_id}"),
(SSDP_DEVICE_TYPE, f"uuid:{self.unique_id}::{SSDP_DEVICE_TYPE}")
(SSDP_DEVICE_TYPE, f"uuid:{self.serial_number}::{SSDP_DEVICE_TYPE}"),
]
for (nt, usn) in notify_types:
notifications.append(