Skip to content

Commit c3c47b6

Browse files
committed
Refactor signal and location configuration methods for clarity and validation
- Updated get_signal to correctly parse response and return signal value. - Modified get_location_z_clearance to return z_world as a boolean. - Enhanced set_location_z_clearance to convert z_world to an integer for command. - Improved get_location_config and set_location_config to handle bit mask configurations with validation checks.
1 parent a4fda8e commit c3c47b6

File tree

2 files changed

+136
-31
lines changed

2 files changed

+136
-31
lines changed

pylabrobot/arms/precise_flex/precise_flex_backend_api.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,8 @@ async def get_signal(self, signal_number: int) -> int:
333333
int: The current signal value.
334334
"""
335335
response = await self.send_command(f"sig {signal_number}")
336-
return int(response)
336+
sig_id, sig_val = response.split()
337+
return int(sig_val)
337338

338339
async def set_signal(self, signal_number: int, value: int) -> None:
339340
"""Set the specified digital input or output signal.
@@ -513,7 +514,7 @@ async def set_location_xyz(self, location_index: int, x: float, y: float, z: flo
513514
"""
514515
await self.send_command(f"locXyz {location_index} {x} {y} {z} {yaw} {pitch} {roll}")
515516

516-
async def get_location_z_clearance(self, location_index: int) -> tuple[int, float, float]:
517+
async def get_location_z_clearance(self, location_index: int) -> tuple[int, float, bool]:
517518
"""Get the ZClearance and ZWorld properties for the specified location.
518519
519520
Parameters:
@@ -530,11 +531,11 @@ async def get_location_z_clearance(self, location_index: int) -> tuple[int, floa
530531

531532
station_index = int(parts[0])
532533
z_clearance = float(parts[1])
533-
z_world = float(parts[2])
534+
z_world = True if float(parts[2]) != 0 else False
534535

535536
return (station_index, z_clearance, z_world)
536537

537-
async def set_location_z_clearance(self, location_index: int, z_clearance: float, z_world: float | None = None) -> None:
538+
async def set_location_z_clearance(self, location_index: int, z_clearance: float, z_world: bool | None = None) -> None:
538539
"""Set the ZClearance and ZWorld properties for the specified location.
539540
540541
Parameters:
@@ -545,7 +546,8 @@ async def set_location_z_clearance(self, location_index: int, z_clearance: float
545546
if z_world is None:
546547
await self.send_command(f"locZClearance {location_index} {z_clearance}")
547548
else:
548-
await self.send_command(f"locZClearance {location_index} {z_clearance} {z_world}")
549+
z_world_int = 1 if z_world else 0
550+
await self.send_command(f"locZClearance {location_index} {z_clearance} {z_world_int}")
549551

550552
async def get_location_config(self, location_index: int) -> tuple[int, int]:
551553
"""Get the Config property for the specified location.
@@ -554,7 +556,17 @@ async def get_location_config(self, location_index: int) -> tuple[int, int]:
554556
location_index (int): The station index, from 1 to N_LOC.
555557
556558
Returns:
557-
tuple: A tuple containing (station_index, config_value). config_value: 1 = Righty, 2 = Lefty.
559+
tuple: A tuple containing (station_index, config_value)
560+
config_value is a bit mask where:
561+
- 0 = None (no configuration specified)
562+
- 0x01 = GPL_Righty (right shouldered configuration)
563+
- 0x02 = GPL_Lefty (left shouldered configuration)
564+
- 0x04 = GPL_Above (elbow above the wrist)
565+
- 0x08 = GPL_Below (elbow below the wrist)
566+
- 0x10 = GPL_Flip (wrist pitched up)
567+
- 0x20 = GPL_NoFlip (wrist pitched down)
568+
- 0x1000 = GPL_Single (restrict wrist axis to +/- 180 degrees)
569+
Values can be combined using bitwise OR.
558570
"""
559571
data = await self.send_command(f"locConfig {location_index}")
560572
parts = data.split(" ")
@@ -572,8 +584,46 @@ async def set_location_config(self, location_index: int, config_value: int) -> N
572584
573585
Parameters:
574586
location_index (int): The station index, from 1 to N_LOC.
575-
config_value (int): The new Config property value. 1 = Righty, 2 = Lefty.
587+
config_value (int): The new Config property value as a bit mask where:
588+
- 0 = None (no configuration specified)
589+
- 0x01 = GPL_Righty (right shouldered configuration)
590+
- 0x02 = GPL_Lefty (left shouldered configuration)
591+
- 0x04 = GPL_Above (elbow above the wrist)
592+
- 0x08 = GPL_Below (elbow below the wrist)
593+
- 0x10 = GPL_Flip (wrist pitched up)
594+
- 0x20 = GPL_NoFlip (wrist pitched down)
595+
- 0x1000 = GPL_Single (restrict wrist axis to +/- 180 degrees)
596+
Values can be combined using bitwise OR.
597+
598+
Raises:
599+
ValueError: If config_value contains invalid bits or conflicting configurations.
576600
"""
601+
# Define valid bit masks
602+
GPL_RIGHTY = 0x01
603+
GPL_LEFTY = 0x02
604+
GPL_ABOVE = 0x04
605+
GPL_BELOW = 0x08
606+
GPL_FLIP = 0x10
607+
GPL_NOFLIP = 0x20
608+
GPL_SINGLE = 0x1000
609+
610+
# All valid bits
611+
ALL_VALID_BITS = GPL_RIGHTY | GPL_LEFTY | GPL_ABOVE | GPL_BELOW | GPL_FLIP | GPL_NOFLIP | GPL_SINGLE
612+
613+
# Check for invalid bits
614+
if config_value & ~ALL_VALID_BITS:
615+
raise ValueError(f"Invalid config bits specified: 0x{config_value:X}")
616+
617+
# Check for conflicting configurations
618+
if (config_value & GPL_RIGHTY) and (config_value & GPL_LEFTY):
619+
raise ValueError("Cannot specify both GPL_Righty and GPL_Lefty")
620+
621+
if (config_value & GPL_ABOVE) and (config_value & GPL_BELOW):
622+
raise ValueError("Cannot specify both GPL_Above and GPL_Below")
623+
624+
if (config_value & GPL_FLIP) and (config_value & GPL_NOFLIP):
625+
raise ValueError("Cannot specify both GPL_Flip and GPL_NoFlip")
626+
577627
await self.send_command(f"locConfig {location_index} {config_value}")
578628

579629

pylabrobot/arms/precise_flex/precise_flex_tests.py

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ async def asyncSetUp(self):
1616
self.robot = PreciseFlexBackendApi("192.168.0.1", 10100)
1717
# Configuration constants - modify these for your testing needs
1818
self.TEST_PROFILE_ID = 20
19-
self.TEST_LOCATION_ID = 20
19+
self.TEST_LOCATION_ID = 20 # Default upper limit of station indices offered by GPL program running the TCS server
20+
self.TEST_PARAMETER_ID = 17018 # last parameter Id of "Custom Calibration Data and Test Results" parameters
21+
self.TEST_SIGNAL_ID = 20064 # unused software I/O
2022
await self.robot.setup()
2123
await self.robot.attach()
2224
await self.robot.set_power(True, timeout=20)
@@ -168,23 +170,20 @@ async def test_set_payload(self) -> None:
168170

169171
async def test_parameter_operations(self) -> None:
170172
"""Test get_parameter() and set_parameter()"""
171-
# Test with a safe parameter (example DataID)
172-
test_data_id = 901 # Example parameter ID
173-
174173
# Get original value
175-
original_value = await self.robot.get_parameter(test_data_id)
174+
original_value = await self.robot.get_parameter(self.TEST_PARAMETER_ID)
176175
print(f"Original parameter value: {original_value}")
177176

178177
# Test setting and getting back
179178
test_value = "test_value"
180-
await self.robot.set_parameter(test_data_id, test_value)
179+
await self.robot.set_parameter(self.TEST_PARAMETER_ID, test_value)
181180

182181
# Get the value back
183-
retrieved_value = await self.robot.get_parameter(test_data_id)
182+
retrieved_value = await self.robot.get_parameter(self.TEST_PARAMETER_ID)
184183
print(f"Retrieved parameter value: {retrieved_value}")
185184

186185
# Restore original value
187-
await self.robot.set_parameter(test_data_id, original_value)
186+
await self.robot.set_parameter(self.TEST_PARAMETER_ID, original_value)
188187

189188
async def test_get_selected_robot(self) -> None:
190189
"""Test get_selected_robot()"""
@@ -204,25 +203,27 @@ async def test_select_robot(self) -> None:
204203

205204
async def test_signal_operations(self) -> None:
206205
"""Test get_signal() and set_signal()"""
207-
test_signal = 1 # Example signal number
208206

209207
# Get original signal value
210-
original_value = await self.robot.get_signal(test_signal)
211-
print(f"Original signal {test_signal} value: {original_value}")
208+
original_value = await self.robot.get_signal(self.TEST_SIGNAL_ID)
209+
print(f"Original signal {self.TEST_SIGNAL_ID} value: {original_value}")
212210

213211
try:
214212
# Test setting signal
215213
test_value = 1 if original_value == 0 else 0
216-
await self.robot.set_signal(test_signal, test_value)
214+
await self.robot.set_signal(self.TEST_SIGNAL_ID, test_value)
217215

218216
# Verify the change
219-
new_value = await self.robot.get_signal(test_signal)
220-
self.assertEqual(new_value, test_value)
221-
print(f"Signal {test_signal} set to: {new_value}")
217+
new_value = await self.robot.get_signal(self.TEST_SIGNAL_ID)
218+
if test_value == 0:
219+
self.assertEqual(new_value, 0)
220+
else:
221+
self.assertNotEqual(new_value, 0)
222+
print(f"Signal {self.TEST_SIGNAL_ID} set to: {new_value}")
222223

223224
finally:
224225
# Restore original value
225-
await self.robot.set_signal(test_signal, original_value)
226+
await self.robot.set_signal(self.TEST_SIGNAL_ID, original_value)
226227

227228
async def test_get_system_state(self) -> None:
228229
"""Test get_system_state()"""
@@ -300,7 +301,7 @@ async def test_set_location_angles(self) -> None:
300301

301302
try:
302303
# Test setting angles
303-
test_angles = (15.0, 25.0, 35.0, 45.0, 55.0, 65.0)
304+
test_angles = (15.0, 25.0, 35.0, 45.0, 55.0, 0.0) # last is set to 0.0 as angle7 is typically unused on PF400, some other robots may use fewer angles
304305
await self.robot.set_location_angles(self.TEST_LOCATION_ID, *test_angles)
305306

306307
# Verify the angles were set
@@ -391,13 +392,13 @@ async def test_set_location_z_clearance(self) -> None:
391392
print(f"Z clearance set to: {z_clearance}")
392393

393394
# Test setting both z_clearance and z_world
394-
test_z_world = 75.0
395+
test_z_world = True
395396
await self.robot.set_location_z_clearance(self.TEST_LOCATION_ID, test_z_clearance, test_z_world)
396397

397398
clearance_data = await self.robot.get_location_z_clearance(self.TEST_LOCATION_ID)
398399
_, z_clearance, z_world = clearance_data
399400
self.assertLess(abs(z_clearance - test_z_clearance), 0.001)
400-
self.assertLess(abs(z_world - test_z_world), 0.001)
401+
self.assertEqual(z_world, test_z_world)
401402
print(f"Z clearance and world set to: {z_clearance}, {z_world}")
402403

403404
finally:
@@ -412,23 +413,77 @@ async def test_get_location_config(self) -> None:
412413
station_index, config_value = config_data
413414
self.assertEqual(station_index, self.TEST_LOCATION_ID)
414415
self.assertIsInstance(config_value, int)
415-
self.assertIn(config_value, [1, 2]) # 1 = Righty, 2 = Lefty
416-
print(f"Location {self.TEST_LOCATION_ID} config: {config_value} ({'Righty' if config_value == 1 else 'Lefty'})")
416+
self.assertGreaterEqual(config_value, 0)
417+
418+
# Decode config bits for display
419+
config_bits = []
420+
if config_value == 0:
421+
config_bits.append("None")
422+
else:
423+
if config_value & 0x01:
424+
config_bits.append("Righty")
425+
if config_value & 0x02:
426+
config_bits.append("Lefty")
427+
if config_value & 0x04:
428+
config_bits.append("Above")
429+
if config_value & 0x08:
430+
config_bits.append("Below")
431+
if config_value & 0x10:
432+
config_bits.append("Flip")
433+
if config_value & 0x20:
434+
config_bits.append("NoFlip")
435+
if config_value & 0x1000:
436+
config_bits.append("Single")
437+
438+
config_str = " | ".join(config_bits)
439+
print(f"Location {self.TEST_LOCATION_ID} config: 0x{config_value:X} ({config_str})")
417440

418441
async def test_set_location_config(self) -> None:
419442
"""Test set_location_config()"""
420443
original_config = await self.robot.get_location_config(self.TEST_LOCATION_ID)
421444
_, orig_config_value = original_config
422445

423446
try:
424-
# Test setting different config
425-
test_config = 2 if orig_config_value == 1 else 1
447+
# Test setting basic config (Righty)
448+
test_config = 0x01 # GPL_Righty
449+
await self.robot.set_location_config(self.TEST_LOCATION_ID, test_config)
450+
451+
config_data = await self.robot.get_location_config(self.TEST_LOCATION_ID)
452+
_, config_value = config_data
453+
self.assertEqual(config_value, test_config)
454+
print(f"Location config set to: 0x{config_value:X} (Righty)")
455+
456+
# Test setting combined config (Lefty + Above + NoFlip)
457+
test_config = 0x02 | 0x04 | 0x20 # GPL_Lefty | GPL_Above | GPL_NoFlip
458+
await self.robot.set_location_config(self.TEST_LOCATION_ID, test_config)
459+
460+
config_data = await self.robot.get_location_config(self.TEST_LOCATION_ID)
461+
_, config_value = config_data
462+
self.assertEqual(config_value, test_config)
463+
print(f"Location config set to: 0x{config_value:X} (Lefty | Above | NoFlip)")
464+
465+
# Test setting None config
466+
test_config = 0x00
426467
await self.robot.set_location_config(self.TEST_LOCATION_ID, test_config)
427468

428469
config_data = await self.robot.get_location_config(self.TEST_LOCATION_ID)
429470
_, config_value = config_data
430471
self.assertEqual(config_value, test_config)
431-
print(f"Location config set to: {config_value} ({'Righty' if config_value == 1 else 'Lefty'})")
472+
print(f"Location config set to: 0x{config_value:X} (None)")
473+
474+
# Test invalid config bits
475+
with self.assertRaises(ValueError):
476+
await self.robot.set_location_config(self.TEST_LOCATION_ID, 0x80) # Invalid bit
477+
478+
# Test conflicting configurations
479+
with self.assertRaises(ValueError):
480+
await self.robot.set_location_config(self.TEST_LOCATION_ID, 0x01 | 0x02) # Righty + Lefty
481+
482+
with self.assertRaises(ValueError):
483+
await self.robot.set_location_config(self.TEST_LOCATION_ID, 0x04 | 0x08) # Above + Below
484+
485+
with self.assertRaises(ValueError):
486+
await self.robot.set_location_config(self.TEST_LOCATION_ID, 0x10 | 0x20) # Flip + NoFlip
432487

433488
finally:
434489
# Restore original config

0 commit comments

Comments
 (0)