Hertz is a React framework (or reconciler/renderer) for driving hardware peripherals. It projects the internal state of your React app to the physical world instead of a screen, video or terminal.
NOTE: This is still a work in progress. APIs may change and coverage is incomplete, but the project is usable for experimentation.
Hertz gives you React ergonomics for hardware control while keeping execution in a Node runtime.
- Declarative hardware tree: describe target hardware state as components and props.
- Built-in lifecycle mapping: mount/update/unmount map to peripheral init/apply/disown methods.
- Parent-first initialization: parent peripherals initialize before children.
- Poll-driven input updates: readable hardware values are queried and surfaced via
on...Changecallbacks. - Error propagation through React boundaries: hardware/runtime errors bubble to standard React error boundaries.
- External telemetry store: publish selected robot state for CLIs, dashboards, and monitoring.
Hertz is primarily a framework for building hardware reconcilers with React. This repository currently includes:
- the reconciler/runtime,
- telemetry utilities for publishing robot state outside React,
- working ClearCore and Raspberry Pi bridges with examples,
- a hardware-free simulation bridge for demos and visualisation.
| Bridge | Status | Notes |
|---|---|---|
| ClearCore | Available | Serial bridge to Teknic ClearCore. See src/bridges/clearcore/README.md |
| Raspberry Pi | Available | Direct GPIO via rpi-io. See src/bridges/raspberrypi/README.md |
| Simulation | Available | In-memory 2D robot, no hardware. See src/bridges/sim/README.md |
| Arduino | Planned | Not implemented yet |
See also:
- Bring your own hardware:
docs/bring-your-own-hardware.md - Testing:
docs/testing.md
Hertz runs in a Node.js-like runtime, not in a browser.
Node.jsis the primary target.Bun/Denomight work but are not officially tested yet.- Devices that cannot run Node directly (for example microcontrollers) are controlled from a host process over a transport such as serial.
- Devices that can run Node (for example Raspberry Pi) can run Hertz directly with local GPIO access.
- Install dependencies in your project:
pnpm add react github:zigapk/hertz
pnpm add serialport
pnpm add llamajet-driver-ts-
Flash ClearCore firmware from
src/bridges/clearcore/firmware/firmware.ino. -
Create the most basic program (blink one digital output pin):
import { ClearCore } from "llamajet-driver-ts";
import { useEffect, useState } from "react";
import { SerialPort } from "serialport";
import { CCDPinOut, clearCorePeripherals, createReconciler } from "hertz";
const Blink = () => {
const [value, setValue] = useState(false);
useEffect(() => {
const timer = setInterval(() => {
setValue((current) => !current);
}, 1000);
return () => clearInterval(timer);
}, []);
return <CCDPinOut pin={3} value={value} />;
};
async function main() {
const clearcore = new ClearCore(
new SerialPort({
path: "/dev/ttyACM0",
baudRate: 115200,
}),
);
await clearcore.connect();
const { render, runEventLoop } = createReconciler(
clearCorePeripherals,
clearcore,
);
render(<Blink />);
await runEventLoop();
}
void main();- Use one of the richer examples as a next step:
src/examples/clearcore-blink.tsxsrc/examples/clearcore-complex.tsxsrc/examples/clearcore-motor-position.tsxsrc/examples/clearcore-motor-velocity.tsxsrc/examples/clearcore-error-boundary.tsxsrc/examples/clearcore-blink-with-telemetry.tsx
The ClearCore bridge communicates over serial through llamajet-driver-ts, which expects the firmware protocol implemented by firmware.ino.
The Raspberry Pi bridge runs Hertz directly on the Pi, controlling GPIO pins through the rpi-io native module. No external controller or serial connection needed.
Prerequisites: Raspberry Pi (4B, 5 and Zero 2, might also work on other models) running Raspberry Pi OS with Node.js >= 23 and libgpiod installed (included by default on Bookworm/Trixie).
- Install dependencies on the Pi:
pnpm add react github:zigapk/hertz
# rpi-io is an optional dependency of hertz -- it installs and compiles
# automatically on ARM Linux. It is skipped on other platforms.- Create a blink program:
import { useEffect, useState } from "react";
import { RpiDOut, rpiPeripherals, RpiHardware, createReconciler } from "hertz";
const Blink = () => {
const [value, setValue] = useState(false);
useEffect(() => {
const timer = setInterval(() => {
setValue((current) => !current);
}, 1000);
return () => clearInterval(timer);
}, []);
return <RpiDOut gpio={17} value={value} />;
};
async function main() {
const hardware = new RpiHardware();
const { render, runEventLoop } = createReconciler(
rpiPeripherals,
hardware,
);
render(<Blink />);
await runEventLoop();
}
void main();- More examples:
src/examples/rpi-blink.tsx-- blink an LED on GPIO 17.src/examples/rpi-follow.tsx-- output mirrors an input pin.src/examples/rpi-loopback-test.tsx-- toggle output and verify via input with timing.
The simulation bridge runs entirely in memory and needs no hardware, which makes it the easiest way to try Hertz. It drives a 2D differential-drive robot and can stream its state to a browser viewer.
- Install dependencies in your project:
pnpm add react github:zigapk/hertz- Create a program that drives the simulated robot and streams it:
import {
SimEngine,
SimMotor,
simPeripherals,
startSSEServer,
} from "hertz";
import { createReconciler } from "hertz";
const Program = () => (
<>
<SimMotor side="L" velocity={1} />
<SimMotor side="R" velocity={1} />
</>
);
async function main() {
const engine = new SimEngine();
// Advance the physics at 60 Hz.
setInterval(() => engine.tick(1 / 60), 1000 / 60);
// Stream state to the viewer at 30 Hz.
const { broadcast } = startSSEServer(engine);
setInterval(broadcast, 1000 / 30);
const { render, runEventLoop } = createReconciler(simPeripherals, engine);
render(<Program />);
await runEventLoop();
}
void main();-
Open
src/examples/sim-roomba-viewer.htmlin a browser to watch the robot move. -
For a complete program, see
src/examples/sim-roomba.tsx, which implements a naive Roomba as a React state machine (drive forward, back off on collision, rotate, repeat). Seesrc/bridges/sim/README.mdfor the full bridge reference.
Hertz peripherals are wrapped by higher-level React components (createHigherLevelComponent) that propagate peripheral errors into the React tree:
- Peripheral lifecycle/read methods catch errors.
- They call an internal
onErrorcallback. - The wrapper stores the error in state and throws it on the next render.
- A React
ErrorBoundaryabove the peripheral catches it.
That means you can use standard React error boundaries to isolate failing robot subtrees and render safe fallbacks.
import { ErrorBoundary } from "react-error-boundary";
import { CCDPinIn, CCDPinOut } from "hertz";
const FaultyPeripheral = () => {
return (
<>
<CCDPinOut pin={0} value={true} />
<CCDPinIn
pin={1}
onValueChange={(value) => {
if (value) {
throw new Error("Pin 1 is HIGH");
}
}}
/>
</>
);
};
const Fallback = (_props: { resetErrorBoundary: () => void }) => {
return <CCDPinOut pin={2} value={true} />;
};
const Program = () => {
return (
<ErrorBoundary FallbackComponent={Fallback}>
<FaultyPeripheral />
</ErrorBoundary>
);
};For a complete runnable example, see src/examples/clearcore-error-boundary.tsx.
Telemetry lets you project selected React state into an external store so non-React code can observe it (CLI, dashboards, logs, safety monitors).
type Telemetry = {
blink: {
phase: "on" | "off";
toggles: number;
};
};
const telemetry = createTelemetryStore<Telemetry>({
blink: { phase: "off", toggles: 0 },
});
useTelemetry<Telemetry, ["blink"]>(["blink"], {
phase: value ? "on" : "off",
toggles,
});
telemetry.subscribeSelector(
(snapshot) => snapshot.blink.phase,
(next, previous) => console.log(`Phase changed: ${previous} -> ${next}`),
);See src/examples/clearcore-blink-with-telemetry.tsx for an end-to-end example.
This software controls physical hardware. Misconfiguration can damage equipment or cause injury. By using Hertz, you accept responsibility for safe setup, safe operation, and validation on your target hardware. The project maintainers are not liable for hardware damage, data loss, or personal injury resulting from use of this software.