ppscpi
ppscpi is a standalone network server that runs on a PulsePins target board and accepts remote-control commands using
the Standard Commands for Programmable Instruments
(SCPI) protocol. It provides a minimal SCPI
interface for simple sequence streaming.
The IVI Foundation maintains the SCPI standard and hosts the SCPI-99 specification; for a quick non-normative overview, see the SCPI overview.
ppscpi is also convenient for controlling PulsePins from remote clients, including Python or Jupyter notebooks and any environment that can use TCP sockets.
The implementation is in c++/ppscpi.cc and the SCPI session/server helpers are in c++/scpi_server.hh.
Transport and startup
ppscpi listens on standard SCPI TCP port 5025. By default it binds to
0.0.0.0, which accepts connections on all interfaces.
Command-line options:
-ip <addr>: bind address, default0.0.0.0
When bound to 0.0.0.0, ppscpi prints a red WARNING line, states waiting one second, and waits one second before accepting client traffic. Use -ip 127.0.0.1 for local-only access or another specific interface address when remote access should be limited.
ppscpi does not provide authentication or transport encryption. Treat the default bind as network-exposed hardware control and use it only on a trusted network, with a restricted bind address, or behind appropriate network controls.
On startup it:
- configures realtime scheduling and locks memory pages
- creates the shared
HostRuntimeand top-levelFPGAwrapper - applies the requested clock and PLL settings; the FPGA reset-manager pulse runs only if
-reset_FPGAorPP_RESET_FPGAis requested - reports the measured clocks using the frequency-meter block
- accepts SCPI-style commands over the network
The transport is line-oriented. A single line may contain multiple commands separated by
semicolons (;); ppscpi trims and dispatches each segment in order, with responses emitted
in the same order. Empty segments are ignored. Each segment is parsed as a complete command
path, so relative SCPI path continuation is not implemented.
The SEQ payload uses the canonical PulsePins text sequence format. SCPI supplies transport and session execution around that format; it does not define alternate record spellings, and clock, initial-output, trigger-routing, gate, and output settings remain external execution context.
Session model
Each client connection gets its own SCPI session object.
Session state includes:
- one
streamer,readback, andcounterwrapper set bound to the sharedFPGA - the loaded
Sequence - whether readback checking is enabled
- whether streaming should use forced triggering
The session does not persist across reconnects.
Supported commands
Standard commands:
*IDN?- identify the instrument*RST- clear loaded sequence/session state and reset the hardware run path (streamer, readback, counters, mux, output combiner, and trigger combiner)*CLS- clear status and error queue*OPC/*OPC?- operation complete flag/query*WAI- no-op;STREAMitself is synchronous*ESR?- standard event status register*STB?- status byteSYST:ERR?- query and drain the error queue
PulsePins-specific commands:
TEST1- run a built-in short self-test sequenceSEQ <data>- parse and load a sequence using the canonical PulsePins text grammarCHECK <bool>- enable or disable readback checking duringSTREAMCHECK?- query the check settingCLOCK:STREAMER?- query the measured streamer clock frequency in HzSTREAM- send the loaded sequence and trigger executionDISCONNECT- close the client session; theppscpiserver keeps runningTERMINATE- stop theppscpiserver process
Typical flow
- Connect to TCP port
5025 - Send
*RST - Send
SEQ ...with the sequence payload - Optionally send
CHECK ON - Send
STREAM - Query
SYST:ERR?if needed
Workstation Python and Jupyter
Jupyter should normally run on a workstation, not on the DE10-Nano. The target board only needs to run ppscpi; the notebook talks to it over Ethernet.
The lightweight workstation Python client lives in python/pulsepins/ and uses only the Python standard library. From a checkout, make that directory importable first:
export PYTHONPATH=/path/to/PulsePins/python
For notebooks, an editable install on the workstation is often more convenient:
python3 -m pip install -e /path/to/PulsePins/python
That install also provides example commands such as pulsepins-ppscpi-check, pulsepins-ppscpi-hello, pulsepins-notebook-workflow, pulsepins-timeline-preview, pulsepins-timeline-stream, and pulsepins-timeline-sweep. Add --self-test to pulsepins-ppscpi-check to run the built-in TEST1 hardware smoke path after connecting.
Then a notebook or script can drive the target board with:
from pulsepins import PulsePins
with PulsePins("de10nano") as pp:
print(pp.idn())
print(pp.streamer_clock_hz())
pp.reset()
pp.load_sequence("""
d 10 0xff
d 5 0x00
d 2 0b0101
f
""")
pp.check(False)
pp.stream()
load_sequence(...) accepts normal multiline PulsePins text sequence input and flattens it into the single-line SEQ ... command that ppscpi expects. Because the SCPI transport is line-oriented, one uploaded sequence command must fit within the server's 64 KiB line limit and must not contain semicolons.
If a high-level Python client command receives an error or failure response, it drains SYST:ERR? and raises PulsePinsCommandError with the queued server-side diagnostic text.
For notebook-oriented pulse construction, the same package provides Timeline:
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, include_final=True)
Timeline.to_sequence(...) returns the generated text sequence, Timeline.to_csv() writes browser-compatible Timeline CSV, Timeline.to_draft_json() writes browser-compatible draft JSON, Timeline.to_vcd(...) writes a scalar VCD waveform preview, and notebooks render a lightweight SVG preview when the timeline object is evaluated. Use include_final=True when streaming finite Timeline pulses so owned channels return to their resting value after the last pulse.
The same workflow is available as 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
With the editable install, use the installed command names instead:
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
Live Timeline stream/sweep commands query CLOCK:STREAMER? before converting absolute-time pulses; pass --clock-hz only when you need to override the streamer-clock frequency measured by ppscpi at startup. Hardware-free preview and sweep --dry-run use the supplied/default dry-run clock.
Notes
STREAMuses the same send/trigger path as the local tools, including optional readback verification.*RSTrestores a clean remote-control run state; it does not reprogram clocks or PLLs.SEQstores the parsed sequence in memory; nothing is transmitted to the streamer untilSTREAMis issued.- Semicolons separate SCPI commands before command parsing, so they are not valid inside a
SEQpayload. - If the loaded sequence does not end with terminal
final Vand the server was not started with-t,-random_final, orPP_RANDOM_FINAL,STREAMappends a no-modify final terminator so outputs remain at the last sequence value. Thefrecord only requests forced triggering. - Repeated
STREAMcommands reuse the unchanged parsed sequence; the cached session sequence is not rewritten by readback checking or final-output preparation. - Before each hardware-touching run (
TEST1andSTREAM),ppscpiresets the streamer core, readback encoder, and counters so repeated commands in the same process/session start from a clean hardware state. - If
TEST1orSTREAMreturnsFAILURE,ppscpialso pushes an execution-error record intoSYST:ERR?with the aggregated PulsePins return-code bits so remote clients can distinguish timeout, readback, CRC, buffer, and overflow failures. - When
CHECK ONis active and no explicit startup-timeoutor-hard-timeoutwas supplied, the shared workflow uses a conservative default readback timeout: 2 s for the first readback element and 2 s for later idle gaps. Startppscpiwith-timeout 0to disable idle-timeout protection,-timeout <value>to set the idle-gap timeout, or-hard-timeout <value>to set a total limit from the start of each readback-check phase. Sequence transmission and trigger activation occur before that phase. - Finite
STREAMruns also inherit the internal 10 s streamer-completion timeout from the shared playback path. When that timeout fires, the user-facing message istimed out waiting for streamer completion (10 s internal limit). - Hardware-touching commands are serialized across sessions through the shared FPGA lock, so multiple clients can stay connected without racing each other on streamer/reset state.
- The server is intended for remote orchestration, not for high-throughput binary bulk transfer.
- Command-handler exceptions are converted into SCPI error/status state instead of tearing down the whole server process.
- After
DISCONNECT, clients can reconnect and start a fresh independent session. TERMINATEis the explicit server-shutdown command; it closes the client session and stops the process.