fix random Timer too close or Move queue overflow errors (#123)
This commit is contained in:
@@ -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 = []
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)')
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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}'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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...')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user