Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
0f1067a
feat: add auto threading functionality and UI components
Pawcu Aug 16, 2025
50858f7
Started adding logic for automatic threading bar
Pawcu Aug 20, 2025
09fa82b
Added logic for binding to the scales;
Pawcu Aug 21, 2025
e8b6ffe
Renamed from auto threading to assisted threading;
Pawcu Aug 23, 2025
611a786
Added settings for Left hand threads and internal threads;
Pawcu Aug 23, 2025
3a7f057
Added logic for disabling action button
Pawcu Aug 23, 2025
93dfa9f
Fixed bug with the action_button condition not being reset on reset o…
Pawcu Aug 23, 2025
855a9e4
Added step 3
Pawcu Aug 24, 2025
4a8a56f
Removed diameter mode settings - this shouldn;t be needed since we co…
Pawcu Aug 25, 2025
a0e5eef
WIP - started adding logic for servo movement; requires testing;
Pawcu Oct 5, 2025
370abe9
Added custom popup;
Pawcu Oct 12, 2025
53548c2
Added custom hold button so that it can keep tracking of press state …
Pawcu Oct 19, 2025
1d02abe
Fixed issue with fastdata assisted threading params to use uint16_t i…
Pawcu Dec 29, 2025
70382f9
Merge remote-tracking branch 'upstream/HEAD' into feature/AutomaticTh…
Pawcu Dec 30, 2025
2476cf0
Added log_caller to the communication.py for debugging purposes;
Pawcu Dec 30, 2025
0cb0096
Setting the thread pitch in the assisted threading popup now correctl…
Pawcu Jan 3, 2026
2b6166d
Additional bug fixes:
Pawcu Jan 22, 2026
c8b958d
Clicking retract goes back to the go to start step;
Pawcu Jan 24, 2026
baa4f30
Added logic for proper backlash preload;
Pawcu Jan 25, 2026
cd60918
WIP - requires testing.
Pawcu Mar 11, 2026
dc2c1cb
WIP - added better handling for different thread pitches and depth ca…
paulcamilleri Mar 14, 2026
3d01a3e
Moved Cross Slide Diameter setting to the AT settings;
Pawcu Mar 14, 2026
0604701
Fixed bug with settings not being persisted because of a type mismatc…
paulcamilleri Mar 14, 2026
24d1668
Removed check for valid cutting depth since we are now using absolute…
paulcamilleri Mar 14, 2026
b6aec40
Added different accellerations for threading and reversing
Pawcu Mar 15, 2026
fe3af00
Bug fixes for showing remaining depths - still need to fix issue with…
Pawcu Mar 16, 2026
f65d5eb
Fixed cutting depth scale display;
Pawcu Mar 21, 2026
365278b
Added logic to properly sync spindle ratio num and den values based o…
Pawcu Mar 21, 2026
ccf7811
Minor fixes for ui;
Pawcu Mar 21, 2026
f2a45ac
Fix for sporadic issue where the watch for servo reached position was…
paulcamilleri Mar 21, 2026
8ce54bb
Further fix for the watch servo issue
paulcamilleri Mar 21, 2026
48ba5b3
Fixed bug with servo max speed not being reset when retracting
paulcamilleri Mar 21, 2026
7a0c8f2
1.3.0
Mar 23, 2026
838a756
Added logic to check that spindle is turning in the CCW direction
paulcamilleri Mar 23, 2026
79e7cfc
Minor UI improvements
Pawcu Mar 23, 2026
49aab90
Added extra check to make sure that the spindle RPM is not too high f…
paulcamilleri Mar 23, 2026
ae7d272
Merge branch 'feature/AutomaticThreading' of https://github.com/Pawcu…
paulcamilleri Mar 23, 2026
7c6edb8
Fixed issue with spindle speed check
paulcamilleri Mar 26, 2026
f7b67dc
Fixing direction for when cutting LHT
paulcamilleri Mar 28, 2026
cdde806
Added logging
Pawcu Mar 28, 2026
25935ef
Merge branch 'feature/AutomaticThreading' of https://github.com/Pawcu…
Pawcu Mar 28, 2026
3a5fb38
Fixed issue with cutting LHT
paulcamilleri Mar 28, 2026
8862749
Added fix to update spindle sync ratio based on whether its LH or RH …
paulcamilleri Mar 28, 2026
10dc4a8
Updated the _check_spindle_speed_for_pitch to also show the max RPM f…
paulcamilleri Mar 28, 2026
ac874ee
fix: prevent RecursionError in CoordBar.update_scaledPosition on spin…
paulcamilleri Mar 28, 2026
380eb90
Add more metric thread pitches
paulcamilleri Mar 28, 2026
e06862b
WIP - started merging upstream/main into feature/AutomaticThreading
Pawcu Mar 28, 2026
0089721
Enable unicode character support for Axis names (parse escape sequences)
Mar 28, 2026
4404f17
Made axis row height logic consistent across modes, and made max row …
Mar 28, 2026
a8fa56a
Merge pull request #43 from Funkenjaeger/feat/axis-row-height
bartei Mar 29, 2026
a953127
Merge pull request #42 from Funkenjaeger/feat/unicode-axis-names
bartei Mar 29, 2026
32d5de1
WIP - minor fixes to get app to compile and run
Pawcu Mar 29, 2026
6d37ea6
WIP - added logic for AT layout and screen
paulcamilleri Mar 29, 2026
1d46c93
Fixed Dispatcher in assisted_threading_bar
paulcamilleri Mar 29, 2026
421c12b
WIP - more fixes following merge from upstream main
paulcamilleri Mar 29, 2026
5210300
WIP - Additional clean-up of existing code;
paulcamilleri Mar 29, 2026
9e533d9
Merge branch 'main' of https://github.com/bartei/rotary-controller-py…
paulcamilleri Mar 29, 2026
751ef7f
Added check to make sure that the scales are set before starting the …
paulcamilleri Mar 29, 2026
3eb4652
Fixed issues with some properties still not migrated to app.board
paulcamilleri Mar 30, 2026
87ffed1
WIP - fixed bug when getting distance from scale
paulcamilleri Mar 30, 2026
2718a52
Fixed issue with spindle sync calculation taking the degrees into con…
Pawcu Mar 31, 2026
e97cef5
Merge branch 'dev' into feature/AutomaticThreading
Pawcu Mar 31, 2026
e3d387e
Merge remote-tracking branch 'upstream/dev' into feature/AutomaticThr…
paulcamilleri Apr 6, 2026
e7c6f13
Split the assisted threading wizard in separate files;
paulcamilleri Apr 6, 2026
9448b53
Added logic for compound slide cutting - requires testing
paulcamilleri Apr 7, 2026
ae9e650
Added help files for assisted threading settings;
paulcamilleri Apr 14, 2026
cddec71
Bumped version to 1.3.0-rc.23
paulcamilleri Apr 14, 2026
22b6c35
Fixed bug in compound offset calculation when in diameter mode
paulcamilleri Apr 20, 2026
b6402da
Bumped version to 1.3.0-rc.24
paulcamilleri Apr 23, 2026
915cc2f
Fixed bug in thread depth calculation when working in TPI
paulcamilleri Apr 27, 2026
6573a7d
Committed fixed tests
paulcamilleri Apr 27, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,4 @@ cython_debug/
/.idea/
config.ini
/rcp/settings/
.claude/settings.local.json
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rcp"
version = "1.3.0-rc.21"
version = "1.3.0-rc.25"
description = "Rotary Controller Python"
authors = [
{ name = "Stefano Bertelli", email = "stefano@provvedo.com" }
Expand Down
Empty file.
72 changes: 72 additions & 0 deletions rcp/components/home/assisted_threading/bar.kv
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<AssistedThreadingBar>:
orientation: "horizontal"
size_hint_y: None
height: 128

Button:
width: 96
size_hint_x: None
text: "Stop" if root.is_running else "Start"
font_size: self.height / 4
font_style: "bold"
background_color: [1, 0.2, 0.2, 1] if root.is_running else [0.3, 0.3, 0.3, 1]
on_release: root.toggle_is_running()

BoxLayout:
id: wizard_area
orientation: "vertical"
size_hint_x: 0.8

# Default display when NOT running
Label:
id: label_instruction
text: root.label_text
text_size: self.size
size_hint_y: 0.15
font_size: 16
color: app.display_color
halign: 'center'
valign: 'top'

Button:
id: btn_value
size_hint_y: 0.30
font_name: "fonts/iosevka-regular.ttf"
font_size: self.height / 2
font_style: "bold"
background_color: [0.2, 0.2, 0.2, 1]
color: app.formats.display_color
text: root.display_value
text_size: self.size
halign: 'center'
valign: 'middle'

ProgressBar:
id: progress_servo
size_hint_y: 0.20
max: int(app.servo.maxSpeed)
value: int(abs(app.servo.speed))

Button:
id: btn_action
width: self.height
size_hint_x: None
font_name: "fonts/iosevka-regular.ttf" if root.is_running else "fonts/Font Awesome 6 Free-Solid-900.otf"
text: root.next_button_text if root.is_running else "\uf013"
font_size: self.height / 4
halign: "center"
on_release: root.on_action_button_clicked()
disabled: not root.action_button_enabled

HoldButton:
id: btn_retract
font_name: "fonts/iosevka-regular.ttf"
size_hint_x: None if root.retract_button_visible else 0
width: self.height if root.retract_button_visible else 0
opacity: 1 if root.retract_button_visible else 0
disabled: not root.retract_button_enabled
text: 'Retract'
font_size: self.height / 4
halign: "center"
on_press: root.on_retract_button_pressed()
on_release: root.on_retract_button_released()
238 changes: 238 additions & 0 deletions rcp/components/home/assisted_threading/bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
from kivy.logger import Logger
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty, BooleanProperty, StringProperty

from rcp import feeds
from rcp.components.widgets.custom_popup import CustomPopup
from rcp.components.widgets.hold_button import HoldButton
from rcp.components.home.assisted_threading.wizard import AssistedThreadingWizard
from rcp.components.home.assisted_threading.thread_type import ThreadType
from rcp.dispatchers.saving_dispatcher import SavingDispatcher
from rcp.utils.kv_loader import load_kv

log = Logger.getChild(__name__)

load_kv(__file__)

class AssistedThreadingBar(BoxLayout, SavingDispatcher):
# ── Per-job thread settings (saved on the bar) ────────────────────
metric_mode = BooleanProperty(True) # This is for the actual threading logic
selected_pitch = StringProperty("")
current_feeds_index = NumericProperty(0)
thread_profile_type = StringProperty("ISO_METRIC")
shaft_diameter = NumericProperty(1)
left_hand_thread = BooleanProperty(False)
inner_thread = BooleanProperty(False)

compound_infeed_mode = BooleanProperty(False)
compound_infeed_offset_degrees = NumericProperty(1.0)

is_running = BooleanProperty(False)
action_button_enabled = BooleanProperty(True)
label_text = StringProperty("")
display_value = StringProperty("")
next_button_text = StringProperty("")
start_position = NumericProperty(0)
stop_position = NumericProperty(0)
material_width = NumericProperty(0)
cutting_depth = NumericProperty(0)
last_cutting_depth = NumericProperty(0)
retract_button_visible = BooleanProperty(False)
retract_button_enabled = BooleanProperty(True)
_skip_save = [
"is_running",
"action_button_enabled",
"label_text",
"display_value",
"start_position",
"stop_position",
"material_width",
"cutting_depth",
"last_cutting_depth",
"retract_button_visible",
"retract_button_enabled"
]

def __init__(self, **kv):
from rcp.app import MainApp
self.app: MainApp = MainApp.get_running_app()
self.action_button_condition_fn = None
self.retract_button_condition_fn = None
super().__init__(**kv)

self.current_feeds_table = feeds.table["Thread MM"] if self.metric_mode else feeds.table["Thread IN"]
self.update_feeds_ratio(self, None)

# Initialize with default thread type if not set
if not self.thread_profile_type:
self.thread_profile_type = ThreadType.ISO_METRIC.value
self.wizard = AssistedThreadingWizard(self)

self.app.bind(current_mode=self.on_mode_change)
self.bind(left_hand_thread=self.update_feeds_ratio)

def toggle_is_running(self):
if not self.is_running:
missing = []
if self.app.els.get_spindle_axis() is None:
missing.append("Spindle")
if self.app.els.get_z_axis() is None:
missing.append("Saddle (Z)")
if self.app.els.get_x_axis() is None:
missing.append("Cross-slide (X)")
if missing:
CustomPopup(
title="Axes Not Configured",
message=f"The following axes are not set in ELS: {', '.join(missing)}. Please configure them in Settings.",
button_text="OK",
).open()
return
self.is_running = not self.is_running
if self.is_running:
self.wizard.start()
else:
self.stop_wizard()

def stop_wizard(self):
self.wizard.stop()

def on_metric_mode(self, instance, value):
self.current_feeds_table = feeds.table["Thread MM"] if value else feeds.table["Thread IN"]

def on_mode_change(self, instance, mode):
if mode == 5: # AT mode
self.update_feeds_ratio(None, None)

def on_retract_button_pressed(self):
"""Called when the retract button is pressed."""
if not self.retract_button_enabled:
return
self.wizard.start_retracting()

def on_retract_button_released(self):
"""Called when the retract button is released."""
if not self.retract_button_enabled:
return
self.wizard.stop_retracting()

def on_action_button_clicked(self):
"""Called when the right button is pressed."""
if self.is_running:
self.wizard.goto_next_step()
else:
self.open_settings()

def update_feeds_ratio(self, instance, value):
if self.app.current_mode != 5:
return # only sync in AT mode

ratio = self.current_feeds_table[self.current_feeds_index].ratio
spindle_axis = self.app.els.get_spindle_axis()
if spindle_axis is not None:
direction = -1 if self.left_hand_thread else 1
spindle_axis.syncRatioNum = ratio.numerator * direction
spindle_axis.syncRatioDen = ratio.denominator
log.info(f"Configured ratio is: {ratio.numerator}/{ratio.denominator}, left_hand_thread={self.left_hand_thread}")

def open_settings(self):
from rcp.components.home.assisted_threading.settings_popup import AssistedThreadingSettingsPopup
popup = AssistedThreadingSettingsPopup(assistedThreadingBar=self)
popup.open()

def bind_display_value_to_scale(self, axis):
"""Bind display_value to an AxisDispatcher's formattedPosition with strict keypad override support."""

# Unbind any previous bindings
self.unbind_all_display_value()

# Store the axis (AxisDispatcher) for later unbind
self._bound_scale = axis
inp = axis._primary_input() if axis is not None else None

# --- Encoder update handler (fires on raw encoder tick) ---
def on_encoder_update(*_):
# Cancel manual override if the encoder moves
if self.wizard and self.wizard.manual_stop_length is not None:
log.info("Scale encoder moved — discarding manual stop length override")
self.wizard.manual_stop_length = None
# Display the axis formatted position (not raw encoder ticks)
self.display_value = axis.formattedPosition
self.update_buttons_state()

# --- Format update handler ---
def on_format_update(instance, value):
# Only update display if NOT in manual override
if not (self.wizard and self.wizard.manual_stop_length is not None):
self.display_value = value

# Keep references so we can unbind later
self._on_encoder_update = on_encoder_update
self._on_format_update = on_format_update

# encoderCurrent lives on InputDispatcher; formattedPosition on AxisDispatcher
if inp is not None:
inp.bind(encoderCurrent=on_encoder_update)
axis.bind(formattedPosition=on_format_update)

# Initial display
self.display_value = axis.formattedPosition

def bind_display_value_to_servo_position(self):
"""Bind display_value to the servo's formattedPosition."""
# Unbind any previous bindings
self.unbind_all_display_value()
self._bound_servo = self.app.servo

def on_servo_position_update(instance, value):
self.display_value = value

self._on_servo_position_update = on_servo_position_update

# Bind to servo's formattedPosition
self.app.servo.bind(formattedPosition=on_servo_position_update)

def bind_btn_value_on_release(self, on_release_fn):
"""Bind the value button to a function."""
# Unbind old function if it exists
if hasattr(self, "_on_value_button_release") and self._on_value_button_release is not None:
self.ids.btn_value.unbind(on_release=self._on_value_button_release)

# Store the binding function
self._on_value_button_release = on_release_fn

if(on_release_fn is None):
# If None is passed, disable the button
self.ids.btn_value.disabled = True
return

self.ids.btn_value.disabled = False
# Bind the new function
self.ids.btn_value.bind(on_release=on_release_fn)

def unbind_all_display_value(self):
if hasattr(self, "_bound_scale") and self._bound_scale is not None:
inp = self._bound_scale._primary_input()
if inp is not None:
inp.unbind(encoderCurrent=self._on_encoder_update)
self._bound_scale.unbind(formattedPosition=self._on_format_update)
self._bound_scale = None
if hasattr(self, "_bound_servo") and self._bound_servo is not None:
self._bound_servo.unbind(formattedPosition=self._on_servo_position_update)
self._bound_servo = None
# Unbind threading progress display if it was bound
if hasattr(self.wizard, "_progress_display_scale") and self.wizard._progress_display_scale is not None:
if hasattr(self.wizard, "_on_threading_progress_update"):
self.wizard._progress_display_scale.unbind(encoderCurrent=self.wizard._on_threading_progress_update)
self.wizard._progress_display_scale = None

def update_buttons_state(self):
"""Evaluate whether the action/retract buttons should be enabled."""
if self.action_button_condition_fn:
self.action_button_enabled = self.action_button_condition_fn()
else:
self.action_button_enabled = True

if self.retract_button_condition_fn:
self.retract_button_enabled = self.retract_button_condition_fn()
else:
self.retract_button_enabled = True
Loading