11
22
3- import json
4- import ssl
5- import traceback
6- from threading import Thread
7- from typing import Any , Iterable
3+ import json
4+ import ssl
5+ import traceback
6+ from threading import Thread
7+ from typing import Any , Iterable
8+ from urllib .parse import urlparse
89
910import paho .mqtt .client as mqtt
1011
4445
4546PENDING_PRINT_METADATA = {}
4647FILAMENT_TRACKER = FilamentUsageTracker ()
47- LOG_FILE = "/home/app/logs/mqtt.log"
48-
49- def getPrinterModel ():
48+ LOG_FILE = "/home/app/logs/mqtt.log"
49+
50+ def _is_local_project_file (print_obj : dict | None ) -> bool :
51+ if not print_obj :
52+ return False
53+ if print_obj .get ("print_type" ) == "local" :
54+ return True
55+ url = print_obj .get ("url" ) or ""
56+ if not url :
57+ return False
58+ scheme = urlparse (url ).scheme
59+ return scheme in ("file" , "local" , "ftp" , "ftps" )
60+
61+ def _load_project_metadata (url : str ) -> dict | None :
62+ metadata = getMetaDataFrom3mf (url )
63+ if not metadata or not metadata .get ("filaments" ):
64+ log ("[filament-tracker] No metadata/filaments found in 3MF; skipping tracking for this job" )
65+ return None
66+ metadata ["metadata_loaded" ] = True
67+ return metadata
68+
69+ def _insert_filament_usage_entries (print_id , filaments : dict ) -> None :
70+ for id , filament in filaments .items ():
71+ parsed_grams = _parse_grams (filament .get ("used_g" ))
72+ parsed_length_m = _parse_grams (filament .get ("used_m" ))
73+ estimated_length_mm = parsed_length_m * 1000 if parsed_length_m is not None else None
74+ grams_used = parsed_grams if parsed_grams is not None else 0.0
75+ length_used = estimated_length_mm if estimated_length_mm is not None else 0.0
76+ if TRACK_LAYER_USAGE :
77+ grams_used = 0.0
78+ length_used = 0.0
79+ insert_filament_usage (
80+ print_id ,
81+ filament ["type" ],
82+ filament ["color" ],
83+ grams_used ,
84+ id ,
85+ estimated_grams = parsed_grams ,
86+ length_used = length_used ,
87+ estimated_length = estimated_length_mm ,
88+ )
89+
90+ def getPrinterModel ():
5091 global PRINTER_ID
5192 model_code = PRINTER_ID [:3 ]
5293
@@ -265,71 +306,84 @@ def processMessage(data):
265306 global LAST_AMS_CONFIG , PRINTER_STATE , PRINTER_STATE_LAST , PENDING_PRINT_METADATA
266307
267308 # Prepare AMS spending estimation
268- if "print" in data :
269- update_dict (PRINTER_STATE , data )
270-
309+ if "print" in data :
310+ update_dict (PRINTER_STATE , data )
311+
312+ # Handle project_file events (3MF metadata load) separately for local vs cloud jobs.
271313 if data ["print" ].get ("command" ) == "project_file" and data ["print" ].get ("url" ):
272- PENDING_PRINT_METADATA = getMetaDataFrom3mf (data ["print" ]["url" ])
273- if not PENDING_PRINT_METADATA or not PENDING_PRINT_METADATA .get ("filaments" ):
274- log ("[filament-tracker] No metadata/filaments found in 3MF; skipping tracking for this job" )
314+ # Local project_file: stash metadata for the local flow and wait for RUNNING.
315+ if _is_local_project_file (data ["print" ]):
316+ metadata = _load_project_metadata (data ["print" ]["url" ])
317+ if metadata is None :
318+ PENDING_PRINT_METADATA = {}
319+ PRINTER_STATE_LAST = copy .deepcopy (PRINTER_STATE )
320+ return
321+ PENDING_PRINT_METADATA = metadata
322+ PENDING_PRINT_METADATA ["print_type" ] = "local"
323+ PENDING_PRINT_METADATA ["task_id" ] = PRINTER_STATE ["print" ].get ("task_id" )
324+ PENDING_PRINT_METADATA ["subtask_id" ] = PRINTER_STATE ["print" ].get ("subtask_id" )
325+
326+ normalized = normalize_ams_mapping2 (
327+ PRINTER_STATE ["print" ].get ("ams_mapping2" ),
328+ PRINTER_STATE ["print" ].get ("ams_mapping" ),
329+ )
330+ if normalized :
331+ PENDING_PRINT_METADATA ["ams_mapping" ] = normalized
332+ PENDING_PRINT_METADATA ["ams_mapping2" ] = normalized
333+ PRINTER_STATE_LAST = copy .deepcopy (PRINTER_STATE )
334+ return
335+
336+ # Cloud project_file: fully initialize tracking and upfront usage records.
337+ metadata = _load_project_metadata (data ["print" ]["url" ])
338+ if metadata is None :
275339 PENDING_PRINT_METADATA = {}
276340 return
277- PENDING_PRINT_METADATA [ "metadata_loaded" ] = True
341+ PENDING_PRINT_METADATA = metadata
278342 PENDING_PRINT_METADATA ["print_type" ] = PRINTER_STATE ["print" ].get ("print_type" )
279343 PENDING_PRINT_METADATA ["task_id" ] = PRINTER_STATE ["print" ].get ("task_id" )
280344 PENDING_PRINT_METADATA ["subtask_id" ] = PRINTER_STATE ["print" ].get ("subtask_id" )
281345 if TRACK_LAYER_USAGE :
282- FILAMENT_TRACKER .set_print_metadata (PENDING_PRINT_METADATA )
283-
284- print_id = insert_print (PRINTER_STATE ["print" ]["subtask_name" ], "cloud" , PENDING_PRINT_METADATA ["image" ])
285-
286- normalized = normalize_ams_mapping2 (
287- PRINTER_STATE ["print" ].get ("ams_mapping2" ),
288- PRINTER_STATE ["print" ].get ("ams_mapping" ),
289- )
290- use_ams_flag = PRINTER_STATE ["print" ].get ("use_ams" )
291- use_ams = bool (use_ams_flag ) if use_ams_flag is not None else bool (normalized )
292- if not use_ams :
293- normalized = [normalize_ams_mapping_entry (EXTERNAL_SPOOL_ID )]
294-
295- PENDING_PRINT_METADATA ["ams_mapping" ] = normalized
296- PENDING_PRINT_METADATA ["ams_mapping2" ] = normalized
297-
298- PENDING_PRINT_METADATA ["print_id" ] = print_id
299- PENDING_PRINT_METADATA ["complete" ] = True
300-
301- for id , filament in PENDING_PRINT_METADATA ["filaments" ].items ():
302- parsed_grams = _parse_grams (filament .get ("used_g" ))
303- parsed_length_m = _parse_grams (filament .get ("used_m" ))
304- estimated_length_mm = parsed_length_m * 1000 if parsed_length_m is not None else None
305- grams_used = parsed_grams if parsed_grams is not None else 0.0
306- length_used = estimated_length_mm if estimated_length_mm is not None else 0.0
307- if TRACK_LAYER_USAGE :
308- grams_used = 0.0
309- length_used = 0.0
310- insert_filament_usage (
311- print_id ,
312- filament ["type" ],
313- filament ["color" ],
314- grams_used ,
315- id ,
316- estimated_grams = parsed_grams ,
317- length_used = length_used ,
318- estimated_length = estimated_length_mm ,
319- )
346+ FILAMENT_TRACKER .set_print_metadata (PENDING_PRINT_METADATA )
347+
348+ print_id = insert_print (PRINTER_STATE ["print" ]["subtask_name" ], "cloud" , PENDING_PRINT_METADATA ["image" ])
349+
350+ normalized = normalize_ams_mapping2 (
351+ PRINTER_STATE ["print" ].get ("ams_mapping2" ),
352+ PRINTER_STATE ["print" ].get ("ams_mapping" ),
353+ )
354+ use_ams_flag = PRINTER_STATE ["print" ].get ("use_ams" )
355+ use_ams = bool (use_ams_flag ) if use_ams_flag is not None else bool (normalized )
356+ if not use_ams :
357+ normalized = [normalize_ams_mapping_entry (EXTERNAL_SPOOL_ID )]
358+
359+ PENDING_PRINT_METADATA ["ams_mapping" ] = normalized
360+ PENDING_PRINT_METADATA ["ams_mapping2" ] = normalized
361+
362+ PENDING_PRINT_METADATA ["print_id" ] = print_id
363+ PENDING_PRINT_METADATA ["complete" ] = True
364+
365+ _insert_filament_usage_entries (print_id , PENDING_PRINT_METADATA ["filaments" ])
320366
321367 #if ("gcode_state" in data["print"] and data["print"]["gcode_state"] == "RUNNING") and ("print_type" in data["print"] and data["print"]["print_type"] != "local") \
322368 # and ("tray_tar" in data["print"] and data["print"]["tray_tar"] != "255") and ("stg_cur" in data["print"] and data["print"]["stg_cur"] == 0 and PRINT_CURRENT_STAGE != 0):
323369
324370 #TODO: What happens when printed from external spool, is ams and tray_tar set?
325- if PRINTER_STATE .get ("print" , {}).get ("print_type" ) == "local" and PRINTER_STATE_LAST .get ("print" ):
326-
327- if (
328- PRINTER_STATE ["print" ].get ("gcode_state" ) == "RUNNING" and
329- PRINTER_STATE_LAST ["print" ].get ("gcode_state" ) == "PREPARE" and
330- PRINTER_STATE ["print" ].get ("gcode_file" )
331- ):
332-
371+ # Local print flow: start tracking once the job is RUNNING and metadata is available.
372+ if PRINTER_STATE .get ("print" , {}).get ("print_type" ) == "local" and PRINTER_STATE_LAST .get ("print" ):
373+ if (
374+ PRINTER_STATE ["print" ].get ("gcode_state" ) == "RUNNING" and
375+ PRINTER_STATE ["print" ].get ("gcode_file" ) and
376+ (
377+ PRINTER_STATE_LAST ["print" ].get ("gcode_state" ) == "PREPARE" or
378+ (
379+ PENDING_PRINT_METADATA
380+ and PENDING_PRINT_METADATA .get ("metadata_loaded" )
381+ and not PENDING_PRINT_METADATA .get ("tracking_started" )
382+ )
383+ )
384+ ):
385+
386+ # Ensure metadata is loaded from the 3MF before starting tracking.
333387 if not PENDING_PRINT_METADATA or not PENDING_PRINT_METADATA .get ("metadata_loaded" ):
334388 PENDING_PRINT_METADATA = getMetaDataFrom3mf (PRINTER_STATE ["print" ]["gcode_file" ])
335389 if PENDING_PRINT_METADATA and PENDING_PRINT_METADATA .get ("filaments" ):
@@ -338,86 +392,106 @@ def processMessage(data):
338392 PENDING_PRINT_METADATA ["print_type" ] = PRINTER_STATE ["print" ].get ("print_type" )
339393 PENDING_PRINT_METADATA ["task_id" ] = PRINTER_STATE ["print" ].get ("task_id" )
340394 PENDING_PRINT_METADATA ["subtask_id" ] = PRINTER_STATE ["print" ].get ("subtask_id" )
341-
342- if not PENDING_PRINT_METADATA .get ("tracking_started" ):
343- print_id = insert_print (PENDING_PRINT_METADATA ["file" ], PRINTER_STATE ["print" ]["print_type" ], PENDING_PRINT_METADATA ["image" ])
344-
345- PENDING_PRINT_METADATA ["ams_mapping" ] = []
346- PENDING_PRINT_METADATA ["ams_mapping2" ] = []
347- PENDING_PRINT_METADATA ["filamentChanges" ] = []
348- PENDING_PRINT_METADATA ["assigned_trays" ] = []
349- PENDING_PRINT_METADATA ["complete" ] = False
350- PENDING_PRINT_METADATA ["print_id" ] = print_id
351- FILAMENT_TRACKER .start_local_print_from_metadata (PENDING_PRINT_METADATA )
352-
353- for id , filament in PENDING_PRINT_METADATA ["filaments" ].items ():
354- parsed_grams = _parse_grams (filament .get ("used_g" ))
355- parsed_length_m = _parse_grams (filament .get ("used_m" ))
356- estimated_length_mm = parsed_length_m * 1000 if parsed_length_m is not None else None
357- grams_used = parsed_grams if parsed_grams is not None else 0.0
358- length_used = estimated_length_mm if estimated_length_mm is not None else 0.0
359- if TRACK_LAYER_USAGE :
360- grams_used = 0.0
361- length_used = 0.0
362- insert_filament_usage (
363- print_id ,
364- filament ["type" ],
365- filament ["color" ],
366- grams_used ,
367- id ,
368- estimated_grams = parsed_grams ,
369- length_used = length_used ,
370- estimated_length = estimated_length_mm ,
371- )
372-
373- PENDING_PRINT_METADATA ["tracking_started" ] = True
374-
375- #TODO
395+
396+ # Start tracking once per job, using AMS mapping when available.
397+ if not PENDING_PRINT_METADATA .get ("tracking_started" ):
398+ if FILAMENT_TRACKER .active_model is not None :
399+ PENDING_PRINT_METADATA ["tracking_started" ] = True
400+ else :
401+ print_id = PENDING_PRINT_METADATA .get ("print_id" )
402+ if not print_id :
403+ print_id = insert_print (PENDING_PRINT_METADATA ["file" ], PRINTER_STATE ["print" ]["print_type" ], PENDING_PRINT_METADATA ["image" ])
404+
405+ normalized = normalize_ams_mapping2 (
406+ PRINTER_STATE ["print" ].get ("ams_mapping2" ),
407+ PRINTER_STATE ["print" ].get ("ams_mapping" ),
408+ )
409+ use_ams_flag = PRINTER_STATE ["print" ].get ("use_ams" )
410+ has_mapping = any (entry is not None for entry in normalized )
411+ use_ams = bool (use_ams_flag ) if use_ams_flag is not None else has_mapping
412+ if use_ams and normalized :
413+ PENDING_PRINT_METADATA ["ams_mapping" ] = normalized
414+ PENDING_PRINT_METADATA ["ams_mapping2" ] = normalized
415+ else :
416+ PENDING_PRINT_METADATA .setdefault ("ams_mapping" , [])
417+ PENDING_PRINT_METADATA .setdefault ("ams_mapping2" , [])
418+
419+ PENDING_PRINT_METADATA ["filamentChanges" ] = []
420+ PENDING_PRINT_METADATA ["assigned_trays" ] = []
421+
422+ filament_order = PENDING_PRINT_METADATA .get ("filamentOrder" ) or {}
423+ target_filaments = set ()
424+ for filament_id in filament_order .keys ():
425+ try :
426+ target_filaments .add (int (filament_id ))
427+ except (TypeError , ValueError ):
428+ continue
429+ assigned_filaments = {
430+ idx for idx , tray in enumerate (PENDING_PRINT_METADATA .get ("ams_mapping" ) or [])
431+ if tray is not None
432+ }
433+ if target_filaments :
434+ mapping_complete = target_filaments .issubset (assigned_filaments )
435+ else :
436+ mapping_complete = any (
437+ tray is not None for tray in (PENDING_PRINT_METADATA .get ("ams_mapping" ) or [])
438+ )
439+ PENDING_PRINT_METADATA ["complete" ] = mapping_complete
440+ PENDING_PRINT_METADATA ["print_id" ] = print_id
441+ FILAMENT_TRACKER .start_local_print_from_metadata (PENDING_PRINT_METADATA )
442+
443+ _insert_filament_usage_entries (print_id , PENDING_PRINT_METADATA ["filaments" ])
444+
445+ PENDING_PRINT_METADATA ["tracking_started" ] = True
446+
447+ #TODO
376448
377- # When stage changed to "change filament" and PENDING_PRINT_METADATA is set
378- if (PENDING_PRINT_METADATA and
379- (
380- (
381- int (PRINTER_STATE ["print" ].get ("stg_cur" , - 1 )) == 4 and # change filament stage (beginning of print)
382- (
383- PRINTER_STATE_LAST ["print" ].get ("stg_cur" , - 1 ) == - 1 or # last stage not known
384- (
385- int (PRINTER_STATE_LAST ["print" ].get ("stg_cur" )) != int (PRINTER_STATE ["print" ].get ("stg_cur" )) and
386- PRINTER_STATE_LAST ["print" ].get ("ams" , {}).get ("tray_tar" ) == "255" # stage has changed and last state was 255 (retract to ams)
387- )
388- or not PRINTER_STATE_LAST ["print" ].get ("ams" ) # ams not set in last state
389- )
390- )
391- or # filament changes during printing are in mc_print_sub_stage
392- (
393- int (PRINTER_STATE_LAST ["print" ].get ("mc_print_sub_stage" , - 1 )) == 4 # last state was change filament
394- and int (PRINTER_STATE ["print" ].get ("mc_print_sub_stage" , - 1 )) == 2 # current state
395- )
396- or (
397- PRINTER_STATE ["print" ].get ("ams" , {}).get ("tray_tar" ) == "254"
398- )
399- or
400- (
401- int (PRINTER_STATE ["print" ].get ("stg_cur" , - 1 )) == 24 and int (PRINTER_STATE_LAST ["print" ].get ("stg_cur" , - 1 )) == 13
402- )
403- or (
404- int (PRINTER_STATE ["print" ].get ("stg_cur" , - 1 )) == 4 and
405- PRINTER_STATE ["print" ].get ("ams" , {}).get ("tray_tar" ) not in (None , "255" ) and
406- (PRINTER_STATE_LAST ["print" ].get ("ams" , {}).get ("tray_tar" ) is None or PRINTER_STATE_LAST ["print" ].get ("ams" , {}).get ("tray_tar" ) != PRINTER_STATE ["print" ].get ("ams" , {}).get ("tray_tar" ))
407- )
408-
409- )
410- ):
411- if PRINTER_STATE ["print" ].get ("ams" ):
412- mapped = False
413- tray_tar_value = PRINTER_STATE ["print" ].get ("ams" ).get ("tray_tar" )
414- if tray_tar_value and tray_tar_value != "255" :
415- mapped = map_filament (int (tray_tar_value ))
416- FILAMENT_TRACKER .apply_ams_mapping (PENDING_PRINT_METADATA .get ("ams_mapping" ) or [])
417- if mapped :
418- PENDING_PRINT_METADATA ["complete" ] = True
419-
420- if PENDING_PRINT_METADATA and PENDING_PRINT_METADATA .get ("complete" ):
449+ # Update AMS mapping once the printer reports concrete tray assignments.
450+ # When stage changed to "change filament" and PENDING_PRINT_METADATA is set
451+ if (PENDING_PRINT_METADATA and
452+ (
453+ (
454+ int (PRINTER_STATE ["print" ].get ("stg_cur" , - 1 )) == 4 and # change filament stage (beginning of print)
455+ (
456+ PRINTER_STATE_LAST ["print" ].get ("stg_cur" , - 1 ) == - 1 or # last stage not known
457+ (
458+ int (PRINTER_STATE_LAST ["print" ].get ("stg_cur" )) != int (PRINTER_STATE ["print" ].get ("stg_cur" )) and
459+ PRINTER_STATE_LAST ["print" ].get ("ams" , {}).get ("tray_tar" ) == "255" # stage has changed and last state was 255 (retract to ams)
460+ )
461+ or not PRINTER_STATE_LAST ["print" ].get ("ams" ) # ams not set in last state
462+ )
463+ )
464+ or # filament changes during printing are in mc_print_sub_stage
465+ (
466+ int (PRINTER_STATE_LAST ["print" ].get ("mc_print_sub_stage" , - 1 )) == 4 # last state was change filament
467+ and int (PRINTER_STATE ["print" ].get ("mc_print_sub_stage" , - 1 )) == 2 # current state
468+ )
469+ or (
470+ PRINTER_STATE ["print" ].get ("ams" , {}).get ("tray_tar" ) == "254"
471+ )
472+ or
473+ (
474+ int (PRINTER_STATE ["print" ].get ("stg_cur" , - 1 )) == 24 and int (PRINTER_STATE_LAST ["print" ].get ("stg_cur" , - 1 )) == 13
475+ )
476+ or (
477+ int (PRINTER_STATE ["print" ].get ("stg_cur" , - 1 )) == 4 and
478+ PRINTER_STATE ["print" ].get ("ams" , {}).get ("tray_tar" ) not in (None , "255" ) and
479+ (PRINTER_STATE_LAST ["print" ].get ("ams" , {}).get ("tray_tar" ) is None or PRINTER_STATE_LAST ["print" ].get ("ams" , {}).get ("tray_tar" ) != PRINTER_STATE ["print" ].get ("ams" , {}).get ("tray_tar" ))
480+ )
481+
482+ )
483+ ):
484+ if PRINTER_STATE ["print" ].get ("ams" ):
485+ mapped = False
486+ tray_tar_value = PRINTER_STATE ["print" ].get ("ams" ).get ("tray_tar" )
487+ if tray_tar_value and tray_tar_value != "255" :
488+ mapped = map_filament (int (tray_tar_value ))
489+ FILAMENT_TRACKER .apply_ams_mapping (PENDING_PRINT_METADATA .get ("ams_mapping" ) or [])
490+ if mapped :
491+ PENDING_PRINT_METADATA ["complete" ] = True
492+
493+ # Finalize or spend once metadata is complete.
494+ if PENDING_PRINT_METADATA and PENDING_PRINT_METADATA .get ("complete" ):
421495 if not PENDING_PRINT_METADATA .get ("complete_handled" ):
422496 if TRACK_LAYER_USAGE :
423497 if PENDING_PRINT_METADATA .get ("print_type" ) == "local" :
0 commit comments