Joint-python-library 0.0.3__tar.gz → 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/Joint_python_library.egg-info/PKG-INFO +1 -1
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/Joint_python_library.egg-info/SOURCES.txt +7 -1
- joint_python_library-0.0.4/Joint_python_library.egg-info/entry_points.txt +2 -0
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/Joint_python_library.egg-info/top_level.txt +1 -0
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/PKG-INFO +1 -1
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/acrome_joint/Slave_Device.py +1 -1
- joint_python_library-0.0.4/acrome_joint/__init__.py +0 -0
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/acrome_joint/serial_port.py +31 -2
- joint_python_library-0.0.4/gui/__init__.py +0 -0
- joint_python_library-0.0.4/gui/joint_device.py +6 -0
- joint_python_library-0.0.4/gui/limits.py +17 -0
- joint_python_library-0.0.4/gui/main.py +858 -0
- joint_python_library-0.0.4/gui/ramp_trajectory.py +96 -0
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/setup.py +6 -1
- joint_python_library-0.0.3/acrome_joint/__init__.py +0 -1
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/Joint_python_library.egg-info/dependency_links.txt +0 -0
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/Joint_python_library.egg-info/requires.txt +0 -0
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/LICENSE +0 -0
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/README.md +0 -0
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/acrome_joint/joint.py +0 -0
- {joint_python_library-0.0.3 → joint_python_library-0.0.4}/setup.cfg +0 -0
{joint_python_library-0.0.3 → joint_python_library-0.0.4}/Joint_python_library.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Joint-python-library
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Python library for interfacing with Acrome Robotic Arm Joint BLDC Motor Controllers
|
|
5
5
|
Home-page: https://github.com/Acrome-Smart-Motion-Devices/python-library-new
|
|
6
6
|
Author: BeratComputer
|
{joint_python_library-0.0.3 → joint_python_library-0.0.4}/Joint_python_library.egg-info/SOURCES.txt
RENAMED
|
@@ -4,9 +4,15 @@ setup.py
|
|
|
4
4
|
Joint_python_library.egg-info/PKG-INFO
|
|
5
5
|
Joint_python_library.egg-info/SOURCES.txt
|
|
6
6
|
Joint_python_library.egg-info/dependency_links.txt
|
|
7
|
+
Joint_python_library.egg-info/entry_points.txt
|
|
7
8
|
Joint_python_library.egg-info/requires.txt
|
|
8
9
|
Joint_python_library.egg-info/top_level.txt
|
|
9
10
|
acrome_joint/Slave_Device.py
|
|
10
11
|
acrome_joint/__init__.py
|
|
11
12
|
acrome_joint/joint.py
|
|
12
|
-
acrome_joint/serial_port.py
|
|
13
|
+
acrome_joint/serial_port.py
|
|
14
|
+
gui/__init__.py
|
|
15
|
+
gui/joint_device.py
|
|
16
|
+
gui/limits.py
|
|
17
|
+
gui/main.py
|
|
18
|
+
gui/ramp_trajectory.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Joint-python-library
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Python library for interfacing with Acrome Robotic Arm Joint BLDC Motor Controllers
|
|
5
5
|
Home-page: https://github.com/Acrome-Smart-Motion-Devices/python-library-new
|
|
6
6
|
Author: BeratComputer
|
|
@@ -3,7 +3,7 @@ import struct
|
|
|
3
3
|
from crccheck.crc import Crc32Mpeg2 as CRC32
|
|
4
4
|
import time
|
|
5
5
|
import enum
|
|
6
|
-
from acrome_joint.serial_port import
|
|
6
|
+
from acrome_joint.serial_port import *
|
|
7
7
|
'''
|
|
8
8
|
COMMUNICATION PACKAGE =>
|
|
9
9
|
HEADER, ID, DEVICE_FAMILY, PACKAGE_SIZE, COMMAND, STATUS, .............. DATA ................. , CRC
|
|
File without changes
|
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
import serial
|
|
2
|
+
import serial.tools.list_ports
|
|
3
|
+
import platform
|
|
4
|
+
|
|
5
|
+
def whichOS():
|
|
6
|
+
return platform.system()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def USB_serial_port(keyword_for_WINDOWS:str='USB Serial Port', keyword_for_LINUX:str='/dev/ttyUSB'):
|
|
10
|
+
|
|
11
|
+
if whichOS() == "Windows":
|
|
12
|
+
ports = list(serial.tools.list_ports.comports())
|
|
13
|
+
if ports:
|
|
14
|
+
for port, desc, hwid in sorted(ports):
|
|
15
|
+
#print(f"{port}: {desc} [{hwid}]")
|
|
16
|
+
#print(type(port))
|
|
17
|
+
if keyword_for_WINDOWS in desc:
|
|
18
|
+
return port
|
|
19
|
+
else:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
if whichOS() == "Linux":
|
|
23
|
+
ports = list(serial.tools.list_ports.comports())
|
|
24
|
+
if ports:
|
|
25
|
+
for port, desc, hwid in sorted(ports):
|
|
26
|
+
#print(f"{port}: {desc} [{hwid}]")
|
|
27
|
+
#print(type(port))
|
|
28
|
+
if keyword_for_LINUX in port:
|
|
29
|
+
return port
|
|
30
|
+
else:
|
|
31
|
+
return None
|
|
32
|
+
|
|
2
33
|
|
|
3
34
|
class SerialPort:
|
|
4
35
|
def __init__(self, port_name, baudrate=921600, timeout=0.1, isTest:bool=False):
|
|
@@ -55,5 +86,3 @@ class SerialPort:
|
|
|
55
86
|
else:
|
|
56
87
|
self._ph.timeout = timeout
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
# SPEED = RPM, ACC= RPM/s
|
|
3
|
+
ENCODER_CPR = 4096
|
|
4
|
+
|
|
5
|
+
MOTOR_VEL_MAX = 150.0 # in rpm
|
|
6
|
+
MOTOR_ACC_MAX = 500.0
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MOTOR_VEL_MAX_IN_ENC_TYPE = MOTOR_VEL_MAX * 68.2666
|
|
10
|
+
MOTOR_ACC_MAX_IN_ENC_TYPE = MOTOR_ACC_MAX * 68.2666
|
|
11
|
+
|
|
12
|
+
def rpm_to_tick_per_second(rpm:float):
|
|
13
|
+
return rpm*ENCODER_CPR/60
|
|
14
|
+
|
|
15
|
+
def tick_per_second_to_rpm(tick_per_second:float):
|
|
16
|
+
return tick_per_second*60/ENCODER_CPR
|
|
17
|
+
|
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
# app.py
|
|
2
|
+
# pip install PySide6 pyqtgraph numpy
|
|
3
|
+
|
|
4
|
+
from PySide6 import QtCore, QtGui, QtWidgets
|
|
5
|
+
import pyqtgraph as pg
|
|
6
|
+
import numpy as np
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from gui.joint_device import *
|
|
10
|
+
from gui.ramp_trajectory import * # <-- S-Curve Ramp sınıfını kullanacağız
|
|
11
|
+
from gui.limits import *
|
|
12
|
+
|
|
13
|
+
NUM_FMT = "{:.3f}" # tabloda göstereceğimiz sayısal format
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =========================
|
|
17
|
+
# OPERATION için parametre grupları
|
|
18
|
+
# =========================
|
|
19
|
+
OP_PARAM_GROUPS = {
|
|
20
|
+
1: [ # Plot 1 yanındaki tablo (8 adet)
|
|
21
|
+
"current_Id",
|
|
22
|
+
"current_Iq",
|
|
23
|
+
"currentId_loop_kp",
|
|
24
|
+
"currentId_loop_ki",
|
|
25
|
+
"currentId_loop_kd",
|
|
26
|
+
"currentIq_loop_kp",
|
|
27
|
+
"currentIq_loop_ki",
|
|
28
|
+
"currentIq_loop_kd",
|
|
29
|
+
],
|
|
30
|
+
2: [ # Plot 2 yanındaki tablo (4 adet)
|
|
31
|
+
"current_velocity",
|
|
32
|
+
"velocity_loop_kp",
|
|
33
|
+
"velocity_loop_ki",
|
|
34
|
+
"velocity_loop_kd",
|
|
35
|
+
],
|
|
36
|
+
3: [ # Plot 3 yanındaki tablo (4 adet)
|
|
37
|
+
"current_position",
|
|
38
|
+
"position_loop_kp",
|
|
39
|
+
"position_loop_ki",
|
|
40
|
+
"position_loop_kd",
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# =========================
|
|
46
|
+
# Ortak Signal Bus
|
|
47
|
+
# =========================
|
|
48
|
+
class SignalBus(QtCore.QObject):
|
|
49
|
+
tabChanged = QtCore.Signal(int, str)
|
|
50
|
+
configParamChanged = QtCore.Signal(str, str)
|
|
51
|
+
operationParamChanged = QtCore.Signal(int, str, str)
|
|
52
|
+
refreshClicked = QtCore.Signal()
|
|
53
|
+
restartClicked = QtCore.Signal()
|
|
54
|
+
configSaveClicked = QtCore.Signal()
|
|
55
|
+
FactoryResetClicked = QtCore.Signal()
|
|
56
|
+
newTimedData = QtCore.Signal(float, float, float, float, float, float, float, float) # y1,y2,y3,y4, s1,s2,s3, t
|
|
57
|
+
idChanged = QtCore.Signal(str) # Uygulama kimliği değiştiğinde yayınlanır
|
|
58
|
+
enableToggled = QtCore.Signal(bool) # ON/OFF değişimi için
|
|
59
|
+
enableStateUpdated = QtCore.Signal(bool) # cihazdan gelen enable -> UI
|
|
60
|
+
|
|
61
|
+
# --- S-Curve eklentisi için yeni sinyaller ---
|
|
62
|
+
sCurveToggled = QtCore.Signal(bool) # S-Curve enable/disable
|
|
63
|
+
sCurvePlanRequested = QtCore.Signal(float, float, float, float) # target, vmax, a_des, t_des
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# =========================
|
|
67
|
+
# Başlangıç ID Diyaloğu
|
|
68
|
+
# =========================
|
|
69
|
+
class StartupDialog(QtWidgets.QDialog):
|
|
70
|
+
def __init__(self, parent=None):
|
|
71
|
+
super().__init__(parent)
|
|
72
|
+
self.setWindowTitle("Cihaz ID Girişi")
|
|
73
|
+
self.setModal(True)
|
|
74
|
+
self.id_value: Optional[str] = None
|
|
75
|
+
|
|
76
|
+
layout = QtWidgets.QVBoxLayout(self)
|
|
77
|
+
|
|
78
|
+
form = QtWidgets.QFormLayout()
|
|
79
|
+
self.leID = QtWidgets.QLineEdit()
|
|
80
|
+
self.leID.setPlaceholderText("Örn: 42")
|
|
81
|
+
form.addRow("ID:", self.leID)
|
|
82
|
+
|
|
83
|
+
self.cbRemember = QtWidgets.QCheckBox("Bu ID'yi hatırla")
|
|
84
|
+
layout.addLayout(form)
|
|
85
|
+
layout.addWidget(self.cbRemember)
|
|
86
|
+
|
|
87
|
+
btns = QtWidgets.QDialogButtonBox(
|
|
88
|
+
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
|
|
89
|
+
)
|
|
90
|
+
btns.accepted.connect(self._on_ok)
|
|
91
|
+
btns.rejected.connect(self.reject)
|
|
92
|
+
layout.addWidget(btns)
|
|
93
|
+
|
|
94
|
+
# QSettings: Son ID’yi yükle
|
|
95
|
+
sett = QtCore.QSettings("Acrome", "UartUiApp")
|
|
96
|
+
last_id = sett.value("last_id", "", type=str)
|
|
97
|
+
if last_id:
|
|
98
|
+
self.leID.setText(last_id)
|
|
99
|
+
self.cbRemember.setChecked(True)
|
|
100
|
+
|
|
101
|
+
def _on_ok(self):
|
|
102
|
+
text = self.leID.text().strip()
|
|
103
|
+
if not text:
|
|
104
|
+
QtWidgets.QMessageBox.warning(self, "Uyarı", "Lütfen bir ID girin.")
|
|
105
|
+
return
|
|
106
|
+
self.id_value = text
|
|
107
|
+
# Hatırlama
|
|
108
|
+
sett = QtCore.QSettings("Acrome", "UartUiApp")
|
|
109
|
+
if self.cbRemember.isChecked():
|
|
110
|
+
sett.setValue("last_id", text)
|
|
111
|
+
else:
|
|
112
|
+
sett.remove("last_id")
|
|
113
|
+
self.accept()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# =========================
|
|
117
|
+
# CONFIG Sekmesi
|
|
118
|
+
# =========================
|
|
119
|
+
class ConfigTab(QtWidgets.QWidget):
|
|
120
|
+
def __init__(self, bus: SignalBus, parent=None):
|
|
121
|
+
super().__init__(parent)
|
|
122
|
+
self.bus = bus
|
|
123
|
+
self._suppress_cell_signal = False
|
|
124
|
+
|
|
125
|
+
layout = QtWidgets.QHBoxLayout(self)
|
|
126
|
+
|
|
127
|
+
self.table = QtWidgets.QTableWidget(self)
|
|
128
|
+
self.table.setColumnCount(2)
|
|
129
|
+
self.table.setHorizontalHeaderLabels(["Parameter", "Value"])
|
|
130
|
+
self.table.horizontalHeader().setStretchLastSection(True)
|
|
131
|
+
self.table.verticalHeader().setVisible(False)
|
|
132
|
+
self.table.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers)
|
|
133
|
+
|
|
134
|
+
self.parameters = [
|
|
135
|
+
{"name": 'Header', "value": "", "read_only": True},
|
|
136
|
+
{"name": 'DeviceID', "value": "", "read_only": True},
|
|
137
|
+
{"name": 'DeviceFamily', "value": "", "read_only": True},
|
|
138
|
+
{"name": 'PackageSize', "value": "", "read_only": True},
|
|
139
|
+
{"name": 'Command', "value": "", "read_only": True},
|
|
140
|
+
{"name": 'Status', "value": "", "read_only": True},
|
|
141
|
+
{"name": 'HardwareVersion', "value": "", "read_only": True},
|
|
142
|
+
{"name": 'SoftwareVersion', "value": "", "read_only": True},
|
|
143
|
+
{"name": 'Baudrate', "value": "", "read_only": True},
|
|
144
|
+
{"name": 'OperationMode', "value": "", "read_only": False},
|
|
145
|
+
{"name": 'Enable', "value": "", "read_only": False},
|
|
146
|
+
{"name": 'Vbus_read', "value": "", "read_only": True},
|
|
147
|
+
{"name": 'Temprature_read', "value": "", "read_only": True},
|
|
148
|
+
{"name": 'currentId_loop_kp', "value": "", "read_only": False},
|
|
149
|
+
{"name": 'currentId_loop_ki', "value": "", "read_only": False},
|
|
150
|
+
{"name": 'currentId_loop_kd', "value": "", "read_only": False},
|
|
151
|
+
{"name": 'currentIq_loop_kp', "value": "", "read_only": False},
|
|
152
|
+
{"name": 'currentIq_loop_ki', "value": "", "read_only": False},
|
|
153
|
+
{"name": 'currentIq_loop_kd', "value": "", "read_only": False},
|
|
154
|
+
{"name": 'velocity_loop_kp', "value": "", "read_only": False},
|
|
155
|
+
{"name": 'velocity_loop_ki', "value": "", "read_only": False},
|
|
156
|
+
{"name": 'velocity_loop_kd', "value": "", "read_only": False},
|
|
157
|
+
{"name": 'position_loop_kp', "value": "", "read_only": False},
|
|
158
|
+
{"name": 'position_loop_ki', "value": "", "read_only": False},
|
|
159
|
+
{"name": 'position_loop_kd', "value": "", "read_only": False},
|
|
160
|
+
{"name": 'max_position', "value": "", "read_only": False},
|
|
161
|
+
{"name": 'min_position', "value": "", "read_only": False},
|
|
162
|
+
{"name": 'max_velocity', "value": "", "read_only": False},
|
|
163
|
+
{"name": 'max_current', "value": "", "read_only": False},
|
|
164
|
+
{"name": 'current_Va', "value": "", "read_only": True},
|
|
165
|
+
{"name": 'current_Vb', "value": "", "read_only": True},
|
|
166
|
+
{"name": 'current_Vc', "value": "", "read_only": True},
|
|
167
|
+
{"name": 'current_Ia', "value": "", "read_only": True},
|
|
168
|
+
{"name": 'current_Ib', "value": "", "read_only": True},
|
|
169
|
+
{"name": 'current_Ic', "value": "", "read_only": True},
|
|
170
|
+
{"name": 'current_Id', "value": "", "read_only": True},
|
|
171
|
+
{"name": 'current_Iq', "value": "", "read_only": True},
|
|
172
|
+
{"name": 'current_velocity', "value": "", "read_only": True},
|
|
173
|
+
{"name": 'current_position', "value": "", "read_only": True},
|
|
174
|
+
{"name": 'current_electrical_degree', "value": "", "read_only": True},
|
|
175
|
+
{"name": 'current_electrical_radian', "value": "", "read_only": True},
|
|
176
|
+
{"name": 'setpoint_current', "value": "", "read_only": False},
|
|
177
|
+
{"name": 'setpoint_velocity', "value": "", "read_only": False},
|
|
178
|
+
{"name": 'setpoint_position', "value": "", "read_only": False},
|
|
179
|
+
{"name": 'openloop_voltage_size', "value": "", "read_only": False},
|
|
180
|
+
{"name": 'openloop_angle_degree', "value": "", "read_only": False},
|
|
181
|
+
{"name": 'current_lock_angle_degree', "value": "", "read_only": False},
|
|
182
|
+
{"name": 'Config_TimeStamp', "value": "", "read_only": False},
|
|
183
|
+
{"name": 'Config_Description', "value": "", "read_only": False},
|
|
184
|
+
{"name": 'CRCValue', "value": "", "read_only": True},
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
# İlk değerleri cihazdan çek
|
|
188
|
+
for var in self.parameters:
|
|
189
|
+
var["value"] = Device.get_variables(Index_Joint[var["name"]])[0]
|
|
190
|
+
|
|
191
|
+
self._populate_table()
|
|
192
|
+
self.table.cellChanged.connect(self._on_cell_changed)
|
|
193
|
+
|
|
194
|
+
# Sağ buton sütunu
|
|
195
|
+
btn_col = QtWidgets.QVBoxLayout()
|
|
196
|
+
self.btnRefresh = QtWidgets.QPushButton("Refresh")
|
|
197
|
+
self.btnRestart = QtWidgets.QPushButton("Restart")
|
|
198
|
+
self.btnConfigSave = QtWidgets.QPushButton("Config Save")
|
|
199
|
+
self.btnApply = QtWidgets.QPushButton("Apply")
|
|
200
|
+
for b in (self.btnRefresh, self.btnRestart, self.btnConfigSave, self.btnApply):
|
|
201
|
+
b.setMinimumHeight(40)
|
|
202
|
+
btn_col.addWidget(b)
|
|
203
|
+
btn_col.addStretch()
|
|
204
|
+
|
|
205
|
+
self.btnRefresh.clicked.connect(self.bus.refreshClicked)
|
|
206
|
+
self.btnRestart.clicked.connect(self.bus.restartClicked)
|
|
207
|
+
self.btnConfigSave.clicked.connect(self.bus.configSaveClicked)
|
|
208
|
+
self.btnApply.clicked.connect(self.bus.FactoryResetClicked)
|
|
209
|
+
|
|
210
|
+
layout.addWidget(self.table, 3)
|
|
211
|
+
layout.addLayout(btn_col, 1)
|
|
212
|
+
|
|
213
|
+
print("config tab init")
|
|
214
|
+
|
|
215
|
+
def _populate_table(self):
|
|
216
|
+
self._suppress_cell_signal = True
|
|
217
|
+
self.table.setRowCount(len(self.parameters))
|
|
218
|
+
for row, p in enumerate(self.parameters):
|
|
219
|
+
name_item = QtWidgets.QTableWidgetItem(p["name"])
|
|
220
|
+
name_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
|
221
|
+
self.table.setItem(row, 0, name_item)
|
|
222
|
+
|
|
223
|
+
val_item = QtWidgets.QTableWidgetItem(str(p["value"]))
|
|
224
|
+
if p["read_only"]:
|
|
225
|
+
val_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
|
226
|
+
self.table.setItem(row, 1, val_item)
|
|
227
|
+
self._suppress_cell_signal = False
|
|
228
|
+
|
|
229
|
+
def _on_cell_changed(self, row, column):
|
|
230
|
+
if self._suppress_cell_signal or column != 1:
|
|
231
|
+
return
|
|
232
|
+
name = self.table.item(row, 0).text()
|
|
233
|
+
value = self.table.item(row, 1).text()
|
|
234
|
+
if self.parameters[row]["read_only"]:
|
|
235
|
+
self._suppress_cell_signal = True
|
|
236
|
+
self.table.item(row, 1).setText(str(self.parameters[row]["value"]))
|
|
237
|
+
self._suppress_cell_signal = False
|
|
238
|
+
return
|
|
239
|
+
self.parameters[row]["value"] = value
|
|
240
|
+
self.bus.configParamChanged.emit(name, value)
|
|
241
|
+
|
|
242
|
+
def refresh_from_device(self, new_params: dict):
|
|
243
|
+
# Şu an parametreleri zaten self.parameters'tan alıyoruz
|
|
244
|
+
self._populate_table()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# =========================
|
|
248
|
+
# OPERATION Sekmesi
|
|
249
|
+
# =========================
|
|
250
|
+
class OperationPanel(QtWidgets.QGroupBox):
|
|
251
|
+
def __init__(self, bus: SignalBus, panel_id: int, title: str, plot_mode: str = "single",
|
|
252
|
+
extra_param_label: str | None = None, extra_param_key: str | None = None, parent=None):
|
|
253
|
+
super().__init__(title, parent)
|
|
254
|
+
self.bus = bus
|
|
255
|
+
self.panel_id = panel_id
|
|
256
|
+
self.plot_mode = plot_mode
|
|
257
|
+
self._suppress_cell_signal = False
|
|
258
|
+
|
|
259
|
+
# --- ekstra parametre meta ---
|
|
260
|
+
self.extra_param_label = extra_param_label # UI'da gösterilecek isim (ör. current_setpoint)
|
|
261
|
+
self.extra_param_key = extra_param_key # Index_Joint anahtarı (ör. setpoint_current)
|
|
262
|
+
|
|
263
|
+
layout = QtWidgets.QHBoxLayout(self)
|
|
264
|
+
|
|
265
|
+
# Plot
|
|
266
|
+
self.plot = pg.PlotWidget()
|
|
267
|
+
self.plot.showGrid(x=True, y=True, alpha=0.3)
|
|
268
|
+
self.plot.setLabel('bottom', 'Time', units='s')
|
|
269
|
+
self.plot.setLabel('left', 'Value', units='')
|
|
270
|
+
|
|
271
|
+
self.curve1 = self.plot.plot(pen=pg.mkPen(width=2))
|
|
272
|
+
self.curve2 = None
|
|
273
|
+
if self.plot_mode == "dual":
|
|
274
|
+
self.curve2 = self.plot.plot(pen=pg.mkPen(color='c', width=2))
|
|
275
|
+
|
|
276
|
+
# --- setpoint curve (yellow)
|
|
277
|
+
self.curve_sp = self.plot.plot(pen=pg.mkPen('y', width=2))
|
|
278
|
+
|
|
279
|
+
self.max_points = 1000
|
|
280
|
+
self.tbuf, self.y1buf, self.y2buf, self.spbuf = [], [], [], []
|
|
281
|
+
|
|
282
|
+
# Sağ kolon: tablo + alt setpoint alanı + (panel3 için S-Curve kutusu eklenecek)
|
|
283
|
+
self.right_col = QtWidgets.QVBoxLayout()
|
|
284
|
+
|
|
285
|
+
# Parametre tablosu (scrollable)
|
|
286
|
+
self.table = QtWidgets.QTableWidget()
|
|
287
|
+
self.table.setColumnCount(2)
|
|
288
|
+
self.table.setHorizontalHeaderLabels(["Parameter", "Value"])
|
|
289
|
+
self.table.verticalHeader().setVisible(False)
|
|
290
|
+
self.table.horizontalHeader().setStretchLastSection(True)
|
|
291
|
+
self.table.setRowCount(10)
|
|
292
|
+
for i in range(10):
|
|
293
|
+
name_item = QtWidgets.QTableWidgetItem(f"P{i+1}")
|
|
294
|
+
name_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
|
295
|
+
self.table.setItem(i, 0, name_item)
|
|
296
|
+
value_item = QtWidgets.QTableWidgetItem("0")
|
|
297
|
+
self.table.setItem(i, 1, value_item)
|
|
298
|
+
self.table.cellChanged.connect(self._on_cell_changed)
|
|
299
|
+
|
|
300
|
+
self.right_col.addWidget(self.table, 1)
|
|
301
|
+
|
|
302
|
+
# Alt sabit setpoint alanı (tablodan ayrı ve sabit)
|
|
303
|
+
if self.extra_param_label and self.extra_param_key:
|
|
304
|
+
self.extraBox = QtWidgets.QGroupBox("Setpoint")
|
|
305
|
+
form = QtWidgets.QHBoxLayout(self.extraBox)
|
|
306
|
+
self.lblExtra = QtWidgets.QLabel(self.extra_param_label)
|
|
307
|
+
self.leExtra = QtWidgets.QLineEdit()
|
|
308
|
+
self.btnExtraSet = QtWidgets.QPushButton("Set")
|
|
309
|
+
self.leExtra.setPlaceholderText("value")
|
|
310
|
+
form.addWidget(self.lblExtra)
|
|
311
|
+
form.addWidget(self.leExtra, 1)
|
|
312
|
+
form.addWidget(self.btnExtraSet)
|
|
313
|
+
self.right_col.addWidget(self.extraBox, 0)
|
|
314
|
+
|
|
315
|
+
# Enter veya buton ile SET
|
|
316
|
+
self.leExtra.returnPressed.connect(self._emit_extra_set)
|
|
317
|
+
self.btnExtraSet.clicked.connect(self._emit_extra_set)
|
|
318
|
+
else:
|
|
319
|
+
self.extraBox = None
|
|
320
|
+
self.lblExtra = None
|
|
321
|
+
self.leExtra = None
|
|
322
|
+
self.btnExtraSet = None
|
|
323
|
+
|
|
324
|
+
layout.addWidget(self.plot, 3)
|
|
325
|
+
layout.addLayout(self.right_col, 2)
|
|
326
|
+
|
|
327
|
+
# ---- alt setpoint alanını programatik güncellemek için (şu an otomatik yazmıyoruz)
|
|
328
|
+
def set_extra_param_value(self, text: str):
|
|
329
|
+
if self.leExtra is not None:
|
|
330
|
+
self.leExtra.setText(str(text))
|
|
331
|
+
|
|
332
|
+
def set_extra_enabled(self, enabled: bool):
|
|
333
|
+
"""Setpoint giriş kutusu ve butonunu aç/kapat."""
|
|
334
|
+
if self.leExtra is not None:
|
|
335
|
+
self.leExtra.setEnabled(enabled)
|
|
336
|
+
if self.btnExtraSet is not None:
|
|
337
|
+
self.btnExtraSet.setEnabled(enabled)
|
|
338
|
+
|
|
339
|
+
def add_widget_to_side(self, w: QtWidgets.QWidget):
|
|
340
|
+
"""Sağ kolona harici bir widget eklemek için yardımcı."""
|
|
341
|
+
self.right_col.addWidget(w, 0)
|
|
342
|
+
|
|
343
|
+
def _emit_extra_set(self):
|
|
344
|
+
if self.extra_param_key and self.leExtra:
|
|
345
|
+
value = self.leExtra.text()
|
|
346
|
+
# Panel kimliği ve INDEX anahtar adı ile yayınla
|
|
347
|
+
self.bus.operationParamChanged.emit(self.panel_id, self.extra_param_key, value)
|
|
348
|
+
|
|
349
|
+
# ---- OPERATION panel yardımcıları ----
|
|
350
|
+
def set_param_names(self, names: list[str]):
|
|
351
|
+
"""Sağdaki tabloya verilen isimleri yazar; kalan satırlara P# bırakır."""
|
|
352
|
+
self._suppress_cell_signal = True
|
|
353
|
+
for i in range(10):
|
|
354
|
+
nm = names[i] if i < len(names) else f"P{i+1}"
|
|
355
|
+
self.table.item(i, 0).setText(str(nm))
|
|
356
|
+
self._suppress_cell_signal = False
|
|
357
|
+
|
|
358
|
+
def set_read_only(self, readonly_names: set[str]):
|
|
359
|
+
"""Value hücresini verilen isimler için salt-okunur yapar."""
|
|
360
|
+
self._suppress_cell_signal = True
|
|
361
|
+
for r in range(self.table.rowCount()):
|
|
362
|
+
nm = self.table.item(r, 0).text()
|
|
363
|
+
val_item = self.table.item(r, 1)
|
|
364
|
+
if nm in readonly_names:
|
|
365
|
+
val_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
|
366
|
+
else:
|
|
367
|
+
val_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
|
|
368
|
+
self._suppress_cell_signal = False
|
|
369
|
+
|
|
370
|
+
def _on_cell_changed(self, row, column):
|
|
371
|
+
if self._suppress_cell_signal or column != 1:
|
|
372
|
+
return
|
|
373
|
+
name = self.table.item(row, 0).text()
|
|
374
|
+
value = self.table.item(row, 1).text()
|
|
375
|
+
self.bus.operationParamChanged.emit(self.panel_id, name, value)
|
|
376
|
+
|
|
377
|
+
def push_data(self, t: float, y1: float, y2: Optional[float] = None, sp: Optional[float] = None):
|
|
378
|
+
self.tbuf.append(t)
|
|
379
|
+
self.y1buf.append(y1)
|
|
380
|
+
if self.curve2 is not None:
|
|
381
|
+
self.y2buf.append(y2 if (y2 is not None) else 0.0)
|
|
382
|
+
self.spbuf.append(sp if (sp is not None) else 0.0)
|
|
383
|
+
|
|
384
|
+
if len(self.tbuf) > self.max_points:
|
|
385
|
+
self.tbuf = self.tbuf[-self.max_points:]
|
|
386
|
+
self.y1buf = self.y1buf[-self.max_points:]
|
|
387
|
+
if self.curve2 is not None:
|
|
388
|
+
self.y2buf = self.y2buf[-self.max_points:]
|
|
389
|
+
self.spbuf = self.spbuf[-self.max_points:]
|
|
390
|
+
|
|
391
|
+
self.curve1.setData(self.tbuf, self.y1buf)
|
|
392
|
+
if self.curve2 is not None:
|
|
393
|
+
self.curve2.setData(self.tbuf, self.y2buf)
|
|
394
|
+
self.curve_sp.setData(self.tbuf, self.spbuf)
|
|
395
|
+
|
|
396
|
+
def set_params_bulk(self, mapping: dict):
|
|
397
|
+
name_to_row = {self.table.item(r, 0).text(): r for r in range(self.table.rowCount())}
|
|
398
|
+
self._suppress_cell_signal = True
|
|
399
|
+
for name, val in mapping.items():
|
|
400
|
+
if name in name_to_row:
|
|
401
|
+
r = name_to_row[name]
|
|
402
|
+
self.table.item(r, 1).setText(str(val))
|
|
403
|
+
self._suppress_cell_signal = False
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class OperationTab(QtWidgets.QWidget):
|
|
407
|
+
def __init__(self, bus: SignalBus, parent=None):
|
|
408
|
+
super().__init__(parent)
|
|
409
|
+
self.bus = bus
|
|
410
|
+
self._suppress_enable_signal = False # UI güncellerken sinyali bastırmak için
|
|
411
|
+
self.bus.enableStateUpdated.connect(self.set_enable_ui)
|
|
412
|
+
|
|
413
|
+
layout = QtWidgets.QVBoxLayout(self)
|
|
414
|
+
|
|
415
|
+
# ---- Üst bar: Enable switch
|
|
416
|
+
topbar = QtWidgets.QHBoxLayout()
|
|
417
|
+
topbar.addStretch()
|
|
418
|
+
self.enableSwitch = QtWidgets.QCheckBox("Enable")
|
|
419
|
+
self.enableSwitch.setTristate(False)
|
|
420
|
+
# (İsteğe bağlı) switch görünümü için basit bir stil:
|
|
421
|
+
self.enableSwitch.setStyleSheet("""
|
|
422
|
+
QCheckBox::indicator { width: 46px; height: 26px; }
|
|
423
|
+
QCheckBox::indicator:unchecked { image: none; border-radius: 13px; background: #bbb; }
|
|
424
|
+
QCheckBox::indicator:unchecked:hover { background: #aaa; }
|
|
425
|
+
QCheckBox::indicator:checked { image: none; border-radius: 13px; background: #4caf50; }
|
|
426
|
+
QCheckBox { padding: 4px; }
|
|
427
|
+
""")
|
|
428
|
+
self.enableSwitch.toggled.connect(self._on_enable_toggled)
|
|
429
|
+
topbar.addWidget(self.enableSwitch)
|
|
430
|
+
layout.addLayout(topbar)
|
|
431
|
+
|
|
432
|
+
# ---- Paneller
|
|
433
|
+
self.panel1 = OperationPanel(
|
|
434
|
+
bus, panel_id=1, title="Plot 1 (Two Traces)", plot_mode="dual",
|
|
435
|
+
extra_param_label="current_setpoint", extra_param_key="setpoint_current"
|
|
436
|
+
)
|
|
437
|
+
self.panel2 = OperationPanel(
|
|
438
|
+
bus, panel_id=2, title="Plot 2 (Single Trace)", plot_mode="single",
|
|
439
|
+
extra_param_label="velocity_setpoint", extra_param_key="setpoint_velocity"
|
|
440
|
+
)
|
|
441
|
+
self.panel3 = OperationPanel(
|
|
442
|
+
bus, panel_id=3, title="Plot 3 (Single Trace)", plot_mode="single",
|
|
443
|
+
extra_param_label="position_setpoint", extra_param_key="setpoint_position"
|
|
444
|
+
)
|
|
445
|
+
layout.addWidget(self.panel1)
|
|
446
|
+
layout.addWidget(self.panel2)
|
|
447
|
+
layout.addWidget(self.panel3)
|
|
448
|
+
|
|
449
|
+
# İsimleri yaz
|
|
450
|
+
self.panel1.set_param_names(OP_PARAM_GROUPS[1])
|
|
451
|
+
self.panel2.set_param_names(OP_PARAM_GROUPS[2])
|
|
452
|
+
self.panel3.set_param_names(OP_PARAM_GROUPS[3])
|
|
453
|
+
|
|
454
|
+
# --- Panel 3 (Position) için S-Curve Planner UI'sı ---
|
|
455
|
+
self._build_scurve_ui(self.panel3)
|
|
456
|
+
|
|
457
|
+
# Cihazdan veri akışı
|
|
458
|
+
self.bus.newTimedData.connect(self._on_new_timed_data)
|
|
459
|
+
|
|
460
|
+
print("operation tab init")
|
|
461
|
+
|
|
462
|
+
# S-Curve planner kutusu
|
|
463
|
+
def _build_scurve_ui(self, position_panel: OperationPanel):
|
|
464
|
+
gb = QtWidgets.QGroupBox("S-Curve Planner")
|
|
465
|
+
vbox = QtWidgets.QVBoxLayout(gb)
|
|
466
|
+
|
|
467
|
+
# Enable checkbox
|
|
468
|
+
self.cbSCurve = QtWidgets.QCheckBox("Enable S-Curve")
|
|
469
|
+
vbox.addWidget(self.cbSCurve)
|
|
470
|
+
|
|
471
|
+
# Form inputs
|
|
472
|
+
form = QtWidgets.QFormLayout()
|
|
473
|
+
self.leTarget = QtWidgets.QLineEdit()
|
|
474
|
+
self.leVmax = QtWidgets.QLineEdit()
|
|
475
|
+
self.leAmax = QtWidgets.QLineEdit()
|
|
476
|
+
self.leTdes = QtWidgets.QLineEdit()
|
|
477
|
+
|
|
478
|
+
self.leTarget.setPlaceholderText("target_position")
|
|
479
|
+
self.leVmax.setPlaceholderText("max_velocity")
|
|
480
|
+
self.leAmax.setPlaceholderText("desired_acceleration")
|
|
481
|
+
self.leTdes.setPlaceholderText("desired_time")
|
|
482
|
+
|
|
483
|
+
form.addRow("Target Position:", self.leTarget)
|
|
484
|
+
form.addRow("Max Velocity:", self.leVmax)
|
|
485
|
+
form.addRow("Desired Accel.:", self.leAmax)
|
|
486
|
+
form.addRow("Desired Time:", self.leTdes)
|
|
487
|
+
vbox.addLayout(form)
|
|
488
|
+
|
|
489
|
+
# Buttons
|
|
490
|
+
hbtn = QtWidgets.QHBoxLayout()
|
|
491
|
+
self.btnPlan = QtWidgets.QPushButton("Plan")
|
|
492
|
+
self.btnPlan.setToolTip("Plan now (x0 = current_position)")
|
|
493
|
+
hbtn.addStretch()
|
|
494
|
+
hbtn.addWidget(self.btnPlan)
|
|
495
|
+
vbox.addLayout(hbtn)
|
|
496
|
+
|
|
497
|
+
# Add to panel 3 right side
|
|
498
|
+
position_panel.add_widget_to_side(gb)
|
|
499
|
+
|
|
500
|
+
# Connections
|
|
501
|
+
self.cbSCurve.toggled.connect(self._on_scurve_toggled_ui)
|
|
502
|
+
self.btnPlan.clicked.connect(self._emit_plan_from_ui)
|
|
503
|
+
self.leTarget.returnPressed.connect(self._emit_plan_from_ui) # target girilince anında planla
|
|
504
|
+
|
|
505
|
+
def _on_scurve_toggled_ui(self, checked: bool):
|
|
506
|
+
# UI etkisi: S-Curve açıldığında manuel position_setpoint girişini kapat
|
|
507
|
+
self.bus.sCurveToggled.emit(checked)
|
|
508
|
+
|
|
509
|
+
def _emit_plan_from_ui(self):
|
|
510
|
+
def _as_float(le: QtWidgets.QLineEdit, default: float = 0.0) -> float:
|
|
511
|
+
try:
|
|
512
|
+
return float(le.text())
|
|
513
|
+
except Exception:
|
|
514
|
+
return default
|
|
515
|
+
|
|
516
|
+
target = _as_float(self.leTarget, 0.0)
|
|
517
|
+
vmax = _as_float(self.leVmax, 0.0)
|
|
518
|
+
a_des = _as_float(self.leAmax, 0.0)
|
|
519
|
+
t_des = _as_float(self.leTdes, 0.0)
|
|
520
|
+
self.bus.sCurvePlanRequested.emit(target, vmax, a_des, t_des)
|
|
521
|
+
|
|
522
|
+
def _on_new_timed_data(self, y1, y2, y3, y4, s1, s2, s3, t):
|
|
523
|
+
# Plot lines + yellow setpoints
|
|
524
|
+
self.panel1.push_data(t, y1, y2, s1) # Id, Iq, setpoint_current
|
|
525
|
+
self.panel2.push_data(t, y3, sp=s2) # current_velocity, setpoint_velocity
|
|
526
|
+
self.panel3.push_data(t, y4, sp=s3) # current_position, setpoint_position
|
|
527
|
+
|
|
528
|
+
# Keep the small tables synced with live values
|
|
529
|
+
self.panel1.set_params_bulk({
|
|
530
|
+
"current_Id": NUM_FMT.format(y1),
|
|
531
|
+
"current_Iq": NUM_FMT.format(y2),
|
|
532
|
+
})
|
|
533
|
+
self.panel2.set_params_bulk({
|
|
534
|
+
"current_velocity": NUM_FMT.format(y3),
|
|
535
|
+
})
|
|
536
|
+
self.panel3.set_params_bulk({
|
|
537
|
+
"current_position": NUM_FMT.format(y4),
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
def reload_from_device(self):
|
|
541
|
+
"""Panel isimlerine göre Device'tan değer çek ve tabloları doldur."""
|
|
542
|
+
def pull(names: list[str]) -> dict:
|
|
543
|
+
out = {}
|
|
544
|
+
for nm in names:
|
|
545
|
+
if not nm or nm.startswith("P"):
|
|
546
|
+
continue
|
|
547
|
+
try:
|
|
548
|
+
idx = Index_Joint[nm]
|
|
549
|
+
out[nm] = Device.get_variables(idx)[0]
|
|
550
|
+
except Exception:
|
|
551
|
+
out[nm] = ""
|
|
552
|
+
return out
|
|
553
|
+
|
|
554
|
+
m1 = pull(OP_PARAM_GROUPS[1])
|
|
555
|
+
m2 = pull(OP_PARAM_GROUPS[2])
|
|
556
|
+
m3 = pull(OP_PARAM_GROUPS[3])
|
|
557
|
+
|
|
558
|
+
self.panel1.set_params_bulk(m1)
|
|
559
|
+
self.panel2.set_params_bulk(m2)
|
|
560
|
+
self.panel3.set_params_bulk(m3)
|
|
561
|
+
|
|
562
|
+
def _on_enable_toggled(self, checked: bool):
|
|
563
|
+
if self._suppress_enable_signal:
|
|
564
|
+
return
|
|
565
|
+
# Switch değişince bus üzerinden ana pencereye bildir
|
|
566
|
+
self.bus.enableToggled.emit(checked)
|
|
567
|
+
|
|
568
|
+
def set_enable_ui(self, checked: bool):
|
|
569
|
+
"""Cihazdan okunan enable durumunu UI’a yaz (sinyal üretmeden)."""
|
|
570
|
+
self._suppress_enable_signal = True
|
|
571
|
+
try:
|
|
572
|
+
self.enableSwitch.setChecked(bool(checked))
|
|
573
|
+
finally:
|
|
574
|
+
self._suppress_enable_signal = False
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# =========================
|
|
578
|
+
# Ana Pencere
|
|
579
|
+
# =========================
|
|
580
|
+
class MainWindow(QtWidgets.QMainWindow):
|
|
581
|
+
def __init__(self, device_id: str):
|
|
582
|
+
super().__init__()
|
|
583
|
+
self.setWindowTitle("ACROME JOINT BLDC DRIVER GUI")
|
|
584
|
+
self.bus = SignalBus()
|
|
585
|
+
self.current_id = device_id # Girilen ID
|
|
586
|
+
self.bus.idChanged.emit(self.current_id)
|
|
587
|
+
|
|
588
|
+
self.tabs = QtWidgets.QTabWidget()
|
|
589
|
+
self.setCentralWidget(self.tabs)
|
|
590
|
+
|
|
591
|
+
self.configTab = ConfigTab(self.bus)
|
|
592
|
+
self.operationTab = OperationTab(self.bus)
|
|
593
|
+
self.tabs.addTab(self.configTab, "CONFIG")
|
|
594
|
+
self.tabs.addTab(self.operationTab, "OPERATION")
|
|
595
|
+
|
|
596
|
+
self.tabs.currentChanged.connect(self._on_tab_changed)
|
|
597
|
+
self._connect_bus_handlers()
|
|
598
|
+
|
|
599
|
+
self.resize(1200, 900)
|
|
600
|
+
|
|
601
|
+
self._op_timer = QtCore.QTimer(self)
|
|
602
|
+
self._op_timer.timeout.connect(self._operation_tick)
|
|
603
|
+
self._op_t0 = None # OPERATION sekmesine girildiği anın referansı
|
|
604
|
+
|
|
605
|
+
# ---- Read-only bilgisini CONFIG'ten al ve OPERATION tablolara uygula
|
|
606
|
+
ro_names = {p["name"] for p in self.configTab.parameters if p.get("read_only", False)}
|
|
607
|
+
self.operationTab.panel1.set_read_only(ro_names)
|
|
608
|
+
self.operationTab.panel2.set_read_only(ro_names)
|
|
609
|
+
self.operationTab.panel3.set_read_only(ro_names)
|
|
610
|
+
|
|
611
|
+
# ---- S-Curve çalışma durumları ----
|
|
612
|
+
self._s_curve_enabled: bool = False
|
|
613
|
+
self._ramp: Optional[Ramp] = None
|
|
614
|
+
self._last_current_position: float = 0.0 # her tick'te güncellenir
|
|
615
|
+
|
|
616
|
+
print("main window init")
|
|
617
|
+
|
|
618
|
+
# ------- Hook Bağlantıları -------
|
|
619
|
+
def _connect_bus_handlers(self):
|
|
620
|
+
self.bus.tabChanged.connect(self._on_tab_hook)
|
|
621
|
+
self.bus.configParamChanged.connect(self._on_config_param_set)
|
|
622
|
+
self.bus.operationParamChanged.connect(self._on_operation_param_set)
|
|
623
|
+
self.bus.refreshClicked.connect(self._on_refresh_clicked)
|
|
624
|
+
self.bus.restartClicked.connect(self._on_restart_clicked)
|
|
625
|
+
self.bus.configSaveClicked.connect(self._on_config_save_clicked)
|
|
626
|
+
self.bus.FactoryResetClicked.connect(self._on_config_reset_clicked)
|
|
627
|
+
self.bus.enableToggled.connect(self._on_enable_toggled)
|
|
628
|
+
|
|
629
|
+
# S-Curve sinyalleri
|
|
630
|
+
self.bus.sCurveToggled.connect(self._on_scurve_toggled)
|
|
631
|
+
self.bus.sCurvePlanRequested.connect(self._on_scurve_plan_requested)
|
|
632
|
+
|
|
633
|
+
# ------- Tab Geçiş Kancası -------
|
|
634
|
+
def _on_tab_changed(self, index: int):
|
|
635
|
+
name = self.tabs.tabText(index)
|
|
636
|
+
self.bus.tabChanged.emit(index, name)
|
|
637
|
+
|
|
638
|
+
if name == "OPERATION":
|
|
639
|
+
# OPERATION'a girildi: zaman referansı ve periyodik GET
|
|
640
|
+
self._op_t0 = time.perf_counter()
|
|
641
|
+
self._op_timer.start(30) # ~33 Hz
|
|
642
|
+
else:
|
|
643
|
+
# OPERATION’dan çıkıldı: durdur
|
|
644
|
+
self._op_timer.stop()
|
|
645
|
+
|
|
646
|
+
def _operation_tick(self):
|
|
647
|
+
"""
|
|
648
|
+
Periyodik GET: Device'tan değerleri çek ve zaman damgası ile yayınla.
|
|
649
|
+
Ayrıca S-Curve açıksa her tick'te step() çağırıp position setpoint yollar.
|
|
650
|
+
"""
|
|
651
|
+
parameters = Device.get_FOC_parameters(3)
|
|
652
|
+
t_abs = time.perf_counter()
|
|
653
|
+
t_rel = t_abs - (self._op_t0 or t_abs)
|
|
654
|
+
|
|
655
|
+
y1 = parameters[1]
|
|
656
|
+
y2 = parameters[2]
|
|
657
|
+
y3 = parameters[3]
|
|
658
|
+
y4 = parameters[4]
|
|
659
|
+
self._last_current_position = float(y4) # plan için güncel x0
|
|
660
|
+
|
|
661
|
+
s1 = parameters[6]
|
|
662
|
+
s2 = parameters[7]
|
|
663
|
+
s3 = parameters[8]
|
|
664
|
+
|
|
665
|
+
# OPERATION sekmesine veri yay
|
|
666
|
+
self.bus.newTimedData.emit(
|
|
667
|
+
float(y1), float(y2), float(y3), float(y4),
|
|
668
|
+
float(s1), float(s2), float(s3),
|
|
669
|
+
float(t_rel)
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# Enable durumunu UI'a yansıt
|
|
673
|
+
enable_state = parameters[0]
|
|
674
|
+
self.bus.enableStateUpdated.emit(enable_state)
|
|
675
|
+
|
|
676
|
+
# --- S-Curve aktifse: ramp step ve setpoint gönder ---
|
|
677
|
+
if self._s_curve_enabled and (self._ramp is not None) and (not self._ramp.done()):
|
|
678
|
+
try:
|
|
679
|
+
sp = float(self._ramp.step())
|
|
680
|
+
# Cihaza setpoint_position yaz
|
|
681
|
+
Device.set_variables([Index_Joint.setpoint_position, sp])
|
|
682
|
+
except Exception as e:
|
|
683
|
+
print(f"[S-CURVE] step/send failed: {e}")
|
|
684
|
+
|
|
685
|
+
def _on_tab_hook(self, index: int, name: str):
|
|
686
|
+
if name == "OPERATION":
|
|
687
|
+
Device.enter_operation()
|
|
688
|
+
# OPERATION panel tablolarını cihazdan güncelle
|
|
689
|
+
self.operationTab.reload_from_device()
|
|
690
|
+
else:
|
|
691
|
+
Device.enter_configuration()
|
|
692
|
+
print(f"[HOOK] Entered tab {index}: {name} (ID={self.current_id})")
|
|
693
|
+
|
|
694
|
+
# ------- CONFIG/OPERATION SET Kancaları -------
|
|
695
|
+
def _on_config_param_set(self, name: str, value: str):
|
|
696
|
+
Device.set_variables([Index_Joint[name], int(value)])
|
|
697
|
+
print(f"[CONFIG SET] {name} = {value} | ID={self.current_id}")
|
|
698
|
+
|
|
699
|
+
def _on_operation_param_set(self, panel_id: int, name: str, value: str):
|
|
700
|
+
"""
|
|
701
|
+
Not: S-Curve açıkken position_setpoint kullanıcı tarafından girilemez (UI'da disable).
|
|
702
|
+
Diğer paneller (current/velocity) normal çalışır.
|
|
703
|
+
"""
|
|
704
|
+
try:
|
|
705
|
+
idx = Index_Joint[name]
|
|
706
|
+
v = float(value)
|
|
707
|
+
if v.is_integer():
|
|
708
|
+
v = int(v)
|
|
709
|
+
Device.set_variables([idx, v])
|
|
710
|
+
print(f"[OP SET] Panel {panel_id} | {name} = {v} , idx = {idx}| ID={self.current_id}")
|
|
711
|
+
except Exception as e:
|
|
712
|
+
print(f"[OP SET] FAIL: idx = {idx if 'idx' in locals() else '?'} , {panel_id} {name} {value} -> {e}")
|
|
713
|
+
|
|
714
|
+
# ------- S-Curve event handlers -------
|
|
715
|
+
def _on_scurve_toggled(self, checked: bool):
|
|
716
|
+
self._s_curve_enabled = bool(checked)
|
|
717
|
+
# UI: manuel position_setpoint girişini aç/kapat
|
|
718
|
+
self.operationTab.panel3.set_extra_enabled(not self._s_curve_enabled)
|
|
719
|
+
|
|
720
|
+
if not checked:
|
|
721
|
+
# Kapandı -> planlayıcıyı bırak
|
|
722
|
+
self._ramp = None
|
|
723
|
+
print("[S-CURVE] Disabled and cleared ramp.")
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
# Açıldı; mevcut timer aralığına göre dt ayarla (saniye)
|
|
727
|
+
dt = (self._op_timer.interval() / 1000.0) if self._op_timer.isActive() else 0.03
|
|
728
|
+
|
|
729
|
+
# Ramp'i sadece bir kez yarat
|
|
730
|
+
try:
|
|
731
|
+
# Buradaki vmax/amax üst limit; asıl istek plan()'da vmax_des/a_des ile gelecek
|
|
732
|
+
self._ramp = Ramp(dt=dt, vmax=MOTOR_VEL_MAX_IN_ENC_TYPE, amax=MOTOR_ACC_MAX_IN_ENC_TYPE)
|
|
733
|
+
print(f"dt 1 = {dt}")
|
|
734
|
+
print("[S-CURVE] Enabled (created Ramp once).")
|
|
735
|
+
except Exception as e:
|
|
736
|
+
print(f"[S-CURVE] Ramp init failed: {e}")
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _on_scurve_plan_requested(self, target: float, vmax: float, a_des: float, t_des: float):
|
|
740
|
+
"""
|
|
741
|
+
Kullanıcı 'Plan' dediğinde ya da target_position Enter ile girildiğinde çağrılır.
|
|
742
|
+
x0 = son okunan current_position
|
|
743
|
+
"""
|
|
744
|
+
if not self._s_curve_enabled:
|
|
745
|
+
print("[S-CURVE] Plan requested but S-Curve is disabled.")
|
|
746
|
+
return
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
dt = (self._op_timer.interval() / 1000.0) if self._op_timer.isActive() else 0.03
|
|
750
|
+
print(f"dt 2 = {dt}")
|
|
751
|
+
if self._ramp is None:
|
|
752
|
+
# Normalde buraya düşülmez; yine de güvenlik için:
|
|
753
|
+
self._ramp = Ramp(dt=dt, vmax=MOTOR_VEL_MAX_IN_ENC_TYPE, amax=MOTOR_ACC_MAX_IN_ENC_TYPE)
|
|
754
|
+
print("ERROR RAMP RECREATE")
|
|
755
|
+
else:
|
|
756
|
+
# Aynı ramp nesnesini koru; sadece parametrelerini güncelle
|
|
757
|
+
try:
|
|
758
|
+
self._ramp.dt = dt
|
|
759
|
+
except Exception:
|
|
760
|
+
pass
|
|
761
|
+
# Eğer Ramp sınıfı bu alanları public tutuyorsa güncelle:
|
|
762
|
+
try:
|
|
763
|
+
self._ramp.vmax = MOTOR_VEL_MAX_IN_ENC_TYPE
|
|
764
|
+
self._ramp.amax = MOTOR_ACC_MAX_IN_ENC_TYPE
|
|
765
|
+
except Exception:
|
|
766
|
+
# Public değilse sorun değil; plan() içindeki vmax_des/a_des zaten kullanılacak
|
|
767
|
+
pass
|
|
768
|
+
|
|
769
|
+
x0 = float(self._last_current_position)
|
|
770
|
+
xg = float(target)
|
|
771
|
+
t_d = float(t_des)
|
|
772
|
+
a_d = rpm_to_tick_per_second(float(a_des)) # conversion for unit (rpm/s)
|
|
773
|
+
v_d = rpm_to_tick_per_second(float(vmax)) # conversion for unit (rpm)
|
|
774
|
+
|
|
775
|
+
# Asıl hedef/istekler plan'a gidiyor
|
|
776
|
+
self._ramp.plan(x0=x0, xg=xg, t_des=t_d, a_des=a_d, vmax_des=v_d)
|
|
777
|
+
print(f"[S-CURVE] Planned: x0={x0}, xg={xg}, t_des={t_d}, a_des={a_d}, vmax_des={v_d}, dt={dt}")
|
|
778
|
+
print(f"t = {self._ramp.t}, a = {self._ramp.a}, dir ={self._ramp.dir}")
|
|
779
|
+
print(f"t1 = {self._ramp.t1}, t2 = {self._ramp.t2}, vp = {self._ramp.Vp}")
|
|
780
|
+
except Exception as e:
|
|
781
|
+
print(f"[S-CURVE] Plan failed: {e}")
|
|
782
|
+
|
|
783
|
+
# ------- Sağ Buton Kancaları (CONFIG) -------
|
|
784
|
+
def _on_refresh_clicked(self):
|
|
785
|
+
for var in self.configTab.parameters:
|
|
786
|
+
var["value"] = Device.get_variables(Index_Joint[var["name"]])[0]
|
|
787
|
+
print(var["value"])
|
|
788
|
+
print(f"[BTN] Refresh | ID={self.current_id}")
|
|
789
|
+
|
|
790
|
+
self.configTab.refresh_from_device(self.configTab.parameters)
|
|
791
|
+
|
|
792
|
+
def _on_restart_clicked(self):
|
|
793
|
+
Device.reboot()
|
|
794
|
+
print(f"[BTN] Restart | ID={self.current_id}")
|
|
795
|
+
|
|
796
|
+
def _on_config_save_clicked(self):
|
|
797
|
+
Device.eeprom_save()
|
|
798
|
+
print(f"[BTN] Config Save | ID={self.current_id}")
|
|
799
|
+
|
|
800
|
+
def _on_config_reset_clicked(self):
|
|
801
|
+
Device.factory_reset()
|
|
802
|
+
print(f"[BTN] Apply | ID={self.current_id}")
|
|
803
|
+
|
|
804
|
+
# (Opsiyonel) ID değiştirme API’sı
|
|
805
|
+
def change_id(self, new_id: str):
|
|
806
|
+
self.current_id = new_id
|
|
807
|
+
self.bus.idChanged.emit(new_id)
|
|
808
|
+
print(f"[INFO] Active ID changed to {new_id}")
|
|
809
|
+
# TODO: ID değişince yapılacak işler (örn. port yeniden açma)
|
|
810
|
+
|
|
811
|
+
def _on_enable_toggled(self, is_on: bool):
|
|
812
|
+
try:
|
|
813
|
+
Device.set_variables([Index_Joint.Enable, 1 if is_on else 0])
|
|
814
|
+
print(f"[OP ENABLE] -> {is_on}")
|
|
815
|
+
except Exception as e:
|
|
816
|
+
print(f"[OP ENABLE] FAIL -> {e}")
|
|
817
|
+
# Hata olursa UI’yı geri çevir
|
|
818
|
+
self.operationTab.set_enable_ui(not is_on)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
# =========================
|
|
822
|
+
# Uygulama Başlatma Akışı
|
|
823
|
+
# =========================
|
|
824
|
+
def pre_start_handshake(device_id: str) -> bool:
|
|
825
|
+
try:
|
|
826
|
+
dev = Joint(int(device_id), port)
|
|
827
|
+
except Exception:
|
|
828
|
+
return False
|
|
829
|
+
print(f"[PRE-START] Handshake with ID={device_id}...")
|
|
830
|
+
return dev.ping()
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def main():
|
|
834
|
+
app = QtWidgets.QApplication([])
|
|
835
|
+
# 1) Başlangıçta dialog göster
|
|
836
|
+
while True:
|
|
837
|
+
dlg = StartupDialog()
|
|
838
|
+
if dlg.exec() != QtWidgets.QDialog.Accepted:
|
|
839
|
+
return
|
|
840
|
+
device_id = dlg.id_value or ""
|
|
841
|
+
# 2) PRE-START HOOK (Handshake)
|
|
842
|
+
ok = pre_start_handshake(device_id)
|
|
843
|
+
if ok:
|
|
844
|
+
break
|
|
845
|
+
else:
|
|
846
|
+
QtWidgets.QMessageBox.critical(
|
|
847
|
+
None, "Bağlantı Hatası",
|
|
848
|
+
f"ID={device_id} için ön iletişim başarısız. Lütfen tekrar deneyin."
|
|
849
|
+
)
|
|
850
|
+
Device._id = int(device_id)
|
|
851
|
+
# 3) Ana pencereyi aç (CONFIG sekmesi ile başlayacak)
|
|
852
|
+
win = MainWindow(device_id=device_id)
|
|
853
|
+
win.show()
|
|
854
|
+
app.exec()
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
if __name__ == "__main__":
|
|
858
|
+
main()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from math import sqrt
|
|
2
|
+
|
|
3
|
+
class Ramp:
|
|
4
|
+
def __init__(self, dt=0.001, vmax=1.0, amax=1.0):
|
|
5
|
+
self.dt, self.vmax, self.amax = dt, vmax, amax
|
|
6
|
+
self.reset()
|
|
7
|
+
|
|
8
|
+
def reset(self):
|
|
9
|
+
self.x0 = self.xg = 0.0
|
|
10
|
+
self.t = self.t1 = self.t2 = self.tp = 0.0
|
|
11
|
+
self.Vp = self.a = 0.0
|
|
12
|
+
self.dir = 1
|
|
13
|
+
self._x = 0.0
|
|
14
|
+
self._x_t1 = 0.0 # hızlanma fazında alınan mesafe (t1_pos)
|
|
15
|
+
|
|
16
|
+
def plan(self, x0, xg, t_des=0.0, a_des=0.0, vmax_des=0.0):
|
|
17
|
+
"""Trajektoriyi hazırla; ardından step() her çağrıda bir sonraki setpoint'i üretir."""
|
|
18
|
+
self.reset()
|
|
19
|
+
self.x0, self.xg = x0, xg
|
|
20
|
+
e = xg - x0
|
|
21
|
+
self.dir = 1 if e >= 0 else -1
|
|
22
|
+
e = abs(e)
|
|
23
|
+
|
|
24
|
+
Vmax = self.vmax if (vmax_des <= 0 or vmax_des > self.vmax) else vmax_des
|
|
25
|
+
acc = self.amax if (a_des <= 0 or a_des > self.amax) else a_des
|
|
26
|
+
|
|
27
|
+
# 1) ZAMAN+İVME verildiyse dene
|
|
28
|
+
if t_des > 0 and acc > 0:
|
|
29
|
+
D = t_des*t_des - 4.0*(e/acc)
|
|
30
|
+
if D >= 0:
|
|
31
|
+
t1 = (t_des - sqrt(D)) / 2.0 # her zaman <= t_des/2
|
|
32
|
+
Vp = acc * t1
|
|
33
|
+
if 0 <= t1 and Vp < Vmax:
|
|
34
|
+
t2 = t_des - 2.0*t1
|
|
35
|
+
self._finalize(t1, t2, Vp, acc)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# 2) Sadece ZAMAN verildiyse
|
|
39
|
+
if t_des > 0:
|
|
40
|
+
if Vmax * t_des > e:
|
|
41
|
+
Vp = 2.0*e / t_des
|
|
42
|
+
if Vp <= Vmax:
|
|
43
|
+
t1, t2 = t_des/2.0, 0.0
|
|
44
|
+
else:
|
|
45
|
+
t1 = t_des - e / Vmax
|
|
46
|
+
t2 = t_des - 2.0*t1
|
|
47
|
+
Vp = Vmax
|
|
48
|
+
a = Vp / max(t1, 1e-12)
|
|
49
|
+
self._finalize(t1, t2, Vp, a)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# 3) İVME (veya hiçbiri) verildiyse
|
|
53
|
+
if acc <= 0: # her ikisi de yoksa C kodundaki gibi varsayılan
|
|
54
|
+
acc = Vmax / 2.0
|
|
55
|
+
t1 = sqrt(e / acc)
|
|
56
|
+
Vp = acc * t1
|
|
57
|
+
if Vp > Vmax:
|
|
58
|
+
t1 = Vmax / acc
|
|
59
|
+
t2 = (e - Vmax*t1) / Vmax
|
|
60
|
+
Vp = Vmax
|
|
61
|
+
else:
|
|
62
|
+
t2 = 0.0
|
|
63
|
+
self._finalize(t1, t2, Vp, acc)
|
|
64
|
+
|
|
65
|
+
def _finalize(self, t1, t2, Vp, a):
|
|
66
|
+
self.t1, self.t2, self.Vp, self.a = t1, t2, Vp, a
|
|
67
|
+
self.tp = 2.0*t1 + t2
|
|
68
|
+
self._x = self.x0
|
|
69
|
+
self._x_t1 = 0.5 * a * t1 * t1 # hızlanma mesafesi
|
|
70
|
+
|
|
71
|
+
def step(self):
|
|
72
|
+
"""Bir kontrol tikinde bir sonraki setpoint."""
|
|
73
|
+
t, a, d = self.t, self.a, self.dir
|
|
74
|
+
x0, xg = self.x0, self.xg
|
|
75
|
+
t1, t2, tp, Vp = self.t1, self.t2, self.tp, self.Vp
|
|
76
|
+
|
|
77
|
+
if t < t1: # hızlan
|
|
78
|
+
x = x0 + 0.5 * a * t*t * d
|
|
79
|
+
elif t < t1 + t2: # sabit hız
|
|
80
|
+
x = x0 + (self._x_t1 + Vp*(t - t1)) * d
|
|
81
|
+
elif t < tp: # yavaşla
|
|
82
|
+
dt = tp - t
|
|
83
|
+
x = xg - 0.5 * a * dt*dt * d
|
|
84
|
+
else:
|
|
85
|
+
x = xg
|
|
86
|
+
|
|
87
|
+
# hedefi aşma koruması
|
|
88
|
+
if (d > 0 and x > xg) or (d < 0 and x < xg):
|
|
89
|
+
x = xg
|
|
90
|
+
|
|
91
|
+
self._x = x
|
|
92
|
+
self.t += self.dt
|
|
93
|
+
return x
|
|
94
|
+
|
|
95
|
+
def done(self):
|
|
96
|
+
return self._x == self.xg
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="Joint-python-library",
|
|
8
|
-
version="0.0.
|
|
8
|
+
version="0.0.4",
|
|
9
9
|
author="BeratComputer",
|
|
10
10
|
author_email="beratdogan@acrome.net",
|
|
11
11
|
description="Python library for interfacing with Acrome Robotic Arm Joint BLDC Motor Controllers \n \n This Python library provides an easy-to-use interface for communication and control of BLDC motor controllers used in Acrome robotic arm joints. It is designed to simplify the integration of Acrome’s robotic joint actuators into custom applications, allowing developers and researchers to focus on building advanced robotic systems without dealing with low-level communication details.",
|
|
@@ -21,6 +21,11 @@ setuptools.setup(
|
|
|
21
21
|
"Operating System :: OS Independent",
|
|
22
22
|
],
|
|
23
23
|
packages=setuptools.find_packages(exclude=['tests', 'test']),
|
|
24
|
+
entry_points={
|
|
25
|
+
"console_scripts": [
|
|
26
|
+
"joint_GUI=gui.main:main",
|
|
27
|
+
]
|
|
28
|
+
},
|
|
24
29
|
install_requires=["pyserial>=3.5", "stm32loader>=0.5.1", "crccheck>=1.3.0", "requests>=2.31.0", "packaging>=23.2"],
|
|
25
30
|
python_requires=">=3.7"
|
|
26
31
|
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from joint import *
|
|
File without changes
|
{joint_python_library-0.0.3 → joint_python_library-0.0.4}/Joint_python_library.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|