实现下位机版本检测升级功能以及优化断电续打
# Conflicts: # config/moonraker.conf resolved by f5461563b8a7a5a25470a8763b8513779dbff253 version # scripts/sync_dependencies.py resolved by f5461563b8a7a5a25470a8763b8513779dbff253 version
This commit is contained in:
parent
8633a9e349
commit
08648da7a4
@ -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") %}
|
||||
|
206
moonraker/components/firmware_manager.py
Normal file
206
moonraker/components/firmware_manager.py
Normal 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)
|
1201
moonraker/components/flash_tool.py
Normal file
1201
moonraker/components/flash_tool.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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]:
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user