mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-08-22 14:54:19 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2a083fb94 | ||
|
|
1bbeef7a47 | ||
|
|
40ec163134 | ||
|
|
61cfd3f5a9 | ||
|
|
1e35c48be1 | ||
|
|
b16f4b3cac | ||
|
|
58400f16cf | ||
|
|
6f281a0cab | ||
|
|
4db0a7d728 |
@@ -81,5 +81,12 @@ class _Settings:
|
|||||||
def CLOUD_SYNC_DAYS(self):
|
def CLOUD_SYNC_DAYS(self):
|
||||||
return get_setting("BAMBU_RUN_CLOUD_SYNC_DAYS", 30)
|
return get_setting("BAMBU_RUN_CLOUD_SYNC_DAYS", 30)
|
||||||
|
|
||||||
|
# Seconds of silence on the MQTT report topic that, once broken by a new
|
||||||
|
# message, is treated as "the printer was probably offline" and triggers
|
||||||
|
# a pushall re-sync instead of trusting the partial delta to fill in stale
|
||||||
|
# fields left over from before the gap.
|
||||||
|
@property
|
||||||
|
def MQTT_RESYNC_GAP_SECONDS(self):
|
||||||
|
return get_setting("BAMBU_RUN_MQTT_RESYNC_GAP_SECONDS", 90)
|
||||||
|
|
||||||
app_settings = _Settings()
|
app_settings = _Settings()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import select
|
import select
|
||||||
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -486,15 +487,29 @@ class PrinterState:
|
|||||||
|
|
||||||
wifi_signal = print_data.get("wifi_signal", "")
|
wifi_signal = print_data.get("wifi_signal", "")
|
||||||
|
|
||||||
# H2C dual-nozzle decoding. The H2C reports per-extruder temperatures
|
# Dual-nozzle decoding (H2C, X2D). Dual-nozzle printers report per-extruder
|
||||||
# under `print.device.extruder.info[]` as a 2-element array (index 0 =
|
# temperatures under `print.device.extruder.info[]` as a 2-element array
|
||||||
# right, index 1 = left). The `temp` field is bit-packed:
|
# (index 0 = right, index 1 = left). The `temp` field is bit-packed:
|
||||||
# `temp_raw = (target << 16) | current`, both °C as ints.
|
# `temp_raw = (target << 16) | current`, both °C as ints.
|
||||||
|
#
|
||||||
|
# The legacy top-level `nozzle_temper`/`nozzle_target_temper` fields track
|
||||||
|
# whichever nozzle is currently *active*, not specifically the right one —
|
||||||
|
# on printers like the X2D they report the left nozzle's temp while it's
|
||||||
|
# printing, even though the dashboard's "Right Nozzle" card reads them.
|
||||||
|
# Prefer the decoded right-side value from extruder.info[0] when present.
|
||||||
nozzle_temp_left = None
|
nozzle_temp_left = None
|
||||||
nozzle_target_temp_left = None
|
nozzle_target_temp_left = None
|
||||||
|
nozzle_temp_right = None
|
||||||
|
nozzle_target_temp_right = None
|
||||||
device = print_data.get("device") or {}
|
device = print_data.get("device") or {}
|
||||||
extruders = (device.get("extruder") or {}).get("info") or []
|
extruders = (device.get("extruder") or {}).get("info") or []
|
||||||
if len(extruders) >= 2:
|
if len(extruders) >= 2:
|
||||||
|
right = extruders[0]
|
||||||
|
t = right.get("temp")
|
||||||
|
if isinstance(t, int):
|
||||||
|
nozzle_target_temp_right = float((t >> 16) & 0xFFFF)
|
||||||
|
nozzle_temp_right = float(t & 0xFFFF)
|
||||||
|
|
||||||
left = extruders[1]
|
left = extruders[1]
|
||||||
t = left.get("temp")
|
t = left.get("temp")
|
||||||
if isinstance(t, int):
|
if isinstance(t, int):
|
||||||
@@ -510,8 +525,14 @@ class PrinterState:
|
|||||||
return cls(
|
return cls(
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
sequence_id=str(print_data.get("sequence_id", "")),
|
sequence_id=str(print_data.get("sequence_id", "")),
|
||||||
nozzle_temp=float(print_data.get("nozzle_temper", 0.0)),
|
nozzle_temp=(
|
||||||
nozzle_target_temp=float(print_data.get("nozzle_target_temper", 0.0)),
|
nozzle_temp_right if nozzle_temp_right is not None
|
||||||
|
else float(print_data.get("nozzle_temper", 0.0))
|
||||||
|
),
|
||||||
|
nozzle_target_temp=(
|
||||||
|
nozzle_target_temp_right if nozzle_target_temp_right is not None
|
||||||
|
else float(print_data.get("nozzle_target_temper", 0.0))
|
||||||
|
),
|
||||||
bed_temp=float(print_data.get("bed_temper", 0.0)),
|
bed_temp=float(print_data.get("bed_temper", 0.0)),
|
||||||
bed_target_temp=float(print_data.get("bed_target_temper", 0.0)),
|
bed_target_temp=float(print_data.get("bed_target_temper", 0.0)),
|
||||||
chamber_temp=float(print_data.get("chamber_temper", 0.0)),
|
chamber_temp=float(print_data.get("chamber_temper", 0.0)),
|
||||||
@@ -809,6 +830,7 @@ class BambuPrinter:
|
|||||||
self._accumulator = PrinterStateAccumulator()
|
self._accumulator = PrinterStateAccumulator()
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._devices: List[Dict[str, Any]] = []
|
self._devices: List[Dict[str, Any]] = []
|
||||||
|
self._last_message_at: Optional[float] = None
|
||||||
|
|
||||||
def _get_fresh_token(self, verification_code_timeout: int = 300) -> str:
|
def _get_fresh_token(self, verification_code_timeout: int = 300) -> str:
|
||||||
"""Get a fresh token using credentials."""
|
"""Get a fresh token using credentials."""
|
||||||
@@ -914,6 +936,30 @@ class BambuPrinter:
|
|||||||
"""Internal MQTT message handler"""
|
"""Internal MQTT message handler"""
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# The printer publishes partial deltas most of the time; the accumulator
|
||||||
|
# merges them onto whatever it already has. If the printer went offline
|
||||||
|
# (power cycle) and just reconnected, the first delta after the gap would
|
||||||
|
# otherwise be merged onto stale pre-outage state, leaving fields like
|
||||||
|
# nozzle_temp frozen at their last value until some unrelated full report
|
||||||
|
# happens to refresh them. Detect the gap and force a full pushall so the
|
||||||
|
# accumulator gets a clean, complete state instead.
|
||||||
|
now = time.time()
|
||||||
|
if (
|
||||||
|
self._last_message_at is not None
|
||||||
|
and now - self._last_message_at > app_settings.MQTT_RESYNC_GAP_SECONDS
|
||||||
|
and self._mqtt is not None
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
self._mqtt.request_full_status()
|
||||||
|
logger.info(
|
||||||
|
"MQTT report gap of %.0fs detected for %s; requested full status re-sync",
|
||||||
|
now - self._last_message_at, device_id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Full status re-sync request failed (non-fatal): %s", e)
|
||||||
|
self._last_message_at = now
|
||||||
|
|
||||||
state = self._accumulator.update(data)
|
state = self._accumulator.update(data)
|
||||||
if self._on_update:
|
if self._on_update:
|
||||||
self._on_update(state)
|
self._on_update(state)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end">
|
<div class="col-md-4 text-end">
|
||||||
{% if not is_basic_user %}
|
{% if not is_basic_user %}
|
||||||
<a href="{% url 'bambu_run:filament_color_create' %}" class="btn btn-primary">
|
<a href="{% url 'bambu_run:filament_color_create' %}" class="btn btn-primary{% if not can_write %} disabled" aria-disabled="true" tabindex="-1" title="Read-only access{% endif %}">
|
||||||
<i class="bi bi-plus-circle"></i> Add New Color
|
<i class="bi bi-plus-circle"></i> Add New Color
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
<td class="align-middle">{{ color.brand }}</td>
|
<td class="align-middle">{{ color.brand }}</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
{% if not is_basic_user %}
|
{% if not is_basic_user %}
|
||||||
<a href="{% url 'bambu_run:filament_color_update' color.pk %}" class="btn btn-sm btn-warning">Edit</a>
|
<a href="{% url 'bambu_run:filament_color_update' color.pk %}" class="btn btn-sm btn-warning{% if not can_write %} disabled" aria-disabled="true" tabindex="-1" title="Read-only access{% endif %}">Edit</a>
|
||||||
<a href="{% url 'bambu_run:filament_color_delete' color.pk %}" class="btn btn-sm btn-danger">Delete</a>
|
<a href="{% url 'bambu_run:filament_color_delete' color.pk %}" class="btn btn-sm btn-danger{% if not can_write %} disabled" aria-disabled="true" tabindex="-1" title="Read-only access{% endif %}">Delete</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
{% if not is_basic_user %}
|
{% if not is_basic_user %}
|
||||||
<a href="{% url 'bambu_run:filament_update' filament.pk %}" class="btn btn-warning">Edit</a>
|
<a href="{% url 'bambu_run:filament_update' filament.pk %}" class="btn btn-warning{% if not can_write %} disabled" aria-disabled="true" tabindex="-1" title="Read-only access{% endif %}">Edit</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'bambu_run:filament_list' %}" class="btn btn-secondary">Back to List</a>
|
<a href="{% url 'bambu_run:filament_list' %}" class="btn btn-secondary">Back to List</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<a href="{% url 'bambu_run:filament_color_list' %}" class="btn btn-outline-info me-2">
|
<a href="{% url 'bambu_run:filament_color_list' %}" class="btn btn-outline-info me-2">
|
||||||
<i class="bi bi-palette"></i> Manage Colors
|
<i class="bi bi-palette"></i> Manage Colors
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'bambu_run:filament_create' %}" class="btn btn-primary">
|
<a href="{% url 'bambu_run:filament_create' %}" class="btn btn-primary{% if not can_write %} disabled" aria-disabled="true" tabindex="-1" title="Read-only access{% endif %}">
|
||||||
<i class="bi bi-plus-circle"></i> Add Filament
|
<i class="bi bi-plus-circle"></i> Add Filament
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<a href="{% url 'bambu_run:filament_detail' filament.pk %}" class="btn btn-sm btn-info">View</a>
|
<a href="{% url 'bambu_run:filament_detail' filament.pk %}" class="btn btn-sm btn-info">View</a>
|
||||||
{% if not is_basic_user %}
|
{% if not is_basic_user %}
|
||||||
<a href="{% url 'bambu_run:filament_update' filament.pk %}" class="btn btn-sm btn-warning">Edit</a>
|
<a href="{% url 'bambu_run:filament_update' filament.pk %}" class="btn btn-sm btn-warning{% if not can_write %} disabled" aria-disabled="true" tabindex="-1" title="Read-only access{% endif %}">Edit</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end">
|
<div class="col-md-4 text-end">
|
||||||
{% if not is_basic_user %}
|
{% if not is_basic_user %}
|
||||||
<a href="{% url 'bambu_run:filament_type_create' %}" class="btn btn-primary">
|
<a href="{% url 'bambu_run:filament_type_create' %}" class="btn btn-primary{% if not can_write %} disabled" aria-disabled="true" tabindex="-1" title="Read-only access{% endif %}">
|
||||||
<i class="bi bi-plus-circle"></i> Add New Type
|
<i class="bi bi-plus-circle"></i> Add New Type
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -63,8 +63,8 @@
|
|||||||
<td class="align-middle">{{ ft.brand }}</td>
|
<td class="align-middle">{{ ft.brand }}</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
{% if not is_basic_user %}
|
{% if not is_basic_user %}
|
||||||
<a href="{% url 'bambu_run:filament_type_update' ft.pk %}" class="btn btn-sm btn-warning">Edit</a>
|
<a href="{% url 'bambu_run:filament_type_update' ft.pk %}" class="btn btn-sm btn-warning{% if not can_write %} disabled" aria-disabled="true" tabindex="-1" title="Read-only access{% endif %}">Edit</a>
|
||||||
<a href="{% url 'bambu_run:filament_type_delete' ft.pk %}" class="btn btn-sm btn-danger">Delete</a>
|
<a href="{% url 'bambu_run:filament_type_delete' ft.pk %}" class="btn btn-sm btn-danger{% if not can_write %} disabled" aria-disabled="true" tabindex="-1" title="Read-only access{% endif %}">Delete</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -28,7 +28,13 @@ _METRICS_API_FIELDS = [
|
|||||||
'gcode_state', 'print_type', 'subtask_name',
|
'gcode_state', 'print_type', 'subtask_name',
|
||||||
'external_spool',
|
'external_spool',
|
||||||
]
|
]
|
||||||
_MAX_CHART_POINTS = 3000
|
# 24h at the collector's 30s cadence is ~2800 readings, and every one of them
|
||||||
|
# also drags in ~9 FilamentSnapshot rows — that snapshot fetch, not the metrics
|
||||||
|
# query, is what dominated the dashboard's load time (measured: 2.09s of context
|
||||||
|
# building and a 410 KB payload at 3000). 1440 caps the series at roughly one
|
||||||
|
# point per minute over a day, which is finer than any chart can resolve on
|
||||||
|
# screen, and cuts both the server time and the payload by ~4x.
|
||||||
|
_MAX_CHART_POINTS = 1440
|
||||||
# Fallback window for requests that don't specify a full date range. Without it a
|
# Fallback window for requests that don't specify a full date range. Without it a
|
||||||
# bare API call scans the entire metrics table.
|
# bare API call scans the entire metrics table.
|
||||||
_DEFAULT_WINDOW = timedelta(hours=24)
|
_DEFAULT_WINDOW = timedelta(hours=24)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "bambu-run"
|
name = "bambu-run"
|
||||||
version = "0.1.10"
|
version = "0.1.13"
|
||||||
description = "Django reusable app for Bambu Lab 3D printer monitoring and filament inventory management"
|
description = "Django reusable app for Bambu Lab 3D printer monitoring and filament inventory management"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|||||||
Reference in New Issue
Block a user