moteus-gui 0.3.89__tar.gz → 0.3.91__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.
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/PKG-INFO +1 -1
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui/tview.py +411 -48
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui/version.py +1 -1
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui.egg-info/PKG-INFO +1 -1
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui.egg-info/requires.txt +1 -1
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/setup.py +2 -2
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/README.md +0 -0
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui/__init__.py +0 -0
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui/tview_main_window.ui +0 -0
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui.egg-info/SOURCES.txt +0 -0
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui.egg-info/dependency_links.txt +0 -0
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui.egg-info/entry_points.txt +0 -0
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/moteus_gui.egg-info/top_level.txt +0 -0
- {moteus-gui-0.3.89 → moteus-gui-0.3.91}/setup.cfg +0 -0
@@ -88,7 +88,6 @@ RIGHT_LEGEND_LOC = 2
|
|
88
88
|
|
89
89
|
DEFAULT_RATE = 100
|
90
90
|
MAX_HISTORY_SIZE = 100
|
91
|
-
MAX_SEND = 61
|
92
91
|
POLL_TIMEOUT_S = 0.1
|
93
92
|
STARTUP_TIMEOUT_S = 0.5
|
94
93
|
|
@@ -107,6 +106,32 @@ def _has_nonascii(data):
|
|
107
106
|
return any([ord(x) > 127 for x in data])
|
108
107
|
|
109
108
|
|
109
|
+
def calculate_optimal_uuid_prefix(full_uuid, other_uuids):
|
110
|
+
"""Pure function to find the shortest unique UUID prefix.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
full_uuid: The full 16-byte UUID to find a prefix for
|
114
|
+
other_uuids: List of other UUIDs to check conflicts against
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
The shortest unique prefix (4, 8, 12, or 16 bytes)
|
118
|
+
"""
|
119
|
+
for prefix_len in [4, 8, 12, 16]:
|
120
|
+
proposed_prefix = full_uuid[:prefix_len]
|
121
|
+
|
122
|
+
# Check if this prefix conflicts with any other UUID
|
123
|
+
has_conflict = any(
|
124
|
+
other_uuid[:prefix_len] == proposed_prefix
|
125
|
+
for other_uuid in other_uuids
|
126
|
+
)
|
127
|
+
|
128
|
+
if not has_conflict:
|
129
|
+
return proposed_prefix
|
130
|
+
|
131
|
+
# Should never happen, but return full UUID as fallback
|
132
|
+
return full_uuid
|
133
|
+
|
134
|
+
|
110
135
|
# TODO jpieper: Factor these out of tplot.py
|
111
136
|
def _get_data(value, name):
|
112
137
|
fields = name.split('.')
|
@@ -502,6 +527,9 @@ class DeviceStream:
|
|
502
527
|
self.emit_count = 0
|
503
528
|
self.poll_count = 0
|
504
529
|
|
530
|
+
def update_controller(self, controller):
|
531
|
+
self.controller = controller
|
532
|
+
|
505
533
|
def ignore_all(self):
|
506
534
|
self._read_data = b''
|
507
535
|
|
@@ -518,8 +546,9 @@ class DeviceStream:
|
|
518
546
|
|
519
547
|
self.emit_count += 1
|
520
548
|
|
549
|
+
max_send = self.controller.max_diagnostic_write
|
521
550
|
to_write, self._write_data = (
|
522
|
-
self._write_data[0:
|
551
|
+
self._write_data[0:max_send], self._write_data[max_send:])
|
523
552
|
await self.transport.write(self.controller.make_diagnostic_write(to_write))
|
524
553
|
|
525
554
|
async def process_message(self, message):
|
@@ -532,7 +561,7 @@ class DeviceStream:
|
|
532
561
|
return False
|
533
562
|
if data[1] != 1:
|
534
563
|
return False
|
535
|
-
if data[2] >
|
564
|
+
if data[2] > 61:
|
536
565
|
return False
|
537
566
|
datalen = data[2]
|
538
567
|
if datalen > (len(data) - 3):
|
@@ -598,15 +627,28 @@ class Device:
|
|
598
627
|
STATE_SCHEMA = 3
|
599
628
|
STATE_DATA = 4
|
600
629
|
|
601
|
-
def __init__(self,
|
602
|
-
config_tree_item, data_tree_item,
|
630
|
+
def __init__(self, address, source_can_id, transport, console, prefix,
|
631
|
+
config_tree_item, data_tree_item,
|
632
|
+
can_prefix, main_window, can_id):
|
603
633
|
self.error_count = 0
|
604
634
|
self.poll_count = 0
|
635
|
+
self.poll_lock = asyncio.Lock() # Lock for poll-address change synchronization
|
636
|
+
|
637
|
+
self.address = address
|
638
|
+
self.source_can_id = source_can_id
|
639
|
+
self._can_prefix = can_prefix
|
605
640
|
|
606
|
-
|
607
|
-
|
641
|
+
# We keep around an estimate of the current CAN ID to enable
|
642
|
+
# user friendly commands.
|
643
|
+
self.can_id = can_id
|
644
|
+
|
645
|
+
self.controller = moteus.Controller(
|
646
|
+
address,
|
647
|
+
source_can_id=source_can_id,
|
648
|
+
can_prefix=can_prefix)
|
608
649
|
self._transport = transport
|
609
650
|
self._stream = DeviceStream(transport, self.controller)
|
651
|
+
self._main_window = main_window
|
610
652
|
|
611
653
|
self._console = console
|
612
654
|
self._prefix = prefix
|
@@ -634,11 +676,132 @@ class Device:
|
|
634
676
|
|
635
677
|
self._stream.ignore_all()
|
636
678
|
|
679
|
+
# Make sure we have a UUID based address available in case we
|
680
|
+
# need it later.
|
681
|
+
if isinstance(self.address, int) or self.address.can_id is not None:
|
682
|
+
self.uuid_address = await self._get_uuid_address()
|
683
|
+
else:
|
684
|
+
self.uuid_address = self.address
|
685
|
+
|
686
|
+
# Save the full UUID for later.
|
687
|
+
self.full_uuid = (self.uuid_address.uuid
|
688
|
+
if self.uuid_address
|
689
|
+
else None)
|
690
|
+
|
691
|
+
# Are we able to be addressed by UUID?
|
692
|
+
has_uuid_capability = self.uuid_address is not None
|
693
|
+
|
694
|
+
# Register UUID query completion with main window
|
695
|
+
await self._main_window.register_uuid_query_complete(
|
696
|
+
self, has_uuid_capability)
|
697
|
+
|
637
698
|
await self.update_config()
|
638
699
|
await self.update_telemetry()
|
639
700
|
|
640
701
|
await self.run()
|
641
702
|
|
703
|
+
async def _get_uuid_address(self):
|
704
|
+
try:
|
705
|
+
to_query = {
|
706
|
+
moteus.Register.UUID1 : moteus.INT32,
|
707
|
+
moteus.Register.UUID2 : moteus.INT32,
|
708
|
+
moteus.Register.UUID3 : moteus.INT32,
|
709
|
+
moteus.Register.UUID4 : moteus.INT32,
|
710
|
+
moteus.Register.UUID_MASK_CAPABLE : moteus.INT32,
|
711
|
+
}
|
712
|
+
result = await self.controller.custom_query(to_query)
|
713
|
+
|
714
|
+
|
715
|
+
if result.values.get(moteus.Register.UUID_MASK_CAPABLE, None) is None:
|
716
|
+
return None
|
717
|
+
|
718
|
+
# We'll just use the full 16 byte UUID in this case for
|
719
|
+
# now. Eventually maybe we could find an appropriate
|
720
|
+
# shorter prefix.
|
721
|
+
uuid_bytes = struct.pack(
|
722
|
+
'<iiii',
|
723
|
+
*[result.values[reg] for reg in [
|
724
|
+
moteus.Register.UUID1,
|
725
|
+
moteus.Register.UUID2,
|
726
|
+
moteus.Register.UUID3,
|
727
|
+
moteus.Register.UUID4]])
|
728
|
+
|
729
|
+
return moteus.DeviceAddress(
|
730
|
+
uuid=uuid_bytes,
|
731
|
+
transport_device=self.address.transport_device
|
732
|
+
if isinstance(self.address, moteus.DeviceAddress)
|
733
|
+
else None)
|
734
|
+
except Exception as e:
|
735
|
+
print(f"UUID query failed: {e}")
|
736
|
+
return None
|
737
|
+
|
738
|
+
def _update_tree_items(self, tree_key):
|
739
|
+
"""Update tree items with the new tree key."""
|
740
|
+
self._config_tree_item.setText(0, tree_key)
|
741
|
+
self._data_tree_item.setText(0, tree_key)
|
742
|
+
self._prefix = f'{tree_key}>'
|
743
|
+
|
744
|
+
async def _handle_id_change(self):
|
745
|
+
if self.uuid_address is None:
|
746
|
+
# We don't have a UUID to work with, so this controller
|
747
|
+
# may become not addressable.
|
748
|
+
print(f"WARNING: controller {self.address} may now be unreachable")
|
749
|
+
return
|
750
|
+
|
751
|
+
if self.uuid_address == self.address:
|
752
|
+
# We are already using UUID based addressing, so nothing
|
753
|
+
# to do.
|
754
|
+
return
|
755
|
+
|
756
|
+
# Wait for all devices to complete UUID queries if not done
|
757
|
+
if not self._main_window.uuid_query_event.is_set():
|
758
|
+
print("Waiting for all devices to complete UUID queries...")
|
759
|
+
await self._main_window.uuid_query_event.wait()
|
760
|
+
|
761
|
+
# Check if all devices on this transport support UUID
|
762
|
+
transport_device = self._main_window._get_transport_device(self.address)
|
763
|
+
|
764
|
+
if not self._main_window.can_use_uuid_on_transport(transport_device):
|
765
|
+
print(f"WARNING: Not all devices on transport {transport_device} support UUID addressing")
|
766
|
+
print(f"Device {self.address} will remain on CAN ID addressing and may become unreachable")
|
767
|
+
return
|
768
|
+
|
769
|
+
# Acquire lock to ensure no poll is in progress when changing address
|
770
|
+
async with self.poll_lock:
|
771
|
+
await asyncio.sleep(0.1)
|
772
|
+
|
773
|
+
# Calculate optimal UUID prefix.
|
774
|
+
optimal_uuid = self.uuid_address # Default to full UUID
|
775
|
+
|
776
|
+
other_uuids = self._main_window.get_other_device_uuids(self, transport_device)
|
777
|
+
|
778
|
+
optimal_prefix = calculate_optimal_uuid_prefix(self.full_uuid, other_uuids)
|
779
|
+
|
780
|
+
# Create new address with optimal prefix
|
781
|
+
optimal_uuid = moteus.DeviceAddress(
|
782
|
+
uuid=optimal_prefix,
|
783
|
+
transport_device=self.address.transport_device
|
784
|
+
if isinstance(self.address, moteus.DeviceAddress)
|
785
|
+
else None)
|
786
|
+
|
787
|
+
print(f"Switching device {self.address} to UUID addressing: {optimal_prefix.hex()}")
|
788
|
+
|
789
|
+
# Now perform the state change (while holding lock)
|
790
|
+
self.address = optimal_uuid
|
791
|
+
|
792
|
+
self.controller = moteus.Controller(
|
793
|
+
self.address,
|
794
|
+
source_can_id=self.source_can_id,
|
795
|
+
can_prefix=self._can_prefix)
|
796
|
+
|
797
|
+
self._stream.update_controller(self.controller)
|
798
|
+
|
799
|
+
# Update tree items (can be done outside lock)
|
800
|
+
if self._main_window:
|
801
|
+
new_tree_key = self._main_window._calculate_tree_key(
|
802
|
+
self.address, self._transport)
|
803
|
+
self._update_tree_items(new_tree_key)
|
804
|
+
|
642
805
|
async def update_config(self):
|
643
806
|
self._updating_config = True
|
644
807
|
|
@@ -686,9 +849,15 @@ class Device:
|
|
686
849
|
|
687
850
|
_add_schema_item(item, archive, terminal_flags=flags)
|
688
851
|
self._config_tree_items[element] = item
|
689
|
-
|
690
|
-
_set_tree_widget_data(item,
|
852
|
+
data_struct = archive.read(reader.Stream(io.BytesIO(data)))
|
853
|
+
_set_tree_widget_data(item, data_struct, archive, terminal_flags=flags)
|
854
|
+
|
855
|
+
# Try to grab our current can_id.
|
856
|
+
if (element == 'id' and
|
857
|
+
getattr(data_struct, 'id', None) is not None
|
858
|
+
and self.can_id is None):
|
691
859
|
|
860
|
+
self.can_id = data_struct.id
|
692
861
|
|
693
862
|
async def update_telemetry(self):
|
694
863
|
self._data_tree_item.takeChildren()
|
@@ -814,6 +983,32 @@ class Device:
|
|
814
983
|
def write(self, data):
|
815
984
|
self._stream.write(data)
|
816
985
|
|
986
|
+
line = data.decode('latin1')
|
987
|
+
|
988
|
+
# For some commands, we need to take special actions.
|
989
|
+
if line.startswith('conf set id.id '):
|
990
|
+
# Extract the new CAN ID from the command
|
991
|
+
try:
|
992
|
+
new_id_str = line.split('conf set id.id ')[1].strip()
|
993
|
+
new_can_id = int(new_id_str)
|
994
|
+
# Update our current CAN ID for future matching
|
995
|
+
self.can_id = new_can_id
|
996
|
+
except (IndexError, ValueError):
|
997
|
+
# Invalid command format, ignore
|
998
|
+
pass
|
999
|
+
|
1000
|
+
asyncio.create_task(self._handle_id_change())
|
1001
|
+
elif line.startswith('conf default') or line.startswith('conf load'):
|
1002
|
+
# Eventually it would be nice to reload the configuration
|
1003
|
+
# here so the UI stays consistent. For now, we'll satisfy
|
1004
|
+
# ourselves with trying to switch to UUID based operation
|
1005
|
+
# so that we don't lose communication.
|
1006
|
+
|
1007
|
+
# Reloading configuration is complicated, as we need to
|
1008
|
+
# dispense with the OK that the above commands would
|
1009
|
+
# create, but the current layering doesn't make that easy.
|
1010
|
+
asyncio.create_task(self._handle_id_change())
|
1011
|
+
|
817
1012
|
def config_item_changed(self, name, value, schema):
|
818
1013
|
if self._updating_config:
|
819
1014
|
return
|
@@ -937,6 +1132,13 @@ class TviewMainWindow():
|
|
937
1132
|
|
938
1133
|
self.user_task = None
|
939
1134
|
|
1135
|
+
# UUID coordination infrastructure
|
1136
|
+
self.uuid_query_event = asyncio.Event()
|
1137
|
+
self.uuid_query_count = 0
|
1138
|
+
self.expected_device_count = 0
|
1139
|
+
self.device_uuid_support = {}
|
1140
|
+
self.uuid_query_lock = asyncio.Lock()
|
1141
|
+
|
940
1142
|
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
941
1143
|
uifilename = os.path.join(current_script_dir, "tview_main_window.ui")
|
942
1144
|
|
@@ -1005,25 +1207,147 @@ class TviewMainWindow():
|
|
1005
1207
|
self.transport = self._make_transport()
|
1006
1208
|
asyncio.create_task(self._run_transport())
|
1007
1209
|
|
1210
|
+
asyncio.create_task(self._populate_devices())
|
1211
|
+
|
1212
|
+
def _calculate_tree_key(self, device_address, transport):
|
1213
|
+
"""Calculate the tree key for a device based on its address."""
|
1214
|
+
needs_suffix = (transport.count() > 1 and
|
1215
|
+
not isinstance(device_address, int) and
|
1216
|
+
hasattr(device_address, 'transport_device') and
|
1217
|
+
device_address.transport_device)
|
1218
|
+
|
1219
|
+
suffix_str = f'/{device_address.transport_device}' if needs_suffix else ''
|
1220
|
+
|
1221
|
+
tree_key = (
|
1222
|
+
str(device_address) if isinstance(device_address, int)
|
1223
|
+
else f'{device_address.can_id}{suffix_str}' if hasattr(device_address, 'can_id') and device_address.can_id
|
1224
|
+
else f'{device_address.uuid.hex()}{suffix_str}')
|
1225
|
+
|
1226
|
+
return tree_key
|
1227
|
+
|
1228
|
+
def _init_uuid_coordination(self, device_count):
|
1229
|
+
"""Initialize UUID query coordination for a set of devices"""
|
1230
|
+
self.expected_device_count = device_count
|
1231
|
+
|
1232
|
+
async def register_uuid_query_complete(self, device, has_uuid):
|
1233
|
+
"""Called by each device when UUID query completes"""
|
1234
|
+
async with self.uuid_query_lock:
|
1235
|
+
# Track device UUID capability
|
1236
|
+
transport_device = self._get_transport_device(device.address)
|
1237
|
+
if transport_device not in self.device_uuid_support:
|
1238
|
+
self.device_uuid_support[transport_device] = []
|
1239
|
+
self.device_uuid_support[transport_device].append((device, has_uuid))
|
1240
|
+
|
1241
|
+
# Update counter
|
1242
|
+
self.uuid_query_count += 1
|
1243
|
+
|
1244
|
+
# Signal if all complete
|
1245
|
+
if self.uuid_query_count >= self.expected_device_count:
|
1246
|
+
self.uuid_query_event.set()
|
1247
|
+
|
1248
|
+
def _get_transport_device(self, address):
|
1249
|
+
"""Extract transport device from an address"""
|
1250
|
+
if isinstance(address, int):
|
1251
|
+
return None # Default transport
|
1252
|
+
elif hasattr(address, 'transport_device'):
|
1253
|
+
return address.transport_device
|
1254
|
+
return None
|
1255
|
+
|
1256
|
+
def can_use_uuid_on_transport(self, transport_device):
|
1257
|
+
"""Check if all devices on a transport support UUID"""
|
1258
|
+
if transport_device not in self.device_uuid_support:
|
1259
|
+
return False
|
1260
|
+
|
1261
|
+
devices_on_transport = self.device_uuid_support[transport_device]
|
1262
|
+
return all(has_uuid for _, has_uuid in devices_on_transport)
|
1263
|
+
|
1264
|
+
def get_other_device_uuids(self, device, transport_device):
|
1265
|
+
"""Get the UUIDs of other devices on the same transport.
|
1266
|
+
|
1267
|
+
Args:
|
1268
|
+
device: The device to exclude from the list
|
1269
|
+
transport_device: The transport to query
|
1270
|
+
|
1271
|
+
Returns:
|
1272
|
+
List of full UUIDs from other devices on this transport
|
1273
|
+
"""
|
1274
|
+
other_uuids = []
|
1275
|
+
devices_on_transport = self.device_uuid_support.get(transport_device, [])
|
1276
|
+
|
1277
|
+
for other_device, has_uuid in devices_on_transport:
|
1278
|
+
if other_device != device and has_uuid:
|
1279
|
+
# Check if device has stored full UUID
|
1280
|
+
if hasattr(other_device, 'full_uuid'):
|
1281
|
+
other_uuids.append(other_device.full_uuid)
|
1282
|
+
|
1283
|
+
return other_uuids
|
1284
|
+
|
1285
|
+
def is_can_id_unique(self, can_id):
|
1286
|
+
"""Check if a CAN ID is unique across all devices in the system."""
|
1287
|
+
matching_devices = [d for d in self.devices
|
1288
|
+
if d.can_id == can_id]
|
1289
|
+
return len(matching_devices) == 1
|
1290
|
+
|
1291
|
+
async def _populate_devices(self):
|
1008
1292
|
self.devices = []
|
1293
|
+
|
1294
|
+
targets = moteus.moteus_tool.expand_targets(self.options.devices)
|
1295
|
+
if not targets:
|
1296
|
+
discovered = await self.transport.discover(
|
1297
|
+
can_prefix=self.options.can_prefix, source=0x7e)
|
1298
|
+
not_addressable = [x for x in discovered if x.address is None]
|
1299
|
+
|
1300
|
+
if len(not_addressable) > 0:
|
1301
|
+
print("No target specified, and one or more devices are not addressable", file=sys.stderr)
|
1302
|
+
print(file=sys.stderr)
|
1303
|
+
for x in not_addressable:
|
1304
|
+
print(f' * {x}', file=sys.stderr)
|
1305
|
+
sys.exit(1)
|
1306
|
+
|
1307
|
+
targets = [x.address for x in discovered]
|
1308
|
+
|
1009
1309
|
self.ui.configTreeWidget.clear()
|
1010
1310
|
self.ui.telemetryTreeWidget.clear()
|
1011
1311
|
|
1012
|
-
for
|
1013
|
-
|
1312
|
+
# Initialize UUID coordination for all devices
|
1313
|
+
self._init_uuid_coordination(len(targets))
|
1314
|
+
|
1315
|
+
device_count = 0
|
1316
|
+
source_can_id = 0x7d
|
1317
|
+
|
1318
|
+
for device_address in targets:
|
1319
|
+
# Extract current CAN ID from the target specification
|
1320
|
+
current_can_id = None
|
1321
|
+
if isinstance(device_address, int):
|
1322
|
+
# Direct integer CAN ID specification
|
1323
|
+
current_can_id = device_address
|
1324
|
+
elif getattr(device_address, 'can_id', None) is not None:
|
1325
|
+
# DeviceAddress with CAN ID
|
1326
|
+
current_can_id = device_address.can_id
|
1327
|
+
|
1328
|
+
# UUID-only addresses will have current_can_id = None
|
1329
|
+
|
1330
|
+
tree_key = self._calculate_tree_key(device_address, self.transport)
|
1331
|
+
|
1014
1332
|
config_item = QtWidgets.QTreeWidgetItem()
|
1015
|
-
|
1333
|
+
|
1334
|
+
config_item.setText(0, tree_key)
|
1016
1335
|
self.ui.configTreeWidget.addTopLevelItem(config_item)
|
1017
1336
|
|
1018
1337
|
data_item = QtWidgets.QTreeWidgetItem()
|
1019
|
-
data_item.setText(0,
|
1338
|
+
data_item.setText(0, tree_key)
|
1020
1339
|
self.ui.telemetryTreeWidget.addTopLevelItem(data_item)
|
1021
1340
|
|
1022
|
-
device = Device(
|
1023
|
-
self.
|
1341
|
+
device = Device(device_address, source_can_id,
|
1342
|
+
self.transport,
|
1343
|
+
self.console, '{}>'.format(tree_key),
|
1024
1344
|
config_item,
|
1025
1345
|
data_item,
|
1026
|
-
self.options.can_prefix
|
1346
|
+
self.options.can_prefix,
|
1347
|
+
self,
|
1348
|
+
current_can_id)
|
1349
|
+
|
1350
|
+
source_can_id -= 1
|
1027
1351
|
|
1028
1352
|
config_item.setData(0, QtCore.Qt.UserRole, device)
|
1029
1353
|
asyncio.create_task(device.start())
|
@@ -1039,10 +1363,15 @@ class TviewMainWindow():
|
|
1039
1363
|
message = await self.transport.read()
|
1040
1364
|
if message is None:
|
1041
1365
|
continue
|
1042
|
-
source_id = (message.arbitration_id >> 8) &
|
1366
|
+
source_id = (message.arbitration_id >> 8) & 0x7f
|
1367
|
+
dest_id = (message.arbitration_id & 0x7f)
|
1043
1368
|
any_data_read = False
|
1044
1369
|
for device in self.devices:
|
1045
|
-
if device.
|
1370
|
+
if ((device.address.transport_device is None or
|
1371
|
+
device.address.transport_device == message.channel) and
|
1372
|
+
device.source_can_id == dest_id and
|
1373
|
+
(device.address.can_id is None or
|
1374
|
+
device.address.can_id == source_id)):
|
1046
1375
|
any_data_read = await device.process_message(message)
|
1047
1376
|
break
|
1048
1377
|
if predicate(message):
|
@@ -1072,24 +1401,29 @@ class TviewMainWindow():
|
|
1072
1401
|
device.poll_count -= 1
|
1073
1402
|
continue
|
1074
1403
|
|
1075
|
-
|
1404
|
+
# Acquire lock for the entire poll-response cycle
|
1405
|
+
async with device.poll_lock:
|
1406
|
+
await device.poll()
|
1076
1407
|
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1408
|
+
try:
|
1409
|
+
this_data_read = await asyncio.wait_for(
|
1410
|
+
self._dispatch_until(
|
1411
|
+
lambda x: (
|
1412
|
+
(x.arbitration_id & 0x7f) == device.source_can_id and
|
1413
|
+
(device.address.can_id is None or
|
1414
|
+
(x.arbitration_id >> 8) & 0x7f == device.address.can_id))),
|
1415
|
+
timeout = POLL_TIMEOUT_S)
|
1416
|
+
|
1417
|
+
device.error_count = 0
|
1418
|
+
device.poll_count = 0
|
1419
|
+
|
1420
|
+
if this_data_read:
|
1421
|
+
any_data_read = True
|
1422
|
+
except asyncio.TimeoutError:
|
1423
|
+
# Mark this device as error-full, which will then
|
1424
|
+
# result in backoff in polling.
|
1425
|
+
device.error_count = min(1000, device.error_count + 1)
|
1426
|
+
device.poll_count = device.error_count
|
1093
1427
|
|
1094
1428
|
return any_data_read
|
1095
1429
|
|
@@ -1118,12 +1452,41 @@ class TviewMainWindow():
|
|
1118
1452
|
|
1119
1453
|
# Otherwise ignore problems so that tview keeps running.
|
1120
1454
|
|
1455
|
+
def _match(self, device, s):
|
1456
|
+
# Try to parse as integer for CAN ID matching
|
1457
|
+
try:
|
1458
|
+
target_id = int(s)
|
1459
|
+
if target_id < 1 or target_id > 126:
|
1460
|
+
# These are not valid IDs.
|
1461
|
+
target_id = None
|
1462
|
+
except:
|
1463
|
+
target_id = None
|
1464
|
+
|
1465
|
+
# Check current address CAN ID.
|
1466
|
+
if device.address.can_id is not None and target_id is not None:
|
1467
|
+
if target_id == device.address.can_id:
|
1468
|
+
return True
|
1469
|
+
|
1470
|
+
# Check UUID addressing.
|
1471
|
+
if device.address.uuid is not None:
|
1472
|
+
if s.upper() == device.address.uuid.hex().upper():
|
1473
|
+
return True
|
1474
|
+
|
1475
|
+
# Check tracked CAN ID if it's unique in the system
|
1476
|
+
if (target_id is not None and
|
1477
|
+
getattr(device, 'can_id', None) is not None and
|
1478
|
+
target_id == device.can_id and
|
1479
|
+
self.is_can_id_unique(target_id)):
|
1480
|
+
return True
|
1481
|
+
|
1482
|
+
return False
|
1483
|
+
|
1121
1484
|
async def _wait_user_query(self, maybe_id):
|
1122
|
-
|
1123
|
-
if maybe_id:
|
1124
|
-
device_nums = [int(maybe_id)]
|
1485
|
+
devices = [self.devices[0]]
|
1125
1486
|
|
1126
|
-
|
1487
|
+
if maybe_id:
|
1488
|
+
devices = [x for x in self.devices if
|
1489
|
+
self._match(x, maybe_id)]
|
1127
1490
|
|
1128
1491
|
record = 'servo_stats'
|
1129
1492
|
|
@@ -1145,10 +1508,10 @@ class TviewMainWindow():
|
|
1145
1508
|
|
1146
1509
|
async def _run_user_command(self, command):
|
1147
1510
|
delay_re = re.search(r"^:(\d+)$", command)
|
1148
|
-
device_re = re.search(r"^(A|\d
|
1149
|
-
traj_re = re.search(r"^(\?(\d
|
1511
|
+
device_re = re.search(r"^(A|\d+|[a-fA-F0-9]{8,32})>\s*(.*)$", command)
|
1512
|
+
traj_re = re.search(r"^(\?(\d+|[a-fA-F0-9]{8,32})?)$", command)
|
1150
1513
|
|
1151
|
-
|
1514
|
+
devices = [self.devices[0]]
|
1152
1515
|
|
1153
1516
|
if traj_re:
|
1154
1517
|
await self._wait_user_query(traj_re.group(2))
|
@@ -1159,11 +1522,12 @@ class TviewMainWindow():
|
|
1159
1522
|
elif device_re:
|
1160
1523
|
command = device_re.group(2)
|
1161
1524
|
if device_re.group(1) == 'A':
|
1162
|
-
|
1525
|
+
devices = self.devices
|
1163
1526
|
else:
|
1164
|
-
|
1527
|
+
devices = [x for x in self.devices
|
1528
|
+
if self._match(x, device_re.group(1))]
|
1165
1529
|
|
1166
|
-
for device in
|
1530
|
+
for device in devices:
|
1167
1531
|
device.write((command + '\n').encode('latin1'))
|
1168
1532
|
|
1169
1533
|
def _handle_tree_expanded(self, item):
|
@@ -1289,8 +1653,6 @@ def main():
|
|
1289
1653
|
action='append', type=str, default=[])
|
1290
1654
|
parser.add_argument('--can-prefix', type=int, default=0)
|
1291
1655
|
|
1292
|
-
parser.add_argument('--max-receive-bytes', default=48, type=int)
|
1293
|
-
|
1294
1656
|
moteus.make_transport_args(parser)
|
1295
1657
|
|
1296
1658
|
args = parser.parse_args()
|
@@ -1303,7 +1665,8 @@ def main():
|
|
1303
1665
|
loop = asyncqt.QEventLoop(app)
|
1304
1666
|
asyncio.set_event_loop(loop)
|
1305
1667
|
|
1306
|
-
#
|
1668
|
+
# Currently there are many things that can barf on exit, let's
|
1669
|
+
# just ignore all of them because, hey, we're about to exit!
|
1307
1670
|
app.aboutToQuit.connect(lambda: os._exit(0))
|
1308
1671
|
|
1309
1672
|
tv = TviewMainWindow(args)
|
@@ -24,7 +24,7 @@ long_description = (here / 'README.md').read_text(encoding='utf-8')
|
|
24
24
|
|
25
25
|
setuptools.setup(
|
26
26
|
name = 'moteus-gui',
|
27
|
-
version = "0.3.
|
27
|
+
version = "0.3.91",
|
28
28
|
description = 'moteus brushless controller graphical user interfaces',
|
29
29
|
long_description = long_description,
|
30
30
|
long_description_content_type = 'text/markdown',
|
@@ -51,7 +51,7 @@ setuptools.setup(
|
|
51
51
|
python_requires = '>=3.7, <4',
|
52
52
|
install_requires = [
|
53
53
|
'asyncqt>=0.8',
|
54
|
-
'moteus>=0.3.
|
54
|
+
'moteus>=0.3.91',
|
55
55
|
'matplotlib>=3.9',
|
56
56
|
# For some reason, matplotlib can barf without this, but
|
57
57
|
# doesn't actually list it as a dependency on Windows.
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|