"""A module for the FastGait ActorUnit driver implementations.
In case of FOG, the ActorUnit is alerted via BlueTooth and starts
vibrating in pulses, giving the patient a tactile cueing impulse.
"""
__author__ = "Oliver Maye"
__version__ = "0.1"
__all__ = ["Event", \
"Default", "Intensity", "Motor", "TimerControl", \
"Configuration", \
"ActorUnit",]
from dataclasses import dataclass
from enum import auto, unique, Enum, IntEnum, IntFlag
from philander.actuator import Actuator, Direction
from philander.ble import BLE
from philander.configurable import ConfigItem, Configuration as BaseConfiguration, Configurable
from philander.primitives import Percentage
from philander.systypes import ErrorCode
[docs]
@unique
class Event( Enum ):
"""Data class to represent events emitted by the ActorUnit.
"""
cueStandard = auto()
cueStop = auto()
[docs]
@dataclass
class Default:
"""Container for default values that are not part of any other data structure.
"""
FIRST_DELAY = 0 # immediately
"""Delay of the first pulse, given in milliseconds 0...65535 (0xFFFF). Zero (0) to start immediately."""
PULSE_PERIOD = 200 # ms
"""Pulse period in milliseconds 0...65535 (0xFFFF)."""
PULSE_ON_DURATION = 120 # ms; 60% duty cycle
"""Pulse ON duration in milliseconds 0...65535 (0xFFFF). Must be less than the period."""
PULSE_COUNT = 3
"""Total number of pulses 0...255. Zero (0) means infinitely."""
[docs]
class Intensity( Percentage ):
"""Structure to reflect the intensity that the vibration motors run on.
"""
MIN = 0
"""Minimal intensity."""
OFF = MIN
"""Least possible intensity, actually no vibration."""
WEAK = 20
"""Weak intensity."""
MEDIUM = 50
"""Medium vibration intensity."""
STRONG = 80
"""Strong intensity."""
MAX = 100
"""Maximum possible intensity."""
DEFAULT = STRONG
"""The default intensity."""
[docs]
class Motor(IntFlag):
"""Motor selection used for vibration: Motor #1, or #2 or both."""
NONE = 0
"""Mnemonics for no actuator"""
ONE = 1
"""First actuator"""
TWO = 2
"""Second actuator"""
ALL = ONE | TWO
"""Mnemonics for all motors"""
DEFAULT = ALL
"""Default motor selection."""
[docs]
class TimerControl(IntEnum):
"""Structure to reflect the timer control setting as part of a vibration command
"""
KEEP = 0x00
"""Keep the current timer setting."""
RESET = 0x01
"""Reset the timer."""
DEFAULT = RESET
"""Default timer control value."""
[docs]
@dataclass
class Configuration( BaseConfiguration ):
"""Data class to represent a (default) configuration of the ActorUnit.
Pulses are emitted periodically in rectangle form and the low-level
API allows to configure:
- the length of one period,
- the length of the on-part,
- an initial delay and
- the number of periods to run.
::
|< PULSE ON >|
_____________ _____________ ______ ON
...........| |______| |______| ... OFF
|< DELAY >|< PERIOD >|
"""
onDuration : int = Default.PULSE_ON_DURATION
"""Length of the duty cycle, given in milliseconds."""
period : int = Default.PULSE_PERIOD
"""Total length of each interval in milliseconds. Must be larger than the onDuration."""
delay : int = Default.FIRST_DELAY
"""Wait time before the first interval, specified in milliseconds."""
numPulses : int = Default.PULSE_COUNT
"""Number of repetitions. Zero means infinitely."""
intensity : int = Intensity.DEFAULT
"""Intensity of the vibration [0...100]."""
motors : int = Motor.DEFAULT
"""Motor(s) to use for vibration. 0=none, 1=left, 2=right, 3=both."""
resetTimer : int = TimerControl.DEFAULT
"""Whether or not to reset the pulse timer. 0=keep, 1=reset."""
[docs]
class ActorUnit( BLE, Actuator, Configurable ):
"""Implementation of the vibration belt driver, also called ActorUnit.
"""
#
# Public attributes
#
CMD_START = 0x01
"""Command to start a vibration as specified by further parameters."""
CMD_STOP = 0x02
"""Command to immediately stop vibration."""
CMD_SET_DEFAULT = 0x03
"""Configure default vibration parameters."""
CMD_GET_DEFAULT = 0x04
"""Retrieve current default configuration:"""
CMD_START_DEFAULT = 0x05
"""Start vibration as specified by the default parameters."""
CMDBUF_STOP = bytearray( [CMD_STOP] )
"""Command buffer to stop vibration."""
CMDBUF_GET_DEFAULT = bytearray( [CMD_GET_DEFAULT] )
"""Command buffer to retrieve default vibration parameters."""
CMDBUF_START_DEFAULT = bytearray( [CMD_START_DEFAULT] )
"""Complete command buffer to start vibration using the default parameter set."""
# BLE UUIDs
#DEVICE_UUID = '0000fa01-0000-1000-8000-00805f9b34fb'
CLIENT_NAME = "FastGait AU"
CHARACTERISTIC_UUID = '0000fa61-0000-1000-8000-00805f9b34fb'
#
# Private attributes
#
#
# Module API
#
def __init__( self ):
# Initialize base class attributes
super().__init__()
# Create instance attributes
self.vibConfig = Configuration( ConfigItem.implicit )
self.dataBuf = bytearray( 11 )
[docs]
@classmethod
def Params_init( cls, paramDict ):
"""Initialize parameters with their defaults.
The following settings are supported:
=============================== ==========================================================================================================
Key name Value type, meaning and default
=============================== ==========================================================================================================
ActorUnit.delay ``int`` [0...65535] Initial delay in ms; :attr:`DELAY_DEFAULT`
ActorUnit.pulsePeriod ``int`` [0...65535] Length of one period in ms; :attr:`PULSE_PERIOD_DEFAULT`
ActorUnit.pulseOn ``int`` [0...pulsePeriod] Length of the active part in that period in ms; :attr:`PULSE_ON_DEFAULT`
ActorUnit.pulseCount ``int`` [0...255] Number of pulses. Zero (0) means infinite pulses. :attr:`PULSE_COUNT_DEFAULT`
ActorUnit.pulseIntensity ``int`` [0...100] Intensity of the pulses given as a percentage %. :attr:`PULSE_INTENSITY_DEFAULT`
ActorUnit.motors Motors to be used for the pulses [0...3] meaning none, left, right, both motors; :attr:`MOTORS_DEFAULT`
All other BLE.* settings as documented at :meth:`.BLE.Params_init`.
=============================================================================================================================================
Also see: :meth:`.Module.Params_init`.
:param dict(str, object) paramDict: The configuration dictionary.
:returns: none
:rtype: None
"""
# ActorUnit pulse configuration
if not "ActorUnit.delay" in paramDict:
paramDict["ActorUnit.delay"] = Default.FIRST_DELAY
if not "ActorUnit.pulsePeriod" in paramDict:
paramDict["ActorUnit.pulsePeriod"] = Default.PULSE_PERIOD
if not "ActorUnit.pulseOn" in paramDict:
paramDict["ActorUnit.pulseOn"] = Default.PULSE_ON_DURATION
if not "ActorUnit.pulseCount" in paramDict:
paramDict["ActorUnit.pulseCount"] = Default.PULSE_COUNT
if not "ActorUnit.pulseIntensity" in paramDict:
paramDict["ActorUnit.pulseIntensity"] = Intensity.DEFAULT
if not "ActorUnit.motors" in paramDict:
paramDict["ActorUnit.motors"] = Motor.DEFAULT
# BLE UUIDs
if not "BLE.client.name" in paramDict:
paramDict["BLE.client.name"] = ActorUnit.CLIENT_NAME
if not "BLE.characteristic.uuid" in paramDict:
paramDict["BLE.characteristic.uuid"] = ActorUnit.CHARACTERISTIC_UUID
# General BLE configuration
super(ActorUnit, cls).Params_init( paramDict )
return None
@classmethod
def _extractParameterInt( cls, val, default, lowLimit=0, highLimit=0xFFFF ):
err = ErrorCode.errOk
if not isinstance(val, int):
try:
intValue = int( val, 0 )
except ValueError:
intValue = default
err = ErrorCode.errInvalidParameter
else:
intValue = val
if (intValue < lowLimit) or (intValue > highLimit):
intValue = default
err = ErrorCode.errInvalidParameter
return intValue, err
[docs]
def open( self, paramDict ):
"""Initialize an instance and prepare it for use.
Also see: :meth:`.Module.open`.
:param dict(str, object) paramDict: Configuration parameters as\
possibly obtained from :meth:`Params_init`.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
result = ErrorCode.errOk
defParam = {}
ActorUnit.Params_init( defParam )
sKey = "ActorUnit.delay"
val = paramDict.get( sKey, defParam[sKey] )
val, result = ActorUnit._extractParameterInt( val, defParam[sKey], 0, 0xFFFF )
paramDict[sKey] = val
self.vibConfig.delay = val
sKey = "ActorUnit.pulsePeriod"
val = paramDict.get( sKey, defParam[sKey] )
val, result = ActorUnit._extractParameterInt( val, defParam[sKey], 0, 0xFFFF )
paramDict[sKey] = val
self.vibConfig.period = val
sKey = "ActorUnit.pulseOn"
val = paramDict.get( sKey, defParam[sKey] )
val, result = ActorUnit._extractParameterInt( val, self.vibConfig.period/2, 0, self.vibConfig.period )
paramDict[sKey] = val
self.vibConfig.onDuration = val
sKey = "ActorUnit.pulseCount"
val = paramDict.get( sKey, defParam[sKey] )
val, result = ActorUnit._extractParameterInt( val, defParam[sKey], 0, 0xFF )
paramDict[sKey] = val
self.vibConfig.numPulses = val
sKey = "ActorUnit.pulseIntensity"
val = paramDict.get( sKey, defParam[sKey] )
val, result = ActorUnit._extractParameterInt( val, defParam[sKey], Intensity.MIN, Intensity.MAX )
paramDict[sKey] = val
self.vibConfig.intensity = val
sKey = "ActorUnit.motors"
val = paramDict.get( sKey, defParam[sKey] )
if isinstance(val, Motor):
pass
elif isinstance(val, int):
val = Motor(val)
else:
try:
val = int( val, 0 )
except ValueError as e:
val = defParam[sKey]
err = ErrorCode.errInvalidParameter
if (val < Motor.NONE) or (val > Motor.ALL):
val = defParam[sKey]
result = ErrorCode.errInvalidParameter
paramDict[sKey] = val
self.vibConfig.motors = val
if (result == ErrorCode.errOk):
sKey = "BLE.client.name"
paramDict[sKey] = paramDict.get( sKey, defParam[sKey] )
sKey = "BLE.characteristic.uuid"
paramDict[sKey] = paramDict.get( sKey, defParam[sKey] )
result = super().open( paramDict )
#self.couple()
return result
#
# Configurable API
#
#
# Actuator API
#
[docs]
def action(self, pattern=None):
"""Executes a predefined action or movement pattern with this actuator.
:param int pattern: The action pattern to execute.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
del pattern
result, _ = self.writeCharacteristic( ActorUnit.CMDBUF_START_DEFAULT )
return result
[docs]
def startOperation(self, direction=Direction.positive,
strengthIntensity = None,
onSpeedDuty = None,
ctrlInterval = None,
durationLengthCycles = None ):
"""Issue a start command to the actuator unit.
Make the actor unit start cueing.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
del direction
if (strengthIntensity is None):
strengthIntensity = self.vibConfig.intensity
if (onSpeedDuty is None):
onSpeedDuty = self.vibConfig.onDuration
if (ctrlInterval is None):
ctrlInterval = self.vibConfig.period
if (durationLengthCycles is None):
durationLengthCycles = self.vibConfig.numPulses
#Create parametric start-vibration-command buffer
self.dataBuf[0] = ActorUnit.CMD_START
self.dataBuf[1] = onSpeedDuty & 0xFF
self.dataBuf[2] = onSpeedDuty >> 8
self.dataBuf[3] = ctrlInterval & 0xFF
self.dataBuf[4] = ctrlInterval >> 8
self.dataBuf[5] = self.vibConfig.delay & 0xFF
self.dataBuf[6] = self.vibConfig.delay >> 8
self.dataBuf[7] = durationLengthCycles
self.dataBuf[8] = strengthIntensity
self.dataBuf[9] = self.vibConfig.motors
self.dataBuf[10] = self.vibConfig.resetTimer
result, _ = self.writeCharacteristic( self.dataBuf )
return result
[docs]
def stopOperation(self):
"""Issue a stop command to the actuator unit.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
result, _ = self.writeCharacteristic( ActorUnit.CMDBUF_STOP )
return result
#
# Individual specific API
#
[docs]
def getDefault(self):
"""Retrieve default configuration from the remote client unit.
:return: The configuration and an error code indicating either success or the reason of failure.
:rtype: Configuration, ErrorCode
"""
cfg = Configuration()
err, dataBuf = self.writeCharacteristic( ActorUnit.CMDBUF_GET_DEFAULT,
readResponse = True )
if (err == ErrorCode.errOk):
if ( (dataBuf is None) or (len(dataBuf) < 11) ):
err = ErrorCode.errFewData
else:
# neglect CMD byte at index 0
cfg.onDuration = (dataBuf[2] << 8) + dataBuf[1]
cfg.period = (dataBuf[4] << 8) + dataBuf[3]
cfg.delay = (dataBuf[6] << 8) + dataBuf[5]
cfg.numPulses = dataBuf[7]
cfg.intensity = dataBuf[8]
cfg.motors = Motor( dataBuf[9] )
cfg.resetTimer = TimerControl( dataBuf[10] )
return cfg, err
[docs]
def setDefault(self, newDefault: Configuration):
"""Store default configuration onto the remote client unit.
:param newDefault: The configuration to store as the new default.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
self.dataBuf[0] = ActorUnit.CMD_SET_DEFAULT
self.dataBuf[1] = newDefault.onDuration & 0xFF
self.dataBuf[2] = newDefault.onDuration >> 8
self.dataBuf[3] = newDefault.period & 0xFF
self.dataBuf[4] = newDefault.period >> 8
self.dataBuf[5] = newDefault.delay & 0xFF
self.dataBuf[6] = newDefault.delay >> 8
self.dataBuf[7] = newDefault.numPulses
self.dataBuf[8] = newDefault.intensity
self.dataBuf[9] = newDefault.motors
self.dataBuf[10] = newDefault.resetTimer
result, _ = self.writeCharacteristic( self.dataBuf )
return result