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:

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

Minimal implementation sequence

  1. Add a function declaration in c++/pptool_commands.hh:
int ppmytool(FPGA &fpga, const InputParser &input, const Verbosity &v);
  1. Implement the handler in the most appropriate existing source file.

  2. Register the program name in the dispatch table in c++/pptool.cc.

  3. If the command should be callable as its own shell name, add it to the SYMLINKS list in c++/Makefile.

  4. Add or update tests.

  5. Add the command doc page and link it from docs/mkdocs.yml.

Design hints

  • Prefer reusing shared startup/runtime code through HostRuntime and the existing wrappers.
  • For streaming operations, look at the helpers in ppworkflow.hh before inventing a new send/trigger/readback path.
  • For parsing command options, follow the existing InputParser pattern 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.hh owns:
    • the el representation
    • control classification
    • regular token mapping
    • raw reconstruction helpers
    • per-element text serialization
  • sequence.hh owns:
    • stream/file conversion around Sequence
    • parsing loops and sequence-level operations
  1. Add tests first in c++/unit_tests.cc.
  2. Extend elements.hh shared helpers if the feature affects element semantics.
  3. Extend sequence.hh only where the sequence container or parser/writer glue must change.
  4. Update Python bindings if the feature is surfaced there.
  5. 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:

When to touch which file

Typical sequence

  1. Add the binding in the relevant python/pp_bind_*.cc file.
  2. If the Python tests need a constant from C++, export it in python/pp_impl.cc.
  3. Add host-safe tests to python/test.py.
  4. 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:

  1. Add or extend the value type in ppwebgui_types.hh.
  2. Extend the service interface in ppwebgui_service_api.hh if the HTTP layer needs a new operation.
  3. Implement the hardware-owning logic in ppwebgui_service.cc.
  4. Add the HTTP parsing/route logic in ppwebgui_http.cc.
  5. Extend JSON rendering if needed.
  6. 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 (Makefile or 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.yml navigation
  • recipes/ if the feature benefits from reusable command examples
  • README.md if it materially changes the project’s discoverability

If the feature is mainly for contributors, update:

Quick checklists

New pp... command

New Python API surface

New ppwebgui operation

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