Extension cookbook
This page is a practical guide for extending PulsePins. It is intentionally procedural: the goal is to help contributors make focused additions without first reverse-engineering the entire repository.
For build and onboarding background, also see:
- Build and deployment
- Development
- Hacking on PulsePins
- C++ application programming interface
- Python bindings
General advice
Before adding new functionality:
- keep the change narrow
- update tests in the same change when practical
- update docs for user-visible tools and APIs
- prefer existing shared helpers over new one-off parsing or formatting logic
Recipe 1: add a new pp... CLI tool
The pptool executable family is dispatched by program name. Most commands are symlink-style entry points into the same binary.
Files to touch
c++/pptool_commands.hh- declare the handlerc++/pptool.cc- register the handler in the dispatch table- one implementation file under
c++/:- often
pptool.cc - or
pptool_streaming.cc - or
pptool_measurement.cc - or a new dedicated
.cc/.hhpair if the command is large enough
- often
c++/Makefileif the command needs a new symlink name on deploymentdocs/docs/<tool>.mdfor the command reference page- optionally
recipes/<tool>for reusable command examples
Minimal implementation sequence
- Add a function declaration in
c++/pptool_commands.hh:
int ppmytool(FPGA &fpga, const InputParser &input, const Verbosity &v);
-
Implement the handler in the most appropriate existing source file.
-
Register the program name in the dispatch table in
c++/pptool.cc. -
If the command should be callable as its own shell name, add it to the
SYMLINKSlist inc++/Makefile. -
Add or update tests.
-
Add the command doc page and link it from
docs/mkdocs.yml.
Design hints
- Prefer reusing shared startup/runtime code through
HostRuntimeand the existing wrappers. - For streaming operations, look at the helpers in
ppworkflow.hhbefore inventing a new send/trigger/readback path. - For parsing command options, follow the existing
InputParserpattern and keep validation close to the command handler.
Recipe 2: add or change a sequence construct
If the new feature changes the meaning or representation of sequence elements, the core files are:
What lives where
elements.hhowns:- the
elrepresentation - control classification
- regular token mapping
- raw reconstruction helpers
- per-element text serialization
- the
sequence.hhowns:- stream/file conversion around
Sequence - parsing loops and sequence-level operations
- stream/file conversion around
Recommended order
- Add tests first in
c++/unit_tests.cc. - Extend
elements.hhshared helpers if the feature affects element semantics. - Extend
sequence.hhonly where the sequence container or parser/writer glue must change. - Update Python bindings if the feature is surfaced there.
- Update C++/Python docs if the API or grammar changed.
Important rule
Do not add parallel token/control/behavior mappings in several places if the feature is really an el-level concern. The preferred pattern is to centralize the semantics in elements.hh and let text/binary I/O call into those helpers.
Recipe 3: add a Python binding
The Python bindings live in:
python/pp.cc- nanobind module entry point forpppython/pp_bind_*.cc- the split high-level object bindings forpppython/pp_impl.cc- constants and low-level exported valuespython/test.py- Python-side tests
When to touch which file
- add or update a high-level class or method wrapper: the relevant
python/pp_bind_*.ccfile - export a new constant or symbolic string:
python/pp_impl.cc - verify the binding surface:
python/test.py
Typical sequence
- Add the binding in the relevant
python/pp_bind_*.ccfile. - If the Python tests need a constant from C++, export it in
python/pp_impl.cc. - Add host-safe tests to
python/test.py. - If the test requires a live board or
/dev/mem, mark it with@pytest.mark.hardware.
CI note
Host CI runs:
make -C python USE_PREGENERATED=1 build test-host
So Python tests that do not need real hardware should stay unmarked and should pass in a normal Linux build environment.
Recipe 4: add a ppwebgui feature
The ppwebgui stack is deliberately split into layers.
Main files:
c++/ppwebgui_types.hh- request/response/value typesc++/ppwebgui_service_api.hh- service interface exposed to HTTP/UI codec++/ppwebgui_service.hhandppwebgui_service.cc- hardware-owning implementationc++/ppwebgui_http.hhandppwebgui_http.cc- route registration, request parsing, HTTP errorsc++/ppwebgui_json.cc- JSON renderingc++/ppwebgui_assets.cc- embedded frontend assetsc++/unit_tests.cc- host-side HTTP and request-validation tests
Recommended order
- Add or extend the value type in
ppwebgui_types.hh. - Extend the service interface in
ppwebgui_service_api.hhif the HTTP layer needs a new operation. - Implement the hardware-owning logic in
ppwebgui_service.cc. - Add the HTTP parsing/route logic in
ppwebgui_http.cc. - Extend JSON rendering if needed.
- Add unit tests in
c++/unit_tests.cc.
Important ownership rule
Keep the hardware-owning object graph anchored in WebGuiController. Do not copy or re-own that graph from higher layers. New GUI/HTTP features should normally pass values through the service interface, not bypass it.
Recipe 5: add a new tool helper under tools/
Use tools/ when the code is useful but too device-specific or too experimental for the main CLI.
Good candidates:
- sequence generators for a specific peripheral
- conversion utilities
- specialized bring-up helpers
Recommended structure:
- keep a local
README - keep the build minimal (
Makefileor one small CMake target) - include exact qout wiring or hardware assumptions
- if the helper emits PulsePins sequences, document the expected playback command
The tool tools/spi_payload/ is a good example of this pattern.
Recipe 6: add tests in the right place
C++ core and host-side logic
Use:
Best for:
- sequence semantics
- parsing/formatting
- HTTP validation
- helper wrappers that are safe on the host
Python binding surface
Use:
Best for:
- constructor coverage
- binding method coverage
- round-trip tests through Python-facing APIs
Mark board-backed tests with:
@pytest.mark.hardware
HDL and RTL behavior
Use:
ip/test benches
Best for:
- hardware logic correctness
- CDC/reset-sensitive functionality
- pre-software interface validation
Recipe 7: document a new feature properly
For user-visible functionality, update docs in the same change.
Typical places:
- command reference page in
docs/docs/ docs/mkdocs.ymlnavigationrecipes/if the feature benefits from reusable command examplesREADME.mdif it materially changes the project’s discoverability
If the feature is mainly for contributors, update:
HACKING.mdCONTRIBUTING.mdc++/README.mdorpython/README*if they are the best maintainer-facing entry point
Quick checklists
New pp... command
- handler declared in
pptool_commands.hh - handler registered in
pptool.cc - symlink added to
c++/Makefileif needed - tests added
- docs page added
New Python API surface
- binding added in the relevant
python/pp_bind_*.ccfile - constants added in
python/pp_impl.ccif needed - tests added in
python/test.py - hardware-only tests marked
- Python docs updated
New ppwebgui operation
- type added in
ppwebgui_types.hhif needed - service API updated
- controller implementation updated
- route/parser added in
ppwebgui_http.cc - unit tests added in
c++/unit_tests.cc
Final advice
When in doubt:
- start from an existing nearby feature
- follow the same layering pattern
- add tests before broad refactors
- prefer one shared semantic helper over repeated switch statements in multiple files