Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ target/
profile_default/
ipython_config.py

# LLM files
.repotomdrc

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
Expand Down
18 changes: 17 additions & 1 deletion bpod_core/bpod.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def __init__( # noqa: PLR0913
softcode_handler: Callable,
state_transitions: NDArray[np.uint8],
use_back_op: bool,
event_names: list[str],
) -> None:
"""
Initialize the FSMThread.
Expand Down Expand Up @@ -168,6 +169,7 @@ def __init__( # noqa: PLR0913
self._softcode_handler = softcode_handler
self._state_transitions = state_transitions
self._use_back_op = use_back_op
self._event_names = event_names

def stop(self):
self._stop_event.set()
Expand Down Expand Up @@ -228,7 +230,9 @@ def run(self) -> None:
events = event_data_view[:param]
for event in events:
if debug:
logger.debug('%d µs: Event %d', micros, event)
if not(event == 255): # exit event
event_name = self._event_names[event] if event < len(self._event_names) else "Unknown"
logger.debug('%d µs: Event: %s (%d)', micros, event_name, event)
# TODO: handle event

# handle state transitions / exit event
Expand Down Expand Up @@ -788,6 +792,7 @@ def _compile_event_names(self) -> None:
]:
self.event_names.extend(event_name.format(i + 1) for i in range(n))
self.event_names.append('Tup')
logger.debug('Compiled event names: %s', self.event_names)

def _compile_output_actions(self) -> None:
"""Compile the list of output actions supported by the Bpod hardware."""
Expand All @@ -814,6 +819,8 @@ def _compile_output_actions(self) -> None:
if self.version.machine == 4:
self.actions.extend(['AnalogThreshEnable', 'AnalogThreshDisable'])



@property
def port(self) -> str | None:
"""The port of the Bpod's primary serial device."""
Expand Down Expand Up @@ -895,6 +902,12 @@ def update_modules(self) -> None:
self._compile_event_names()
self._compile_output_actions()

def load_serial_message(self, module_index, message_id, message_bytes):
# Format: ord('L'), module_index(0-based), n_messages(1), msg_id, msg_len, msg_payload
header = struct.pack('<BBBB', module_index, 1, message_id, len(message_bytes))
self.serial0.write(b'L' + header + message_bytes)
return self.serial0.read(1) == b'\x01'

def validate_state_machine(self, state_machine: StateMachine) -> None:
"""
Validate the provided state machine for compatibility with the hardware.
Expand Down Expand Up @@ -1252,6 +1265,8 @@ def pack_values(values: list[int], format_str: str) -> None:
f'<c2?H{n_bytes}s', b'C', run_asap, self._use_back_op, n_bytes, byte_array
)
self._waiting_for_confirmation = True
SM_definition = struct.pack(f'<c2?H{n_bytes}s', b'C', run_asap, self._use_back_op, n_bytes, byte_array)
logger.debug(SM_definition)

if run_asap:
self._run_state_machine(blocking=False)
Expand Down Expand Up @@ -1292,6 +1307,7 @@ def _run_state_machine(self, *, blocking: bool) -> None:
self._softcode_handler,
self._state_transitions,
self._use_back_op,
self.event_names,
)
self._fsm_thread.start()
self._waiting_for_confirmation = False
Expand Down
2 changes: 1 addition & 1 deletion bpod_core/fsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ def set_global_counter(
threshold: GlobalCounterThreshold,
) -> None:
"""
Configure a global timer with the specified parameters.
Configure a global counter with the specified parameters.

Parameters
----------
Expand Down
222 changes: 222 additions & 0 deletions examples/Graph_global_counter_SM.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "e8a973cd",
"metadata": {},
"outputs": [],
"source": [
"from bpod_core.fsm import StateMachine"
]
},
{
"cell_type": "markdown",
"id": "09331e75",
"metadata": {},
"source": [
"### Define a state machine"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "1dfc3950",
"metadata": {},
"outputs": [],
"source": [
"fsm = StateMachine()\n",
"\n",
"fsm.set_global_timer(\n",
" index=1,\n",
" duration=5\n",
")\n",
"\n",
"\n",
"fsm.add_state(\n",
" name='StartGlobalTimer',\n",
" timer=0.25,\n",
" transitions={'Tup': 'Port1Light'},\n",
" actions={'GlobalTimerTrig': 1},\n",
")\n",
"fsm.add_state(\n",
" name='Port1Light',\n",
" timer=0.25,\n",
" transitions={\n",
" 'Tup': 'Port3Light',\n",
" 'GlobalTimer1_End': '>exit',\n",
" },\n",
" actions={'PWM1': 255},\n",
")\n",
"fsm.add_state(\n",
" name='Port3Light',\n",
" timer=0.25,\n",
" transitions={\n",
" 'Tup': 'Port1Light',\n",
" 'GlobalTimer1_End': '>exit',\n",
" },\n",
" actions={'PWM3': 255},\n",
")"
]
},
{
"cell_type": "markdown",
"id": "a32cfda8",
"metadata": {},
"source": [
"### Display the statemechine for easy verification"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "2c77e81e",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
"<!-- Generated by graphviz version 14.0.1 (20251006.0113)\n",
" -->\n",
"<!-- Title: State Machine Pages: 1 -->\n",
"<svg width=\"637pt\" height=\"76pt\"\n",
" viewBox=\"0.00 0.00 637.00 76.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 71.99)\">\n",
"<title>State Machine</title>\n",
"<g id=\"node1\" class=\"node\">\n",
"<title></title>\n",
"<ellipse fill=\"black\" stroke=\"black\" cx=\"9\" cy=\"-46.5\" rx=\"9\" ry=\"9\"/>\n",
"</g>\n",
"<!-- StartGlobalTimer -->\n",
"<g id=\"node2\" class=\"node\">\n",
"<title>StartGlobalTimer</title>\n",
"<polygon fill=\"WHITE\" stroke=\"none\" points=\"55,-28 55,-65 189.5,-65 189.5,-28 55,-28\"/>\n",
"<polygon fill=\"LIGHTBLUE\" stroke=\"none\" points=\"56,-46.5 56,-64 155.25,-64 155.25,-46.5 56,-46.5\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"58\" y=\"-52.55\" font-family=\"Helvetica,Arial,sans-serif\" font-weight=\"bold\" font-size=\"11.00\">StartGlobalTimer &#160;</text>\n",
"<polygon fill=\"LIGHTBLUE\" stroke=\"none\" points=\"155.25,-46.5 155.25,-64 188.5,-64 188.5,-46.5 155.25,-46.5\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"157.25\" y=\"-51.55\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"11.00\">0.25 s</text>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"58\" y=\"-34.05\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"11.00\">GlobalTimerTrig</text>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"180.5\" y=\"-34.05\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"11.00\">1</text>\n",
"<polygon fill=\"none\" stroke=\"black\" points=\"55,-28 55,-65 189.5,-65 189.5,-28 55,-28\"/>\n",
"</g>\n",
"<!-- &#45;&gt;StartGlobalTimer -->\n",
"<g id=\"edge1\" class=\"edge\">\n",
"<title>&#45;&gt;StartGlobalTimer</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M18.36,-46.5C24.48,-46.5 33.47,-46.5 43.63,-46.5\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"43.51,-50 53.51,-46.5 43.51,-43 43.51,-50\"/>\n",
"</g>\n",
"<!-- Port1Light -->\n",
"<g id=\"node4\" class=\"node\">\n",
"<title>Port1Light</title>\n",
"<polygon fill=\"WHITE\" stroke=\"none\" points=\"242,-28 242,-65 342.75,-65 342.75,-28 242,-28\"/>\n",
"<polygon fill=\"LIGHTBLUE\" stroke=\"none\" points=\"243,-46.5 243,-64 308.5,-64 308.5,-46.5 243,-46.5\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"245\" y=\"-52.55\" font-family=\"Helvetica,Arial,sans-serif\" font-weight=\"bold\" font-size=\"11.00\">Port1Light &#160;</text>\n",
"<polygon fill=\"LIGHTBLUE\" stroke=\"none\" points=\"308.5,-46.5 308.5,-64 341.75,-64 341.75,-46.5 308.5,-46.5\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"310.5\" y=\"-51.55\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"11.00\">0.25 s</text>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"245\" y=\"-34.05\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"11.00\">PWM1</text>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"321.75\" y=\"-34.05\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"11.00\">255</text>\n",
"<polygon fill=\"none\" stroke=\"black\" points=\"242,-28 242,-65 342.75,-65 342.75,-28 242,-28\"/>\n",
"</g>\n",
"<!-- StartGlobalTimer&#45;&gt;Port1Light -->\n",
"<g id=\"edge2\" class=\"edge\">\n",
"<title>StartGlobalTimer&#45;&gt;Port1Light</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M189.21,-46.5C203.01,-46.5 217.5,-46.5 231.08,-46.5\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"230.72,-50 240.72,-46.5 230.72,-43 230.72,-50\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"215.75\" y=\"-49\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"10.00\">Tup</text>\n",
"</g>\n",
"<!-- exit -->\n",
"<g id=\"node3\" class=\"node\">\n",
"<title>exit</title>\n",
"<ellipse fill=\"black\" stroke=\"black\" cx=\"620.75\" cy=\"-45.5\" rx=\"4.5\" ry=\"4.5\"/>\n",
"<ellipse fill=\"none\" stroke=\"black\" cx=\"620.75\" cy=\"-45.5\" rx=\"8.5\" ry=\"8.5\"/>\n",
"</g>\n",
"<!-- Port1Light&#45;&gt;exit -->\n",
"<g id=\"edge4\" class=\"edge\">\n",
"<title>Port1Light&#45;&gt;exit</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M342.42,-49.36C387.12,-51.82 454.97,-55.16 514,-56.5 549.66,-57.31 559.32,-63.71 594.25,-56.5 597.04,-55.92 599.9,-55.03 602.65,-53.99\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"603.8,-57.31 611.48,-50.01 600.92,-50.93 603.8,-57.31\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"445.62\" y=\"-58.49\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"10.00\">GlobalTimer1_End</text>\n",
"</g>\n",
"<!-- Port3Light -->\n",
"<g id=\"node5\" class=\"node\">\n",
"<title>Port3Light</title>\n",
"<polygon fill=\"WHITE\" stroke=\"none\" points=\"395.25,0 395.25,-37 496,-37 496,0 395.25,0\"/>\n",
"<polygon fill=\"LIGHTBLUE\" stroke=\"none\" points=\"396.25,-18.5 396.25,-36 461.75,-36 461.75,-18.5 396.25,-18.5\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"398.25\" y=\"-24.55\" font-family=\"Helvetica,Arial,sans-serif\" font-weight=\"bold\" font-size=\"11.00\">Port3Light &#160;</text>\n",
"<polygon fill=\"LIGHTBLUE\" stroke=\"none\" points=\"461.75,-18.5 461.75,-36 495,-36 495,-18.5 461.75,-18.5\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"463.75\" y=\"-23.55\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"11.00\">0.25 s</text>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"398.25\" y=\"-6.05\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"11.00\">PWM3</text>\n",
"<text xml:space=\"preserve\" text-anchor=\"start\" x=\"475\" y=\"-6.05\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"11.00\">255</text>\n",
"<polygon fill=\"none\" stroke=\"black\" points=\"395.25,0 395.25,-37 496,-37 496,0 395.25,0\"/>\n",
"</g>\n",
"<!-- Port1Light&#45;&gt;Port3Light -->\n",
"<g id=\"edge3\" class=\"edge\">\n",
"<title>Port1Light&#45;&gt;Port3Light</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M342.48,-37.41C355.89,-34.93 370.57,-32.21 384.52,-29.63\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"384.82,-33.13 394.02,-27.87 383.55,-26.25 384.82,-33.13\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"369\" y=\"-35.84\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"10.00\">Tup</text>\n",
"</g>\n",
"<!-- Port3Light&#45;&gt;exit -->\n",
"<g id=\"edge6\" class=\"edge\">\n",
"<title>Port3Light&#45;&gt;exit</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M495.77,-26.16C530.85,-31.63 576.07,-38.69 601.06,-42.58\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"600.21,-45.99 610.63,-44.08 601.29,-39.08 600.21,-45.99\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"554.12\" y=\"-43.47\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"10.00\">GlobalTimer1_End</text>\n",
"</g>\n",
"<!-- Port3Light&#45;&gt;Port1Light -->\n",
"<g id=\"edge5\" class=\"edge\">\n",
"<title>Port3Light&#45;&gt;Port1Light</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M395.47,-12.64C383.98,-12.4 371.85,-13.07 360.75,-15.5 352.62,-17.28 344.3,-20.14 336.41,-23.41\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"335.05,-20.19 327.35,-27.46 337.91,-26.58 335.05,-20.19\"/>\n",
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"369\" y=\"-18\" font-family=\"Helvetica,Arial,sans-serif\" font-size=\"10.00\">Tup</text>\n",
"</g>\n",
"</g>\n",
"</svg>\n"
],
"text/plain": [
"<graphviz.graphs.Digraph at 0x1fe5a1eb0e0>"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fsm.to_digraph() "
]
},
{
"cell_type": "markdown",
"id": "ecf44170",
"metadata": {},
"source": [
"### Running a state machine / bpod communication does not seem to work from a jupyter notebook..."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "bpod-core",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
2 changes: 1 addition & 1 deletion examples/global_counters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
fsm = StateMachine()

fsm.set_global_counter(
index=1,
index=0, # this is zero based, so this is Global Counter 1
event='Port1High',
threshold=5,
)
Expand Down
39 changes: 39 additions & 0 deletions examples/global_timers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Global Timers.

A global timer ends an infinite loop when 5 seconds have passed.
"""

from bpod_core.fsm import StateMachine

fsm = StateMachine()

fsm.set_global_timer(
index=0, # this is zero based, so this is Global Timer 1
duration=5
)


fsm.add_state(
name='StartGlobalTimer',
timer=0.25,
transitions={'Tup': 'Port1Light'},
actions={'GlobalTimerTrig': 1}, # this is 1 based
)
fsm.add_state(
name='Port1Light',
timer=0.25,
transitions={
'Tup': 'Port3Light',
'GlobalTimer1_End': '>exit', # this is 1 based
},
actions={'PWM1': 255},
)
fsm.add_state(
name='Port3Light',
timer=0.25,
transitions={
'Tup': 'Port1Light',
'GlobalTimer1_End': '>exit', # this is 1 based
},
actions={'PWM3': 255},
)
Loading
Loading