mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-06-24 23:00:20 +01:00
Feature/multi printer support (#12)
* Initial implementation of multi-printer support. * Always show device dropdown and add bambu_diagnose for multi-printer troubleshooting. * Add multi-AMS support: per-unit snapshot/usage tracking, grouped dashboard panels with real type labels, and dual-nozzle card UX fixes. Fixes a real-world AMS info-code parsing bug found by inspecting live H2C data. * Add Vortek hotend rack tracking: per-SN registry with slot mapping confirmed against live MQTT capture, plus a fallback for non-inductive nozzles (e.g. H2C's fixed left nozzle) shown read-only without fabricated identity. New dashboard card hides entirely on printers with no Vortek/nozzle-info data at all.
This commit is contained in:
@@ -14,6 +14,20 @@
|
||||
Real-time monitoring for {{ device_name }}
|
||||
</p>
|
||||
</div>
|
||||
{% if show_printer_switcher %}
|
||||
<div class="col-auto d-flex align-items-center gap-2">
|
||||
<label for="printerSwitcher" class="form-label mb-0 text-nowrap">Device:</label>
|
||||
<select id="printerSwitcher" class="form-select" aria-label="Select printer"
|
||||
onchange="if (this.value) { window.location.href = this.value; }">
|
||||
{% for p in all_printers %}
|
||||
<option value="{% url 'bambu_run:printer_dashboard' pk=p.pk %}"
|
||||
{% if printer_device.pk == p.pk %}selected{% endif %}>
|
||||
{{ p.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
@@ -23,32 +37,38 @@
|
||||
<!-- Summary Cards Row -->
|
||||
<div class="row g-3 mb-4">
|
||||
{% if stats.is_dual_nozzle %}
|
||||
<!-- Right Nozzle (dual-nozzle printers, e.g. H2C) -->
|
||||
<!-- Left Nozzle (dual-nozzle printers, e.g. H2C) -->
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card infra-card-warning">
|
||||
<div class="card infra-card-warning h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="stat-label">Right Nozzle</div>
|
||||
<div class="stat-value">{{ stats.nozzle_temp|floatformat:1 }}°C</div>
|
||||
<div class="text-muted small">target {{ stats.nozzle_target_temp|floatformat:0 }}°C
|
||||
{% if stats.nozzle_type %}· {{ stats.nozzle_type }}{% endif %}</div>
|
||||
<div class="stat-label d-flex align-items-center gap-1">
|
||||
<svg class="icon" style="width: 1.25rem; height: 1.25rem;"><use href="{% static 'bambu_run/vendors/coreui-icons-free.svg' %}#cil-arrow-thick-left"></use></svg>
|
||||
Left Nozzle
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.nozzle_temp_left|floatformat:1 }}°C</div>
|
||||
<div class="text-muted small">target {{ stats.nozzle_target_temp_left|floatformat:0 }}°C
|
||||
{% if stats.nozzle_type_left %}· Nozzle {{ stats.nozzle_type_left }}{% endif %}</div>
|
||||
</div>
|
||||
<i class="bi bi-thermometer-high" style="font-size: 2rem; opacity: 0.3;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Left Nozzle -->
|
||||
<!-- Right Nozzle -->
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card infra-card-warning">
|
||||
<div class="card infra-card-warning h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="stat-label">Left Nozzle</div>
|
||||
<div class="stat-value">{{ stats.nozzle_temp_left|floatformat:1 }}°C</div>
|
||||
<div class="text-muted small">target {{ stats.nozzle_target_temp_left|floatformat:0 }}°C
|
||||
{% if stats.nozzle_type_left %}· {{ stats.nozzle_type_left }}{% endif %}</div>
|
||||
<div class="stat-label d-flex align-items-center gap-1">
|
||||
Right Nozzle
|
||||
<svg class="icon" style="width: 1.25rem; height: 1.25rem;"><use href="{% static 'bambu_run/vendors/coreui-icons-free.svg' %}#cil-arrow-thick-right"></use></svg>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.nozzle_temp|floatformat:1 }}°C</div>
|
||||
<div class="text-muted small">target {{ stats.nozzle_target_temp|floatformat:0 }}°C
|
||||
{% if stats.nozzle_type %}· Nozzle {{ stats.nozzle_type }}{% endif %}</div>
|
||||
</div>
|
||||
<i class="bi bi-thermometer-high" style="font-size: 2rem; opacity: 0.3;"></i>
|
||||
</div>
|
||||
@@ -58,7 +78,7 @@
|
||||
{% else %}
|
||||
<!-- Nozzle Temperature Card (single-nozzle printers) -->
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card infra-card-warning">
|
||||
<div class="card infra-card-warning h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
@@ -74,7 +94,7 @@
|
||||
|
||||
<!-- Bed Temperature Card -->
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card infra-card-danger">
|
||||
<div class="card infra-card-danger h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
@@ -89,7 +109,7 @@
|
||||
|
||||
<!-- Print Progress Card -->
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card infra-card-info">
|
||||
<div class="card infra-card-info h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
@@ -104,7 +124,7 @@
|
||||
|
||||
<!-- Chamber Light Card -->
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card {% if stats.chamber_light == 'on' %}infra-card-success{% else %}infra-card-secondary{% endif %}">
|
||||
<div class="card h-100 {% if stats.chamber_light == 'on' %}infra-card-success{% else %}infra-card-secondary{% endif %}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
@@ -184,34 +204,59 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if stats.filaments %}
|
||||
<div class="row g-3">
|
||||
{% for filament in stats.filaments %}
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card filament-card" data-filament-color="{{ filament.color|slice:':6' }}"{% if filament.is_transparent %} data-filament-transparent="true"{% endif %}>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Tray {{ filament.tray_id }}</h6>
|
||||
{% if filament.filament_pk %}
|
||||
<a href="{% url 'bambu_run:filament_detail' filament.filament_pk %}" class="text-decoration-none" title="View in inventory">
|
||||
<svg class="icon icon-sm text-body-secondary"><use href="{% static 'bambu_run/vendors/coreui-icons-free.svg' %}#cil-external-link"></use></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="mb-1 small"><strong>{{ filament.type }}</strong> - {{ filament.brand }}</p>
|
||||
{% if filament.color_name %}<p class="mb-1 small text-body-secondary">{{ filament.color_name }}</p>{% endif %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="small">Remaining</span>
|
||||
<span class="badge filament-badge">{{ filament.remain_percent }}%</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px; background-color: rgba(0,0,0,0.1);">
|
||||
<div class="progress-bar filament-progress" role="progressbar" style="width: {{ filament.remain_percent }}%;" aria-valuenow="{{ filament.remain_percent }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
{% if stats.ams_units|length > 1 %}
|
||||
<div class="ams-filter-pills mb-3" id="amsFilterPills">
|
||||
<button type="button" class="btn ams-filter-pill active" data-ams-filter="all">All</button>
|
||||
{% for unit in stats.ams_units %}
|
||||
<button type="button" class="btn ams-filter-pill ams-badge-{{ unit.ams_type|slugify }}" data-ams-filter="{{ unit.ams_unit_id }}">{{ unit.ams_type|default:"AMS" }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="ams-groups">
|
||||
{% for group in stats.ams_groups %}
|
||||
<div class="ams-group ams-badge-bg-{{ group.ams_type|slugify }} {% if group.filaments|length > 1 %}ams-group--wide{% else %}ams-group--compact{% endif %}" data-ams-unit-id="{{ group.unit_id }}">
|
||||
<div class="ams-group-header d-flex justify-content-between align-items-center mb-2">
|
||||
<strong class="small">{{ group.label }}</strong>
|
||||
{% if group.humidity is not None or group.temp is not None %}
|
||||
<span class="small text-body-secondary">
|
||||
{% if group.humidity is not None %}{{ group.humidity }}%RH{% endif %}
|
||||
{% if group.temp is not None %}· {{ group.temp }}°C{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
{% for filament in group.filaments %}
|
||||
<div class="col-12 {% if group.filaments|length > 1 %}col-md-6 col-lg-3{% endif %}">
|
||||
<div class="card filament-card" data-filament-color="{{ filament.color|slice:':6' }}"{% if filament.is_transparent %} data-filament-transparent="true"{% endif %}>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Tray {{ filament.tray_id }}</h6>
|
||||
{% if filament.filament_pk %}
|
||||
<a href="{% url 'bambu_run:filament_detail' filament.filament_pk %}" class="text-decoration-none" title="View in inventory">
|
||||
<svg class="icon icon-sm text-body-secondary"><use href="{% static 'bambu_run/vendors/coreui-icons-free.svg' %}#cil-external-link"></use></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="mb-1 small"><strong>{{ filament.type }}</strong> - {{ filament.brand }}</p>
|
||||
{% if filament.color_name %}<p class="mb-1 small text-body-secondary">{{ filament.color_name }}</p>{% endif %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="small">Remaining</span>
|
||||
<span class="badge filament-badge">{{ filament.remain_percent }}%</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px; background-color: rgba(0,0,0,0.1);">
|
||||
<div class="progress-bar filament-progress" role="progressbar" style="width: {{ filament.remain_percent }}%;" aria-valuenow="{{ filament.remain_percent }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if stats.external_spool.type %}
|
||||
{% if stats.external_spool.type %}
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card filament-card" data-filament-color="{{ stats.external_spool.color|slice:':6' }}">
|
||||
<div class="card-body">
|
||||
@@ -227,8 +272,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-body-secondary">No filament data available</p>
|
||||
{% endif %}
|
||||
@@ -237,6 +282,64 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hotends Section (Vortek rack + any plain/non-inductive nozzles) -->
|
||||
{% if stats.hotends or stats.nozzle_positions %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Hotends</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
{% for hotend in stats.hotends %}
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card filament-card" data-filament-color="{{ hotend.last_color|default:'888888' }}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">
|
||||
{% if hotend.is_toolhead %}Toolhead{% elif hotend.slot_number %}Slot {{ hotend.slot_number }}{% else %}Rack{% endif %}
|
||||
</h6>
|
||||
{% if hotend.is_toolhead %}<span class="badge filament-badge">Toolhead</span>{% endif %}
|
||||
</div>
|
||||
<p class="mb-1 small text-body-secondary">SN {{ hotend.serial_number }}</p>
|
||||
<p class="mb-1 small"><strong>{{ hotend.nozzle_type }}</strong>{% if hotend.diameter %} · {{ hotend.diameter }}mm{% endif %}</p>
|
||||
{% if hotend.last_filament_profile_id %}<p class="mb-1 small text-body-secondary">Last: {{ hotend.last_filament_profile_id }}</p>{% endif %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="small">Used time</span>
|
||||
<span class="small">{{ hotend.used_time_display }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="small">Wear</span>
|
||||
<span class="badge filament-badge">{{ hotend.wear_percent|floatformat:0 }}%</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px; background-color: rgba(0,0,0,0.1);">
|
||||
<div class="progress-bar filament-progress" role="progressbar" style="width: {{ hotend.wear_percent }}%;" aria-valuenow="{{ hotend.wear_percent }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for nozzle in stats.nozzle_positions %}
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">{% if nozzle.is_toolhead %}Toolhead{% else %}Fixed Nozzle{% endif %}</h6>
|
||||
</div>
|
||||
<p class="mb-1 small"><strong>{{ nozzle.nozzle_type }}</strong>{% if nozzle.diameter %} · {{ nozzle.diameter }}mm{% endif %}</p>
|
||||
<p class="mb-0 small text-body-secondary">No induction chip data</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Date/Time Filter Controls -->
|
||||
{% if not is_basic_user %}
|
||||
<div class="row mb-4">
|
||||
@@ -423,12 +526,12 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1"></script>
|
||||
<script src="{% static 'bambu_run/js/printer_charts.js' %}"></script>
|
||||
<script src="{% static 'bambu_run/js/printer_charts_control.js' %}"></script>
|
||||
{% if not is_basic_user %}
|
||||
<div id="printerApiUrl" data-url="{% url 'bambu_run:printer_api' %}" style="display: none;"></div>
|
||||
{% if not is_basic_user and printer_device %}
|
||||
<div id="printerApiUrl" data-url="{% url 'bambu_run:printer_api' pk=printer_device.pk %}" style="display: none;"></div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const printerData = {{ printer_data_json|safe }};
|
||||
const apiUrl = '{% url "bambu_run:printer_api" %}';
|
||||
const apiUrl = '{% url "bambu_run:printer_api" pk=printer_device.pk %}';
|
||||
initPrinterCharts(printerData, apiUrl);
|
||||
|
||||
// Add project markers if they exist
|
||||
@@ -453,4 +556,27 @@
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const pillsContainer = document.getElementById('amsFilterPills');
|
||||
if (!pillsContainer) return;
|
||||
const items = document.querySelectorAll('.ams-groups .ams-group');
|
||||
|
||||
pillsContainer.addEventListener('click', function(e) {
|
||||
const pill = e.target.closest('.ams-filter-pill');
|
||||
if (!pill) return;
|
||||
|
||||
pillsContainer.querySelectorAll('.ams-filter-pill').forEach(function(p) {
|
||||
p.classList.remove('active');
|
||||
});
|
||||
pill.classList.add('active');
|
||||
|
||||
const filter = pill.dataset.amsFilter;
|
||||
items.forEach(function(item) {
|
||||
const show = filter === 'all' || item.dataset.amsUnitId === filter;
|
||||
item.classList.toggle('d-none', !show);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user