Skip to content

Contributing Guide

This guide covers setting up your development environment, building PolyBoard, running tests, and contributing changes.

PolyBoard uses Nix for reproducible builds. This ensures all developers use identical toolchains.

Terminal window
# Install Nix
sh <(curl -L https://nixos.org/nix/install)
# Enable flakes (add to ~/.config/nix/nix.conf)
experimental-features = nix-command flakes
Terminal window
git clone --recursive https://github.com/b4nst/PolyBoard.git
cd PolyBoard
Terminal window
# Enter development shell
nix develop
# Or run commands directly
nix run .#default -- make
Terminal window
# Full build (firmware + tests)
make
# Build firmware only
make build/polyboard.syx
# Clean build artifacts
make clean
# Build and run tests
make test
# Run linter
make lint
FileDescription
build/polyboard.elfELF binary with debug symbols
build/polyboard.hexIntel HEX format
build/polyboard.syxSysEx file for uploading
Terminal window
# Run all tests
make test
# Run specific test file
./build/tests/page_test.run

Tests use CMocka framework:

tests/
├── mocks/
│ └── hal.c # HAL function mocks
├── app_state_test.c # Runtime state tests
├── flash_test.c # Persistence tests
├── layout_test.c # LED rendering tests
├── note_test.c # Scale calculation tests
├── page_test.c # Page management tests
└── surface_test.c # Button detection tests
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include "page.h"
static void test_change_page_valid(void **state) {
(void)state;
// Setup
app_state_init();
// Act
const Page *page = change_page(2);
// Assert
assert_non_null(page);
assert_int_equal(get_current_page(), 2);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_change_page_valid),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

Tests use mock HAL functions in tests/mocks/hal.c:

// Track LED state for assertions
static u32 mock_leds[100];
void hal_plot_led(u8 type, u8 index, u8 red, u8 green, u8 blue) {
if (type == TYPEPAD && index < 100) {
mock_leds[index] = (red << 16) | (green << 8) | blue;
}
}
u32 mock_get_led(u8 index) {
return mock_leds[index];
}

PolyBoard uses LLVM style via clang-format:

Terminal window
# Check formatting
make lint
# Auto-format (run before committing)
clang-format -i src/*.c include/*.h
  • Braces: K&R style (opening brace on same line)
  • Indentation: 2 spaces (no tabs)
  • Line length: 80 characters soft limit
  • Naming:
    • Functions: snake_case
    • Constants: UPPER_CASE
    • Types: PascalCase
// Good
void change_page_root(unsigned char root) {
if (root > 11) {
return;
}
// ...
}
// Bad
void ChangePageRoot(unsigned char root)
{
if(root>11) return;
}
  1. Add to note.h:

    enum ScaleIndex {
    // ... existing scales
    SCALE_NEW_SCALE, // Add here
    };
    #define SCALE_COUNT 11 // Increment
  2. Add scale pattern to scales array:

    // Scale pattern: 1=root, 0=in scale, -1=chromatic
    static const short scales[SCALE_COUNT][12] = {
    // ... existing
    // New Scale: semitones 0, 2, 4, 5, 7, 9, 10
    {1, -1, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1},
    };
  3. Add degree count:

    static const unsigned char SCALE_DEGREE_COUNT[SCALE_COUNT] = {
    // ... existing
    7, // New Scale
    };
  4. Add degree-to-semitone mapping:

    static const unsigned char DEGREE_TO_SEMITONE[SCALE_COUNT][7] = {
    // ... existing
    {0, 2, 4, 5, 7, 9, 10}, // New Scale
    };
  5. Add color in colors.h:

    #define COLOR_SCALE_NEW 0x3F003FU
  6. Add to scale selection map in surface.c

  7. Add tests in note_test.c

  1. Add field to Page struct in page.h:

    typedef struct Page {
    // ... existing
    unsigned char new_setting;
    } Page;
  2. Add default value:

    #define DEFAULT_NEW_SETTING 0
  3. Update page.c initialization

  4. Increment FLASH_VERSION in flash.h

  5. Add validation in flash_load():

    if (data.pages[i].new_setting > MAX_VALUE) {
    data.pages[i].new_setting = DEFAULT_NEW_SETTING;
    }
  6. Add getter/setter functions

  7. Add UI handling in app.c (Setup mode)

  8. Add LED feedback in layout.c

  9. Add tests

  1. Add to mode.h:

    typedef enum {
    MODE_PLAY,
    MODE_ROOT_SELECT,
    MODE_SCALE_SELECT,
    MODE_SETUP,
    MODE_NEW_MODE, // Add here
    } AppMode;
  2. Add handler in app.c:

    static void handle_new_mode(u8 index, u8 value) {
    // Handle button presses
    }
  3. Add to event routing:

    switch (get_mode()) {
    // ... existing
    case MODE_NEW_MODE:
    handle_new_mode(index, value);
    break;
    }
  4. Add renderer in layout.c:

    void render_new_mode(void) {
    // LED layout
    }
  5. Add to refresh_display():

    case MODE_NEW_MODE:
    render_new_mode();
    break;
  6. Add entry/exit triggers

  7. Add tests

  1. Download Novation Components
  2. Put Launchpad Pro in bootloader mode (hold Setup while powering on)
  3. Drag build/polyboard.syx to the updater
Terminal window
# Find MIDI device
amidi -l
# Upload firmware
amidi -p hw:1,0,0 -s build/polyboard.syx
  1. Create a feature branch:

    Terminal window
    git checkout -b feature/my-feature
  2. Make changes and commit:

    Terminal window
    git add .
    git commit -m "feat: add new feature"
  3. Ensure tests pass:

    Terminal window
    make clean && make && make test
  4. Ensure code is formatted:

    Terminal window
    make lint
  5. Push and create PR:

    Terminal window
    git push -u origin feature/my-feature
    gh pr create

Use Conventional Commits:

PrefixUsage
feat:New feature
fix:Bug fix
refactor:Code change that neither fixes nor adds
docs:Documentation only
test:Adding or fixing tests
chore:Build, CI, tooling changes