Spectral-BLDC 0.1__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.
- Spectral_BLDC-0.1/LICENSE +21 -0
- Spectral_BLDC-0.1/PKG-INFO +18 -0
- Spectral_BLDC-0.1/README.md +2 -0
- Spectral_BLDC-0.1/Spectral_BLDC/CAN_utils.py +135 -0
- Spectral_BLDC-0.1/Spectral_BLDC/Spectral_BLDC.py +558 -0
- Spectral_BLDC-0.1/Spectral_BLDC/__init__.py +1 -0
- Spectral_BLDC-0.1/Spectral_BLDC.egg-info/PKG-INFO +18 -0
- Spectral_BLDC-0.1/Spectral_BLDC.egg-info/SOURCES.txt +11 -0
- Spectral_BLDC-0.1/Spectral_BLDC.egg-info/dependency_links.txt +1 -0
- Spectral_BLDC-0.1/Spectral_BLDC.egg-info/requires.txt +1 -0
- Spectral_BLDC-0.1/Spectral_BLDC.egg-info/top_level.txt +1 -0
- Spectral_BLDC-0.1/setup.cfg +4 -0
- Spectral_BLDC-0.1/setup.py +32 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Petar Crnjak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: Spectral_BLDC
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Python module for Spectral BLDC motor controllers
|
|
5
|
+
Author: Source robotics (Petar Crnjak)
|
|
6
|
+
Author-email: <info@source-robotics.com>
|
|
7
|
+
Keywords: python,BLDC,CANBUS,Robot,Source robotics,robotics
|
|
8
|
+
Classifier: Development Status :: 1 - Planning
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: Unix
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Spectral-BLDC
|
|
18
|
+
Python lib for controlling spectral BLDC controllers.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
int_to_3_bytes = struct.Struct('>I').pack # BIG endian order
|
|
5
|
+
|
|
6
|
+
# Fuses 3 bytes to 1 signed int
|
|
7
|
+
def fuse_3_bytes(var_in):
|
|
8
|
+
value = struct.unpack(">I", bytearray(var_in))[0] # converts bytes array to int
|
|
9
|
+
|
|
10
|
+
# convert to negative number if it is negative
|
|
11
|
+
if value >= 1<<23:
|
|
12
|
+
value -= 1<<24
|
|
13
|
+
|
|
14
|
+
return value
|
|
15
|
+
|
|
16
|
+
# Fuses 2 bytes to 1 signed int
|
|
17
|
+
def fuse_2_bytes(var_in):
|
|
18
|
+
value = struct.unpack(">I", bytearray(var_in))[0] # converts bytes array to int
|
|
19
|
+
|
|
20
|
+
# convert to negative number if it is negative
|
|
21
|
+
if value >= 1<<15:
|
|
22
|
+
value -= 1<<16
|
|
23
|
+
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
# Fuses 4 bytes to 1 signed int
|
|
27
|
+
def fuse_4_bytes(bytes_array):
|
|
28
|
+
# Unpack 4 bytes into an unsigned integer
|
|
29
|
+
value = struct.unpack(">I", bytearray(bytes_array))[0]
|
|
30
|
+
# Convert to a negative number if it is negative
|
|
31
|
+
if value & 0x80000000:
|
|
32
|
+
value -= 1 << 32
|
|
33
|
+
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
# Fuses 4 bytes to 1 32bit float value
|
|
37
|
+
def fuse_float_bytes(bytes_array):
|
|
38
|
+
# Unpack 4 bytes into a float using IEEE 754 single-precision format (32 bits)
|
|
39
|
+
float_value = struct.unpack('>f', bytearray(bytes_array))[0]
|
|
40
|
+
return float_value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Split data to 3 bytes
|
|
44
|
+
def split_2_3_bytes(var_in):
|
|
45
|
+
y = int_to_3_bytes(var_in & 0xFFFFFF) # converts my int value to bytes array
|
|
46
|
+
return y[1:4]
|
|
47
|
+
|
|
48
|
+
# Splits data to 2 bytes
|
|
49
|
+
def split_2_2_bytes(var_in):
|
|
50
|
+
return struct.pack(">H", var_in & 0xFFFF)
|
|
51
|
+
|
|
52
|
+
# Splits int data to 4 bytes
|
|
53
|
+
def split_2_4_bytes(var_in):
|
|
54
|
+
return struct.pack(">I", var_in & 0xFFFFFFFF)
|
|
55
|
+
|
|
56
|
+
# Splits float data to 4 bytes
|
|
57
|
+
def float_to_bytes(float_value):
|
|
58
|
+
# Pack the float value into 4 bytes using IEEE 754 single-precision format (32 bits)
|
|
59
|
+
return struct.pack('>f', float_value)
|
|
60
|
+
|
|
61
|
+
def extract_from_can_id(can_id):
|
|
62
|
+
# Extracting ID2 (first 4 MSB)
|
|
63
|
+
id2 = (can_id >> 7) & 0xF
|
|
64
|
+
|
|
65
|
+
# Extracting CAN Command (next 6 bits)
|
|
66
|
+
can_command = (can_id >> 1) & 0x3F
|
|
67
|
+
|
|
68
|
+
# Extracting Error Bit (last bit)
|
|
69
|
+
error_bit = can_id & 0x1
|
|
70
|
+
|
|
71
|
+
return id2, can_command, error_bit
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def combine_2_can_id(id2, can_command, error_bit):
|
|
75
|
+
# Combine components into an 11-bit CAN ID
|
|
76
|
+
can_id = 0
|
|
77
|
+
|
|
78
|
+
# Add ID2 (first 4 MSB)
|
|
79
|
+
can_id |= (id2 & 0xF) << 7
|
|
80
|
+
|
|
81
|
+
# Add CAN Command (next 6 bits)
|
|
82
|
+
can_id |= (can_command & 0x3F) << 1
|
|
83
|
+
|
|
84
|
+
# Add Error Bit (last bit)
|
|
85
|
+
can_id |= (error_bit & 0x1)
|
|
86
|
+
|
|
87
|
+
return can_id
|
|
88
|
+
|
|
89
|
+
# Fuse bitfield list to byte
|
|
90
|
+
def fuse_bitfield_2_bytearray(var_in):
|
|
91
|
+
number = 0
|
|
92
|
+
for b in var_in:
|
|
93
|
+
number = (2 * number) + b
|
|
94
|
+
return bytes([number])
|
|
95
|
+
|
|
96
|
+
# Splits byte to bitfield list
|
|
97
|
+
def split_2_bitfield(var_in):
|
|
98
|
+
#return [var_in >> i & 1 for i in range(7,-1,-1)]
|
|
99
|
+
return [(var_in >> i) & 1 for i in range(7, -1, -1)]
|
|
100
|
+
|
|
101
|
+
# Combine 2 bits into int
|
|
102
|
+
def combine_bits(bit1, bit2):
|
|
103
|
+
# Ensure that bit1 and bit2 are either 0 or 1
|
|
104
|
+
bit1 = 1 if bit1 else 0
|
|
105
|
+
bit2 = 1 if bit2 else 0
|
|
106
|
+
|
|
107
|
+
# Combine the bits using bitwise OR
|
|
108
|
+
result = (bit1 << 1) | bit2
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
# Packing data to send on can bus
|
|
113
|
+
pos_ = -150
|
|
114
|
+
speed_ = -187
|
|
115
|
+
torque_ = -3047
|
|
116
|
+
a = split_2_3_bytes(pos_)
|
|
117
|
+
b = split_2_3_bytes(speed_)
|
|
118
|
+
c = split_2_2_bytes(torque_)
|
|
119
|
+
print(a)
|
|
120
|
+
print(b)
|
|
121
|
+
print(c)
|
|
122
|
+
|
|
123
|
+
# Unpacking data we receive from can bus
|
|
124
|
+
message = bytearray(b'\xff\xffj\xff\xffE\x0b\xe7')
|
|
125
|
+
print(message)
|
|
126
|
+
position = fuse_3_bytes(b'\x00' + message[0:3])
|
|
127
|
+
speed = fuse_3_bytes( b'\x00' + message[3:6])
|
|
128
|
+
torque = fuse_2_bytes(b'\x00'+ b'\x00' + message[6:8])
|
|
129
|
+
print(position)
|
|
130
|
+
print(speed)
|
|
131
|
+
print(torque)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import can
|
|
2
|
+
import CAN_utils as util
|
|
3
|
+
import logging
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
|
|
6
|
+
logging.basicConfig(level = logging.DEBUG,
|
|
7
|
+
format='%(asctime)s.%(msecs)03d %(levelname)s:\t%(message)s',
|
|
8
|
+
datefmt='%H:%M:%S'
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
#logging.disable(logging.DEBUG)
|
|
12
|
+
|
|
13
|
+
CanMessage = namedtuple("CanMessage", ["node_id", "command_id", "error_bit"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CanCommunication:
|
|
17
|
+
def __init__(self, bustype='slcan', channel='COM41', bitrate=1000000):
|
|
18
|
+
self.bustype=bustype
|
|
19
|
+
self.channel=channel
|
|
20
|
+
self.bitrate=bitrate
|
|
21
|
+
self.bus = can.interface.Bus(bustype = self.bustype, channel = self.channel, bitrate = self.bitrate)
|
|
22
|
+
|
|
23
|
+
def send_can_message(self, message_id, data=b'', remote_frame=False):
|
|
24
|
+
message = can.Message(arbitration_id=message_id, data=data, is_remote_frame=remote_frame, is_extended_id=False)
|
|
25
|
+
self.bus.send(message)
|
|
26
|
+
|
|
27
|
+
def receive_can_messages(self,timeout = None):
|
|
28
|
+
self.timeout = timeout
|
|
29
|
+
message = self.bus.recv(timeout = self.timeout)
|
|
30
|
+
if message is not None:
|
|
31
|
+
node_id,command_id,error_bit = util.extract_from_can_id(message.arbitration_id)
|
|
32
|
+
can_message = CanMessage(node_id, command_id, error_bit)
|
|
33
|
+
else:
|
|
34
|
+
return None,None
|
|
35
|
+
|
|
36
|
+
return message, can_message
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SpectralCAN:
|
|
41
|
+
|
|
42
|
+
SEND_PD_GAINS_ID = 16
|
|
43
|
+
SEND_CURRENT_GAINS_ID = 17
|
|
44
|
+
SEND_VELOCITY_GAINS_ID = 18
|
|
45
|
+
SEND_POSITION_GAINS_ID = 19
|
|
46
|
+
SEND_LIMITS_ID = 20
|
|
47
|
+
SEND_KT_ID = 22
|
|
48
|
+
SEND_CAN_ID = 11
|
|
49
|
+
SEND_ESTOP_ID = 0
|
|
50
|
+
SEND_IDLE_ID = 12
|
|
51
|
+
SEND_SAVE_CONFIG_ID = 13
|
|
52
|
+
SEND_RESET_ID = 14
|
|
53
|
+
SEND_CLEAR_ERROR_ID = 1
|
|
54
|
+
SEND_WATCHDOG_ID = 15
|
|
55
|
+
SEND_HEARTBEAT_SETUP_ID = 30
|
|
56
|
+
SEND_CYCLIC_COMMAND_ID = 29
|
|
57
|
+
|
|
58
|
+
SEND_RESPOND_TEMPERATURE_ID = 23
|
|
59
|
+
SEND_RESPOND_DEVICE_INFO_ID = 25
|
|
60
|
+
SEND_RESPOND_ENCODER_DATA_ID = 28
|
|
61
|
+
SEND_RESPOND_VOLTAGE_ID = 24
|
|
62
|
+
SEND_RESPOND_STATE_OF_ERRORS_ID = 26
|
|
63
|
+
SEND_RESPOND_IQ_DATA_ID = 27
|
|
64
|
+
SEND_RESPOND_PING_ID = 10
|
|
65
|
+
|
|
66
|
+
SEND_DATA_PACK_1_ID = 2
|
|
67
|
+
SEND_DATA_PACK_2_ID = 6
|
|
68
|
+
SEND_DATA_PACK_3_ID = 8
|
|
69
|
+
SEND_DATA_PACK_PD_ID = 4
|
|
70
|
+
|
|
71
|
+
SEND_GRIPPER_DATA_PACK_ID = 61
|
|
72
|
+
SEND_GRIPPER_CALIBRATE_ID = 62
|
|
73
|
+
|
|
74
|
+
RESPOND_HEARTBEAT_ID = 9
|
|
75
|
+
RESPOND_DATA_PACK_1_ID = 3
|
|
76
|
+
RESPOND_DATA_PACK_2_ID = 5
|
|
77
|
+
RESPOND_DATA_PACK_3_ID = 7
|
|
78
|
+
RESPOND_GRIPPER_DATA_PACK_ID = 60
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def __init__(self, node_id, communication):
|
|
82
|
+
self.node_id = node_id
|
|
83
|
+
self.communication = communication
|
|
84
|
+
|
|
85
|
+
self.temperature = None
|
|
86
|
+
self.position = None
|
|
87
|
+
self.speed = None
|
|
88
|
+
self.current = None
|
|
89
|
+
self.voltage = None
|
|
90
|
+
self.heartbeat = None
|
|
91
|
+
|
|
92
|
+
self.gripper_position = None
|
|
93
|
+
self.gripper_speed = None
|
|
94
|
+
self.gripper_current = None
|
|
95
|
+
self.gripper_activated = None
|
|
96
|
+
self.gripper_action_status = None
|
|
97
|
+
self.gripper_object_detection = None
|
|
98
|
+
self.gripper_temperature_error = None
|
|
99
|
+
self.gripper_timeout_error = None
|
|
100
|
+
self.gripper_estop_error = None
|
|
101
|
+
self.gripper_calibrated = None
|
|
102
|
+
|
|
103
|
+
self.error_bit = None
|
|
104
|
+
self.error_bit = None
|
|
105
|
+
self.error = None
|
|
106
|
+
self.temperature_error = None
|
|
107
|
+
self.encoder_error = None
|
|
108
|
+
self.vbus_error = None
|
|
109
|
+
self.driver_error = None
|
|
110
|
+
self.velocity_error = None
|
|
111
|
+
self.current_error = None
|
|
112
|
+
self.estop_error = None
|
|
113
|
+
self.calibrated = None
|
|
114
|
+
self.activated = None
|
|
115
|
+
self.watchdog_error = None
|
|
116
|
+
|
|
117
|
+
self.ping = None
|
|
118
|
+
self.timestamp = None
|
|
119
|
+
|
|
120
|
+
self.HARDWARE_VERSION = None
|
|
121
|
+
self.BATCH_DATE = None
|
|
122
|
+
self.SOFTWARE_VERSION = None
|
|
123
|
+
self.SERIAL_NUMBER = None
|
|
124
|
+
|
|
125
|
+
self.DLC_warrning = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Configuration variables
|
|
129
|
+
self._velocity_limit = 0
|
|
130
|
+
|
|
131
|
+
def Set_2_none(self):
|
|
132
|
+
self.temperature = None
|
|
133
|
+
self.position = None
|
|
134
|
+
self.speed = None
|
|
135
|
+
self.current = None
|
|
136
|
+
self.voltage = None
|
|
137
|
+
self.heartbeat = None
|
|
138
|
+
|
|
139
|
+
self.error_bit = None
|
|
140
|
+
self.error = None
|
|
141
|
+
self.temperature_error = None
|
|
142
|
+
self.encoder_error = None
|
|
143
|
+
self.vbus_error = None
|
|
144
|
+
self.driver_error = None
|
|
145
|
+
self.velocity_error = None
|
|
146
|
+
self.current_error = None
|
|
147
|
+
self.estop_error = None
|
|
148
|
+
self.calibrated = None
|
|
149
|
+
self.activated = None
|
|
150
|
+
|
|
151
|
+
self.ping = None
|
|
152
|
+
self.timestamp = None
|
|
153
|
+
|
|
154
|
+
self.HARDWARE_VERSION = None
|
|
155
|
+
self.BATCH_DATE = None
|
|
156
|
+
self.SOFTWARE_VERSION = None
|
|
157
|
+
self.SERIAL_NUMBER = None
|
|
158
|
+
|
|
159
|
+
self.DLC_warrning = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def velocity_limit(self):
|
|
164
|
+
return self._velocity_limit
|
|
165
|
+
|
|
166
|
+
@velocity_limit.setter
|
|
167
|
+
def velocity_limit(self, value):
|
|
168
|
+
# Ensure the value is within acceptable range or add additional validation logic
|
|
169
|
+
self._velocity_limit = value
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def UnpackData(self,message,UnpackedMessageID):
|
|
173
|
+
|
|
174
|
+
#logging.debug(message.data)
|
|
175
|
+
#logging.debug(UnpackedMessageID.node_id)
|
|
176
|
+
self.Set_2_none()
|
|
177
|
+
|
|
178
|
+
self.timestamp = message.timestamp
|
|
179
|
+
match UnpackedMessageID.command_id:
|
|
180
|
+
|
|
181
|
+
case self.RESPOND_HEARTBEAT_ID:
|
|
182
|
+
self.heartbeat = 1
|
|
183
|
+
logging.debug("Got heartbeat")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
case self.RESPOND_DATA_PACK_1_ID:
|
|
187
|
+
if message.dlc == 8:
|
|
188
|
+
self.position = util.fuse_3_bytes(b'\x00' + message.data[0:3])
|
|
189
|
+
self.speed = util.fuse_3_bytes(b'\x00' + message.data[3:6])
|
|
190
|
+
self.current = util.fuse_2_bytes(b'\x00' + b'\x00' + message.data[6:8])
|
|
191
|
+
else:
|
|
192
|
+
self.DLC_warrning = 1
|
|
193
|
+
logging.debug(f"Encoder position ticks: {self.position}")
|
|
194
|
+
logging.debug(f"Encoder speed: {self.speed}")
|
|
195
|
+
logging.debug(f"current is: {self.current}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
case self.RESPOND_DATA_PACK_2_ID:
|
|
200
|
+
None
|
|
201
|
+
|
|
202
|
+
case self.RESPOND_DATA_PACK_3_ID:
|
|
203
|
+
None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
case self.RESPOND_GRIPPER_DATA_PACK_ID:
|
|
207
|
+
if message.dlc == 4:
|
|
208
|
+
self.gripper_position = message.data[0]
|
|
209
|
+
#self.gripper_speed = message.data[1]
|
|
210
|
+
self.gripper_current = util.fuse_2_bytes(b'\x00' + b'\x00' + message.data[1:3])
|
|
211
|
+
byte1 = util.split_2_bitfield(message.data[3])
|
|
212
|
+
self.gripper_activated = byte1[0]
|
|
213
|
+
self.gripper_action_status = byte1[1]
|
|
214
|
+
self.gripper_object_detection = util.combine_bits(byte1[2],byte1[3])
|
|
215
|
+
self.gripper_temperature_error = byte1[4]
|
|
216
|
+
self.gripper_timeout_error = byte1[5]
|
|
217
|
+
self.gripper_estop_error = byte1[6]
|
|
218
|
+
self.gripper_calibrated = byte1[7]
|
|
219
|
+
else:
|
|
220
|
+
self.DLC_warrning = 1
|
|
221
|
+
|
|
222
|
+
logging.debug("Got gripper data")
|
|
223
|
+
logging.debug(f"Position is: {self.gripper_position}")
|
|
224
|
+
logging.debug(f"Current is: {self.gripper_current}")
|
|
225
|
+
logging.debug(f"Activated is: {self.gripper_activated}")
|
|
226
|
+
logging.debug(f"Action status is: {self.gripper_action_status}")
|
|
227
|
+
logging.debug(f"Object detection is: {self.gripper_object_detection}")
|
|
228
|
+
logging.debug(f"Temperature error is: {self.gripper_temperature_error}")
|
|
229
|
+
logging.debug(f"Timeout error is: {self.gripper_timeout_error}")
|
|
230
|
+
logging.debug(f"Estop error is: {self.gripper_estop_error}")
|
|
231
|
+
logging.debug(f"Calibrated is: {self.gripper_calibrated}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Radi
|
|
237
|
+
case self.SEND_RESPOND_TEMPERATURE_ID:
|
|
238
|
+
if message.dlc == 2:
|
|
239
|
+
self.temperature = util.fuse_2_bytes(b'\x00'+ b'\x00' + message.data[0:2])
|
|
240
|
+
else:
|
|
241
|
+
self.DLC_warrning = 1
|
|
242
|
+
logging.debug(f"Temperature is: {self.temperature}")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Radi
|
|
246
|
+
case self.SEND_RESPOND_DEVICE_INFO_ID:
|
|
247
|
+
if message.dlc == 7:
|
|
248
|
+
self.HARDWARE_VERSION = message.data[0]
|
|
249
|
+
self.BATCH_DATE = message.data[1]
|
|
250
|
+
self.SOFTWARE_VERSION = message.data[2]
|
|
251
|
+
self.SERIAL_NUMBER = util.fuse_4_bytes(message.data[3:7])
|
|
252
|
+
else:
|
|
253
|
+
self.DLC_warrning = 1
|
|
254
|
+
|
|
255
|
+
logging.debug(f"Hardware version is: {self.HARDWARE_VERSION}")
|
|
256
|
+
logging.debug(f"Batch date is: {self.BATCH_DATE}")
|
|
257
|
+
logging.debug(f"Software version is: {self.SOFTWARE_VERSION}")
|
|
258
|
+
logging.debug(f"Serial number is: {self.SERIAL_NUMBER}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
#Radi
|
|
262
|
+
case self.SEND_RESPOND_ENCODER_DATA_ID:
|
|
263
|
+
if message.dlc == 8:
|
|
264
|
+
self.position = util.fuse_4_bytes(message.data[0:4])
|
|
265
|
+
self.speed = util.fuse_4_bytes(message.data[4:8])
|
|
266
|
+
else:
|
|
267
|
+
self.DLC_warrning = 1
|
|
268
|
+
logging.debug(f"Encoder position ticks: {self.position}")
|
|
269
|
+
logging.debug(f"Encoder speed: {self.speed}")
|
|
270
|
+
|
|
271
|
+
# Radi
|
|
272
|
+
case self.SEND_RESPOND_VOLTAGE_ID:
|
|
273
|
+
if message.dlc == 2:
|
|
274
|
+
self.voltage = util.fuse_2_bytes(b'\x00'+ b'\x00' + message.data[0:2])
|
|
275
|
+
else:
|
|
276
|
+
self.DLC_warrning = 1
|
|
277
|
+
logging.debug(f"Voltage is: {self.voltage}")
|
|
278
|
+
|
|
279
|
+
# Radi
|
|
280
|
+
case self.SEND_RESPOND_STATE_OF_ERRORS_ID:
|
|
281
|
+
if message.dlc == 2:
|
|
282
|
+
byte1 = util.split_2_bitfield(message.data[0])
|
|
283
|
+
byte2 = util.split_2_bitfield(message.data[1])
|
|
284
|
+
self.error = byte1[0]
|
|
285
|
+
self.temperature_error = byte1[1]
|
|
286
|
+
self.encoder_error = byte1[2]
|
|
287
|
+
self.vbus_error = byte1[3]
|
|
288
|
+
self.driver_error = byte1[4]
|
|
289
|
+
self.velocity_error = byte1[5]
|
|
290
|
+
self.current_error = byte1[6]
|
|
291
|
+
self.estop_error = byte1[7]
|
|
292
|
+
self.calibrated = byte2[0]
|
|
293
|
+
self.activated = byte2[1]
|
|
294
|
+
self.watchdog_error = byte2[2]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
else:
|
|
298
|
+
self.DLC_warrning = 1
|
|
299
|
+
|
|
300
|
+
logging.debug(f"Error is: {self.error}")
|
|
301
|
+
logging.debug(f"Temperature rror is: {self.temperature_error}")
|
|
302
|
+
logging.debug(f"Encoder error is: {self.encoder_error}")
|
|
303
|
+
logging.debug(f"Vbus error is: {self.vbus_error}")
|
|
304
|
+
logging.debug(f"Driver error is: {self.driver_error}")
|
|
305
|
+
logging.debug(f"Velocity error is: {self.velocity_error}")
|
|
306
|
+
logging.debug(f"Current error is: {self.current_error}")
|
|
307
|
+
logging.debug(f"Estop error is: {self.estop_error}")
|
|
308
|
+
logging.debug(f"Watchdog error is: {self.watchdog_error}")
|
|
309
|
+
logging.debug(f"Calibrated is: {self.calibrated}")
|
|
310
|
+
logging.debug(f"Activated is: {self.activated}")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# Radi
|
|
314
|
+
case self.SEND_RESPOND_IQ_DATA_ID:
|
|
315
|
+
if message.dlc == 2:
|
|
316
|
+
self.current = util.fuse_2_bytes(b'\x00'+ b'\x00' + message.data[0:2])
|
|
317
|
+
else:
|
|
318
|
+
self.DLC_warrning = 1
|
|
319
|
+
logging.debug(f"Current is: {self.current}")
|
|
320
|
+
|
|
321
|
+
# Radi
|
|
322
|
+
case self.SEND_RESPOND_PING_ID:
|
|
323
|
+
self.ping = 1
|
|
324
|
+
logging.debug(f"Ping is: {self.ping}")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def Send_data_pack_1(self,Position = None,Speed = None, Current = None):
|
|
328
|
+
|
|
329
|
+
if(Position != None and Speed != None and Current != None):
|
|
330
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_DATA_PACK_1_ID,0)
|
|
331
|
+
_position = util.split_2_3_bytes(Position)
|
|
332
|
+
_speed = util.split_2_3_bytes(Speed)
|
|
333
|
+
_current = util.split_2_2_bytes(Current)
|
|
334
|
+
combined = _position + _speed + _current
|
|
335
|
+
_combined = bytearray(combined)
|
|
336
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
337
|
+
elif(Position == None and Speed != None and Current != None):
|
|
338
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_DATA_PACK_1_ID,0)
|
|
339
|
+
_speed = util.split_2_3_bytes(Speed)
|
|
340
|
+
_current = util.split_2_2_bytes(Current)
|
|
341
|
+
combined = _speed + _current
|
|
342
|
+
_combined = bytearray(combined)
|
|
343
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
344
|
+
elif(Position == None and Speed == None and Current != None):
|
|
345
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_DATA_PACK_1_ID,0)
|
|
346
|
+
_current = util.split_2_2_bytes(Current)
|
|
347
|
+
combined = _current
|
|
348
|
+
_combined = bytearray(combined)
|
|
349
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
350
|
+
else:
|
|
351
|
+
logging.debug("Invalid command")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
logging.debug("Send data pack 1")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def Send_data_pack_2():
|
|
360
|
+
None
|
|
361
|
+
|
|
362
|
+
def Send_data_pack_3():
|
|
363
|
+
None
|
|
364
|
+
|
|
365
|
+
def Send_data_pack_PD(self,Position = 0,Speed = 0, Current = 0):
|
|
366
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_DATA_PACK_PD_ID,0)
|
|
367
|
+
_position = util.split_2_3_bytes(Position)
|
|
368
|
+
_speed = util.split_2_3_bytes(Speed)
|
|
369
|
+
_current = util.split_2_2_bytes(Current)
|
|
370
|
+
combined = _position + _speed + _current
|
|
371
|
+
_combined = bytearray(combined)
|
|
372
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
373
|
+
|
|
374
|
+
logging.debug("Send data pack PD")
|
|
375
|
+
|
|
376
|
+
def Send_gripper_data_pack(self,position_ = None,speed_ = None,current_ = None,activate_ = None,action_ = None,ESTOP_ = None,Release_dir_ = None):
|
|
377
|
+
|
|
378
|
+
if(position_ != None and speed_!= None and current_ != None and activate_ != None and action_ != None and ESTOP_ != None and Release_dir_ != None):
|
|
379
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_GRIPPER_DATA_PACK_ID,0)
|
|
380
|
+
_current_ = util.split_2_2_bytes(current_)
|
|
381
|
+
bitfield_list = [activate_,action_,ESTOP_,Release_dir_,0,0,0,0]
|
|
382
|
+
fused = util.fuse_bitfield_2_bytearray(bitfield_list)
|
|
383
|
+
_combined = bytearray(bytes([position_]) + bytes([speed_]) + _current_ + fused)
|
|
384
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
385
|
+
logging.debug("Send gripper data pack 1")
|
|
386
|
+
else:
|
|
387
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_GRIPPER_DATA_PACK_ID,0)
|
|
388
|
+
self.communication.send_can_message(message_id = _canid)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def Send_gripper_calibrate(self):
|
|
392
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_GRIPPER_CALIBRATE_ID,0)
|
|
393
|
+
self.communication.send_can_message(message_id = _canid)
|
|
394
|
+
logging.debug("Send gripper calibrate")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# Radi
|
|
398
|
+
def Send_PD_Gains(self,KP,KD):
|
|
399
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_PD_GAINS_ID,0)
|
|
400
|
+
_KP = util.float_to_bytes(KP)
|
|
401
|
+
_KD = util.float_to_bytes(KD)
|
|
402
|
+
combined = _KP + _KD
|
|
403
|
+
_combined = bytearray(combined)
|
|
404
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
405
|
+
logging.debug("Send PD gains")
|
|
406
|
+
|
|
407
|
+
# Radi
|
|
408
|
+
def Send_Current_Gains(self,KPIQ,KIIQ):
|
|
409
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_CURRENT_GAINS_ID,0)
|
|
410
|
+
_KPIQ = util.float_to_bytes(KPIQ)
|
|
411
|
+
_KIIQ = util.float_to_bytes(KIIQ)
|
|
412
|
+
combined = _KPIQ + _KIIQ
|
|
413
|
+
_combined = bytearray(combined)
|
|
414
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
415
|
+
logging.debug("Send current gains")
|
|
416
|
+
|
|
417
|
+
# Radi
|
|
418
|
+
def Send_Velocity_Gains(self,KPV,KIV):
|
|
419
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_VELOCITY_GAINS_ID,0)
|
|
420
|
+
_KPV = util.float_to_bytes(KPV)
|
|
421
|
+
_KIV = util.float_to_bytes(KIV)
|
|
422
|
+
combined = _KPV + _KIV
|
|
423
|
+
_combined = bytearray(combined)
|
|
424
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
425
|
+
logging.debug("Send velocity gains")
|
|
426
|
+
|
|
427
|
+
# Radi
|
|
428
|
+
def Send_Position_Gains(self,KPP):
|
|
429
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_POSITION_GAINS_ID,0)
|
|
430
|
+
_KPP = util.float_to_bytes(KPP)
|
|
431
|
+
combined = _KPP
|
|
432
|
+
_combined = bytearray(combined)
|
|
433
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
434
|
+
logging.debug("Send new position gains")
|
|
435
|
+
|
|
436
|
+
# Radi
|
|
437
|
+
def Send_Limits(self,Velocity_limit,Current_limit):
|
|
438
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_LIMITS_ID,0)
|
|
439
|
+
_v_limit = util.float_to_bytes(Velocity_limit)
|
|
440
|
+
_c_limit = util.float_to_bytes(Current_limit)
|
|
441
|
+
combined = _v_limit + _c_limit
|
|
442
|
+
_combined = bytearray(combined)
|
|
443
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
444
|
+
logging.debug("Send new velocity and current limits")
|
|
445
|
+
|
|
446
|
+
# Radi
|
|
447
|
+
def Send_Kt(self,Kt):
|
|
448
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_KT_ID,0)
|
|
449
|
+
_Kt = util.float_to_bytes(Kt)
|
|
450
|
+
combined = _Kt
|
|
451
|
+
_combined = bytearray(combined)
|
|
452
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
453
|
+
logging.debug("Send new Kt")
|
|
454
|
+
|
|
455
|
+
# Radi čudno
|
|
456
|
+
def Send_CAN_ID(self,_CAN_ID):
|
|
457
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_CAN_ID,0) # Node id, command id, error bit ( 0 )
|
|
458
|
+
_data = _CAN_ID
|
|
459
|
+
self.communication.send_can_message(message_id = _canid, data = [_data])
|
|
460
|
+
logging.debug("Send new CAN ID")
|
|
461
|
+
|
|
462
|
+
# Radi
|
|
463
|
+
def Send_ESTOP(self):
|
|
464
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_ESTOP_ID,0)
|
|
465
|
+
self.communication.send_can_message(message_id = _canid)
|
|
466
|
+
logging.debug("Send ESTOP")
|
|
467
|
+
|
|
468
|
+
#Radi
|
|
469
|
+
def Send_Idle(self):
|
|
470
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_IDLE_ID,0)
|
|
471
|
+
self.communication.send_can_message(message_id = _canid)
|
|
472
|
+
logging.debug("Send Idle")
|
|
473
|
+
|
|
474
|
+
# Radi
|
|
475
|
+
def Send_Save_config(self):
|
|
476
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_SAVE_CONFIG_ID,0)
|
|
477
|
+
self.communication.send_can_message(message_id = _canid)
|
|
478
|
+
logging.debug("Send Save config")
|
|
479
|
+
|
|
480
|
+
# Radi
|
|
481
|
+
def Send_Reset(self):
|
|
482
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_RESET_ID,0)
|
|
483
|
+
self.communication.send_can_message(message_id = _canid)
|
|
484
|
+
logging.debug("Send Reset")
|
|
485
|
+
|
|
486
|
+
# Radi
|
|
487
|
+
def Send_Clear_Error(self):
|
|
488
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_CLEAR_ERROR_ID,0)
|
|
489
|
+
self.communication.send_can_message(message_id = _canid)
|
|
490
|
+
logging.debug("Send Clear Error")
|
|
491
|
+
|
|
492
|
+
# Radi
|
|
493
|
+
def Send_Watchdog(self,_rate,_action = 'Idle'):
|
|
494
|
+
if _action == 'Idle':
|
|
495
|
+
_action_byte = bytes([0x00])
|
|
496
|
+
elif _action == 'Hold':
|
|
497
|
+
_action_byte = bytes([0x01])
|
|
498
|
+
elif _action == 'Brake':
|
|
499
|
+
_action_byte = bytes([0x02])
|
|
500
|
+
|
|
501
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_WATCHDOG_ID,0)
|
|
502
|
+
watch_time = util.split_2_4_bytes(_rate)
|
|
503
|
+
_combined = bytearray(watch_time + _action_byte)
|
|
504
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
505
|
+
|
|
506
|
+
# Radi
|
|
507
|
+
def Send_Heartbeat_Setup(self,_rate):
|
|
508
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_HEARTBEAT_SETUP_ID,0)
|
|
509
|
+
Heart_rate = util.split_2_4_bytes(_rate)
|
|
510
|
+
_combined = bytearray(Heart_rate)
|
|
511
|
+
self.communication.send_can_message(message_id = _canid, data = _combined)
|
|
512
|
+
|
|
513
|
+
# TODO
|
|
514
|
+
def Send_Cyclic_command(self,ID):
|
|
515
|
+
None
|
|
516
|
+
|
|
517
|
+
# The DLC value does not necessarily define the number of bytes of data in a message.
|
|
518
|
+
# Its purpose varies depending on the frame type - for data frames it represents the amount of data contained in the message, in remote frames it represents the amount of data being requested.
|
|
519
|
+
|
|
520
|
+
def Send_Respond_Temperature(self):
|
|
521
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_RESPOND_TEMPERATURE_ID,0)
|
|
522
|
+
self.communication.send_can_message(message_id = _canid, remote_frame=True)
|
|
523
|
+
logging.debug("Request temperature")
|
|
524
|
+
|
|
525
|
+
def Send_Respond_Voltage(self):
|
|
526
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_RESPOND_VOLTAGE_ID,0)
|
|
527
|
+
self.communication.send_can_message(message_id = _canid, remote_frame = True)
|
|
528
|
+
logging.debug("Request voltage")
|
|
529
|
+
|
|
530
|
+
def Send_Respond_Device_Info(self):
|
|
531
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_RESPOND_DEVICE_INFO_ID,0)
|
|
532
|
+
self.communication.send_can_message(message_id = _canid, remote_frame = True)
|
|
533
|
+
logging.debug("Request device info")
|
|
534
|
+
|
|
535
|
+
def Send_Respond_State_of_Errors(self):
|
|
536
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_RESPOND_STATE_OF_ERRORS_ID,0)
|
|
537
|
+
self.communication.send_can_message(message_id = _canid, remote_frame = True)
|
|
538
|
+
logging.debug("Request state of all errors")
|
|
539
|
+
|
|
540
|
+
def Send_Respond_Iq_data(self):
|
|
541
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_RESPOND_IQ_DATA_ID,0)
|
|
542
|
+
self.communication.send_can_message(message_id = _canid, remote_frame = True)
|
|
543
|
+
logging.debug("Request Iq data")
|
|
544
|
+
|
|
545
|
+
def Send_Respond_Encoder_data(self):
|
|
546
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_RESPOND_ENCODER_DATA_ID,0)
|
|
547
|
+
self.communication.send_can_message(message_id = _canid, remote_frame = True)
|
|
548
|
+
logging.debug("Request encoder data")
|
|
549
|
+
|
|
550
|
+
def Send_Respond_Ping(self):
|
|
551
|
+
_canid = util.combine_2_can_id(self.node_id,self.SEND_RESPOND_PING_ID,0)
|
|
552
|
+
self.communication.send_can_message(message_id = _canid, remote_frame = True)
|
|
553
|
+
logging.debug("Request ping")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
if __name__ == "__main__":
|
|
558
|
+
None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ["CAN_utils", "Spectral_BLDC"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: Spectral-BLDC
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Python module for Spectral BLDC motor controllers
|
|
5
|
+
Author: Source robotics (Petar Crnjak)
|
|
6
|
+
Author-email: <info@source-robotics.com>
|
|
7
|
+
Keywords: python,BLDC,CANBUS,Robot,Source robotics,robotics
|
|
8
|
+
Classifier: Development Status :: 1 - Planning
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: Unix
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Spectral-BLDC
|
|
18
|
+
Python lib for controlling spectral BLDC controllers.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
Spectral_BLDC/CAN_utils.py
|
|
5
|
+
Spectral_BLDC/Spectral_BLDC.py
|
|
6
|
+
Spectral_BLDC/__init__.py
|
|
7
|
+
Spectral_BLDC.egg-info/PKG-INFO
|
|
8
|
+
Spectral_BLDC.egg-info/SOURCES.txt
|
|
9
|
+
Spectral_BLDC.egg-info/dependency_links.txt
|
|
10
|
+
Spectral_BLDC.egg-info/requires.txt
|
|
11
|
+
Spectral_BLDC.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python-can
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Spectral_BLDC
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
import codecs
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
here = os.path.abspath(os.path.dirname(__file__))
|
|
6
|
+
|
|
7
|
+
with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as fh:
|
|
8
|
+
long_description = "\n" + fh.read()
|
|
9
|
+
|
|
10
|
+
VERSION = '0.01'
|
|
11
|
+
DESCRIPTION = 'Python module for Spectral BLDC motor controllers'
|
|
12
|
+
|
|
13
|
+
# Setting up
|
|
14
|
+
setup(
|
|
15
|
+
name="Spectral_BLDC",
|
|
16
|
+
version=VERSION,
|
|
17
|
+
author="Source robotics (Petar Crnjak)",
|
|
18
|
+
author_email="<info@source-robotics.com>",
|
|
19
|
+
description=DESCRIPTION,
|
|
20
|
+
long_description_content_type="text/markdown",
|
|
21
|
+
long_description=long_description,
|
|
22
|
+
packages=find_packages(),
|
|
23
|
+
install_requires=['python-can'],
|
|
24
|
+
keywords=['python', 'BLDC', 'CANBUS', 'Robot', 'Source robotics', 'robotics'],
|
|
25
|
+
classifiers=[
|
|
26
|
+
"Development Status :: 1 - Planning",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Operating System :: Unix",
|
|
30
|
+
"Operating System :: Microsoft :: Windows",
|
|
31
|
+
]
|
|
32
|
+
)
|