Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions shaketune/commands/accelerometer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@
# accelerometer measurements and write the data to a file in a blocking manner.


import os
import time

# from ..helpers.console_output import ConsoleOutput
from multiprocessing import Process, Queue


class Accelerometer:
def __init__(self, klipper_accelerometer):
self._k_accelerometer = klipper_accelerometer

self._bg_client = None
self._write_queue = Queue()
Frix-x marked this conversation as resolved.
Show resolved Hide resolved
self._write_processes = []

@staticmethod
def find_axis_accelerometer(printer, axis: str = 'xy'):
Expand All @@ -32,7 +35,6 @@ def find_axis_accelerometer(printer, axis: str = 'xy'):
def start_measurement(self):
if self._bg_client is None:
self._bg_client = self._k_accelerometer.start_internal_client()
# ConsoleOutput.print('Accelerometer measurements started')
else:
raise ValueError('measurements already started!')

Expand All @@ -54,12 +56,30 @@ def stop_measurement(self, name: str = None, append_time: bool = True):
bg_client.finish_measurements()

filename = f'/tmp/shaketune-{name}.csv'
self._write_to_file(bg_client, filename)
# ConsoleOutput.print(f'Accelerometer measurements stopped. Data written to {filename}')
self._queue_file_write(bg_client, filename)

def _queue_file_write(self, bg_client, filename):
self._write_queue.put(filename)
Frix-x marked this conversation as resolved.
Show resolved Hide resolved
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):
try:
os.nice(20)
except Exception:
pass
with open(filename, 'w') as f:
f.write('#time,accel_x,accel_y,accel_z\n')
samples = bg_client.samples or bg_client.get_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')
self._write_queue.get()
Frix-x marked this conversation as resolved.
Show resolved Hide resolved

def wait_for_file_writes(self):
while not self._write_queue.empty():
Frix-x marked this conversation as resolved.
Show resolved Hide resolved
time.sleep(0.1)
for proc in self._write_processes:
proc.join()
self._write_processes = []
2 changes: 2 additions & 0 deletions shaketune/commands/axes_map_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
toolhead.dwell(0.5)
accelerometer.stop_measurement('axesmap_Z', append_time=True)

accelerometer.wait_for_file_writes()

# Re-enable the input shaper if it was active
if input_shaper is not None:
input_shaper.enable_shaping()
Expand Down
2 changes: 2 additions & 0 deletions shaketune/commands/axes_shaper_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
accelerometer.stop_measurement(config['label'], append_time=True)

accelerometer.wait_for_file_writes()

# And finally generate the graph for each measured axis
ConsoleOutput.print(f'{config["axis"].upper()} axis frequency profile generation...')
ConsoleOutput.print('This may take some time (1-3min)')
Expand Down
2 changes: 2 additions & 0 deletions shaketune/commands/compare_belts_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
accelerometer.stop_measurement(config['label'], append_time=True)

accelerometer.wait_for_file_writes()

# Re-enable the input shaper if it was active
if input_shaper is not None:
input_shaper.enable_shaping()
Expand Down
4 changes: 3 additions & 1 deletion shaketune/commands/create_vibrations_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
k_accelerometer = printer.lookup_object(current_accel_chip, None)
if k_accelerometer is None:
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}]')
accelerometer = Accelerometer(k_accelerometer)

# Sweep the speed range to record the vibrations at different speeds
for curr_speed_sample in range(nb_speed_samples):
Expand Down Expand Up @@ -131,6 +131,8 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
toolhead.dwell(0.3)
toolhead.wait_moves()

accelerometer.wait_for_file_writes()

# Restore the previous acceleration values
gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}'
Expand Down
1 change: 1 addition & 0 deletions shaketune/commands/excitate_axis_at_freq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 create_graph:
accelerometer.stop_measurement(f'staticfreq_{axis.upper()}', append_time=True)
accelerometer.wait_for_file_writes()

creator = st_process.get_graph_creator()
creator.configure(freq, duration, accel_per_hz)
Expand Down
35 changes: 30 additions & 5 deletions shaketune/shaketune.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,29 +117,54 @@ def __init__(self, config) -> None:
def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
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)

def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
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)

def cmd_COMPARE_BELTS_RESPONSES(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
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)

def cmd_AXES_SHAPER_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
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)

def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
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)
38 changes: 25 additions & 13 deletions shaketune/shaketune_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,55 @@
# vibration analysis processes in separate system processes.


import multiprocessing
import os
import threading
import traceback
from multiprocessing import Process
from typing import Optional

from .helpers.console_output import ConsoleOutput
from .shaketune_config import ShakeTuneConfig


class ShakeTuneProcess:
def __init__(self, config: ShakeTuneConfig, graph_creator, timeout: Optional[float] = None) -> None:
self._config = config
def __init__(self, st_config: ShakeTuneConfig, reactor, graph_creator, timeout: Optional[float] = None) -> None:
self._config = st_config
self._reactor = reactor
self.graph_creator = graph_creator
self._timeout = timeout

self._process = None

def get_graph_creator(self):
return self.graph_creator

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)
self._process = multiprocessing.Process(
target=self._shaketune_process_wrapper, args=(self.graph_creator, self._timeout)
)
self._process = Process(target=self._shaketune_process_wrapper, args=(self.graph_creator, self._timeout))
Frix-x marked this conversation as resolved.
Show resolved Hide resolved
self._process.start()

def wait_for_completion(self) -> None:
if self._process is not None:
self._process.join()
if self._process is None:
Frix-x marked this conversation as resolved.
Show resolved Hide resolved
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
# 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:
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.start()

try:
self._shaketune_process(graph_creator)
finally:
Expand All @@ -58,10 +68,12 @@ def _handle_timeout(self) -> None:
os._exit(1) # Forcefully exit the process

def _shaketune_process(self, graph_creator) -> None:
# Trying to reduce Shake&Tune process priority to avoid slowing down the main Klipper process
# as this could lead to random "Timer too close" errors when already running CANbus, etc...
# Reducing Shake&Tune process priority by putting the scheduler into batch mode with low priority. This in order to avoid
# 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:
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:
ConsoleOutput.print('Warning: failed reducing Shake&Tune process priority, continuing...')

Expand Down