Skip to content

Commit

Permalink
Merge pull request #13 from TEParsons/main-nf-voicekey
Browse files Browse the repository at this point in the history
NF: Add voicekey support to TPad
  • Loading branch information
TEParsons authored Nov 28, 2024
2 parents 30fcef7 + 139e3c0 commit d22f198
Showing 1 changed file with 133 additions and 3 deletions.
136 changes: 133 additions & 3 deletions psychopy_bbtk/tpad.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
import re
import sys
import time
# voicekey is only available from 2015.1.0 onwards, so import with a safe fallback
try:
from psychopy.hardware.voicekey import BaseVoiceKeyGroup, VoiceKeyResponse
except ImportError:
from psychopy.hardware.base import BaseResponseDevice as BaseVoiceKeyGroup, BaseResponse as VoiceKeyResponse


# check whether FTDI driver is installed
Expand Down Expand Up @@ -305,9 +310,134 @@ def resetTimer(self, clock=logging.defaultClock):
self.parent.resetTimer(clock=clock)


class TPadVoiceKey:
def __init__(self, *args, **kwargs):
pass
class TPadVoiceKey(BaseVoiceKeyGroup):
def __init__(self, pad, channels=1, threshold=None):
_requestedPad = pad
# get associated tpad
self.parent = TPad.resolve(pad)
# reference self in pad
self.parent.nodes.append(self)
# initialise base class
BaseVoiceKeyGroup.__init__(
self, channels=channels, threshold=threshold
)
# set to data collection mode
self.parent.setMode(3)

def resetTimer(self, clock=logging.defaultClock):
self.parent.resetTimer(clock=clock)

def _setThreshold(self, threshold, channel=None):
"""
Device-specific threshold setting method. This will be called by `setThreshold` and should
be overloaded by child classes of BaseVoiceKey.
Parameters
----------
threshold : int
Threshold at which to register a VoiceKey response, with 0 being the lowest possible
volume and 255 being the highest.
channel : int
Channel to set the threshold for (if applicable to device)
Returns
------
bool
True if current decibel level is above the threshold.
"""
if threshold is None:
return
# enter command mode
self.parent.setMode(0)
# send command to set threshold
self.parent.sendMessage(f"AAVK{channel+1} {int(threshold)}")
# force a sleep for diode to settle
time.sleep(0.1)
# get 0 or 1 according to light level
resp = self.parent.awaitResponse(timeout=0.1)
# with this threshold, is the photodiode returning True?
measurement = None
if resp is not None:
if resp.strip() == "1":
measurement = True
if resp.strip() == "0":
measurement = False
# store threshold
self.threshold[channel] = threshold
# return to sampling mode
self.parent.setMode(3)

return measurement

def dispatchMessages(self):
self.parent.dispatchMessages()

def hasUnfinishedMessage(self):
"""
Is the parent TPad waiting for an end-of-line character?
Returns
-------
bool
True if there is a partial message waiting for an end-of-line
"""
return self.parent.hasUnfinishedMessage()

def parseMessage(self, message):
# if given a string, split according to regex
if isinstance(message, str):
message = splitTPadMessage(message)
device, state, channel, time = message
# convert state to bool
if state == "P":
state = True
elif state == "R":
state = False
# create PhotodiodeResponse object
resp = VoiceKeyResponse(
t=time, channel=channel-1, value=state, threshold=self.getThreshold(channel-1)
)

return resp

def isSameDevice(self, other):
"""
Determine whether this object represents the same physical device as a given other object.
Parameters
----------
other : TPadPhotodiodeGroup, dict
Other TPadPhotodiodeGroup to compare against, or a dict of params (which much include
`port` or `pad` as a key)
Returns
-------
bool
True if the two objects represent the same physical device
"""
if isinstance(other, type(self)):
# if given another TPadButtonGroup, compare parent boxes
other = other.parent
elif isinstance(other, dict) and "pad" in other:
# create copy of dict so we don't affect the original
other = other.copy()
# if given a dict, make sure we have a `port` rather than a `pad`
other['port'] = other['pad']
# use parent's comparison method
return self.parent.isSameDevice(other)

@staticmethod
def getAvailableDevices():
devices = []
# iterate through profiles of all serial port devices
for profile in TPad.getAvailableDevices():
devices.append({
'deviceName': profile['deviceName'] + "_voicekey",
'pad': profile['port'],
'channels': 1,
})

return devices


class TPad(sd.SerialDevice):
Expand Down

0 comments on commit d22f198

Please sign in to comment.