Files
test/examples/gripper_force_control.py
2026-06-08 17:52:48 +08:00

298 lines
12 KiB
Python

"""
Closed-loop gripper force control from two tactile sensors.
Run from repository root:
python examples/gripper_force_control.py --port COM3 -v1 0 -v2 1
The controller reads left/right tactile normal force and updates the gripper
target force percentage. It uses the larger side as the safety signal because
the current gripper SDK exposes one shared force command.
"""
import argparse
import os
import sys
import time
from pathlib import Path
import multiprocessing as mp
from two_sensor_force_reader import TwoSensorForceReader
REPO_ROOT = Path(__file__).resolve().parents[1]
SDK_DIR = REPO_ROOT / "sdk"
if str(SDK_DIR) not in sys.path:
sys.path.insert(0, str(SDK_DIR))
class ExponentialFilter:
def __init__(self, alpha, initial=0.0):
self.alpha = max(0.0, min(1.0, float(alpha)))
self.value = float(initial)
self.initialized = False
def update(self, sample):
sample = float(sample)
if not self.initialized:
self.value = sample
self.initialized = True
else:
self.value = self.alpha * sample + (1.0 - self.alpha) * self.value
return self.value
def clamp(value, min_value, max_value):
return max(min_value, min(max_value, value))
def step_toward_open(current_pos, open_pos, delta):
if current_pos is None:
return open_pos
if current_pos > open_pos:
return max(open_pos, current_pos - delta)
if current_pos < open_pos:
return min(open_pos, current_pos + delta)
return open_pos
def calibrate_baseline(reader, seconds):
if seconds <= 0:
return 0.0, 0.0
left_values = []
right_values = []
deadline = time.perf_counter() + seconds
while time.perf_counter() < deadline:
left, right = reader.get_forces(timeout=0.2)
if left is not None:
left_values.append(left)
if right is not None:
right_values.append(right)
time.sleep(0.01)
left_baseline = sum(left_values) / len(left_values) if left_values else 0.0
right_baseline = sum(right_values) / len(right_values) if right_values else 0.0
return left_baseline, right_baseline
def compute_next_force_pct(
current_pct,
control_force,
target_force,
deadband,
force_min,
force_max,
step_up,
step_down,
):
low = target_force - deadband
high = target_force + deadband
if control_force > high:
return clamp(current_pct - step_down, force_min, force_max), "down"
if control_force < low:
return clamp(current_pct + step_up, force_min, force_max), "up"
return current_pct, "hold"
def load_gripper(args):
from changingtek_p_rtu_Servo import MotorController
return MotorController(
args.port,
args.slave_id,
baudrate=args.baudrate,
timeout=args.serial_timeout,
)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--video1", "-v1", type=str, default="0", help="left sensor camera index or video path")
parser.add_argument("--video2", "-v2", type=str, default="1", help="right sensor camera index or video path")
parser.add_argument("--rotation1", "-r1", type=str, default="./config/rotation_config_0.json", help="left sensor orientation config")
parser.add_argument("--rotation2", "-r2", type=str, default="./config/rotation_config_1.json", help="right sensor orientation config")
parser.add_argument("--sensor-config1", type=str, default="./config/ddjx01.json", help="left tactile sensor config")
parser.add_argument("--sensor-config2", type=str, default="./config/ddjx01.json", help="right tactile sensor config")
parser.add_argument("--sensor-fps", type=float, default=30.0, help="target processing FPS per tactile sensor")
parser.add_argument("--motion-threshold", type=float, default=0.0, help="motion threshold used by tactile deformation")
parser.add_argument("--sensor-cpu", action="store_true", help="disable CUDA for tactile workers")
parser.add_argument("--port", type=str, default="COM5", help="gripper serial port")
parser.add_argument("--slave-id", type=int, default=1, help="Modbus slave id")
parser.add_argument("--baudrate", type=int, default=115200, help="serial baudrate")
parser.add_argument("--serial-timeout", type=float, default=0.2, help="serial timeout in seconds")
parser.add_argument("--open-pos", type=int, default=0, help="open gripper position")
parser.add_argument("--close-pos", type=int, default=9000, help="close gripper position")
parser.add_argument("--speed", type=int, default=5, help="gripper speed percent")
parser.add_argument("--accel", type=int, default=60, help="gripper acceleration")
parser.add_argument("--decel", type=int, default=60, help="gripper deceleration")
parser.add_argument("--initial-force", type=int, default=10, help="initial gripper force percent")
parser.add_argument("--force-min", type=int, default=5, help="minimum gripper force percent")
parser.add_argument("--force-max", type=int, default=35, help="maximum gripper force percent")
parser.add_argument("--step-up", type=int, default=1, help="force percent increase per control tick")
parser.add_argument("--step-down", type=int, default=2, help="force percent decrease per control tick")
parser.add_argument("--control-hz", type=float, default=10.0, help="gripper force update frequency")
parser.add_argument("--target-touch-force", type=float, default=800.0, help="target tactile normal force value")
parser.add_argument("--deadband", type=float, default=80.0, help="do not adjust inside target +/- deadband")
parser.add_argument("--emergency-touch-force", type=float, default=1600.0, help="open slightly if either side exceeds this value")
parser.add_argument("--emergency-open-delta", type=int, default=500, help="position step toward open on emergency")
parser.add_argument("--imbalance-threshold", type=float, default=0.0, help="optional left/right force difference warning threshold; 0 disables")
parser.add_argument("--filter-alpha", type=float, default=0.25, help="force low-pass filter alpha")
parser.add_argument("--baseline-seconds", type=float, default=1.0, help="seconds to sample no-contact tactile baseline")
parser.add_argument("--no-initial-move", action="store_true", help="do not send initial close command")
parser.add_argument("--trigger-force-update", action="store_true", help="call trigger_motion after set_target_force")
parser.add_argument("--dry-run", action="store_true", help="print commands without opening the serial port")
return parser.parse_args()
def main():
args = parse_args()
reader = TwoSensorForceReader(
video1=args.video1,
video2=args.video2,
config1=args.sensor_config1,
config2=args.sensor_config2,
rotation1=args.rotation1,
rotation2=args.rotation2,
target_fps=args.sensor_fps,
cuda=not args.sensor_cpu,
motion_threshold=args.motion_threshold,
)
gripper = None
force_pct = int(clamp(args.initial_force, args.force_min, args.force_max))
left_filter = ExponentialFilter(args.filter_alpha)
right_filter = ExponentialFilter(args.filter_alpha)
last_written_force = None
print("Starting tactile sensors...")
reader.start()
try:
print(f"Calibrating tactile baseline for {args.baseline_seconds:.2f}s. Keep sensors unloaded.")
left_base, right_base = calibrate_baseline(reader, args.baseline_seconds)
print(f"baseline: left={left_base:.4f}, right={right_base:.4f}")
if not args.dry_run:
gripper = load_gripper(args)
if not args.no_initial_move:
if args.dry_run:
print(
"[dry-run] temp_move "
f"position={args.close_pos}, speed={args.speed}, force={force_pct}, "
f"accel={args.accel}, decel={args.decel}"
)
else:
gripper.temp_move(
position_mm=args.close_pos,
speed_pct=args.speed,
force_pct=force_pct,
accel=args.accel,
decel=args.decel,
trigger=True,
)
last_written_force = force_pct
interval = 1.0 / args.control_hz if args.control_hz > 0 else 0.1
print("Closed-loop control started. Press Ctrl+C to stop.")
while True:
tick_start = time.perf_counter()
left_raw, right_raw = reader.get_forces(timeout=1.0)
if left_raw is None or right_raw is None:
print(f"waiting for forces: left={left_raw}, right={right_raw}")
time.sleep(0.05)
continue
left_force = max(0.0, float(left_raw) - left_base)
right_force = max(0.0, float(right_raw) - right_base)
left_filtered = left_filter.update(left_force)
right_filtered = right_filter.update(right_force)
control_force = max(left_filtered, right_filtered)
avg_force = 0.5 * (left_filtered + right_filtered)
imbalance = left_filtered - right_filtered
if control_force >= args.emergency_touch_force:
force_pct = args.force_min
action = "emergency"
if args.dry_run:
print(f"[dry-run] set_target_force {force_pct}")
print(f"[dry-run] open toward {args.open_pos} by {args.emergency_open_delta}")
else:
gripper.set_target_force(force_pct)
try:
current_pos = gripper.read_real_position()
except Exception:
current_pos = None
open_pos = step_toward_open(
current_pos,
args.open_pos,
args.emergency_open_delta,
)
gripper.temp_move(
position_mm=int(open_pos),
speed_pct=args.speed,
force_pct=force_pct,
accel=args.accel,
decel=args.decel,
trigger=True,
)
last_written_force = force_pct
else:
force_pct, action = compute_next_force_pct(
force_pct,
control_force,
args.target_touch_force,
args.deadband,
args.force_min,
args.force_max,
args.step_up,
args.step_down,
)
if force_pct != last_written_force:
if args.dry_run:
print(f"[dry-run] set_target_force {force_pct}")
else:
gripper.set_target_force(int(force_pct))
if args.trigger_force_update:
gripper.trigger_motion()
last_written_force = force_pct
imbalance_msg = ""
if args.imbalance_threshold > 0 and abs(imbalance) >= args.imbalance_threshold:
imbalance_msg = " imbalance"
print(
f"L={left_filtered:8.2f} R={right_filtered:8.2f} "
f"avg={avg_force:8.2f} max={control_force:8.2f} "
f"diff={imbalance:8.2f} force_pct={force_pct:3d} "
f"action={action}{imbalance_msg}"
)
elapsed = time.perf_counter() - tick_start
sleep_time = interval - elapsed
if sleep_time > 0:
time.sleep(sleep_time)
except KeyboardInterrupt:
print("\nStopping closed-loop control.")
finally:
reader.stop()
if __name__ == "__main__":
mp.freeze_support()
main()