fix random Timer too close or Move queue overflow errors (#123)

This commit is contained in:
Félix Boisselier
2024-06-17 19:45:20 +02:00
committed by GitHub
parent fb8e1ce98f
commit a9c7a8491b
8 changed files with 90 additions and 24 deletions

View File

@@ -9,15 +9,18 @@
# accelerometer measurements and write the data to a file in a blocking manner. # accelerometer measurements and write the data to a file in a blocking manner.
import os
import time import time
from multiprocessing import Process, Queue
# from ..helpers.console_output import ConsoleOutput
class Accelerometer: class Accelerometer:
def __init__(self, klipper_accelerometer): def __init__(self, klipper_accelerometer):
self._k_accelerometer = klipper_accelerometer self._k_accelerometer = klipper_accelerometer
self._bg_client = None self._bg_client = None
self._write_queue = Queue()
self._write_processes = []
@staticmethod @staticmethod
def find_axis_accelerometer(printer, axis: str = 'xy'): def find_axis_accelerometer(printer, axis: str = 'xy'):
@@ -32,7 +35,6 @@ class Accelerometer:
def start_measurement(self): def start_measurement(self):
if self._bg_client is None: if self._bg_client is None:
self._bg_client = self._k_accelerometer.start_internal_client() self._bg_client = self._k_accelerometer.start_internal_client()
# ConsoleOutput.print('Accelerometer measurements started')
else: else:
raise ValueError('measurements already started!') raise ValueError('measurements already started!')
@@ -54,12 +56,30 @@ class Accelerometer:
bg_client.finish_measurements() bg_client.finish_measurements()
filename = f'/tmp/shaketune-{name}.csv' filename = f'/tmp/shaketune-{name}.csv'
self._write_to_file(bg_client, filename) self._queue_file_write(bg_client, filename)
# ConsoleOutput.print(f'Accelerometer measurements stopped. Data written to {filename}')
def _queue_file_write(self, bg_client, filename):
self._write_queue.put(filename)
write_proc = Process(target=self._write_to_file, args=(bg_client, filename))
write_proc.daemon = True
write_proc.start()
self._write_processes.append(write_proc)
def _write_to_file(self, bg_client, filename): def _write_to_file(self, bg_client, filename):
try:
os.nice(20)
except Exception:
pass
with open(filename, 'w') as f: with open(filename, 'w') as f:
f.write('#time,accel_x,accel_y,accel_z\n') f.write('#time,accel_x,accel_y,accel_z\n')
samples = bg_client.samples or bg_client.get_samples() samples = bg_client.samples or bg_client.get_samples()
for t, accel_x, accel_y, accel_z in samples: for t, accel_x, accel_y, accel_z in samples:
f.write(f'{t:.6f},{accel_x:.6f},{accel_y:.6f},{accel_z:.6f}\n') f.write(f'{t:.6f},{accel_x:.6f},{accel_y:.6f},{accel_z:.6f}\n')
self._write_queue.get()
def wait_for_file_writes(self):
while not self._write_queue.empty():
time.sleep(0.1)
for proc in self._write_processes:
proc.join()
self._write_processes = []

View File

@@ -82,6 +82,8 @@ def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
toolhead.dwell(0.5) toolhead.dwell(0.5)
accelerometer.stop_measurement('axesmap_Z', append_time=True) accelerometer.stop_measurement('axesmap_Z', append_time=True)
accelerometer.wait_for_file_writes()
# Re-enable the input shaper if it was active # Re-enable the input shaper if it was active
if input_shaper is not None: if input_shaper is not None:
input_shaper.enable_shaping() input_shaper.enable_shaping()

View File

@@ -103,6 +103,8 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
vibrate_axis(toolhead, gcode, config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz) vibrate_axis(toolhead, gcode, config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz)
accelerometer.stop_measurement(config['label'], append_time=True) accelerometer.stop_measurement(config['label'], append_time=True)
accelerometer.wait_for_file_writes()
# And finally generate the graph for each measured axis # And finally generate the graph for each measured axis
ConsoleOutput.print(f'{config["axis"].upper()} axis frequency profile generation...') ConsoleOutput.print(f'{config["axis"].upper()} axis frequency profile generation...')
ConsoleOutput.print('This may take some time (1-3min)') ConsoleOutput.print('This may take some time (1-3min)')

View File

@@ -103,6 +103,8 @@ def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None:
vibrate_axis(toolhead, gcode, config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz) vibrate_axis(toolhead, gcode, config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz)
accelerometer.stop_measurement(config['label'], append_time=True) accelerometer.stop_measurement(config['label'], append_time=True)
accelerometer.wait_for_file_writes()
# Re-enable the input shaper if it was active # Re-enable the input shaper if it was active
if input_shaper is not None: if input_shaper is not None:
input_shaper.enable_shaping() input_shaper.enable_shaping()

View File

@@ -90,8 +90,8 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
k_accelerometer = printer.lookup_object(current_accel_chip, None) k_accelerometer = printer.lookup_object(current_accel_chip, None)
if k_accelerometer is None: if k_accelerometer is None:
raise gcmd.error(f'Accelerometer [{current_accel_chip}] not found!') raise gcmd.error(f'Accelerometer [{current_accel_chip}] not found!')
accelerometer = Accelerometer(k_accelerometer)
ConsoleOutput.print(f'Accelerometer chip used for this angle: [{current_accel_chip}]') ConsoleOutput.print(f'Accelerometer chip used for this angle: [{current_accel_chip}]')
accelerometer = Accelerometer(k_accelerometer)
# Sweep the speed range to record the vibrations at different speeds # Sweep the speed range to record the vibrations at different speeds
for curr_speed_sample in range(nb_speed_samples): for curr_speed_sample in range(nb_speed_samples):
@@ -131,6 +131,8 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
toolhead.dwell(0.3) toolhead.dwell(0.3)
toolhead.wait_moves() toolhead.wait_moves()
accelerometer.wait_for_file_writes()
# Restore the previous acceleration values # Restore the previous acceleration values
gcode.run_script_from_command( gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}' f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}'

View File

@@ -100,6 +100,7 @@ def excitate_axis_at_freq(gcmd, config, st_process: ShakeTuneProcess) -> None:
# If the user wanted to create a graph, we stop the recording and generate it # If the user wanted to create a graph, we stop the recording and generate it
if create_graph: if create_graph:
accelerometer.stop_measurement(f'staticfreq_{axis.upper()}', append_time=True) accelerometer.stop_measurement(f'staticfreq_{axis.upper()}', append_time=True)
accelerometer.wait_for_file_writes()
creator = st_process.get_graph_creator() creator = st_process.get_graph_creator()
creator.configure(freq, duration, accel_per_hz) creator.configure(freq, duration, accel_per_hz)

View File

@@ -117,29 +117,54 @@ class ShakeTune:
def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None: def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
static_freq_graph_creator = StaticGraphCreator(self._config) static_freq_graph_creator = StaticGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, static_freq_graph_creator, self.timeout) st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
static_freq_graph_creator,
self.timeout,
)
excitate_axis_at_freq(gcmd, self._pconfig, st_process) excitate_axis_at_freq(gcmd, self._pconfig, st_process)
def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None: def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
axes_map_graph_creator = AxesMapGraphCreator(self._config) axes_map_graph_creator = AxesMapGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, axes_map_graph_creator, self.timeout) st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
axes_map_graph_creator,
self.timeout,
)
axes_map_calibration(gcmd, self._pconfig, st_process) axes_map_calibration(gcmd, self._pconfig, st_process)
def cmd_COMPARE_BELTS_RESPONSES(self, gcmd) -> None: def cmd_COMPARE_BELTS_RESPONSES(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
belt_graph_creator = BeltsGraphCreator(self._config) belt_graph_creator = BeltsGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, belt_graph_creator, self.timeout) st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
belt_graph_creator,
self.timeout,
)
compare_belts_responses(gcmd, self._pconfig, st_process) compare_belts_responses(gcmd, self._pconfig, st_process)
def cmd_AXES_SHAPER_CALIBRATION(self, gcmd) -> None: def cmd_AXES_SHAPER_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
shaper_graph_creator = ShaperGraphCreator(self._config) shaper_graph_creator = ShaperGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, shaper_graph_creator, self.timeout) st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
shaper_graph_creator,
self.timeout,
)
axes_shaper_calibration(gcmd, self._pconfig, st_process) axes_shaper_calibration(gcmd, self._pconfig, st_process)
def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None: def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
vibration_profile_creator = VibrationsGraphCreator(self._config) vibration_profile_creator = VibrationsGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, vibration_profile_creator, self.timeout) st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
vibration_profile_creator,
self.timeout,
)
create_vibrations_profile(gcmd, self._pconfig, st_process) create_vibrations_profile(gcmd, self._pconfig, st_process)

View File

@@ -8,10 +8,10 @@
# vibration analysis processes in separate system processes. # vibration analysis processes in separate system processes.
import multiprocessing
import os import os
import threading import threading
import traceback import traceback
from multiprocessing import Process
from typing import Optional from typing import Optional
from .helpers.console_output import ConsoleOutput from .helpers.console_output import ConsoleOutput
@@ -19,11 +19,11 @@ from .shaketune_config import ShakeTuneConfig
class ShakeTuneProcess: class ShakeTuneProcess:
def __init__(self, config: ShakeTuneConfig, graph_creator, timeout: Optional[float] = None) -> None: def __init__(self, st_config: ShakeTuneConfig, reactor, graph_creator, timeout: Optional[float] = None) -> None:
self._config = config self._config = st_config
self._reactor = reactor
self.graph_creator = graph_creator self.graph_creator = graph_creator
self._timeout = timeout self._timeout = timeout
self._process = None self._process = None
def get_graph_creator(self): def get_graph_creator(self):
@@ -31,22 +31,32 @@ class ShakeTuneProcess:
def run(self) -> None: def run(self) -> None:
# Start the target function in a new process (a thread is known to cause issues with Klipper and CANbus due to the GIL) # Start the target function in a new process (a thread is known to cause issues with Klipper and CANbus due to the GIL)
self._process = multiprocessing.Process( self._process = Process(target=self._shaketune_process_wrapper, args=(self.graph_creator, self._timeout))
target=self._shaketune_process_wrapper, args=(self.graph_creator, self._timeout)
)
self._process.start() self._process.start()
def wait_for_completion(self) -> None: def wait_for_completion(self) -> None:
if self._process is not None: if self._process is None:
self._process.join() return # Nothing to wait for
eventtime = self._reactor.monotonic()
endtime = eventtime + self._timeout
complete = False
while eventtime < endtime:
eventtime = self._reactor.pause(eventtime + 0.05)
if not self._process.is_alive():
complete = True
break
if not complete:
self._handle_timeout()
# This function is a simple wrapper to start the Shake&Tune process. It's needed in order to get the timeout # This function is a simple wrapper to start the Shake&Tune process. It's needed in order to get the timeout
# as a Timer in a thread INSIDE the Shake&Tune child process to not interfere with the main Klipper process # as a Timer in a thread INSIDE the Shake&Tune child process to not interfere with the main Klipper process
def _shaketune_process_wrapper(self, graph_creator, timeout) -> None: def _shaketune_process_wrapper(self, graph_creator, timeout) -> None:
if timeout is not None: if timeout is not None:
# Add 5 seconds to the timeout for safety. The goal is to avoid the Timer to finish before the
# Shake&Tune process is done in case we call the wait_for_completion() function that uses Klipper's reactor.
timeout += 5
timer = threading.Timer(timeout, self._handle_timeout) timer = threading.Timer(timeout, self._handle_timeout)
timer.start() timer.start()
try: try:
self._shaketune_process(graph_creator) self._shaketune_process(graph_creator)
finally: finally:
@@ -58,10 +68,12 @@ class ShakeTuneProcess:
os._exit(1) # Forcefully exit the process os._exit(1) # Forcefully exit the process
def _shaketune_process(self, graph_creator) -> None: def _shaketune_process(self, graph_creator) -> None:
# Trying to reduce Shake&Tune process priority to avoid slowing down the main Klipper process # Reducing Shake&Tune process priority by putting the scheduler into batch mode with low priority. This in order to avoid
# as this could lead to random "Timer too close" errors when already running CANbus, etc... # slowing down the main Klipper process as this can lead to random "Timer too close" or "Move queue overflow" errors
# when also already running CANbus, neopixels and other consumming stuff in Klipper's main process.
try: try:
os.nice(19) param = os.sched_param(os.sched_get_priority_min(os.SCHED_BATCH))
os.sched_setscheduler(0, os.SCHED_BATCH, param)
except Exception: except Exception:
ConsoleOutput.print('Warning: failed reducing Shake&Tune process priority, continuing...') ConsoleOutput.print('Warning: failed reducing Shake&Tune process priority, continuing...')