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.mddevelopment.mdhacking.mdcpp.mdpython.md
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
The project has increasingly moved toward centralized semantics in the core types. When extending sequence behavior or tool behavior, it is usually better to add one shared helper in the right place than to duplicate logic in several command handlers.
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 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:
c++/elements.hhc++/sequence.hhc++/unit_tests.cc
What lives where now
elements.hhowns:- the
elrepresentation - control classification
- regular token mapping
- raw reconstruction helpers
- per-element text serialization
sequence.hhowns:- stream/file conversion around
Sequence - parsing loops and sequence-level operations
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 build
make -C python 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.hhand.cc- hardware-owning implementationc++/ppwebgui_http.hhand.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 recent tools/spi_payload/ work is a good example of this pattern.
Recipe 6: add tests in the right place
C++ core and host-side logic
Use:
c++/unit_tests.cc
Best for:
- sequence semantics
- parsing/formatting
- HTTP validation
- helper wrappers that are safe on the host
Python binding surface
Use:
python/test.py
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
That approach has worked well for the recent elements.hh, Python binding, and ppwebgui cleanup work.