From 1dd67be5b0ee44b736c0cb2f2d0b714a29671799 Mon Sep 17 00:00:00 2001 From: Marc Nause Date: Thu, 18 Dec 2025 10:07:04 +0100 Subject: [PATCH 1/2] Add servo instrument --- ConnectionHandler.py | 8 +++++ Servos.py | 74 ++++++++++++++++++++++++++++++++++++++++++++ SetServoAngleStep.py | 37 ++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 Servos.py create mode 100644 SetServoAngleStep.py diff --git a/ConnectionHandler.py b/ConnectionHandler.py index 5d56212..2b55199 100644 --- a/ConnectionHandler.py +++ b/ConnectionHandler.py @@ -4,6 +4,7 @@ import threading from pslab import ScienceLab +from pslab.external.motor import Servo from pslab.instrument.logic_analyzer import LogicAnalyzer from pslab.instrument.multimeter import Multimeter from pslab.instrument.oscilloscope import Oscilloscope @@ -51,3 +52,10 @@ def getLogicAnalyzer(self) -> LogicAnalyzer: def getMultimeter(self) -> Multimeter: return self.__getScienceLab().multimeter + + def getServos(self, MinAnglePulse: int, MaxAnglePulse: int, AngleRange: int, Frequency: int) -> [Servo]: + pwm_generator = self.getPWMGenerator() + return [Servo("SQ1", pwm_generator, MinAnglePulse, MaxAnglePulse, AngleRange, Frequency), + Servo("SQ2", pwm_generator, MinAnglePulse, MaxAnglePulse, AngleRange, Frequency), + Servo("SQ3", pwm_generator, MinAnglePulse, MaxAnglePulse, AngleRange, Frequency), + Servo("SQ4", pwm_generator, MinAnglePulse, MaxAnglePulse, AngleRange, Frequency)] diff --git a/Servos.py b/Servos.py new file mode 100644 index 0000000..b844260 --- /dev/null +++ b/Servos.py @@ -0,0 +1,74 @@ +""" +Instrument wrapper for PSLab python API +""" + +from enum import Enum + +from OpenTap import Display +from OpenTap import Unit +from opentap import * + +from .ConnectionHandler import ConnectionHandler + +SquareWavePin = Enum('SquareWavePin', ['SQ1', 'SQ2', 'SQ3', 'SQ4']) + +@attribute(Display("Servo", "Servo Instrument", "PSLab")) +class Servos(Instrument): + min_angle_pulse = property(float, 500)\ + .add_attribute(Unit("ms"))\ + .add_attribute(Display("Min. Angle Pulse", "Pulse length corresponding to the minimum (usually 0 degree) angle of the servo.", "", -50)) + + max_angle_pulse = property(float, 2500)\ + .add_attribute(Unit("ms"))\ + .add_attribute(Display("Max. Angle Pulse", "Pulse length corresponding to the maximum (usually 180 degree) angle of the servo.", "", -40)) + + angle_range = property(float, 180)\ + .add_attribute(Unit("°"))\ + .add_attribute(Display("Angle Range", "Range of the servo.", "", -30)) + + frequency = property(float, 50)\ + .add_attribute(Unit("Hz"))\ + .add_attribute(Display("Frequency", "Frequency of the control signal.", "", -20)) + + def __init__(self): + """Set up the properties, methods and default values of the instrument.""" + super(Servos, self).__init__() # The base class initializer must be invoked. + self.instrument = None + self.Name = "Servo" + self.Rules.Add(Rule("min_angle_pulse", lambda: self.min_angle_pulse > 0, lambda: 'Angle pulse must be positive.')) + self.Rules.Add(Rule("max_angle_pulse", lambda: self.max_angle_pulse > 0, lambda: 'Angle pulse must be positive.')) + self.Rules.Add(Rule("angle_range", lambda: self.angle_range > 0, lambda: 'Angle range must be positive.')) + self.Rules.Add(Rule("angle_range", lambda: self.angle_range <= 360, lambda: 'Angle range must not exceed 360°.')) + self.Rules.Add(Rule("frequency", lambda: self.frequency > 0, lambda: 'Frequency must be positive.')) + + def Open(self): + super(Servos, self).Open() + # Open COM connection to instrument using ConnectionHandler + self.instrument = ConnectionHandler.instance().getServos(self.min_angle_pulse, self.max_angle_pulse, self.angle_range, self.frequency) + """Called by TAP when the test plan starts""" + + def Close(self): + """Called by TAP when the test plan ends.""" + super(Servos, self).Close() + + def get_min_angle(self): + return 0 + + def get_max_angle(self): + return self.angle_range + + def _get_servo(self, pin: SquareWavePin): + match pin: + case SquareWavePin.SQ1: + return self.instrument[0] + case SquareWavePin.SQ2: + return self.instrument[1] + case SquareWavePin.SQ3: + return self.instrument[2] + case SquareWavePin.SQ4: + return self.instrument[3] + case _: + raise Exception("Unsupported pin " + pin) + + def set_angle(self, pin: SquareWavePin, angle: int): + self._get_servo(pin).angle = angle diff --git a/SetServoAngleStep.py b/SetServoAngleStep.py new file mode 100644 index 0000000..3ffc568 --- /dev/null +++ b/SetServoAngleStep.py @@ -0,0 +1,37 @@ +""" +Test step to set the angle of a connected servo on a SQ1-4 pin +""" + +from OpenTap import Display, Unit, Verdict +from System import Double +from opentap import * + +from .Servos import * + + +@attribute(Display("Set Servo Angle", "Sets angle of a connected servo", Groups=["PSLab", "Servo"])) +class SetServoAngleStep(TestStep): + # Properties + pin = property(SquareWavePin, SquareWavePin.SQ1) \ + .add_attribute(Display("Pin", "Pin on which the square wave is generated", "", -50)) + + angle = property(float, 0) \ + .add_attribute(Display("Angle", "The angle to be set on a chosen SQ pin", "", -40)) \ + .add_attribute(Unit("°")) + + Servos = property(Servos, None) \ + .add_attribute(Display("Servo", "", "Resources", 0)) + + def __init__(self): + super(SetServoAngleStep, self).__init__() + + self.Rules.Add( + Rule("angle", lambda: self.angle >= self.Servos.get_min_angle(), + lambda: f'Angle must be at least {self.Servos.get_min_angle()}°.')) + self.Rules.Add( + Rule("angle", lambda: self.angle <= self.Servos.get_max_angle(), + lambda: f'Angle must not exceed {self.Servos.get_max_angle()}°.')) + + def Run(self): + self.Servos.set_angle(self.pin, self.angle) + self.UpgradeVerdict(Verdict.Pass) From 635db1d01bb7ecd798a0e32f2af1324b631e1d9e Mon Sep 17 00:00:00 2001 From: Marc Nause Date: Fri, 19 Dec 2025 07:54:11 +0100 Subject: [PATCH 2/2] Add short wait time before servo movements --- Servos.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Servos.py b/Servos.py index b844260..f01ef37 100644 --- a/Servos.py +++ b/Servos.py @@ -2,6 +2,8 @@ Instrument wrapper for PSLab python API """ +from time import sleep + from enum import Enum from OpenTap import Display @@ -71,4 +73,7 @@ def _get_servo(self, pin: SquareWavePin): raise Exception("Unsupported pin " + pin) def set_angle(self, pin: SquareWavePin, angle: int): + """ Wait a few milliseconds to allow the command to be processed between steps. + Otherwise, movements could be omitted if the commands are sent too quickly one after the other. """ + sleep(20/1000) self._get_servo(pin).angle = angle