Skip to content

pisco-de-luz/Pisco-Code

Repository files navigation

Pisco Code

CI Arduino Library License: MIT

Pisco Code is a lightweight mechanism for representing decimal or hexadecimal values using a single status LED or an actuator such as a vibration motor. The method supports both positive and negative values and is designed for use in embedded systems with minimal hardware resources.

Why

In many embedded projects, especially during early prototyping or field maintenance, there is no convenient way to observe internal variables. Serial interfaces, debugging tools, or displays are not always available.

Pisco Code provides a simple and intuitive method for conveying values through blink patterns or vibrations, allowing engineers and technicians to interpret internal states or diagnostic codes without additional hardware.

Conventional Approach

A common practice is to use simple blink patterns on a status LED to indicate conditions or values. For example, blinking the LED n times to represent the value n.

For larger values, some implementations concatenate digit sequences. For example, the code 312 may be shown as three blinks, a pause, one blink, a pause, and two blinks.

Status LED showing the code 312 using a sequence of blinks. code312.png

This method is workable but limited: it does not naturally support zero digits and may be ambiguous when interpreting sequences such as 302.

Proposed Solution

Pisco Code extends the conventional approach by introducing a framing signal. Before transmitting a numeric sequence, the LED is held at a low brightness (or partial duty cycle) to indicate the start of a new sequence. Once the sequence completes, the LED is turned off.

Status LED showing the code 121 using Pisco Code.

Video-Code-121-132x132.gif

This framing mechanism improves readability, ensuring that observers can reliably distinguish between digit groups, including zeros, and recognize the start of a new value.

Representation of Zero

The introduction of a framing signal enables explicit representation of the digit zero, which is often ambiguous in conventional blink-based systems.

For example, in the sequence below representing the value 120, digits 1 and 2 are shown with one and two bright blinks, separated by pauses. The digit 0 is represented not by a blink, but by an intentional gap in the sequence. This makes it possible to represent zero clearly in any position within the code.

Pisco-Code-120-144x144.gif

In some cases, it is also necessary to define a minimum number of digits to convey information accurately. For example, when displaying a voltage measurement between 0–5 V with two decimal places (e.g., 0.02 V), at least three digits must always be shown. With this convention, the observer can infer the decimal point location.

Pisco-Code-002-144x144.gif

Binary and Hexadecimal Support

Pisco Code can be extended to represent binary (0–1) or hexadecimal (0–15) values.

  • Binary values are represented as sequences containing only two possible digits, which may result in longer sequences for larger numbers.

  • Hexadecimal values allow compact representation but may require multiple blinks for a single digit, making interpretation slower.

For decimal use cases, Pisco Code has been successfully applied in the Pisco de Luz project since 2020. The system is used in the field to read operational data such as lighting usage hours, battery or solar voltage, and temperature, without requiring additional display hardware.

Representation of Negative Values

Some applications require distinguishing between positive and negative values. To address this, Pisco Code introduces a negative sign indicator: a long blink at the beginning of the sequence.

After this initial blink, the digit-by-digit representation proceeds as usual. For example, the sequence shown below represents the value -12, with the initial long blink indicating the negative sign.

Pisco-Code-Negative12-144x144.gif

Usage

PiscoCode is a target-independent library for LED blink-coding. You provide:

  • A 1 ms periodic call to loop()
  • A small function to turn your LED on or off when requested

No dynamic memory, no exceptions. Ideal for safety-critical systems.

#include "avr_systick.hpp"
#include "hal_led.hpp"
#include "pisco_code.hpp"

int main()
{
    avr_systick::init_1ms();
    hal_led::init();

    // LED1: Software PWM controller (on/off toggle for onboard LED)
    pisco_code::LedControllerSoftwarePwm controller_led1(hal_led::ledOnboard);
    pisco_code::SignalEmitter            emitter_led1(controller_led1);

    // LED2: Hardware PWM controller (smooth dimming for external LED)
    pisco_code::LedControllerHardwarePwm controller_led2(hal_led::ledPwmSetLevel);
    pisco_code::SignalEmitter            emitter_led2(controller_led2);

    // Start displaying codes
    emitter_led1.showCode(SignalCode{123}, Radix::DEC, NumDigits{0});
    emitter_led2.showCode(SignalCode{-102}, Radix::DEC, NumDigits{0});

    // Main loop - call every 1ms
    while (emitter_led1.isRunning() || emitter_led2.isRunning())
    {
        emitter_led1.loop();
        emitter_led2.loop();
        avr_systick::delay_ms(1);
    }

    // Halt
    for (;;) {}
}

For detailed CMake integration instructions, see INTEGRATION.md.

Resource Footprint

Measured with -Os (size-optimized), no exceptions, no RTTI. One emitter + one controller is all you need per LED.

Target Flash (bytes) RAM per LED (SW PWM) RAM per LED (HW PWM)
AVR ATmega328p 2,846 76 bytes 75 bytes
ARM Cortex-M4 (F410RB) 2,648 88 bytes 84 bytes

No heap allocation. All objects are stack-allocated.

For per-object breakdown, see ArchitectureOverview.md.

AVR Build and Upload Options

The AVR example system supports flexible configuration via CMake variables. You can override variables such as the target board, programmer type, upload port, and baud rate via command-line arguments.

Build Configuration Variables

Configure builds by passing variables to CMake:

# Example: Build with Arduino bootloader programmer
cmake -S . -B build/avr-arduino-nano \
    -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/avr-gcc.cmake \
    -DBOARD=arduino-nano \
    -DEXAMPLES=basic_example \
    -DBOARD_UPLOAD_PROGRAMMER=arduino \
    -DBOARD_UPLOAD_PORT=/dev/ttyUSB0 \
    -DBOARD_UPLOAD_BAUD=57600
# Example: Build with USBasp programmer (default)
cmake -S . -B build/avr-arduino-nano \
    -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/avr-gcc.cmake \
    -DBOARD=arduino-nano \
    -DEXAMPLES=basic_example

Or use the convenience script which handles default configuration:

./scripts/Build.sh avr-arduino-nano

Showing PiscoCodes - The showCode() Method

The showCode() method starts displaying a numeric code on the LED. It takes three arguments.

emitter.showCode(SignalCode{-102}, Radix::DEC, NumDigits{0});

Arguments

Argument Type Description
code SignalCode Signed integer value to be encoded as LED blinks. Valid range depends on radix (e.g., -999999999 to 999999999 for decimal).
radix Radix Numeric radix: Radix::DEC, Radix::HEX, Radix::OCT, or Radix::BIN
num_digits NumDigits Minimum number of digits to display. Set to 0 for automatic, or use a value to pad with leading zeros.

Return Value

Return Value Description
true Code accepted and will start displaying shortly.
false Emitter is busy with a previous code. Wait for isRunning() to return false.

Repeat Control

To repeat the code multiple times, use setRepeatTimes() before calling showCode():

emitter.setRepeatTimes(RepeatTimes{3});  // Repeat 3 times
emitter.showCode(SignalCode{42}, Radix::DEC, NumDigits{0});
Method Description
setRepeatTimes(RepeatTimes times) Sets how many times to repeat the full code sequence. Default is 1.
getRepeatTimes() Returns the current repeat count setting.

Custom Settings

Customize the brightness levels used during the blink sequence. These methods are called on the controller, not the emitter.

Method Description
setPeakLevel(IntensityLevel level) Sets brightness for "on" pulses (blinks). Valid range: 0 to 255.
setBaseLevel(IntensityLevel level) Sets brightness for "base" state between pulses. Valid range: 0 to 255.
getPeakLevel() Returns the current peak level setting.
getBaseLevel() Returns the current base level setting.

Default values:

Constant Value Description
DEFAULT_PEAK_LEVEL 200 Default brightness for blinks
DEFAULT_BASE_LEVEL 50 Default brightness for base state

These settings are optional. If not configured, the library uses defaults suitable for most cases.

pisco_code::LedControllerSoftwarePwm controller(hal_led::ledOnboard);
pisco_code::SignalEmitter            emitter(controller);

// Customize brightness levels on the CONTROLLER
controller.setPeakLevel(180);
controller.setBaseLevel(30);

// Set repeat count on the EMITTER  
emitter.setRepeatTimes(RepeatTimes{2});

// Start displaying
emitter.showCode(SignalCode{123}, Radix::DEC, NumDigits{0});

Controller Types

PiscoCode provides two controller types for different LED hardware configurations:

LedControllerSoftwarePwm

For LEDs controlled via GPIO on/off (software-simulated PWM). The callback receives LedControlCode::ON or LedControlCode::OFF.

// Callback signature: bool function(LedControlCode)
bool ledCallback(LedControlCode code) {
    if (code == LedControlCode::ON) {
        // Turn LED on
    } else {
        // Turn LED off
    }
    return true;
}

pisco_code::LedControllerSoftwarePwm controller(ledCallback);

LedControllerHardwarePwm

For LEDs with hardware PWM support. The callback receives an intensity level (0-255).

// Callback signature: void function(IntensityLevel)
void pwmCallback(IntensityLevel level) {
    // Set PWM duty cycle (0-255)
    TIM2->CCR1 = level;
}

pisco_code::LedControllerHardwarePwm controller(pwmCallback);

Runtime Control

These methods allow you to monitor and control the execution of LED blink sequences.

Method Description
loop() Must be called exactly once per millisecond. Drives the internal timing and state machine. Non-blocking and fast.
isRunning() Returns true if a signal is currently being displayed.
stop() Immediately stops the current sequence and resets the emitter to idle. Useful when you need to start a new code without waiting for the current one to finish.

Example

pisco_code::LedControllerSoftwarePwm controller(hal_led::ledOnboard);
pisco_code::SignalEmitter            emitter(controller);

emitter.showCode(SignalCode{123}, Radix::DEC, NumDigits{0});

while (emitter.isRunning()) {
    emitter.loop();     // Must be called every 1 ms
    delay_1ms();        // Your platform's delay function
}

Notes

  • loop() advances the LED pattern by 1 ms. You are responsible for calling it at a steady 1 kHz rate (e.g., using _delay_ms(1), SysTick, or RTOS task).
  • isRunning() is useful to wait for the current signal to complete before showing another code or putting the device to sleep.

Value Limits by Radix

Radix Max Digits Value Range
BIN 8 ±0b11111111
OCT 9 ±0777777777
DEC 9 ±999999999
HEX 7 ±0xFFFFFFF

Build and Upload

To build and flash the examples on different targets, use the provided scripts.

For the STM32F410RB board:

./scripts/Build.sh stm32-f410rb/basic_example
./scripts/Upload.sh stm32-f410rb/basic_example

For the Arduino Nano:

./scripts/Build.sh avr-arduino-nano/basic_example
./scripts/Upload.sh avr-arduino-nano/basic_example

To run all unit tests on the host:

./scripts/Build.sh native

To test CMake integration (for developers):

./scripts/test-cmake-integration.sh

This script verifies that the library works correctly in all modes: native builds, cross-compilation (AVR/STM32), and as a subproject.

Installation

Download Release Package (Recommended)

  1. Go to Releases
  2. Download the latest pisco-code-vX.X.X.tar.gz
  3. Extract to your project:
tar -xzf pisco-code-v1.0.0.tar.gz -C libs/
  1. Add to your CMakeLists.txt:
add_subdirectory(libs/pisco-code-v1.0.0)
target_link_libraries(your_project PRIVATE pisco_code::core)  # For desktop/tests
# OR
target_link_libraries(your_project PRIVATE pisco_code::bare)  # For embedded (AVR, STM32)

For detailed CMake integration examples including cross-compilation toolchains, FetchContent usage, and subproject integration, see INTEGRATION.md.

Verify Download

sha256sum pisco-code-v1.0.0.tar.gz
# Compare with checksums.txt from release