moteus-gui 0.3.90__tar.gz → 0.3.92__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: moteus-gui
3
- Version: 0.3.90
3
+ Version: 0.3.92
4
4
  Summary: moteus brushless controller graphical user interfaces
5
5
  Home-page: https://github.com/mjbots/moteus
6
6
  Author: mjbots Robotic Systems
@@ -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:MAX_SEND], self._write_data[MAX_SEND:])
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] > MAX_SEND:
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, number, transport, console, prefix,
602
- config_tree_item, data_tree_item, can_prefix=None):
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
- self.number = number
607
- self.controller = moteus.Controller(number, can_prefix=can_prefix)
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
- struct = archive.read(reader.Stream(io.BytesIO(data)))
690
- _set_tree_widget_data(item, struct, archive, terminal_flags=flags)
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 device_id in moteus.moteus_tool.expand_targets(
1013
- self.options.devices or ['1']):
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
- config_item.setText(0, str(device_id))
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, str(device_id))
1338
+ data_item.setText(0, tree_key)
1020
1339
  self.ui.telemetryTreeWidget.addTopLevelItem(data_item)
1021
1340
 
1022
- device = Device(device_id, self.transport,
1023
- self.console, '{}>'.format(device_id),
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) & 0xff
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.number == source_id:
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
- await device.poll()
1404
+ # Acquire lock for the entire poll-response cycle
1405
+ async with device.poll_lock:
1406
+ await device.poll()
1076
1407
 
1077
- try:
1078
- this_data_read = await asyncio.wait_for(
1079
- self._dispatch_until(
1080
- lambda x: (x.arbitration_id >> 8) & 0xff == device.number),
1081
- timeout = POLL_TIMEOUT_S)
1082
-
1083
- device.error_count = 0
1084
- device.poll_count = 0
1085
-
1086
- if this_data_read:
1087
- any_data_read = True
1088
- except asyncio.TimeoutError:
1089
- # Mark this device as error-full, which will then
1090
- # result in backoff in polling.
1091
- device.error_count = min(1000, device.error_count + 1)
1092
- device.poll_count = device.error_count
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
- device_nums = [self.devices[0].number]
1123
- if maybe_id:
1124
- device_nums = [int(maybe_id)]
1485
+ devices = [self.devices[0]]
1125
1486
 
1126
- devices = [x for x in self.devices if x.number in device_nums]
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+)>\s*(.*)$", command)
1149
- traj_re = re.search(r"^(\?(\d+)?)$", command)
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
- device_nums = [self.devices[0].number]
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
- device_nums = [x.number for x in self.devices]
1525
+ devices = self.devices
1163
1526
  else:
1164
- device_nums = [int(device_re.group(1))]
1527
+ devices = [x for x in self.devices
1528
+ if self._match(x, device_re.group(1))]
1165
1529
 
1166
- for device in [x for x in self.devices if x.number in device_nums]:
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
- # To work around https://bugreports.qt.io/browse/PYSIDE-88
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)
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- VERSION="0.3.90"
15
+ VERSION="0.3.92"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: moteus-gui
3
- Version: 0.3.90
3
+ Version: 0.3.92
4
4
  Summary: moteus brushless controller graphical user interfaces
5
5
  Home-page: https://github.com/mjbots/moteus
6
6
  Author: mjbots Robotic Systems
@@ -1,7 +1,7 @@
1
1
  PySide6>=6.8
2
2
  asyncqt>=0.8
3
3
  matplotlib>=3.9
4
- moteus>=0.3.74
4
+ moteus>=0.3.91
5
5
  qtconsole>=5.6
6
6
  qtpy>=2.0.1
7
7
  scipy>=1.14
@@ -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.90",
27
+ version = "0.3.92",
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.74',
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