|
@@ -576,6 +576,21 @@ class MotionControlThread:
|
|
|
'error:21', # Invalid gcode command value
|
|
'error:21', # Invalid gcode command value
|
|
|
'error:22', # Invalid gcode command value in negative
|
|
'error:22', # Invalid gcode command value in negative
|
|
|
'error:23', # Invalid gcode command value in decimal
|
|
'error:23', # Invalid gcode command value in decimal
|
|
|
|
|
+ # FluidNC extended codes (170-181): expression/flow-control parser
|
|
|
|
|
+ # errors. We never send expressions or o-codes, so these can only
|
|
|
|
|
+ # mean a corrupted line (e.g. leading 'G' garbled into 'o')
|
|
|
|
|
+ 'error:170', # Expression divide by zero
|
|
|
|
|
+ 'error:171', # Expression invalid argument
|
|
|
|
|
+ 'error:172', # Expression invalid result
|
|
|
|
|
+ 'error:173', # Expression unknown op
|
|
|
|
|
+ 'error:174', # Expression argument out of range
|
|
|
|
|
+ 'error:175', # Expression syntax error
|
|
|
|
|
+ 'error:176', # Flow control syntax error
|
|
|
|
|
+ 'error:177', # Flow control not executing macro
|
|
|
|
|
+ 'error:178', # Flow control out of memory
|
|
|
|
|
+ 'error:179', # Flow control stack overflow
|
|
|
|
|
+ 'error:180', # Parameter assignment failed
|
|
|
|
|
+ 'error:181', # Gcode value word invalid
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
while True:
|
|
while True:
|
|
@@ -1515,7 +1530,7 @@ async def run_theta_rho_file(file_path, is_playlist=False, clear_pattern=None, c
|
|
|
logger.info("Pattern execution completed, maintaining state for playlist")
|
|
logger.info("Pattern execution completed, maintaining state for playlist")
|
|
|
|
|
|
|
|
|
|
|
|
|
-async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
|
|
|
|
|
|
|
+async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False, pause_from_start=False):
|
|
|
"""Run multiple .thr files in sequence with options.
|
|
"""Run multiple .thr files in sequence with options.
|
|
|
|
|
|
|
|
The playlist now stores only main patterns. Clear patterns are executed dynamically
|
|
The playlist now stores only main patterns. Clear patterns are executed dynamically
|
|
@@ -1564,6 +1579,8 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
|
|
|
# Execute main patterns using index-based access
|
|
# Execute main patterns using index-based access
|
|
|
# This allows the playlist to be reordered during execution
|
|
# This allows the playlist to be reordered during execution
|
|
|
idx = 0
|
|
idx = 0
|
|
|
|
|
+ # Default in case the inner loop doesn't run before the indefinite-restart pause.
|
|
|
|
|
+ pattern_start_time = time.time()
|
|
|
while state.current_playlist and idx < len(state.current_playlist):
|
|
while state.current_playlist and idx < len(state.current_playlist):
|
|
|
state.current_playlist_index = idx
|
|
state.current_playlist_index = idx
|
|
|
|
|
|
|
@@ -1579,6 +1596,10 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
|
|
|
state.pause_time_remaining = 0
|
|
state.pause_time_remaining = 0
|
|
|
state.original_pause_time = None
|
|
state.original_pause_time = None
|
|
|
|
|
|
|
|
|
|
+ # Track pattern start time so pause_from_start mode can subtract
|
|
|
|
|
+ # actual run duration from the cadence interval.
|
|
|
|
|
+ pattern_start_time = time.time()
|
|
|
|
|
+
|
|
|
# Execute the pattern with optional clear pattern
|
|
# Execute the pattern with optional clear pattern
|
|
|
await run_theta_rho_file(
|
|
await run_theta_rho_file(
|
|
|
file_path,
|
|
file_path,
|
|
@@ -1629,19 +1650,26 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
|
|
|
idle_timeout_manager.cancel_timeout()
|
|
idle_timeout_manager.cancel_timeout()
|
|
|
|
|
|
|
|
# Handle pause between patterns
|
|
# Handle pause between patterns
|
|
|
- if state.current_playlist and idx < len(state.current_playlist) - 1 and not state.stop_requested and pause_time > 0 and not state.skip_requested:
|
|
|
|
|
- logger.info(f"Pausing for {pause_time} seconds")
|
|
|
|
|
|
|
+ # In pause_from_start mode, the cadence is measured from the pattern's
|
|
|
|
|
+ # start time, so effective wait = pause_time - (now - pattern_start).
|
|
|
|
|
+ # If the pattern ran longer than the interval, we skip the pause entirely.
|
|
|
|
|
+ if pause_from_start:
|
|
|
|
|
+ effective_pause = max(0, pause_time - (time.time() - pattern_start_time))
|
|
|
|
|
+ else:
|
|
|
|
|
+ effective_pause = pause_time
|
|
|
|
|
+ if state.current_playlist and idx < len(state.current_playlist) - 1 and not state.stop_requested and effective_pause > 0 and not state.skip_requested:
|
|
|
|
|
+ logger.info(f"Pausing for {effective_pause:.1f} seconds")
|
|
|
# Clear current_playing_file to report "idle" state to MQTT/HA during pause
|
|
# Clear current_playing_file to report "idle" state to MQTT/HA during pause
|
|
|
# This will be set again when the next pattern starts
|
|
# This will be set again when the next pattern starts
|
|
|
state.current_playing_file = None
|
|
state.current_playing_file = None
|
|
|
# Trigger idle LED state during pause between patterns
|
|
# Trigger idle LED state during pause between patterns
|
|
|
await start_idle_led_timeout(check_still_sands=True)
|
|
await start_idle_led_timeout(check_still_sands=True)
|
|
|
- state.original_pause_time = pause_time
|
|
|
|
|
|
|
+ state.original_pause_time = effective_pause
|
|
|
pause_start = time.time()
|
|
pause_start = time.time()
|
|
|
# Track Still Sands state for edge detection during long pauses
|
|
# Track Still Sands state for edge detection during long pauses
|
|
|
was_in_still_sands = is_in_scheduled_pause_period() and state.scheduled_pause_control_wled
|
|
was_in_still_sands = is_in_scheduled_pause_period() and state.scheduled_pause_control_wled
|
|
|
- while time.time() - pause_start < pause_time:
|
|
|
|
|
- state.pause_time_remaining = pause_start + pause_time - time.time()
|
|
|
|
|
|
|
+ while time.time() - pause_start < effective_pause:
|
|
|
|
|
+ state.pause_time_remaining = pause_start + effective_pause - time.time()
|
|
|
if state.skip_requested or state.stop_requested:
|
|
if state.skip_requested or state.stop_requested:
|
|
|
if state.stop_requested:
|
|
if state.stop_requested:
|
|
|
logger.info("Pause interrupted by stop request")
|
|
logger.info("Pause interrupted by stop request")
|
|
@@ -1687,17 +1715,23 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
|
|
|
|
|
|
|
|
if run_mode == "indefinite":
|
|
if run_mode == "indefinite":
|
|
|
logger.info("Playlist completed. Restarting as per 'indefinite' run mode")
|
|
logger.info("Playlist completed. Restarting as per 'indefinite' run mode")
|
|
|
- if pause_time > 0:
|
|
|
|
|
|
|
+ # Same from-start adjustment as the inter-pattern pause: the cadence
|
|
|
|
|
+ # is measured from the most recent pattern's start time.
|
|
|
|
|
+ if pause_from_start:
|
|
|
|
|
+ effective_pause = max(0, pause_time - (time.time() - pattern_start_time))
|
|
|
|
|
+ else:
|
|
|
|
|
+ effective_pause = pause_time
|
|
|
|
|
+ if effective_pause > 0:
|
|
|
# Clear current_playing_file to report "idle" state to MQTT/HA during pause
|
|
# Clear current_playing_file to report "idle" state to MQTT/HA during pause
|
|
|
state.current_playing_file = None
|
|
state.current_playing_file = None
|
|
|
# Trigger idle LED state during pause between playlist cycles
|
|
# Trigger idle LED state during pause between playlist cycles
|
|
|
await start_idle_led_timeout(check_still_sands=True)
|
|
await start_idle_led_timeout(check_still_sands=True)
|
|
|
- state.original_pause_time = pause_time
|
|
|
|
|
|
|
+ state.original_pause_time = effective_pause
|
|
|
pause_start = time.time()
|
|
pause_start = time.time()
|
|
|
# Track Still Sands state for edge detection during long pauses
|
|
# Track Still Sands state for edge detection during long pauses
|
|
|
was_in_still_sands = is_in_scheduled_pause_period() and state.scheduled_pause_control_wled
|
|
was_in_still_sands = is_in_scheduled_pause_period() and state.scheduled_pause_control_wled
|
|
|
- while time.time() - pause_start < pause_time:
|
|
|
|
|
- state.pause_time_remaining = pause_start + pause_time - time.time()
|
|
|
|
|
|
|
+ while time.time() - pause_start < effective_pause:
|
|
|
|
|
+ state.pause_time_remaining = pause_start + effective_pause - time.time()
|
|
|
if state.skip_requested or state.stop_requested:
|
|
if state.skip_requested or state.stop_requested:
|
|
|
if state.stop_requested:
|
|
if state.stop_requested:
|
|
|
logger.info("Pause interrupted by stop request")
|
|
logger.info("Pause interrupted by stop request")
|