-
-
Notifications
You must be signed in to change notification settings - Fork 1
Development Workflow
Note
This Wiki page is generated from docs/DEVELOPMENT.md. Edit the repository source rather than the Wiki directly.
For workstation setup and day-one workflows in VSCodium, see DEVELOPER_README.md.
All project-owned source code, comments, identifiers, Doxygen documentation, and developer documentation use US English.
| Element | Convention | Example |
|---|---|---|
| Namespace | lowercase | clockfw::engine |
| Class / struct | PascalCase | ClockEngine |
| Enum class | PascalCase | ChannelMode |
| Enum value | PascalCase | ChannelMode::Euclid |
| Function / method | lowerCamelCase | updateChannel() |
| Local / parameter | lowerCamelCase | channelIndex |
| Constant |
k + PascalCase |
kSchedulerFrequencyHz |
| Private member | lowerCamelCase + _
|
gateOutputs_ |
| Boolean | positive/question-like meaning where practical |
rescheduleChannel, gateHigh
|
Avoid abbreviations unless they are established domain/UI terms such as BPM, PPQN, GPIO, CLK, EUC, or SEQ.
Every project-owned C++ source/header contains a Doxygen file header with at least:
/**
* @file example.cpp
* @brief One-sentence responsibility of the file.
* @author Axel Napolitano
* @copyright 2026 Axel Napolitano
* @license PolyForm-Noncommercial-1.0.0
*/Project-owned scripts, workflows, configuration files, and Markdown documentation carry equivalent author/license metadata in the comment syntax appropriate to that file type. LICENSE.md itself and binary assets are naturally exempt.
Every method and function must have a useful Doxygen comment. Relevant constants, shared state, and non-obvious data structures must also be documented.
Good comments explain:
- why a method exists
- ownership and side effects
- timing/thread/ISR constraints
- units and ranges
- non-obvious safety behavior
- meaning of parameters that is not obvious from the type/name
Inline comments should explain why, not narrate obvious code. For example, documenting why output enable stays disabled through boot is useful; commenting ++channelIndex as “increment channel” is not.
Source code is maintained for human review first, not merely for compiler acceptance. Prefer code that makes the control flow, ownership, units, and invariants obvious without requiring the reader to reconstruct intent from tests.
- Keep one function at one conceptual level. Extract helpers when they name a real domain step, state-machine action, validation rule, or serialization operation; do not split code only to reduce line count.
- Prefer
switchdispatch for explicit state machines and enums when it makes supported states visible. Avoid long chains of unrelatedif/else ifconditions. - Use names that include units or representation when ambiguity is possible, for example
periodUs,filteredBpmMilli, orphaseQ32. - Replace sentinel literals and repeated protocol/layout constants with named constants.
- Keep persistence migrations auditable: probe schemas newest-to-oldest, state what a migration injects, and preserve byte-layout reasoning close to the code that depends on it.
- Keep safety defaults visible at the decision point, especially confirmation defaults, boot transport behavior, role reassignment, and Flash-write restrictions.
- Comments document intent, invariants, compatibility constraints, and surprising implementation choices. They must not paraphrase self-explanatory assignments or loops.
- Real-time code is not refactored solely for aesthetics. Readability changes in scheduler/ISR paths must preserve bounded execution, allocation-free behavior, and the existing timing model.
A readability refactor is still a behavioral change risk: run the same regression, coverage, sanitizer, architecture, and simulator gates as for functional work.
The 128x64 Settings renderer distinguishes editable values from read-only informational values. Editable values must remain directly legible and editable in their normal row; do not hide an editable value behind a secondary view merely because it is long. Read-only information may opt into MenuRow::expandableInformation. Only those rows may be shortened with a trailing ... when the label/value pair exceeds the available pixel width. Activating an actually truncated informational row opens the complete value in the standard information popover; values that already fit do nothing. This keeps overflow handling explicit and prevents a generic truncation rule from changing the editing grammar.
Global project configuration is intentionally kept at the top of src/:
-
config.h— compile-time behavior and transport policy -
defaults.h— editable factory user settings -
pin_map.h— physical GPIO/peripheral routing -
ui_text.h— static user-visible text / localization catalog -
version.h— firmware version source of truth
Headers belonging to an implementation module live beside its .cpp file. CI rejects a source implementation whose matching header has drifted back into a separate include tree.
See CONFIGURATION.md and DEPENDENCIES.md.
Code reachable from ClockEngine::processSchedulerTick() must remain:
- allocation-free
- bounded
- non-blocking
- independent from OLED/UI rendering
- independent from serial/logging
Do not introduce floating-point timeline accumulation. Rational timing uses integer/Q32 arithmetic with retained remainder.
Hardware/framework dependencies belong only in src/hal and src/pin_map.h.
Run:
python scripts/check_architecture.pyThe check enforces file headers, HAL-only hardware dependencies, a small main.cpp, and source-size guardrails. See ARCHITECTURE.md for the full dependency policy.
pio run -e blackpill_f401ccCompile-check the alternative SPI display transport with:
pio run -e blackpill_f401cc_spi_ssd1315The STM32 target remains pinned to ststm32@19.7.1 and the project explicitly builds its C++ sources as GNU C++17.
pio test -e nativeThe Native entry point contains 525 named test cases in twelve separately reported PlatformIO suites: 44 test_clock_core, 35 test_realtime, 116 test_sync_behavior, 20 test_sequencer2, 40 test_swing, 12 test_humanize, 24 test_tap_tempo, 51 test_controls, 89 test_settings, 19 test_screensavers, 36 test_easter_eggs, and 39 test_host_firmware. Exhaustive mathematical loops remain in the core, while musical timing, configurable external-input roles, humanization, tap estimation, physical controls, settings and non-performance UI state machines are independently visible by name. The control suite locks down immediate first-detent response, direction changes, counter wraparound, missed/coalesced-transition recovery, menu-entry phase re-anchoring and Fast Turn behavior. Production STM32F401 builds use TIM4 encoder mode on PB6/PB7. The Settings/host suites additionally cover the global INPUTS > and HARDWARE > pages, role exclusivity, full-row inverted Settings selection, deterministic OLED 0°/180° rotation, Custom Groove editor/recorder workflows, One-Shot/Endless capture, recorder Count-In, high-resolution TAP timestamp preservation through debounce, shared RECORD→EDITOR drafts, schema-v11 to schema-v12 migration for Custom Groove slot references, schema-v10 to schema-v11 migration for configurable input assignments, schema-v9 to schema-v10 Groove migration, schema-v8 to schema-v9 Pre-Count migration, schema-v7 smoothing migration, and the earlier migration chain.
Covered areas include:
- rate normalization, meter conversion, Q32 accumulation, swing, phase, probability and gate-width mathematics;
- Euclidean and sequencer invariants across their supported lengths;
- scheduler/GPIO timing, gate release, reset priority, queue overflow and timestamp wrap;
- exact and strongly varying external clocks across 1..999 BPM and 1/2/4/24 PPQN;
- glitch storms, duplicate timestamps, edge polarity, adaptive timeout and all external-loss policies;
- runtime PPQN/edge changes with explicit estimator reacquisition;
- permanently asserted SYNC input, including timeout after prior lock and prevention of synthetic repeated selected edges;
- nominal LM393-front-end hysteresis and 2.5/3/4 V input behavior through a host-only electrical model;
- fast-turn encoder bursts/backlog saturation and concurrent button-bounce activity without detent loss;
- atomic settings limits and invariants for Clock, INPUTS/CONFIG, HARDWARE, screensaver, channel, Euclid, Sequencer, One Clock and Divider Bank pages;
- every screensaver renderer and all five Easter-egg launch/reset/control contracts;
- complete firmware/UI/HAL/persistence scenarios against deterministic framework fakes.
The nominal electrical model is a design-level check, not a substitute for bench measurements. Physical threshold tolerance, noise susceptibility, propagation delay and actual STM32 capture timing remain HIL.
python -m unittest discover -s scripts/tests -p 'test_*.py' -vThese cover:
- firmware version extraction
- exact tag/version matching
- changelog release-note extraction
- missing release-note versions
- deterministic release filenames
- firmware + license packaging
- SHA-256 generation
- missing artifact failure
- repository license-policy checks
The authoritative repository-wide gate is:
python scripts/run_host_tests.py --skip-sanitizers
python scripts/run_host_tests.py --sanitizers-onlyIt compiles and executes the real project-owned firmware across the production SPI profile, legacy I2C regression variants, deterministic-core, and native-smoke variants using GCC coverage instrumentation.
CI requires:
- executable-line coverage >= 95%
- function coverage >= 95%
- non-throw decision-branch coverage >= 90%
The 1.1.0 release baseline after the Custom Groove tap recorder, readability sprint, Groove marker quiet-zone fix, horizontal Channel Mode carousel, named Custom Groove save/load/rename/delete UX, read-only information overflow handling, exhaustive Groove sweeps, and simulator scope phase-lock correction: 9260/9667 lines (95.79%), 805/819 functions (98.29%), and 5639/6246 decision branches (90.28%). Raw compiler branches are 5640/7322 (77.03%) and remain informational.
- AddressSanitizer = clean
- UndefinedBehaviorSanitizer = clean
The stable repository-wide baseline is maintained by run_host_tests.py and must remain above 95% executable lines, 95% functions and 90% source decision branches. The report lists every uncovered production function and decision branch explicitly so release thresholds do not hide newly introduced blind spots.
Raw GCC compiler-branch coverage is reported separately because GCC also emits synthetic throw/exception edges. These are informational only; source decision branches are gated at 90%.
Coverage artifacts are written to coverage/full_coverage.txt and coverage/full_coverage.json.
Production headers must not contain executable inline functions. Keeping executable logic in .cpp files makes every production function visible to coverage and is enforced by scripts/check_architecture.py.
The native simulator is a second hardware platform for the real firmware and uses CMake independently of PlatformIO:
cmake --preset simulator-headless
cmake --build --preset simulator-headless
ctest --preset simulator-headlessThe interactive frontend uses SDL3:
cmake --preset simulator
cmake --build --preset simulator
python scripts/run_simulator.pySee SIMULATOR.md for platform setup, controls, persistence, and developer instrumentation.
.github/workflows/ci.yml runs on pushes, pull requests, and manual dispatch.
Order:
- architecture policy check
- Python release/tooling tests
- native production-core tests
- coverage-instrumented firmware tests and sanitizer gates
- headless full-firmware simulator build/tests on Windows, macOS, and Linux
- SDL3 simulator frontend compile/build gate on Ubuntu
- legacy I2C transport host regression build
- STM32F401 SPI display compile-check
- verify both STM32 target builds and their memory budgets
- verify STM32CubeF4 firmware artifacts and embedded dependency policy before release packaging
The STM32 firmware build only runs after host coverage and both native-simulator CI gates pass.
.github/workflows/release.yml runs for Git tags and can also be manually dispatched for release-candidate builds.
A release tag must exactly match src/version.h:
firmware: 1.1.0
Git tag: v1.1.0
A mismatch fails before publication.
Before tagging, prepare the user-facing release summary and freeze the maintained user manual for the current firmware version:
python scripts/prepare_release_summary.py
# edit docs/releases/<version>/RELEASE_SUMMARY.md until the review placeholders are gone
python scripts/check_release_summary.py docs/releases/<version>/RELEASE_SUMMARY.md --version <version>
python scripts/prepare_release_manual.pyThe summary explains what CLOCK is and the important changes from a user perspective; the detailed engineering history remains in CHANGELOG.md. For a final stable release, manual preparation creates and commits docs/manual/clock-user-manual.<version>.odt; Alpha, Beta and RC manuals are generated transiently and are not archived there. The tag workflow requires the stable frozen ODT when publishing a final release and always requires the version-matched release summary.
For a tag build the workflow:
- validates architecture policy
- validates version/tag consistency
- runs Python tooling tests
- runs native production-core tests
- enforces coverage thresholds
- builds the STM32 firmware
- regenerates the deterministic OLED screenshot catalog from the exact release source and refreshes every screenshot in the frozen ODT
- validates the refreshed versioned ODT manual
- converts that ODT to
clock-user-manual.<version>.pdfwith LibreOffice and validates the PDF/font contract - extracts release notes from the matching
CHANGELOG.mdsection - publishes the GitHub Release with the versioned ODT and PDF manual artifacts attached
GitHub supplies the source archive for the tagged commit. Firmware binaries are now eligible for release packaging because the embedded runtime uses STM32CubeF4/CMSIS without the former STM32duino LGPL dependency. See docs/LICENSING.md.
The release source of truth is:
src/version.hDo not hard-code a second firmware version in workflows or production code. The manual preparation tool reads the same value, stamps it into body text, metadata, and cover/back-cover artwork, and freezes the corresponding release ODT before tagging.
A repository Doxyfile is provided for generated API documentation. The documentation-quality checker validates repository-local links, README footers, fenced Markdown blocks, SVG accessibility metadata, and current-version identity before Doxygen generation.
python scripts/check_documentation.py
doxygen DoxyfileGenerated HTML output goes to build/doxygen/ and is ignored by Git. The Doxyfile uses build/ as its top-level output directory so a plain doxygen Doxyfile also works from a clean checkout.
Project software is licensed under PolyForm-Noncommercial-1.0.0. See LICENSE.md and NOTICE.txt. Third-party components retain their upstream licenses; see THIRD_PARTY_NOTICES.md and third_party/.
CLOCK 1.1.0
- 01 Start here
- 02 Panel, power and boot
- 03 Two-minute quick start
- 04 The control language
- 05 Read the Performance screen
- 06 Select channels and change functions
- 07 Transport and tempo
- 08 Independent channels
- 09 Clock mode
- 10 Euclid mode
- 11 Sequencer mode
- 12 One Clock
- 13 Grooves and Custom Groove Record
- 14 Divider Bank
- 15 Settings map
- 16 Configurable external inputs
- 17 Presets, autosave and templates
- 18 Screensaver and display protection
- 19 Gate outputs and LEDs
- 20 Hidden boot features
- 21 Practical recipes
- 22 Troubleshooting
- 23 Firmware installation and updates
- 24 Technical status and specifications