From 5c684a0360b23e7e22e4d724c39fcab93c0cb007 Mon Sep 17 00:00:00 2001
From: MathiasMahn <53939819+MathiasMahn@users.noreply.github.com>
Date: Thu, 8 Jan 2026 16:41:39 +0100
Subject: [PATCH 1/3] adding event type decoding to FSMThread
---
.gitignore | 3 +
bpod_core/bpod.py | 7 +-
examples/hello_world.ipynb | 222 ++++++++++++++++++++++++++++++++++++
examples/minimal_example.py | 67 +++++++++++
4 files changed, 298 insertions(+), 1 deletion(-)
create mode 100644 examples/hello_world.ipynb
create mode 100644 examples/minimal_example.py
diff --git a/.gitignore b/.gitignore
index 65de09cc..24aa61a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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:
diff --git a/bpod_core/bpod.py b/bpod_core/bpod.py
index 151f062f..85cf40ee 100644
--- a/bpod_core/bpod.py
+++ b/bpod_core/bpod.py
@@ -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.
@@ -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()
@@ -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
@@ -1292,6 +1296,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
diff --git a/examples/hello_world.ipynb b/examples/hello_world.ipynb
new file mode 100644
index 00000000..e8a80b05
--- /dev/null
+++ b/examples/hello_world.ipynb
@@ -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": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "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
+}
diff --git a/examples/minimal_example.py b/examples/minimal_example.py
new file mode 100644
index 00000000..6e1559f1
--- /dev/null
+++ b/examples/minimal_example.py
@@ -0,0 +1,67 @@
+from bpod_core.fsm import StateMachine
+from bpod_core.bpod import Bpod
+import logging
+import sys
+
+
+LOG_FILE = "bpod_debug.log"
+
+logging.basicConfig(
+ level=logging.DEBUG,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ handlers=[
+ logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8'),
+ logging.StreamHandler(sys.stdout) # This also keeps logs appearing in the terminal
+ ]
+)
+
+# 3. Specifically ensure the bpod_core library is set to DEBUG
+logging.getLogger('bpod_core').setLevel(logging.DEBUG)
+
+print(f"Logging initialized. All Bpod traffic will be saved to: {LOG_FILE}")
+
+fsm = StateMachine()
+
+fsm.set_global_timer(
+ index=1,
+ duration=5
+)
+
+
+fsm.add_state(
+ name='StartGlobalTimer',
+ timer=0.25,
+ transitions={'Tup': 'Port1Light'},
+ actions={'GlobalTimerTrig': 1},
+)
+fsm.add_state(
+ name='Port1Light',
+ timer=0.25,
+ transitions={
+ 'Tup': 'Port3Light',
+ 'GlobalTimer1_End': '>exit',
+ },
+ actions={'PWM1': 255},
+)
+fsm.add_state(
+ name='Port3Light',
+ timer=0.25,
+ transitions={
+ 'Tup': '>exit',
+ 'GlobalTimer1_End': '>exit',
+ },
+ actions={'PWM3': 255},
+)
+
+
+
+with Bpod() as bpod:
+ print(f"Connected to Bpod!")
+ print(f"Found Bpod on port {bpod.serial0.port}")
+ print(f"Firmware Version: {bpod.version.firmware}")
+ print(f"Hardware Version: {bpod.version.machine}")
+ print("Send State machine.")
+ bpod.send_state_machine(fsm)
+ print("Run State machine.")
+ bpod.run_state_machine()
+ print("State machine finished.")
\ No newline at end of file
From 39e58145dd9c207287d1dc1a817ad7b072d2f0ec Mon Sep 17 00:00:00 2001
From: MathiasMahn <53939819+MathiasMahn@users.noreply.github.com>
Date: Fri, 9 Jan 2026 13:23:02 +0100
Subject: [PATCH 2/3] additional debugging and working global counter SM and
run added
---
bpod_core/bpod.py | 10 +++++
examples/global_timers.py | 39 +++++++++++++++++++
...=> minimal_example_run_global_timer_SM.py} | 10 ++---
3 files changed, 54 insertions(+), 5 deletions(-)
create mode 100644 examples/global_timers.py
rename examples/{minimal_example.py => minimal_example_run_global_timer_SM.py} (85%)
diff --git a/bpod_core/bpod.py b/bpod_core/bpod.py
index 85cf40ee..a8c866aa 100644
--- a/bpod_core/bpod.py
+++ b/bpod_core/bpod.py
@@ -818,6 +818,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."""
@@ -899,6 +901,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(' None:
"""
Validate the provided state machine for compatibility with the hardware.
@@ -1256,6 +1264,8 @@ def pack_values(values: list[int], format_str: str) -> None:
f'exit', # this is 1 indexed
+ },
+ actions={'PWM1': 255},
+)
+fsm.add_state(
+ name='Port3Light',
+ timer=0.25,
+ transitions={
+ 'Tup': 'Port1Light',
+ 'GlobalTimer1_End': '>exit', # this is 1 indexed
+ },
+ actions={'PWM3': 255},
+)
\ No newline at end of file
diff --git a/examples/minimal_example.py b/examples/minimal_example_run_global_timer_SM.py
similarity index 85%
rename from examples/minimal_example.py
rename to examples/minimal_example_run_global_timer_SM.py
index 6e1559f1..36e2dfbe 100644
--- a/examples/minimal_example.py
+++ b/examples/minimal_example_run_global_timer_SM.py
@@ -23,7 +23,7 @@
fsm = StateMachine()
fsm.set_global_timer(
- index=1,
+ index=0, # this is 0 indexed
duration=5
)
@@ -32,14 +32,14 @@
name='StartGlobalTimer',
timer=0.25,
transitions={'Tup': 'Port1Light'},
- actions={'GlobalTimerTrig': 1},
+ actions={'GlobalTimerTrig': 1}, # this is 1 indexed
)
fsm.add_state(
name='Port1Light',
timer=0.25,
transitions={
'Tup': 'Port3Light',
- 'GlobalTimer1_End': '>exit',
+ 'GlobalTimer1_End': '>exit', # this is 1 indexed
},
actions={'PWM1': 255},
)
@@ -47,8 +47,8 @@
name='Port3Light',
timer=0.25,
transitions={
- 'Tup': '>exit',
- 'GlobalTimer1_End': '>exit',
+ 'Tup': 'Port1Light',
+ 'GlobalTimer1_End': '>exit', # this is 1 indexed
},
actions={'PWM3': 255},
)
From 9a26b385ee8a179dfca0a04cd7b6b044f607c3c6 Mon Sep 17 00:00:00 2001
From: MathiasMahn <53939819+MathiasMahn@users.noreply.github.com>
Date: Fri, 9 Jan 2026 14:04:20 +0100
Subject: [PATCH 3/3] extended debug logging to include event_names. Fixed
global counter index in example
---
bpod_core/bpod.py | 1 +
bpod_core/fsm.py | 2 +-
...ld.ipynb => Graph_global_counter_SM.ipynb} | 0
examples/global_counters.py | 2 +-
examples/global_timers.py | 8 +--
.../minimal_example_run_global_counter_SM.py | 72 +++++++++++++++++++
6 files changed, 79 insertions(+), 6 deletions(-)
rename examples/{hello_world.ipynb => Graph_global_counter_SM.ipynb} (100%)
create mode 100644 examples/minimal_example_run_global_counter_SM.py
diff --git a/bpod_core/bpod.py b/bpod_core/bpod.py
index a8c866aa..288f3081 100644
--- a/bpod_core/bpod.py
+++ b/bpod_core/bpod.py
@@ -792,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."""
diff --git a/bpod_core/fsm.py b/bpod_core/fsm.py
index f2ea482e..cb16d003 100644
--- a/bpod_core/fsm.py
+++ b/bpod_core/fsm.py
@@ -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
----------
diff --git a/examples/hello_world.ipynb b/examples/Graph_global_counter_SM.ipynb
similarity index 100%
rename from examples/hello_world.ipynb
rename to examples/Graph_global_counter_SM.ipynb
diff --git a/examples/global_counters.py b/examples/global_counters.py
index 77534b58..38d0a36b 100644
--- a/examples/global_counters.py
+++ b/examples/global_counters.py
@@ -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,
)
diff --git a/examples/global_timers.py b/examples/global_timers.py
index 0df414dc..69890c87 100644
--- a/examples/global_timers.py
+++ b/examples/global_timers.py
@@ -8,7 +8,7 @@
fsm = StateMachine()
fsm.set_global_timer(
- index=0, # this is 0 indexed
+ index=0, # this is zero based, so this is Global Timer 1
duration=5
)
@@ -17,14 +17,14 @@
name='StartGlobalTimer',
timer=0.25,
transitions={'Tup': 'Port1Light'},
- actions={'GlobalTimerTrig': 1}, # this is 1 indexed
+ actions={'GlobalTimerTrig': 1}, # this is 1 based
)
fsm.add_state(
name='Port1Light',
timer=0.25,
transitions={
'Tup': 'Port3Light',
- 'GlobalTimer1_End': '>exit', # this is 1 indexed
+ 'GlobalTimer1_End': '>exit', # this is 1 based
},
actions={'PWM1': 255},
)
@@ -33,7 +33,7 @@
timer=0.25,
transitions={
'Tup': 'Port1Light',
- 'GlobalTimer1_End': '>exit', # this is 1 indexed
+ 'GlobalTimer1_End': '>exit', # this is 1 based
},
actions={'PWM3': 255},
)
\ No newline at end of file
diff --git a/examples/minimal_example_run_global_counter_SM.py b/examples/minimal_example_run_global_counter_SM.py
new file mode 100644
index 00000000..f275fc53
--- /dev/null
+++ b/examples/minimal_example_run_global_counter_SM.py
@@ -0,0 +1,72 @@
+from bpod_core.fsm import StateMachine
+from bpod_core.bpod import Bpod
+import logging
+import sys
+
+
+LOG_FILE = "bpod_debug.log"
+
+logging.basicConfig(
+ level=logging.DEBUG,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ handlers=[
+ logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8'),
+ logging.StreamHandler(sys.stdout) # This also keeps logs appearing in the terminal
+ ]
+)
+
+# 3. Specifically ensure the bpod_core library is set to DEBUG
+logging.getLogger('bpod_core').setLevel(logging.DEBUG)
+
+print(f"Logging initialized. All Bpod traffic will be saved to: {LOG_FILE}")
+
+fsm = StateMachine()
+
+fsm.set_global_counter(
+ index=0, # this is zero based, so this is Global Counter 1
+ event='BNC1_High',
+ threshold=5,
+)
+
+fsm.add_state(
+ name='InitialDelay',
+ timer=2,
+ transitions={'Tup': 'ResetGlobalCounter'},
+ actions={'PWM2': 255},
+)
+fsm.add_state(
+ name='ResetGlobalCounter',
+ transitions={'Tup': 'Port1Light'},
+ actions={'GlobalCounterReset': 1},
+)
+fsm.add_state(
+ name='Port1Light',
+ timer=0.25,
+ transitions={
+ 'Tup': 'Port3Light',
+ 'GlobalCounter1_End': '>exit',
+ },
+ actions={'PWM1': 255},
+)
+fsm.add_state(
+ name='Port3Light',
+ timer=0.25,
+ transitions={
+ 'Tup': 'Port1Light',
+ 'GlobalCounter1_End': '>exit',
+ },
+ actions={'PWM3': 255},
+)
+
+
+
+with Bpod() as bpod:
+ print(f"Connected to Bpod!")
+ print(f"Found Bpod on port {bpod.serial0.port}")
+ print(f"Firmware Version: {bpod.version.firmware}")
+ print(f"Hardware Version: {bpod.version.machine}")
+ print("Send State machine.")
+ bpod.send_state_machine(fsm)
+ print("Run State machine.")
+ bpod.run_state_machine()
+ print("State machine finished.")
\ No newline at end of file