Creating a new source¶
In the following tutorial, we will show all the basic features that need to be programmed to build a Source from scratch in pybehave. A user might decide to create a new source if their task or interfacing hardware is not handled by the existing Sources. If you want to contribute your Source to the wider community, you can create your new class directly in the pybehave Sources directory and make a pull request. Alternatively, a Sources directory can be made in the Local repository.
Source overview¶
Sources are responsible for sending and receiving information to or from external hardware or interfaces. The pybehave Source class defines a standard framework for implementing these functions.
Choosing a base class¶
There are two primary base classes for sources: Source and ThreadSource. If your source only has to write data then you should override Source. If your source also has to read data then there are two options: use ThreadSource as a base class which runs two separate threads one for handling event information from the task and the other for communicating with the hardware or use Source as a base class but handle reading either when a component is registered or another event is received. Examples of both will be shown in their relevant sections later in this tutorial.
The __init__ method¶
Each source runs in a separate process from the tasks and workstation. When a new source is created, the __init__ method will
be called. However, all attributes declared in this method are created in the main process before being transferred to the
source process by inter process communication. What this means is that all attributes defined in __init__ should only have
placeholder/default values using pickleable variables. Any permanent connection
to hardware or interfaces should be handled instead in the initialize method described later in this tutorial. An example __init__
override for the SerialSource is shown below that declares three new class attributes:
def __init__(self):
super(SerialSource, self).__init__()
self.connections = {}
self.com_tasks = {}
self.closing = {}
initialize¶
In contrast to __init__, initialize is called in the source process. All connections to hardware and external interfaces
should be implemented in this method. If using the ThreadSource method for reading, the initialize method will be called in its own
Thread. As such, polling and event-waiting behavior from the external connection can be handled by this method without interrupting
other event streams. An example of this functionality is shown below for the WhiskerLineSource. This source first opens an
external program and then establishes a connection to it via a socket. Once the connection is established, it waits on messages
over the socket until the source is closed. All of this functionality is implemented in the initialize method without affecting
other source-related processing:
def initialize(self):
win32gui.EnumWindows(look_for_program, 'WhiskerServer')
if not IsWhiskerRunning:
ws = self.whisker_path
os.startfile(ws)
time.sleep(2)
print("WHISKER server started")
win32gui.EnumWindows(look_for_program, 'WhiskerServer')
if IsWhiskerRunning:
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.client.connect((self.address, self.port))
self.client.settimeout(1)
while not self.closing:
try:
new_data = self.client.recv(4096)
self.msg += new_data.decode('UTF-8')
if '\n' in self.msg:
msgs = self.msg.split('\n')
self.msg = msgs[-1]
else:
msgs = []
for msg in msgs[:-1]:
if msg.startswith('Event:'):
div = msg.split(' ')[1].rindex("_")
cid, direction = msg.split(' ')[1][:div], msg.split(' ')[1][div + 1:]
self.update_component(cid, direction == "on")
except socket.timeout:
pass
else:
self.unavailable()
register_component¶
The register_component method is responsible for establishing a connection between a task component and its hardware or interface
representation via the source. The behavior of this method will of course vary heavily between applications but the method
will always receive the component and relevant metadata as inputs. This method can also be used to set up read/polling threads
if necessary. This is done in the SerialSource where a new serial connection is established and a corresponding thread is started
if one does not exist:
def register_component(self, component, metadata):
if component.address not in self.connections:
self.connections[component.address] = serial.Serial(port=component.address, baudrate=component.baudrate, timeout=1)
self.closing[component.address] = False
self.com_tasks[component.address] = threading.Thread(target=self.read, args=[component.address])
self.com_tasks[component.address].start()
def read(self, com):
while not self.closing[com]:
data = self.connections[com].read_until(expected='\n', size=None)
if len(data) > 0:
for comp in self.components.values():
if comp.address == com and (comp.get_type() == Component.Type.DIGITAL_INPUT or
comp.get_type() == Component.Type.INPUT or
comp.get_type() == Component.Type.ANALOG_INPUT or
comp.get_type() == Component.Type.BOTH):
self.update_component(comp.id, data)
del self.com_tasks[com]
del self.closing[com]
self.connections[com].close()
del self.connections[com]
write_component¶
To implement behavior for writing/sending new values to hardware for components, a custom source should override the write_component
method. An example of such an override is shown below for the SerialSource which used the pyserial library to mediate the write
to an external serial connection:
def write_component(self, component_id, msg):
if hasattr(self.components[component_id], "terminator"):
term = self.components[component_id].terminator
else:
term = ""
self.connections[self.components[component_id].address].write(bytes(str(msg) + term, 'utf-8'))
Closing behavior¶
It's often necessary to close or relinquish external connections. This functionality is accomplished by overriding the close_component
or close_source methods which will be called when a task is cleared or the source is removed/application is closed respectively.