Python bindings
PulsePins has two Python-facing surfaces:
pulsepins- a pure-Python, host-side SCPI client for scripts and Jupyter notebooks talking to a board runningppscpipp/pp_impl- nanobind extension modules for the underlying C++ interface
Host-side SCPI client
The host-side client lives in python/pulsepins/ and has no dependency beyond the Python standard library. It is the recommended Python entry point for notebooks running on a laptop or workstation while the DE10-Nano runs ppscpi.
From a repository checkout, either set PYTHONPATH:
export PYTHONPATH=/path/to/PulsePins/python
or install the pure-Python host package in editable mode:
python3 -m pip install -e /path/to/PulsePins/python
The editable install also provides small example commands: pulsepins-ppscpi-check, pulsepins-ppscpi-hello, pulsepins-notebook-workflow, pulsepins-timeline-preview, pulsepins-timeline-stream, and pulsepins-timeline-sweep. Use pulsepins-ppscpi-check --self-test when you want the connectivity check to also run the built-in TEST1 hardware smoke path.
Minimal example:
from pulsepins import PulsePins
with PulsePins("de10nano") as pp:
print(pp.idn())
pp.reset()
pp.load_sequence("""
d 10 0xff
d 5 0x00
f
""")
pp.stream()
The f line requests forced triggering; it does not choose the final output value. If no final ... line is present, STREAM appends a no-modify final terminator and leaves the outputs at the last sequence value.
The client exposes idn(), reset(), clear_status(), streamer_clock_hz(), timeline(...), load_sequence(...), load(...), stream(), run(...), test1(), check(...), check_enabled(), system_error(), and errors(). load_sequence(...) flattens multiline sequence text into one SEQ ... command, so it is still subject to the current ppscpi 64 KiB SCPI line limit.
The same package also includes a dependency-free Timeline builder for simple named-channel pulse programs:
from pulsepins import PulsePins
with PulsePins("de10nano") as pp:
timeline = pp.timeline(unit="us")
timeline.channel("laser", bit=0)
timeline.channel("camera", bit=1)
timeline.pulse("laser", start=10, duration=5)
timeline.pulse("camera", start=20, duration=10)
pp.reset()
pp.run(timeline, force_trigger=True)
timeline
In a notebook, evaluating timeline renders an SVG preview. Timeline.to_sequence(...) returns the generated PulsePins text sequence for inspection or manual editing. Timeline.to_csv() / Timeline.from_csv(...) use the same channel,bit,start,duration,color pulse-table CSV format as the browser Timeline Composer. Timeline.to_draft_json() / Timeline.from_draft_json(...) use the browser draft JSON format. Timeline.to_vcd(...) exports a scalar VCD trace for waveform viewers. Same-channel overlapping pulses are rejected; adjacent pulses are allowed.
Runnable examples:
PYTHONPATH=python python3 python/examples/timeline_preview.py --svg timeline.svg --csv timeline.csv --draft timeline.json --vcd timeline.vcd
PYTHONPATH=python python3 python/examples/timeline_stream.py de10nano --print-sequence
PYTHONPATH=python python3 python/examples/timeline_sweep.py de10nano --delays-us 0 5 10
PYTHONPATH=python python3 python/examples/notebook_workflow.py --output-dir previews
After python3 -m pip install -e python, the same workflows can be run as:
pulsepins-ppscpi-check de10nano --self-test
pulsepins-notebook-workflow de10nano --output-dir previews --run
pulsepins-timeline-preview --svg timeline.svg --csv timeline.csv --draft timeline.json --vcd timeline.vcd
pulsepins-timeline-stream de10nano --print-sequence
pulsepins-timeline-sweep de10nano --delays-us 0 5 10
Board-native bindings
PulsePins uses nanobind to provide Python bindings for the underlying C++ interface.
The Python binding tree lives in python/ and builds two modules:
pppp_impl
At the sequence-serialization level, Python now exposes the same practical formats as the core C++ layer:
- PulsePins text sequence format via
parse_sequence_text(...)andwrite_sequence_text(...) - VCD import/export via
Sequence.load_VCD(...)andSequence.write_VCD_file(...) - exact binary sequence import/export via
read_sequence_binary(...)andSequence.write_binary_file(...)
Text and binary sequence helpers preserve explicit final, trigger, replay, retrigger, and pseudo-random records exactly. VCD export is narrower: Sequence.write_VCD_file(...) only accepts deterministic regular sequences and rejects trigger/final/control-flow elements. The underlying C++ VCD export default uses $timescale 10ns, matching the VCD import default of a 10 ns PulsePins output period.
Supported build modes
There are two practical ways to build/test the Python bindings today:
- build on the DE10-Nano board - this is the supported production path
- build on a host machine - useful for syntax/import/API testing only
True cross-compilation of the Python bindings is not currently supported.
Board build
On the DE10-Nano, the normal workflow is:
cd python
pip3 install pytest
make
make test
The default build now uses -O2 and omits -g to reduce memory pressure on the board.
If you need debug symbols while developing the bindings, use:
make PY_DEBUG=1
Host-side testing
Host-side builds are still useful for checking that the binding code compiles and imports cleanly. That is helpful for contributor workflows without a board, but it should not be treated as a replacement for the board build.
The recommended host-side command is:
make -C python USE_PREGENERATED=1 build test-host
USE_PREGENERATED=1 uses the checked-in c++/artifacts/hps_0.h header instead of the top-level generated hps_0.h, which is ignored and normally produced by the Quartus/Qsys hardware build. test-host intentionally skips tests marked hardware, which require /dev/mem, board-backed MMIO, or a live PulsePins runtime.
Sequence I/O examples
import pp
seq, force_trigger = pp.parse_sequence_text("d 3 0x12\nfinal 0x34\n")
text = pp.write_sequence_text(seq)
seq.write_VCD_file("capture.vcd")
seq.write_binary_file("capture.ppbin")
seq2, force_trigger2 = pp.read_sequence_binary("capture.ppbin")
Sequence element API
Python exposes the same sequence-element model as the C++ layer, but the supported construction paths now follow the flattened el design rather than the older raw (el_type, Counter, Value, control) constructor.
Supported pp.el(...) constructors include:
pp.el()- final element with the default final output valuepp.el(value)- final element with an explicit final output valuepp.el(count, value)- regularBITLOADelementpp.el(counter, value)- regularBITLOADelement with explicitCounterpolicypp.el(counter, value_wrapper)- regular element with explicitCounterandValuewrapper semanticspp.el(pattern, mask, final)- trigger elementpp.el(pp.Replay(), repetitions, length)- replay elementpp.el(pp.Retrig(), value=...)- retrigger elementpp.el(pp.PseudoRandom(), count)- pseudo-random element
Useful element inspectors and helpers now exposed in Python:
kind(),mode(),no_strobe()is_stored(),store_slot(),stored_in(...)trigger_pattern(),trigger_mask(),trigger_is_final()regular_token(),sequence_record()with_control(...),with_count(...),with_counter(...),with_regular_value(...),as_bitload_after(...)
The mutating compatibility methods store(...), set_control(...), set_count(...), and set_value(...) are still available, but new code should prefer the immutable helpers above.
Static reconstruction helpers are also bound:
pp.el.classify_control(...)pp.el.from_raw_triplet(...)pp.el.from_regular_token(...)pp.el.is_regular_token(...)
Example:
import pp
import pp_impl
e = pp.el(pp.NoStrobe(3), pp.BitXor(0x12))
assert e.mode() == pp_impl.BITXOR
converted = e.as_bitload_after(0x01)
assert converted.regular_token() == "dn"
assert converted.sequence_record() == "dn 3 0x13"
decoded = pp.el.from_regular_token("xr", 7, 0x55)
assert decoded.sequence_record() == "xr 7 0x55"
Bindings that wrap MMIO-backed hardware objects should still be treated as owning long-lived board resources even though the module now keeps the immediate mm/FPGA constructor arguments alive for the wrapper object.
The small helper scripts in python/pptool.py and tests/test2.py now use the same conservative defaults as the shared C++ workflow: 2s readback timeout protection and a 10s streamer-completion timeout, with timeout=0 still disabling the readback timeout.
Testing expectations
make -C python test runs the Python test files listed in python/Makefile: test.py, test_cli.py, test_scpi_client.py, and test_timeline.py.
Some Python tests exercise board-backed MMIO/FPGA behavior, so they should not be treated as a strictly hardware-free test battery.
See also:
python/READMEpython/README.develdocs/docs/build.md