Files
Bambu-Run/bambu_run/forms.py
RNL dd57a963ac Add H2C dual-nozzle and multi-AMS-type support
Schema (migration 0004):
- PrinterMetrics: nozzle_temp_left, nozzle_target_temp_left,
  nozzle_diameter_left, nozzle_type_left (all nullable)
- Filament: ams_unit_id (nullable int), ams_type (AMS/AMS 2 Pro/AMS HT)
- AMS_INFO_TO_TYPE map and AMS_TYPE_CHOICES on models

Parser (mqtt_client.py):
- Decode bit-packed temps from device.extruder.info[] for left/right nozzle
- Emit per-nozzle fields in get_snapshot(); legacy keys mirror right side
- AMS unit type from info code per unit dict

Collector (bambu_collector.py):
- Write left-nozzle fields to PrinterMetrics
- Set ams_unit_id + ams_type on Filament records
- Fix: poll MQTTClient.connected before pushall (not BambuPrinter._connected)
- Add 5s post-pushall wait in --once mode so response arrives before collect

Views: API and dashboard include left-nozzle series; is_dual_nozzle flag
Templates: dual-nozzle cards + chart; AMS-type badge + filter on filament list
Charts: left nozzle temp chart with conditional render
Forms: fix tray_id max=3 → max=15; add ams_unit_id, ams_type fields
2026-05-07 14:51:31 +10:00

244 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django import forms
from .models import Filament, FilamentColor, FilamentType
class FilamentTypeForm(forms.ModelForm):
"""Form for managing FilamentType registry"""
PRESET_TYPES = ['PLA', 'PETG', 'PET', 'ABS', 'ASA', 'TPU', 'PA', 'PC', 'PPS']
PRESET_SUB_TYPES = [
'PLA Basic', 'PLA Matte', 'PLA Silk', 'PLA Metal', 'PLA Marble', 'PLA Glow', 'PLA-CF',
'PETG Basic', 'PETG-CF', 'PETG-HF', 'ABS', 'TPU 95A', 'PA6-CF', 'ASA', 'PC', 'PPS-CF',
'Support W', 'Support G',
]
PRESET_BRANDS = [
'Bambu Lab', 'eSUN', 'Polymaker', 'Hatchbox', 'Prusament',
'MatterHackers', 'Overture', '3DXTech', 'ColorFabb',
]
class Meta:
model = FilamentType
fields = ['type', 'sub_type', 'brand']
widgets = {
'type': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., PLA, PETG, ABS'
}),
'sub_type': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., PLA Basic, PLA Matte (optional)'
}),
'brand': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Bambu Lab'
}),
}
class FilamentForm(forms.ModelForm):
color_hex_text = forms.CharField(
required=False,
max_length=7,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '#000000',
'pattern': '#[0-9A-Fa-f]{6}',
'id': 'id_color_hex_text'
}),
label='Color Hex Code'
)
class Meta:
model = Filament
fields = [
'tray_uuid', 'tag_uid', 'tag_id', 'created_by',
'filament_type', 'type', 'sub_type', 'brand', 'color', 'color_hex', 'is_transparent',
'diameter', 'initial_weight_grams',
'remaining_percent', 'remaining_weight_grams',
'is_loaded_in_ams', 'current_tray_id', 'ams_unit_id', 'ams_type',
'purchase_date', 'purchase_price', 'supplier', 'notes'
]
widgets = {
'tray_uuid': forms.TextInput(attrs={
'class': 'form-control font-monospace',
'placeholder': 'Optional - Auto-filled by MQTT',
'style': 'font-size: 0.9em;'
}),
'tag_uid': forms.TextInput(attrs={
'class': 'form-control font-monospace',
'placeholder': 'Optional - RFID chip ID',
'style': 'font-size: 0.9em;'
}),
'tag_id': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Optional - User-defined ID'}),
'created_by': forms.Select(attrs={'class': 'form-select'}),
'filament_type': forms.Select(attrs={'class': 'form-select', 'id': 'id_filament_type'}),
'type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., PLA, PETG, ABS'}),
'sub_type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., PLA Basic (optional)'}),
'brand': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., Bambu Lab'}),
'color': forms.Select(attrs={'class': 'form-select', 'id': 'id_color'}),
'color_hex': forms.TextInput(attrs={
'class': 'form-control',
'type': 'color',
'id': 'id_color_hex_picker'
}),
'diameter': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'initial_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '1000'}),
'remaining_percent': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100'}),
'remaining_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'readonly': 'readonly'}),
'is_transparent': forms.CheckboxInput(attrs={'class': 'form-check-input', 'id': 'id_is_transparent'}),
'is_loaded_in_ams': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'current_tray_id': forms.NumberInput(attrs={
'class': 'form-control', 'min': '0', 'max': '15',
'placeholder': '03 for AMS / AMS 2 Pro, 0 for AMS HT',
}),
'ams_unit_id': forms.NumberInput(attrs={
'class': 'form-control', 'min': '0', 'max': '255',
'placeholder': 'AMS unit id (0,1,… or 128 for AMS HT)',
}),
'ams_type': forms.Select(attrs={'class': 'form-select'}),
'purchase_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'purchase_price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'supplier': forms.TextInput(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.color_hex:
self.fields['color_hex_text'].initial = self.instance.color_hex
self.fields['filament_type'].queryset = FilamentType.objects.all()
self.fields['filament_type'].empty_label = '--- Select Filament Type ---'
self.fields['filament_type'].required = False
self.fields['type'].required = False
self.fields['sub_type'].required = False
self.fields['brand'].required = False
self.fields['ams_unit_id'].required = False
self.fields['ams_type'].required = False
self._populate_color_choices()
def _populate_color_choices(self):
"""Populate color field choices from FilamentColor database with suggested colors"""
from .utils import strip_color_padding, match_filament_color
color_choices = [('', '--- Select Color ---')]
suggested_color = None
all_colors = FilamentColor.objects.all().order_by('filament_type', 'filament_sub_type', 'color_name')
if self.instance and self.instance.type and self.instance.color_hex:
color_code = strip_color_padding(self.instance.color_hex.lstrip('#'))
suggested = match_filament_color(
filament_type=self.instance.type,
filament_sub_type=self.instance.sub_type,
color_code=color_code,
brand=self.instance.brand or 'Bambu Lab'
)
if suggested:
suggested_color = suggested
if suggested_color:
color_choices.append((
suggested_color.color_name,
f"SUGGESTED: {suggested_color.filament_sub_type or suggested_color.filament_type}: {suggested_color.color_name}"
))
color_choices.append(('---separator---', '---' * 20))
for color in all_colors:
if suggested_color and color.pk == suggested_color.pk:
continue
display_name = f"{color.filament_sub_type or color.filament_type}: {color.color_name}"
color_choices.append((color.color_name, display_name))
color_choices.append(('---separator2---', '---' * 20))
color_choices.append(('custom', 'Custom (type in manually)'))
self.fields['color'].widget.choices = color_choices
def clean(self):
cleaned_data = super().clean()
is_loaded = cleaned_data.get('is_loaded_in_ams')
tray_id = cleaned_data.get('current_tray_id')
color_hex_text = cleaned_data.get('color_hex_text')
if color_hex_text:
cleaned_data['color_hex'] = color_hex_text
color = cleaned_data.get('color')
if color and 'separator' in color:
cleaned_data['color'] = ''
ft = cleaned_data.get('filament_type')
if ft:
cleaned_data['type'] = ft.type
cleaned_data['sub_type'] = ft.sub_type or ''
cleaned_data['brand'] = ft.brand
if is_loaded and tray_id is None:
raise forms.ValidationError('Tray ID required when filament is loaded in AMS')
return cleaned_data
class FilamentColorForm(forms.ModelForm):
"""Form for managing FilamentColor database"""
color_code = forms.CharField(
required=False,
widget=forms.HiddenInput()
)
color_hex_input = forms.CharField(
required=True,
max_length=7,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '#000000',
'pattern': '#[0-9A-Fa-f]{6}',
}),
label='Color Hex Code'
)
class Meta:
model = FilamentColor
fields = ['color_code', 'color_name', 'filament_type_fk', 'filament_type', 'filament_sub_type', 'brand']
widgets = {
'color_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., Black, Orange'}),
'filament_type_fk': forms.Select(attrs={'class': 'form-select'}),
'filament_type': forms.HiddenInput(),
'filament_sub_type': forms.HiddenInput(),
'brand': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.color_code:
self.fields['color_hex_input'].initial = f"#{self.instance.color_code}"
self.fields['filament_type_fk'].queryset = FilamentType.objects.all()
self.fields['filament_type_fk'].empty_label = '--- Select Filament Type ---'
self.fields['filament_type_fk'].required = False
self.fields['filament_type'].required = False
self.fields['filament_sub_type'].required = False
self.fields['brand'].required = False
def clean(self):
cleaned_data = super().clean()
color_hex = cleaned_data.get('color_hex_input', '')
if color_hex:
color_code = color_hex.lstrip('#').upper()[:6]
cleaned_data['color_code'] = color_code
ft_fk = cleaned_data.get('filament_type_fk')
if ft_fk:
cleaned_data['filament_type'] = ft_fk.type
cleaned_data['filament_sub_type'] = ft_fk.sub_type or ''
cleaned_data['brand'] = ft_fk.brand
return cleaned_data