Hallonbullar (Swedish for "raspberry buns") is a GPIO and PWM control library for Raspberry Pi, written with Bun. The name is a playful pun: raspberry bun = Raspberry Pi + Bun runtime.
Hallonbullar are Swedish raspberry buns made from a sweet yeasted wheat dough enriched with butter, sugar, and often cardamom. They feature a buttery filling mixed with fresh or frozen raspberries, folded into the dough and shaped into twists or knots before baking. These treats are popular in Sweden, especially during raspberry season in mid to late summer.
Hallonbullar provides a simple, TypeScript-friendly API for controlling GPIO pins and PWM channels on Raspberry Pi. It's built on top of:
- libgpiod v2 for GPIO control (via FFI bindings)
- Linux sysfs PWM interface for hardware PWM
⚠️ Important: This library only works with Bun runtime. It does not work with Node.js. The library uses Bun-specific features likebun:ffifor native bindings and Bun's file system APIs.
The library is designed to be:
- Simple: Easy-to-use classes and methods
- Type-safe: Full TypeScript support
- Bun-only: Built specifically for Bun runtime (not compatible with Node.js)
- Efficient: Direct hardware access with minimal overhead
This repository contains:
packages/hallonbullar/- The main library packagepackages/examples/- Example projects demonstrating usage
Prerequisites: Make sure you have Bun installed. This library requires Bun and will not work with Node.js.
# Clone the repository
git clone <repository-url>
cd bun-blink
# Install dependencies (if using a workspace manager)
bun install- Raspberry Pi (tested on Raspberry Pi 5, should work on other models)
- Bun runtime (latest version) - Required: This library does not work with Node.js
- libgpiod v2 (
libgpiod.so.3) - Linux kernel with GPIO and PWM support
- Permissions: User must be in the
gpiogroup (see Setup below)
Note: This library requires Bun and will not work with Node.js. It uses Bun-specific APIs like
bun:ffifor native library bindings.
For GPIO access, add your user to the gpio group:
sudo usermod -aG gpio $USERThen log out and log back in for the changes to take effect.
For PWM access, you may need to set up udev rules. The library will provide helpful error messages if permissions are not configured correctly.
import { GPIO } from "hallonbullar";
const chip = new GPIO("/dev/gpiochip0");
const led = chip.output(17);
led.on();
setInterval(() => {
led.toggle();
}, 100);
// Cleanup on exit
process.on("SIGINT", () => {
led.off();
chip.close();
process.exit(0);
});import { PWM } from "hallonbullar";
const pwm = new PWM();
const led = pwm.channel(2, {
frequencyHz: 1000,
dutyCycle: 0.5,
});
// Fade in and out
let isIncreasing = true;
setInterval(() => {
const next = led.dutyCycle + (isIncreasing ? 0.01 : -0.01);
led.setDutyCycle(Math.max(0, Math.min(1, next)));
if (led.dutyCycle >= 1) isIncreasing = false;
if (led.dutyCycle <= 0) isIncreasing = true;
}, 10);The GPIO module provides digital input/output control using libgpiod v2.
Main controller for a GPIO chip.
Constructor:
new GPIO(chipPath?: string, libraryPath?: string)chipPath: Path to the GPIO chip (default:"/dev/gpiochip0")libraryPath: Path to libgpiod shared library (default:"/lib/aarch64-linux-gnu/libgpiod.so.3")
Methods:
-
output(pin: number, options?: GPIOOutputOptions): GPIOOutput- Create a digital output pin
options.initialValue: Initial state (default:false)options.activeLow: Treat signal as active-low (default:false)
-
input(pin: number, options?: GPIOInputOptions): GPIOInput- Create a digital input pin
options.bias: Internal bias resistor -"disabled" | "pull-up" | "pull-down"(default:"disabled")options.edge: Edge detection -"none" | "rising" | "falling" | "both"(default:"none")options.activeLow: Treat signal as active-low (default:false)options.debounceMs: Debounce period in milliseconds (default:0)
-
getChipInfo(): { name: string | null; label: string | null; numLines: number }- Get information about the GPIO chip
-
getLineInfo(pin: number): { offset: number; name: string | null; used: boolean; consumer: string | null; direction: "input" | "output" }- Get information about a specific GPIO line
-
findPin(name: string): number- Find a pin number by its name
-
close(): void- Close all GPIO resources and release pins
Properties:
path: string- Path to the GPIO chipclosed: boolean- Whether the controller has been closed
Digital output pin for controlling LEDs, relays, etc.
Methods:
on(): this- Turn the output on (high)off(): this- Turn the output off (low)toggle(): this- Toggle the output statewrite(value: boolean): this- Write a boolean value to the outputclose(): void- Release the GPIO line
Properties:
pin: number- The GPIO pin numberstate: boolean- Current state of the outputclosed: boolean- Whether the output has been closed
Digital input pin for reading buttons, sensors, etc.
Methods:
read(): boolean- Read the current value of the inputvalue: boolean- Alias forread()(getter)onEdge(callback: EdgeCallback): this- Register a callback for edge eventsoffEdge(callback: EdgeCallback): this- Remove an edge callbackwaitForEdge(timeoutMs?: number): Promise<EdgeEvent | null>- Wait for a single edge eventedges(): AsyncGenerator<EdgeEvent, void, unknown>- Async iterator for edge eventsclose(): void- Release the GPIO line
Properties:
pin: number- The GPIO pin numberedge: EdgeSetting- Edge detection settingclosed: boolean- Whether the input has been closed
Types:
interface EdgeEvent {
type: "rising" | "falling";
timestampNs: bigint;
pin: number;
sequence: bigint;
}
type EdgeCallback = (event: EdgeEvent) => void;The PWM module provides hardware PWM control using the Linux sysfs interface.
Main controller for a PWM chip.
Constructor:
new PWM(chipPath?: string)chipPath: Path to the PWM chip (default:"/sys/class/pwm/pwmchip0")
Methods:
-
channel(channel: number, options?: PWMChannelOptions): PWMChannel- Create a PWM channel
options.frequencyHz: Initial frequency in Hz (default:1000)options.dutyCycle: Initial duty cycle 0-1 (default:0.5)
-
checkPermissions(chipPath?: string): PermissionCheckResult- Static method to check PWM permissions
- Returns:
{ canWrite: boolean; inGpioGroup: boolean; message: string }
-
close(): void- Close all PWM channels and release resources
Properties:
path: string- Path to the PWM chipclosed: boolean- Whether the controller has been closed
Hardware PWM channel for controlling motors, LEDs, servos, etc.
Methods:
setDutyCycle(ratio: number): this- Set the duty cycle (0-1)setFrequency(frequencyHz: number): this- Set the frequency in Hzclose(): void- Release the PWM channel (disables output)
Properties:
channel: number- The PWM channel numberperiodNs: number- Current period in nanosecondsdutyCycle: number- Current duty cycle (0-1)frequencyHz: number- Current frequency in Hzclosed: boolean- Whether the channel has been closed
The packages/examples/ directory contains several example projects:
blink.ts- Simple LED blinkingblink-double.ts- Two LEDs alternatingbutton.ts- Button input with edge detectionpwm_led.ts- PWM LED fading
redlight-greenlight/- Web server controlling GPIO pins via HTTP endpoints
Run examples:
cd packages/examples
bun run src/blink.tsThe library throws descriptive errors for common issues:
- Permission errors: Helpful messages about group membership and udev rules
- Pin conflicts: Clear errors when trying to use pins that are already in use
- Invalid parameters: Validation errors for out-of-range values
- GPIO pins are automatically released when the
GPIOcontroller is closed - PWM channels are automatically disabled and unexported when closed
- Edge detection callbacks are polled every 1ms for responsiveness
- The library uses synchronous sysfs operations for PWM (required by the kernel interface)
MIT License - see LICENSE file for details.
This library uses libgpiod (LGPL-2.1) via FFI bindings. The library itself is MIT licensed.
Contributions are welcome! Here's how to get started:
- Fork the repository and create a branch for your changes
- Make your changes - keep code style consistent with the existing codebase
- Test your changes - make sure examples still work and test on a Raspberry Pi if possible
- Submit a pull request with a clear description of what you changed and why
Guidelines:
- Code should be TypeScript and work with Bun (not Node.js)
- Keep the API simple and consistent with existing patterns
- Add examples if you're introducing new features
- Update documentation as needed
Thanks for contributing! 🍞