From 815038eb638160a6b49424654cd2c9accdea1d65 Mon Sep 17 00:00:00 2001 From: tparsons Date: Mon, 16 Oct 2023 15:17:12 +0100 Subject: [PATCH 01/15] NF: Add classes for BBTKForcePad and TPad --- psychopy_bbtk/forcePad.py | 54 +++++++ psychopy_bbtk/tpad.py | 292 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 psychopy_bbtk/forcePad.py create mode 100644 psychopy_bbtk/tpad.py diff --git a/psychopy_bbtk/forcePad.py b/psychopy_bbtk/forcePad.py new file mode 100644 index 0000000..412c438 --- /dev/null +++ b/psychopy_bbtk/forcePad.py @@ -0,0 +1,54 @@ +class BBTKForcePad: + def __init__(self, server=None, port="COM5", interval=0.001): + self.port = port + self.interval = interval + + self.server = server + + def getEvents(self, clear=True): + return self.device.getEvents(clearEvents=clear) + + @property + def device(self): + """ + ioHub device corresponding to this BBTK Force Pad + """ + if self.server is not None: + return self.server.getDevice("bbtk_force_pad") + + @property + def config(self): + """ + Configuration dict to pass to ioHub when starting up. + """ + return { + 'serial.Serial': + { + 'name': 'bbtk_force_pad', + 'monitor_event_types': [ + 'SerialInputEvent', 'SerialByteChangeEvent' + ], + 'port': self.port, + 'baud': 223300, + 'bytesize': 8, + 'parity': 'NONE', + 'stopbits': 'ONE', + 'event_parser': { + 'fixed_length': 12, + 'prefix': None, + 'delimiter': None, + 'byte_diff': False + }, + 'device_timer': { + 'interval': self.interval + }, + 'enable': True, + 'save_events': True, + 'stream_events': True, + 'auto_report_events': True, + 'event_buffer_length': 1024, + 'manufacturer_name': 'BlackBox Toolkit', + 'model_name': 'Force Pad', + 'device_number': 0 + } + } diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py new file mode 100644 index 0000000..7088021 --- /dev/null +++ b/psychopy_bbtk/tpad.py @@ -0,0 +1,292 @@ +from .. import serialdevice as sd, photodiode, button +from psychopy.tools import systemtools as st +from psychopy import logging +import serial +import re + +from ... import layout + +# possible values for self.channel +channelCodes = { + 'A': "Buttons", + 'C': "Optos", + 'M': "Voice key", + 'T': "TTL in", +} +# possible values for self.state +stateCodes = { + 'P': "Pressed/On", + 'R': "Released/Off", +} +# possible values for self.button +buttonCodes = { + '1': "Button 1", + '2': "Button 2", + '3': "Button 2", + '4': "Button 2", + '5': "Button 2", + '6': "Button 2", + '7': "Button 2", + '8': "Button 2", + '9': "Button 2", + '0': "Button 2", + '[': "Opto 1", + ']': "Opto 2", +} + +# define format for messages +messageFormat = ( + r"([{channels}]) ([{states}]) ([{buttons}]) (\d*)" +).format( + channels="".join(re.escape(key) for key in channelCodes), + states="".join(re.escape(key) for key in stateCodes), + buttons="".join(re.escape(key) for key in buttonCodes) +) + + +def splitTPadMessage(message): + return re.match(messageFormat, message).groups() + + +class TPadPhotodiode(photodiode.BasePhotodiode): + def __init__(self, port, number): + # if no TPad device present, try to create one + if sd.ports[port] is None: + pad = TPad(port=port) + pad.photodiodes[number] = self + # initialise base class + photodiode.BasePhotodiode.__init__(self, port) + # store number + self.number = number + + def setThreshold(self, threshold): + self._threshold = threshold + self.device.setMode(0) + self.device.sendMessage(f"AAO{self.number} {threshold}") + self.device.pause() + self.device.setMode(3) + + def parseMessage(self, message): + # if given a string, split according to regex + if isinstance(message, str): + message = splitTPadMessage(message) + # split into variables + # assert isinstance(message, (tuple, list)) and len(message) == 4 + channel, state, number, time = message + # convert state to bool + if state == "P": + state = True + elif state == "R": + state = False + # # validate + # assert channel == "C", ( + # "TPadPhotometer {} received non-photometer message: {}" + # ).format(self.number, message) + # assert number == str(self.number), ( + # "TPadPhotometer {} received message intended for photometer {}: {}" + # ).format(self.number, number, message) + # create PhotodiodeResponse object + resp = photodiode.PhotodiodeResponse( + time, state, threshold=self.getThreshold() + ) + + return resp + + def findPhotodiode(self, win): + # set mode to 3 + self.device.setMode(3) + self.device.pause() + # continue as normal + return photodiode.BasePhotodiode.findPhotodiode(self, win) + + +class TPadButton(button.BaseButton): + def __init__(self, port, number): + # if no TPad device present, try to create one + if sd.ports[port] is None: + pad = TPad(port=port) + pad.buttons[number] = self + else: + pad = sd.ports[port] + # initialise base class + button.BaseButton.__init__(self, device=pad) + # store number + self.number = number + + def parseMessage(self, message): + # if given a string, split according to regex + if isinstance(message, str): + message = splitTPadMessage(message) + # split into variables + # assert isinstance(message, (tuple, list)) and len(message) == 4 + channel, state, number, time = message + # convert state to bool + if state == "P": + state = True + elif state == "R": + state = False + # create PhotodiodeResponse object + resp = button.ButtonResponse( + time, state + ) + + return resp + + +class TPadVoicekey: + def __init__(self, *args, **kwargs): + pass + + +class TPad(sd.SerialDevice): + def __init__(self, port=None, pauseDuration=1/30): + # get port if not given + if port is None: + port = self._detectComPort() + # initialise as a SerialDevice + sd.SerialDevice.__init__(self, port=port, baudrate=115200, pauseDuration=pauseDuration) + # dict of responses by timestamp + self.messages = {} + # inputs + self.photodiodes = {i+1: TPadPhotodiode(port, i+1) for i in range(2)} + self.buttons = {i+1: TPadButton(port, i+1) for i in range(10)} + self.voicekeys = {i+1: TPadVoicekey(port, i+1) for i in range(1)} + # reset timer + self._lastTimerReset = None + self.resetTimer() + + @property + def nodes(self): + """ + Returns + ------- + list + List of nodes (photodiodes, buttons and voicekeys) managed by this TPad. + """ + return list(self.photodiodes.values()) + list(self.buttons.values()) + list(self.voicekeys.values()) + + @staticmethod + def _detectComPort(): + # error to raise if this fails + err = ConnectionError( + "Could not detect COM port for TPad device. Try supplying a COM port directly." + ) + # get device profiles matching what we expect of a TPad + profiles = st.systemProfilerWindowsOS(connected=True, classname="Ports") + + # find which port has FTDI + profile = None + for prf in profiles: + if prf['Manufacturer Name'] == "FTDI": + profile = prf + # if none found, fail + if not profile: + raise err + # find "COM" in profile description + desc = profile['Device Description'] + start = desc.find("COM") + 3 + end = desc.find(")", start) + # if there's no reference to a COM port, fail + if -1 in (start, end): + raise err + # get COM port number + num = desc[start:end] + # if COM port number doesn't look numeric, fail + if not num.isnumeric(): + raise err + # construct COM port string + return f"COM{num}" + + def addListener(self, listener): + """ + Add a listener, which will receive all the messages dispatched by this TPad. + + Parameters + ---------- + listener : hardware.listener.BaseListener + Object to duplicate messages to when dispatched by this TPad. + """ + # add listener to all nodes + for node in self.nodes: + node.addListener(listener) + + def setMode(self, mode): + # dispatch messages now to clear buffer + self.dispatchMessages() + # exit out of whatever mode we're in (effectively set it to 0) + try: + self.sendMessage("X") + self.pause() + except serial.serialutil.SerialException: + pass + # set mode + self.sendMessage(f"MOD{mode}") + self.pause() + # clear messages + self.getResponse() + + def resetTimer(self, clock=logging.defaultClock): + # enter settings mode + self.setMode(0) + # send reset command + self.sendMessage(f"REST") + # store time + self._lastTimerReset = clock.getTime(format=float) + # allow time to process + self.pause() + # reset mode + self.setMode(3) + + def isAwake(self): + self.setMode(0) + # call help and get response + self.sendMessage("HELP") + resp = self.getResponse() + # set to mode 3 + self.setMode(3) + + return bool(resp) + + def dispatchMessages(self, timeout=None): + # if timeout is None, use pause duration + if timeout is None: + timeout = self.pauseDuration + # get data from box + self.pause() + data = sd.SerialDevice.getResponse(self, length=2, timeout=timeout) + self.pause() + # parse lines + for line in data: + if re.match(messageFormat, line): + # if line fits format, split into attributes + channel, state, number, time = splitTPadMessage(line) + # integerise number + number = int(number) + # get time in s using defaultClock units + time = float(time) / 1000 + self._lastTimerReset + # store in array + parts = (channel, state, button, time) + # store message + self.messages[time] = line + # choose object to dispatch to + node = None + if channel == "A" and number in self.buttons: + node = self.buttons[number] + if channel == "C" and number in self.photodiodes: + node = self.photodiodes[number] + if channel == "M" and number in self.voicekeys: + node = self.voicekeys[number] + # dispatch to node + if node is not None: + message = node.parseMessage(parts) + node.receiveMessage(message) + + def calibratePhotodiode(self, level=127): + # set to mode 0 + self.setMode(0) + # call help and get response + self.sendMessage(f"AAO1 {level}") + self.sendMessage(f"AAO2 {level}") + self.getResponse() + # set to mode 3 + self.setMode(3) From 4cef4ac18af111c5e67a2375773aa07eea0437f6 Mon Sep 17 00:00:00 2001 From: TEParsons Date: Fri, 20 Oct 2023 12:36:19 +0100 Subject: [PATCH 02/15] ENH: Add support for TPad to DeviceManager --- psychopy_bbtk/tpad.py | 85 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 7088021..73cd3cc 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -1,11 +1,9 @@ -from .. import serialdevice as sd, photodiode, button +from psychopy.hardware import manager as mgr, serialdevice as sd, photodiode, button +from psychopy import logging, layout from psychopy.tools import systemtools as st -from psychopy import logging import serial import re -from ... import layout - # possible values for self.channel channelCodes = { 'A': "Buttons", @@ -48,6 +46,83 @@ def splitTPadMessage(message): return re.match(messageFormat, message).groups() +class TPadManagerPlugin(mgr.DeviceManager): + """ + Class which plugs in to DeviceManager and adds methods for managing BBTK TPad devices + """ + @mgr.DeviceMethod("tpad", "add") + def addTPad(self, name=None, port=None, pauseDuration=1/240): + """ + Add a BBTK TPad. + + Parameters + ---------- + name : str or None + Arbitrary name to refer to this TPad by. Use None to generate a unique name. + port : str, optional + COM port to which the TPad is conencted. Use None to search for port. + pauseDuration : int, optional + How long to wait after sending a serial command to the TPad + + Returns + ------- + TPad + TPad object. + """ + # make unique name if none given + if name is None: + name = mgr.DeviceManager.makeUniqueName("tpad") + self._assertDeviceNameUnique(name) + # create and store TPad + self._devices['tpad'][name] = TPad(port=port, pauseDuration=pauseDuration) + # return created TPad + return self._devices['tpad'][name] + + @mgr.DeviceMethod("tpad", "remove") + def removeTPad(self, name): + """ + Remove a TPad. + + Parameters + ---------- + name : str + Name of the TPad. + """ + del self._devices['tpad'][name] + + @mgr.DeviceMethod("tpad", "get") + def getTPad(self, name): + """ + Get a TPad by name. + + Parameters + ---------- + name : str + Arbitrary name given to the TPad when it was `add`ed. + + Returns + ------- + TPad + The requested TPad + """ + return self._devices['tpad'].get(name, None) + + @mgr.DeviceMethod("tpad", "getall") + def getTPads(self): + """ + Get a mapping of TPads that have been initialized. + + Returns + ------- + dict + Dictionary of TPads that have been initialized. Where the keys + are the names of the keyboards and the values are the keyboard + objects. + + """ + return self._devices['tpad'] + + class TPadPhotodiode(photodiode.BasePhotodiode): def __init__(self, port, number): # if no TPad device present, try to create one @@ -139,7 +214,7 @@ def __init__(self, *args, **kwargs): class TPad(sd.SerialDevice): - def __init__(self, port=None, pauseDuration=1/30): + def __init__(self, port=None, pauseDuration=1/240): # get port if not given if port is None: port = self._detectComPort() From 933616402aa74c7caf562645289cdd0b296ab6ed Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Wed, 25 Oct 2023 11:54:15 +0100 Subject: [PATCH 03/15] RF: Update TPad infrastructure to work with DeviceManager --- psychopy_bbtk/tpad.py | 118 ++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 44 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 73cd3cc..39eace3 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -1,4 +1,5 @@ -from psychopy.hardware import manager as mgr, serialdevice as sd, photodiode, button +from psychopy.hardware import base, serialdevice as sd, photodiode, button +from psychopy.hardware.manager import deviceManager, DeviceManager, DeviceMethod from psychopy import logging, layout from psychopy.tools import systemtools as st import serial @@ -46,11 +47,11 @@ def splitTPadMessage(message): return re.match(messageFormat, message).groups() -class TPadManagerPlugin(mgr.DeviceManager): +class TPadManagerPlugin(DeviceManager): """ Class which plugs in to DeviceManager and adds methods for managing BBTK TPad devices """ - @mgr.DeviceMethod("tpad", "add") + @DeviceMethod("tpad", "add") def addTPad(self, name=None, port=None, pauseDuration=1/240): """ Add a BBTK TPad. @@ -71,14 +72,14 @@ def addTPad(self, name=None, port=None, pauseDuration=1/240): """ # make unique name if none given if name is None: - name = mgr.DeviceManager.makeUniqueName("tpad") + name = DeviceManager.makeUniqueName(self, "tpad") self._assertDeviceNameUnique(name) # create and store TPad - self._devices['tpad'][name] = TPad(port=port, pauseDuration=pauseDuration) + self._devices['tpad'][name] = TPadDevice(port=port, pauseDuration=pauseDuration) # return created TPad return self._devices['tpad'][name] - @mgr.DeviceMethod("tpad", "remove") + @DeviceMethod("tpad", "remove") def removeTPad(self, name): """ Remove a TPad. @@ -90,7 +91,7 @@ def removeTPad(self, name): """ del self._devices['tpad'][name] - @mgr.DeviceMethod("tpad", "get") + @DeviceMethod("tpad", "get") def getTPad(self, name): """ Get a TPad by name. @@ -107,7 +108,7 @@ def getTPad(self, name): """ return self._devices['tpad'].get(name, None) - @mgr.DeviceMethod("tpad", "getall") + @DeviceMethod("tpad", "getall") def getTPads(self): """ Get a mapping of TPads that have been initialized. @@ -122,15 +123,36 @@ def getTPads(self): """ return self._devices['tpad'] + @DeviceMethod("tpad", "available") + def getAvailableTPads(self): + """ + Get details of all available TPad devices. + + Returns + ------- + dict + Dictionary of information about available TPads connected to the system. + """ + foundDevices = [] + # look for all serial devices + for device in st.systemProfilerWindowsOS(connected=True, classname="Ports"): + # skip non-TPads + if "BBTKTPAD" not in device['Instance ID']: + continue + # get port + port = device['Device Description'].split("(")[1].split(")")[0] + # append + foundDevices.append( + {'port': port} + ) + + return foundDevices + class TPadPhotodiode(photodiode.BasePhotodiode): - def __init__(self, port, number): - # if no TPad device present, try to create one - if sd.ports[port] is None: - pad = TPad(port=port) - pad.photodiodes[number] = self + def __init__(self, pad, number): # initialise base class - photodiode.BasePhotodiode.__init__(self, port) + photodiode.BasePhotodiode.__init__(self, pad) # store number self.number = number @@ -176,13 +198,7 @@ def findPhotodiode(self, win): class TPadButton(button.BaseButton): - def __init__(self, port, number): - # if no TPad device present, try to create one - if sd.ports[port] is None: - pad = TPad(port=port) - pad.buttons[number] = self - else: - pad = sd.ports[port] + def __init__(self, pad, number): # initialise base class button.BaseButton.__init__(self, device=pad) # store number @@ -213,19 +229,27 @@ def __init__(self, *args, **kwargs): pass -class TPad(sd.SerialDevice): - def __init__(self, port=None, pauseDuration=1/240): +class TPad: + def __init__(self, name=None, port=None, pauseDuration=1/240): # get port if not given if port is None: port = self._detectComPort() - # initialise as a SerialDevice - sd.SerialDevice.__init__(self, port=port, baudrate=115200, pauseDuration=pauseDuration) + # get/make device + if deviceManager.checkDeviceNameAvailable(name): + # if no matching device is in DeviceManager, make a new one + self.device = deviceManager.addTPad( + name=name, port=port, pauseDuration=pauseDuration + ) + else: + # otherwise, use the existing device + self.device = deviceManager.getTPad(name) + # dict of responses by timestamp self.messages = {} # inputs - self.photodiodes = {i+1: TPadPhotodiode(port, i+1) for i in range(2)} - self.buttons = {i+1: TPadButton(port, i+1) for i in range(10)} - self.voicekeys = {i+1: TPadVoicekey(port, i+1) for i in range(1)} + self.photodiodes = {i + 1: TPadPhotodiode(self, i + 1) for i in range(2)} + self.buttons = {i + 1: TPadButton(self, i + 1) for i in range(10)} + self.voicekeys = {i + 1: TPadVoicekey(self, i + 1) for i in range(1)} # reset timer self._lastTimerReset = None self.resetTimer() @@ -290,33 +314,33 @@ def setMode(self, mode): self.dispatchMessages() # exit out of whatever mode we're in (effectively set it to 0) try: - self.sendMessage("X") - self.pause() + self.device.sendMessage("X") + self.device.pause() except serial.serialutil.SerialException: pass # set mode - self.sendMessage(f"MOD{mode}") - self.pause() + self.device.sendMessage(f"MOD{mode}") + self.device.pause() # clear messages - self.getResponse() + self.device.getResponse() def resetTimer(self, clock=logging.defaultClock): # enter settings mode self.setMode(0) # send reset command - self.sendMessage(f"REST") + self.device.sendMessage(f"REST") # store time self._lastTimerReset = clock.getTime(format=float) # allow time to process - self.pause() + self.device.pause() # reset mode self.setMode(3) def isAwake(self): self.setMode(0) # call help and get response - self.sendMessage("HELP") - resp = self.getResponse() + self.device.sendMessage("HELP") + resp = self.device.getResponse() # set to mode 3 self.setMode(3) @@ -325,11 +349,11 @@ def isAwake(self): def dispatchMessages(self, timeout=None): # if timeout is None, use pause duration if timeout is None: - timeout = self.pauseDuration + timeout = self.device.pauseDuration # get data from box - self.pause() - data = sd.SerialDevice.getResponse(self, length=2, timeout=timeout) - self.pause() + self.device.pause() + data = sd.SerialDevice.getResponse(self.device, length=2, timeout=timeout) + self.device.pause() # parse lines for line in data: if re.match(messageFormat, line): @@ -360,8 +384,14 @@ def calibratePhotodiode(self, level=127): # set to mode 0 self.setMode(0) # call help and get response - self.sendMessage(f"AAO1 {level}") - self.sendMessage(f"AAO2 {level}") - self.getResponse() + self.device.sendMessage(f"AAO1 {level}") + self.device.sendMessage(f"AAO2 {level}") + self.device.getResponse() # set to mode 3 self.setMode(3) + + +class TPadDevice(sd.SerialDevice, base.BaseDevice): + def __init__(self, port=None, pauseDuration=1/240): + # initialise as a SerialDevice + sd.SerialDevice.__init__(self, port=port, baudrate=115200, pauseDuration=pauseDuration) From 1c66a1b05feeaa8844a0d7a104a82eaa92649108 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Wed, 25 Oct 2023 13:43:42 +0100 Subject: [PATCH 04/15] RF: Use DeviceManager for BBTK stuff where possible --- psychopy_bbtk/tpad.py | 73 ++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 39eace3..745ac4b 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -74,8 +74,13 @@ def addTPad(self, name=None, port=None, pauseDuration=1/240): if name is None: name = DeviceManager.makeUniqueName(self, "tpad") self._assertDeviceNameUnique(name) - # create and store TPad - self._devices['tpad'][name] = TPadDevice(port=port, pauseDuration=pauseDuration) + # take note of ports already initialised + takenPorts = {dev.portString: nm for nm, dev in self.getTPads().items()} + # make/get device + if port not in takenPorts: + self._devices['tpad'][name] = TPadDevice(port=port, pauseDuration=pauseDuration) + else: + self._devices['tpad'][name] = takenPorts[port] # return created TPad return self._devices['tpad'][name] @@ -133,17 +138,33 @@ def getAvailableTPads(self): dict Dictionary of information about available TPads connected to the system. """ + + # error to raise if this fails + err = ConnectionError( + "Could not detect COM port for TPad device. Try supplying a COM port directly." + ) + foundDevices = [] # look for all serial devices - for device in st.systemProfilerWindowsOS(connected=True, classname="Ports"): + for profile in st.systemProfilerWindowsOS(connected=True, classname="Ports"): # skip non-TPads - if "BBTKTPAD" not in device['Instance ID']: + if "BBTKTPAD" not in profile['Instance ID']: continue - # get port - port = device['Device Description'].split("(")[1].split(")")[0] + # find "COM" in profile description + desc = profile['Device Description'] + start = desc.find("COM") + 3 + end = desc.find(")", start) + # if there's no reference to a COM port, fail + if -1 in (start, end): + raise err + # get COM port number + num = desc[start:end] + # if COM port number doesn't look numeric, fail + if not num.isnumeric(): + raise err # append foundDevices.append( - {'port': port} + {'port': f"COM{num}"} ) return foundDevices @@ -266,35 +287,15 @@ def nodes(self): @staticmethod def _detectComPort(): - # error to raise if this fails - err = ConnectionError( - "Could not detect COM port for TPad device. Try supplying a COM port directly." - ) - # get device profiles matching what we expect of a TPad - profiles = st.systemProfilerWindowsOS(connected=True, classname="Ports") - - # find which port has FTDI - profile = None - for prf in profiles: - if prf['Manufacturer Name'] == "FTDI": - profile = prf - # if none found, fail - if not profile: - raise err - # find "COM" in profile description - desc = profile['Device Description'] - start = desc.find("COM") + 3 - end = desc.find(")", start) - # if there's no reference to a COM port, fail - if -1 in (start, end): - raise err - # get COM port number - num = desc[start:end] - # if COM port number doesn't look numeric, fail - if not num.isnumeric(): - raise err - # construct COM port string - return f"COM{num}" + # find available devices + available = deviceManager.getAvailableTPadDevices() + # error if there are none + if not available: + raise ConnectionError( + "Could not find any TPad." + ) + # get all available ports + return [profile['port'] for profile in available] def addListener(self, listener): """ From 86d006777ac906eaea4c12d2322d021e674621f0 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Wed, 25 Oct 2023 16:19:14 +0100 Subject: [PATCH 05/15] FF: Fix some namespace errors --- psychopy_bbtk/tpad.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 745ac4b..9871aaa 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -176,6 +176,8 @@ def __init__(self, pad, number): photodiode.BasePhotodiode.__init__(self, pad) # store number self.number = number + # store device + self.device = self.parent.device def setThreshold(self, threshold): self._threshold = threshold @@ -221,7 +223,7 @@ def findPhotodiode(self, win): class TPadButton(button.BaseButton): def __init__(self, pad, number): # initialise base class - button.BaseButton.__init__(self, device=pad) + button.BaseButton.__init__(self, parent=pad) # store number self.number = number From 288bd72108b6c05261b043b85d3b43168242eb6e Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Wed, 25 Oct 2023 16:39:49 +0100 Subject: [PATCH 06/15] RF: Move SerialDevice stuff over to TPadDevice --- psychopy_bbtk/tpad.py | 85 ++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 9871aaa..05c146e 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -74,13 +74,7 @@ def addTPad(self, name=None, port=None, pauseDuration=1/240): if name is None: name = DeviceManager.makeUniqueName(self, "tpad") self._assertDeviceNameUnique(name) - # take note of ports already initialised - takenPorts = {dev.portString: nm for nm, dev in self.getTPads().items()} - # make/get device - if port not in takenPorts: - self._devices['tpad'][name] = TPadDevice(port=port, pauseDuration=pauseDuration) - else: - self._devices['tpad'][name] = takenPorts[port] + self._devices['tpad'][name] = TPadDevice(port=port, pauseDuration=pauseDuration, baudrate=115200) # return created TPad return self._devices['tpad'][name] @@ -256,7 +250,7 @@ class TPad: def __init__(self, name=None, port=None, pauseDuration=1/240): # get port if not given if port is None: - port = self._detectComPort() + port = self._detectComPort()[0] # get/make device if deviceManager.checkDeviceNameAvailable(name): # if no matching device is in DeviceManager, make a new one @@ -312,24 +306,9 @@ def addListener(self, listener): for node in self.nodes: node.addListener(listener) - def setMode(self, mode): - # dispatch messages now to clear buffer - self.dispatchMessages() - # exit out of whatever mode we're in (effectively set it to 0) - try: - self.device.sendMessage("X") - self.device.pause() - except serial.serialutil.SerialException: - pass - # set mode - self.device.sendMessage(f"MOD{mode}") - self.device.pause() - # clear messages - self.device.getResponse() - def resetTimer(self, clock=logging.defaultClock): # enter settings mode - self.setMode(0) + self.device.setMode(0) # send reset command self.device.sendMessage(f"REST") # store time @@ -337,17 +316,7 @@ def resetTimer(self, clock=logging.defaultClock): # allow time to process self.device.pause() # reset mode - self.setMode(3) - - def isAwake(self): - self.setMode(0) - # call help and get response - self.device.sendMessage("HELP") - resp = self.device.getResponse() - # set to mode 3 - self.setMode(3) - - return bool(resp) + self.device.setMode(3) def dispatchMessages(self, timeout=None): # if timeout is None, use pause duration @@ -355,7 +324,7 @@ def dispatchMessages(self, timeout=None): timeout = self.device.pauseDuration # get data from box self.device.pause() - data = sd.SerialDevice.getResponse(self.device, length=2, timeout=timeout) + data = self.device.getResponse(length=2, timeout=timeout) self.device.pause() # parse lines for line in data: @@ -385,16 +354,50 @@ def dispatchMessages(self, timeout=None): def calibratePhotodiode(self, level=127): # set to mode 0 - self.setMode(0) + self.device.setMode(0) # call help and get response self.device.sendMessage(f"AAO1 {level}") self.device.sendMessage(f"AAO2 {level}") self.device.getResponse() # set to mode 3 - self.setMode(3) + self.device.setMode(3) class TPadDevice(sd.SerialDevice, base.BaseDevice): - def __init__(self, port=None, pauseDuration=1/240): - # initialise as a SerialDevice - sd.SerialDevice.__init__(self, port=port, baudrate=115200, pauseDuration=pauseDuration) + def setMode(self, mode): + print(f"setMode({mode})[") + self.getResponse() + # exit out of whatever mode we're in (effectively set it to 0) + self.sendMessage("X") + self.pause() + # set mode + self.sendMessage(f"MOD{mode}") + self.pause() + # clear messages + self.getResponse() + print("]") + + def isAwake(self): + print("isAwake[") + self.setMode(0) + self.pause() + # call help and get response + self.sendMessage("HELP") + self.pause() # or response won't be ready + resp = self.getResponse() # get all chars (a usage message) + # set to mode 3 + self.setMode(3) + print("]") + return bool(resp) + + def resetTimer(self, clock=logging.defaultClock): + # enter settings mode + self.setMode(0) + # send reset command + self.sendMessage(f"REST") + # store time + self._lastTimerReset = clock.getTime(format=float) + # allow time to process + self.pause() + # reset mode + self.setMode(3) \ No newline at end of file From 49ecbab79861a773d134dd3159d59bc5a825a374 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Wed, 25 Oct 2023 16:40:48 +0100 Subject: [PATCH 07/15] FF: Remove debugging print statements --- psychopy_bbtk/tpad.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 05c146e..5583fa1 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -365,7 +365,6 @@ def calibratePhotodiode(self, level=127): class TPadDevice(sd.SerialDevice, base.BaseDevice): def setMode(self, mode): - print(f"setMode({mode})[") self.getResponse() # exit out of whatever mode we're in (effectively set it to 0) self.sendMessage("X") @@ -375,10 +374,8 @@ def setMode(self, mode): self.pause() # clear messages self.getResponse() - print("]") def isAwake(self): - print("isAwake[") self.setMode(0) self.pause() # call help and get response @@ -387,7 +384,6 @@ def isAwake(self): resp = self.getResponse() # get all chars (a usage message) # set to mode 3 self.setMode(3) - print("]") return bool(resp) def resetTimer(self, clock=logging.defaultClock): From ec21d551ae9561fdfc155f452ca32892b9e98747 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Wed, 25 Oct 2023 16:47:12 +0100 Subject: [PATCH 08/15] FF: Don't call getResponses with a timeout as it's too short --- psychopy_bbtk/tpad.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 5583fa1..820b3db 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -318,13 +318,10 @@ def resetTimer(self, clock=logging.defaultClock): # reset mode self.device.setMode(3) - def dispatchMessages(self, timeout=None): - # if timeout is None, use pause duration - if timeout is None: - timeout = self.device.pauseDuration + def dispatchMessages(self): # get data from box self.device.pause() - data = self.device.getResponse(length=2, timeout=timeout) + data = self.device.getResponse(length=2) self.device.pause() # parse lines for line in data: From fa106da81a437a633b880024b3c10dc0cea5800e Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 26 Oct 2023 18:06:24 +0100 Subject: [PATCH 09/15] RF: Move functions over to TPadDevice so they can be accessed via DeviceManager --- psychopy_bbtk/tpad.py | 182 +++++++++++++++++++++++++++--------------- 1 file changed, 119 insertions(+), 63 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 820b3db..ba6716f 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -102,7 +102,7 @@ def getTPad(self, name): Returns ------- - TPad + TPadDevice The requested TPad """ return self._devices['tpad'].get(name, None) @@ -163,6 +163,49 @@ def getAvailableTPads(self): return foundDevices + @DeviceMethod("tpad") + def getTPadPhotodiode(self, name, number): + pad = self.getTPad(name=name) + + return pad.photodiodes[number] + + @DeviceMethod("tpad") + def getTPadButton(self, name, number): + pad = self.getTPad(name=name) + + return pad.buttons[number] + + @DeviceMethod("tpad") + def configurePhotodiode(self, name, number, threshold=None, pos=None, size=None, units=None): + """ + Configure a photodiode attached to a TPad object. + + Parameters + ---------- + name : _type_ + _description_ + number : _type_ + _description_ + threshold : _type_, optional + _description_, by default None + pos : _type_, optional + _description_, by default None + size : _type_, optional + _description_, by default None + units : _type_, optional + _description_, by default None + + Returns + ------- + _type_ + _description_ + + Raises + ------ + ConnectionError + _description_ + """ + class TPadPhotodiode(photodiode.BasePhotodiode): def __init__(self, pad, number): @@ -170,15 +213,13 @@ def __init__(self, pad, number): photodiode.BasePhotodiode.__init__(self, pad) # store number self.number = number - # store device - self.device = self.parent.device def setThreshold(self, threshold): self._threshold = threshold - self.device.setMode(0) - self.device.sendMessage(f"AAO{self.number} {threshold}") - self.device.pause() - self.device.setMode(3) + self.parent.setMode(0) + self.parent.sendMessage(f"AAO{self.number} {threshold}") + self.parent.pause() + self.parent.setMode(3) def parseMessage(self, message): # if given a string, split according to regex @@ -208,11 +249,18 @@ def parseMessage(self, message): def findPhotodiode(self, win): # set mode to 3 - self.device.setMode(3) - self.device.pause() + self.parent.setMode(3) + self.parent.pause() # continue as normal return photodiode.BasePhotodiode.findPhotodiode(self, win) + def findThreshold(self, win): + # set mode to 3 + self.parent.setMode(3) + self.parent.pause() + # continue as normal + return photodiode.BasePhotodiode.findThreshold(self, win) + class TPadButton(button.BaseButton): def __init__(self, pad, number): @@ -248,9 +296,6 @@ def __init__(self, *args, **kwargs): class TPad: def __init__(self, name=None, port=None, pauseDuration=1/240): - # get port if not given - if port is None: - port = self._detectComPort()[0] # get/make device if deviceManager.checkDeviceNameAvailable(name): # if no matching device is in DeviceManager, make a new one @@ -261,38 +306,51 @@ def __init__(self, name=None, port=None, pauseDuration=1/240): # otherwise, use the existing device self.device = deviceManager.getTPad(name) - # dict of responses by timestamp - self.messages = {} - # inputs + def addListener(self, listener): + self.device.addListener(listener=listener) + + def dispatchMessages(self): + self.device.dispatchMessages() + + def setMode(self, mode): + self.device.setMode(mode=mode) + + def resetTimer(self, clock=logging.defaultClock): + self.device.resetTimer(clock=clock) + + +class TPadDevice(sd.SerialDevice, base.BaseDevice): + def __init__( + self, port=None, baudrate=9600, + byteSize=8, stopBits=1, + parity="N", # 'N'one, 'E'ven, 'O'dd, 'M'ask, + eol=b"\n", + maxAttempts=1, pauseDuration=0.1, + checkAwake=True + ): + # get port if not given + if port is None: + port = self._detectComPort()[0] + # initialise serial + sd.SerialDevice.__init__( + self, port=port, baudrate=baudrate, + byteSize=byteSize, stopBits=stopBits, + parity=parity, # 'N'one, 'E'ven, 'O'dd, 'M'ask, + eol=eol, + maxAttempts=maxAttempts, pauseDuration=pauseDuration, + checkAwake=checkAwake + ) + # nodes self.photodiodes = {i + 1: TPadPhotodiode(self, i + 1) for i in range(2)} self.buttons = {i + 1: TPadButton(self, i + 1) for i in range(10)} self.voicekeys = {i + 1: TPadVoicekey(self, i + 1) for i in range(1)} + + # dict of responses by timestamp + self.messages = {} # reset timer self._lastTimerReset = None self.resetTimer() - @property - def nodes(self): - """ - Returns - ------- - list - List of nodes (photodiodes, buttons and voicekeys) managed by this TPad. - """ - return list(self.photodiodes.values()) + list(self.buttons.values()) + list(self.voicekeys.values()) - - @staticmethod - def _detectComPort(): - # find available devices - available = deviceManager.getAvailableTPadDevices() - # error if there are none - if not available: - raise ConnectionError( - "Could not find any TPad." - ) - # get all available ports - return [profile['port'] for profile in available] - def addListener(self, listener): """ Add a listener, which will receive all the messages dispatched by this TPad. @@ -306,23 +364,11 @@ def addListener(self, listener): for node in self.nodes: node.addListener(listener) - def resetTimer(self, clock=logging.defaultClock): - # enter settings mode - self.device.setMode(0) - # send reset command - self.device.sendMessage(f"REST") - # store time - self._lastTimerReset = clock.getTime(format=float) - # allow time to process - self.device.pause() - # reset mode - self.device.setMode(3) - def dispatchMessages(self): # get data from box - self.device.pause() - data = self.device.getResponse(length=2) - self.device.pause() + self.pause() + data = self.getResponse(length=2) + self.pause() # parse lines for line in data: if re.match(messageFormat, line): @@ -349,18 +395,28 @@ def dispatchMessages(self): message = node.parseMessage(parts) node.receiveMessage(message) - def calibratePhotodiode(self, level=127): - # set to mode 0 - self.device.setMode(0) - # call help and get response - self.device.sendMessage(f"AAO1 {level}") - self.device.sendMessage(f"AAO2 {level}") - self.device.getResponse() - # set to mode 3 - self.device.setMode(3) + @staticmethod + def _detectComPort(): + # find available devices + available = deviceManager.getAvailableTPadDevices() + # error if there are none + if not available: + raise ConnectionError( + "Could not find any TPad." + ) + # get all available ports + return [profile['port'] for profile in available] + @property + def nodes(self): + """ + Returns + ------- + list + List of nodes (photodiodes, buttons and voicekeys) managed by this TPad. + """ + return list(self.photodiodes.values()) + list(self.buttons.values()) + list(self.voicekeys.values()) -class TPadDevice(sd.SerialDevice, base.BaseDevice): def setMode(self, mode): self.getResponse() # exit out of whatever mode we're in (effectively set it to 0) @@ -393,4 +449,4 @@ def resetTimer(self, clock=logging.defaultClock): # allow time to process self.pause() # reset mode - self.setMode(3) \ No newline at end of file + self.setMode(3) From 534a8e21c131a3d92b9f50cc2cdf3ccee9add619 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 26 Oct 2023 18:18:17 +0100 Subject: [PATCH 10/15] Add method to DeviceManager to configure photodiode --- psychopy_bbtk/tpad.py | 48 +++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index ba6716f..8331302 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -182,29 +182,33 @@ def configurePhotodiode(self, name, number, threshold=None, pos=None, size=None, Parameters ---------- - name : _type_ - _description_ - number : _type_ - _description_ - threshold : _type_, optional - _description_, by default None - pos : _type_, optional - _description_, by default None - size : _type_, optional - _description_, by default None - units : _type_, optional - _description_, by default None - - Returns - ------- - _type_ - _description_ - - Raises - ------ - ConnectionError - _description_ + name : str + Name of the TPad whose photodiode to configure + number : int + Number of the photodiode to configure + threshold : int, optional + Light threshold to set the photodiode to, or leave as None for no change. + pos : list, tuple, np.ndarray, layout.Position, optional + Position of the photodiode on the current window, or leave as None for no change. + size : list, tuple, np.ndarray, layout.Size, optional + Size of the rectangle picked up by the photodiode, or leave as None for no change. + units : str, optional + Units in which to interpret pos and size, or leave as None for no change. """ + # get diode + diode = self.getTPadPhotodiode(name=name, number=number) + # set threshold + if threshold is not None: + diode.setThreshold(threshold) + # set units + if units is not None: + diode.units = units + # set pos + if pos is not None: + diode.pos = pos + # set size + if size is not None: + diode.size = size class TPadPhotodiode(photodiode.BasePhotodiode): From d7593b0e2a07b3ea01dfb7fbef8a1fda87d932d4 Mon Sep 17 00:00:00 2001 From: TEParsons Date: Fri, 27 Oct 2023 17:06:26 +0100 Subject: [PATCH 11/15] RF: Alter TPad code to fit new DeviceManager procedure --- psychopy_bbtk/tpad.py | 171 ++---------------------------------------- 1 file changed, 6 insertions(+), 165 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 8331302..d006d4c 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -47,170 +47,6 @@ def splitTPadMessage(message): return re.match(messageFormat, message).groups() -class TPadManagerPlugin(DeviceManager): - """ - Class which plugs in to DeviceManager and adds methods for managing BBTK TPad devices - """ - @DeviceMethod("tpad", "add") - def addTPad(self, name=None, port=None, pauseDuration=1/240): - """ - Add a BBTK TPad. - - Parameters - ---------- - name : str or None - Arbitrary name to refer to this TPad by. Use None to generate a unique name. - port : str, optional - COM port to which the TPad is conencted. Use None to search for port. - pauseDuration : int, optional - How long to wait after sending a serial command to the TPad - - Returns - ------- - TPad - TPad object. - """ - # make unique name if none given - if name is None: - name = DeviceManager.makeUniqueName(self, "tpad") - self._assertDeviceNameUnique(name) - self._devices['tpad'][name] = TPadDevice(port=port, pauseDuration=pauseDuration, baudrate=115200) - # return created TPad - return self._devices['tpad'][name] - - @DeviceMethod("tpad", "remove") - def removeTPad(self, name): - """ - Remove a TPad. - - Parameters - ---------- - name : str - Name of the TPad. - """ - del self._devices['tpad'][name] - - @DeviceMethod("tpad", "get") - def getTPad(self, name): - """ - Get a TPad by name. - - Parameters - ---------- - name : str - Arbitrary name given to the TPad when it was `add`ed. - - Returns - ------- - TPadDevice - The requested TPad - """ - return self._devices['tpad'].get(name, None) - - @DeviceMethod("tpad", "getall") - def getTPads(self): - """ - Get a mapping of TPads that have been initialized. - - Returns - ------- - dict - Dictionary of TPads that have been initialized. Where the keys - are the names of the keyboards and the values are the keyboard - objects. - - """ - return self._devices['tpad'] - - @DeviceMethod("tpad", "available") - def getAvailableTPads(self): - """ - Get details of all available TPad devices. - - Returns - ------- - dict - Dictionary of information about available TPads connected to the system. - """ - - # error to raise if this fails - err = ConnectionError( - "Could not detect COM port for TPad device. Try supplying a COM port directly." - ) - - foundDevices = [] - # look for all serial devices - for profile in st.systemProfilerWindowsOS(connected=True, classname="Ports"): - # skip non-TPads - if "BBTKTPAD" not in profile['Instance ID']: - continue - # find "COM" in profile description - desc = profile['Device Description'] - start = desc.find("COM") + 3 - end = desc.find(")", start) - # if there's no reference to a COM port, fail - if -1 in (start, end): - raise err - # get COM port number - num = desc[start:end] - # if COM port number doesn't look numeric, fail - if not num.isnumeric(): - raise err - # append - foundDevices.append( - {'port': f"COM{num}"} - ) - - return foundDevices - - @DeviceMethod("tpad") - def getTPadPhotodiode(self, name, number): - pad = self.getTPad(name=name) - - return pad.photodiodes[number] - - @DeviceMethod("tpad") - def getTPadButton(self, name, number): - pad = self.getTPad(name=name) - - return pad.buttons[number] - - @DeviceMethod("tpad") - def configurePhotodiode(self, name, number, threshold=None, pos=None, size=None, units=None): - """ - Configure a photodiode attached to a TPad object. - - Parameters - ---------- - name : str - Name of the TPad whose photodiode to configure - number : int - Number of the photodiode to configure - threshold : int, optional - Light threshold to set the photodiode to, or leave as None for no change. - pos : list, tuple, np.ndarray, layout.Position, optional - Position of the photodiode on the current window, or leave as None for no change. - size : list, tuple, np.ndarray, layout.Size, optional - Size of the rectangle picked up by the photodiode, or leave as None for no change. - units : str, optional - Units in which to interpret pos and size, or leave as None for no change. - """ - # get diode - diode = self.getTPadPhotodiode(name=name, number=number) - # set threshold - if threshold is not None: - diode.setThreshold(threshold) - # set units - if units is not None: - diode.units = units - # set pos - if pos is not None: - diode.pos = pos - # set size - if size is not None: - diode.size = size - - class TPadPhotodiode(photodiode.BasePhotodiode): def __init__(self, pad, number): # initialise base class @@ -301,7 +137,7 @@ def __init__(self, *args, **kwargs): class TPad: def __init__(self, name=None, port=None, pauseDuration=1/240): # get/make device - if deviceManager.checkDeviceNameAvailable(name): + if name in DeviceManager.devices: # if no matching device is in DeviceManager, make a new one self.device = deviceManager.addTPad( name=name, port=port, pauseDuration=pauseDuration @@ -454,3 +290,8 @@ def resetTimer(self, clock=logging.defaultClock): self.pause() # reset mode self.setMode(3) + + +# register some aliases for the TPadDevice class with DeviceManager +DeviceManager.registerAlias("tpad", deviceClass="psychopy_bbtk.tpad.TPadDevice") +DeviceManager.registerAlias("psychopy_bbtk.tpad.TPad", deviceClass="psychopy_bbtk.tpad.TPadDevice") From bbe50305e7272ece1aa2d5e31a126a1f4aba24f9 Mon Sep 17 00:00:00 2001 From: TEParsons Date: Wed, 1 Nov 2023 17:26:19 +0000 Subject: [PATCH 12/15] RF: Change BBTK classes to fit new DeviceManager structure --- psychopy_bbtk/tpad.py | 140 ++++++++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 61 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index d006d4c..e315848 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -1,5 +1,5 @@ from psychopy.hardware import base, serialdevice as sd, photodiode, button -from psychopy.hardware.manager import deviceManager, DeviceManager, DeviceMethod +from psychopy.hardware.manager import deviceManager, DeviceManager from psychopy import logging, layout from psychopy.tools import systemtools as st import serial @@ -47,12 +47,40 @@ def splitTPadMessage(message): return re.match(messageFormat, message).groups() -class TPadPhotodiode(photodiode.BasePhotodiode): - def __init__(self, pad, number): +class TPadPhotodiodeGroup(photodiode.BasePhotodiodeGroup): + def __init__(self, pad, channels): + _requestedPad = pad + # try to get associated tpad + if isinstance(_requestedPad, str): + # try getting by name + pad = DeviceManager.getDevice(pad) + # if failed, try getting by port + if pad is None: + pad = DeviceManager.getDeviceBy("port", _requestedPad, deviceClass="psychopy_bbtk.tpad.TPad") + # if still failed, make one + if pad is None: + pad = DeviceManager.addDevice( + deviceClass="psychopy_bbtk.tpad.TPad", + deviceName=_requestedPad, + port=_requestedPad + ) + + # reference self in pad + pad.photodiodes = self # initialise base class - photodiode.BasePhotodiode.__init__(self, pad) - # store number - self.number = number + photodiode.BasePhotodiodeGroup.__init__(self, pad, channels=channels) + + @staticmethod + def getAvailableDevices(): + devices = [] + # iterate through profiles of all serial port devices + for dev in TPad.getAvailableDevices(): + devices.append({ + 'pad': dev['port'], + 'channels': 2, + }) + + return devices def setThreshold(self, threshold): self._threshold = threshold @@ -134,32 +162,7 @@ def __init__(self, *args, **kwargs): pass -class TPad: - def __init__(self, name=None, port=None, pauseDuration=1/240): - # get/make device - if name in DeviceManager.devices: - # if no matching device is in DeviceManager, make a new one - self.device = deviceManager.addTPad( - name=name, port=port, pauseDuration=pauseDuration - ) - else: - # otherwise, use the existing device - self.device = deviceManager.getTPad(name) - - def addListener(self, listener): - self.device.addListener(listener=listener) - - def dispatchMessages(self): - self.device.dispatchMessages() - - def setMode(self, mode): - self.device.setMode(mode=mode) - - def resetTimer(self, clock=logging.defaultClock): - self.device.resetTimer(clock=clock) - - -class TPadDevice(sd.SerialDevice, base.BaseDevice): +class TPad(sd.SerialDevice, base.BaseDevice): def __init__( self, port=None, baudrate=9600, byteSize=8, stopBits=1, @@ -181,9 +184,7 @@ def __init__( checkAwake=checkAwake ) # nodes - self.photodiodes = {i + 1: TPadPhotodiode(self, i + 1) for i in range(2)} - self.buttons = {i + 1: TPadButton(self, i + 1) for i in range(10)} - self.voicekeys = {i + 1: TPadVoicekey(self, i + 1) for i in range(1)} + self.nodes = [] # dict of responses by timestamp self.messages = {} @@ -191,6 +192,32 @@ def __init__( self._lastTimerReset = None self.resetTimer() + @staticmethod + def getAvailableDevices(): + devices = [] + # iterate through profiles of all serial port devices + for profile in st.systemProfilerWindowsOS( + classid="{4d36e978-e325-11ce-bfc1-08002be10318}", + ): + # skip non-bbtk profiles + if "BBTKTPAD" not in profile['Instance ID']: + continue + # find "COM" in profile description + desc = profile['Device Description'] + start = desc.find("COM") + 3 + end = desc.find(")", start) + # if there's no reference to a COM port, skip + if -1 in (start, end): + continue + # get COM port number + num = desc[start:end] + + devices.append({ + 'port': f"COM{num}", + }) + + return devices + def addListener(self, listener): """ Add a listener, which will receive all the messages dispatched by this TPad. @@ -213,32 +240,34 @@ def dispatchMessages(self): for line in data: if re.match(messageFormat, line): # if line fits format, split into attributes - channel, state, number, time = splitTPadMessage(line) + device, state, channel, time = splitTPadMessage(line) # integerise number - number = int(number) + channel = int(channel) # get time in s using defaultClock units time = float(time) / 1000 + self._lastTimerReset # store in array - parts = (channel, state, button, time) + parts = (device, state, button, time) # store message self.messages[time] = line # choose object to dispatch to - node = None - if channel == "A" and number in self.buttons: - node = self.buttons[number] - if channel == "C" and number in self.photodiodes: - node = self.photodiodes[number] - if channel == "M" and number in self.voicekeys: - node = self.voicekeys[number] - # dispatch to node - if node is not None: + for node in self.nodes: + # if device is A, dispatch only to buttons + if device == "A" and not isinstance(node, TPadButton): + continue + # if device is C, dispatch only to photodiodes + if device == "C" and not isinstance(node, TPadPhotodiodeGroup): + continue + # if device is M, dispatch only to voice keys + if device == "M" and not isinstance(node, TPadVoicekey): + continue + # dispatch to node message = node.parseMessage(parts) node.receiveMessage(message) @staticmethod def _detectComPort(): # find available devices - available = deviceManager.getAvailableTPadDevices() + available = TPad.getAvailableDevices() # error if there are none if not available: raise ConnectionError( @@ -247,16 +276,6 @@ def _detectComPort(): # get all available ports return [profile['port'] for profile in available] - @property - def nodes(self): - """ - Returns - ------- - list - List of nodes (photodiodes, buttons and voicekeys) managed by this TPad. - """ - return list(self.photodiodes.values()) + list(self.buttons.values()) + list(self.voicekeys.values()) - def setMode(self, mode): self.getResponse() # exit out of whatever mode we're in (effectively set it to 0) @@ -292,6 +311,5 @@ def resetTimer(self, clock=logging.defaultClock): self.setMode(3) -# register some aliases for the TPadDevice class with DeviceManager -DeviceManager.registerAlias("tpad", deviceClass="psychopy_bbtk.tpad.TPadDevice") -DeviceManager.registerAlias("psychopy_bbtk.tpad.TPad", deviceClass="psychopy_bbtk.tpad.TPadDevice") +# register some aliases for the TPad class with DeviceManager +DeviceManager.registerAlias("tpad", deviceClass="psychopy_bbtk.tpad.TPad") From 113fe61c9eeab6321590368b0c90e8350ab7e607 Mon Sep 17 00:00:00 2001 From: TEParsons Date: Wed, 1 Nov 2023 17:58:22 +0000 Subject: [PATCH 13/15] FF: Set baudrate and portString correctly on TPad --- psychopy_bbtk/tpad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index e315848..8f89a11 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -56,7 +56,7 @@ def __init__(self, pad, channels): pad = DeviceManager.getDevice(pad) # if failed, try getting by port if pad is None: - pad = DeviceManager.getDeviceBy("port", _requestedPad, deviceClass="psychopy_bbtk.tpad.TPad") + pad = DeviceManager.getDeviceBy("portString", _requestedPad, deviceClass="psychopy_bbtk.tpad.TPad") # if still failed, make one if pad is None: pad = DeviceManager.addDevice( @@ -164,7 +164,7 @@ def __init__(self, *args, **kwargs): class TPad(sd.SerialDevice, base.BaseDevice): def __init__( - self, port=None, baudrate=9600, + self, port=None, baudrate=115200, byteSize=8, stopBits=1, parity="N", # 'N'one, 'E'ven, 'O'dd, 'M'ask, eol=b"\n", From edd2a517279c90f3169086fad52b8dbf66ea8f1b Mon Sep 17 00:00:00 2001 From: TEParsons Date: Wed, 1 Nov 2023 18:04:48 +0000 Subject: [PATCH 14/15] FF: Reference BasePhotodiode as groups --- psychopy_bbtk/tpad.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 8f89a11..369d3d0 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -85,8 +85,9 @@ def getAvailableDevices(): def setThreshold(self, threshold): self._threshold = threshold self.parent.setMode(0) - self.parent.sendMessage(f"AAO{self.number} {threshold}") - self.parent.pause() + for n in range(self.channels): + self.parent.sendMessage(f"AAO{n} {threshold}") + self.parent.pause() self.parent.setMode(3) def parseMessage(self, message): @@ -120,14 +121,14 @@ def findPhotodiode(self, win): self.parent.setMode(3) self.parent.pause() # continue as normal - return photodiode.BasePhotodiode.findPhotodiode(self, win) + return photodiode.BasePhotodiodeGroup.findPhotodiode(self, win) def findThreshold(self, win): # set mode to 3 self.parent.setMode(3) self.parent.pause() # continue as normal - return photodiode.BasePhotodiode.findThreshold(self, win) + return photodiode.BasePhotodiodeGroup.findThreshold(self, win) class TPadButton(button.BaseButton): From 805bd527e3e2326205b8fcfd645fe0c0c289d77d Mon Sep 17 00:00:00 2001 From: TEParsons Date: Thu, 2 Nov 2023 10:25:14 +0000 Subject: [PATCH 15/15] FF: Fix value getting in TPad Photodiode --- psychopy_bbtk/tpad.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/psychopy_bbtk/tpad.py b/psychopy_bbtk/tpad.py index 369d3d0..17cdbd6 100644 --- a/psychopy_bbtk/tpad.py +++ b/psychopy_bbtk/tpad.py @@ -66,7 +66,7 @@ def __init__(self, pad, channels): ) # reference self in pad - pad.photodiodes = self + pad.nodes.append(self) # initialise base class photodiode.BasePhotodiodeGroup.__init__(self, pad, channels=channels) @@ -82,10 +82,10 @@ def getAvailableDevices(): return devices - def setThreshold(self, threshold): + def setThreshold(self, threshold, channels=(1, 2)): self._threshold = threshold self.parent.setMode(0) - for n in range(self.channels): + for n in channels: self.parent.sendMessage(f"AAO{n} {threshold}") self.parent.pause() self.parent.setMode(3) @@ -116,19 +116,19 @@ def parseMessage(self, message): return resp - def findPhotodiode(self, win): + def findPhotodiode(self, win, channel): # set mode to 3 self.parent.setMode(3) self.parent.pause() # continue as normal - return photodiode.BasePhotodiodeGroup.findPhotodiode(self, win) + return photodiode.BasePhotodiodeGroup.findPhotodiode(self, win, channel) - def findThreshold(self, win): + def findThreshold(self, win, channel): # set mode to 3 self.parent.setMode(3) self.parent.pause() # continue as normal - return photodiode.BasePhotodiodeGroup.findThreshold(self, win) + return photodiode.BasePhotodiodeGroup.findThreshold(self, win, channel) class TPadButton(button.BaseButton): @@ -169,7 +169,7 @@ def __init__( byteSize=8, stopBits=1, parity="N", # 'N'one, 'E'ven, 'O'dd, 'M'ask, eol=b"\n", - maxAttempts=1, pauseDuration=0.1, + maxAttempts=1, pauseDuration=1/240, checkAwake=True ): # get port if not given @@ -247,7 +247,7 @@ def dispatchMessages(self): # get time in s using defaultClock units time = float(time) / 1000 + self._lastTimerReset # store in array - parts = (device, state, button, time) + parts = (device, state, channel, time) # store message self.messages[time] = line # choose object to dispatch to