Hardware: GPIO, ADC, PWM, I2C, SPI
Kog treats hardware state the same way it treats UI state: a pin is a signal. An interrupt edge or an ADC sample writes the signal natively; only the parts of your UI that read it update. No polling loops in JS, ever.
The easy layer: hooks
import { useButton, useDigitalWrite, useAnalogRead } from '@kog/hardware';
function Panel() {
const [led, setLed] = useDigitalWrite(2); // useState-style: [value, setValue]
const { pressed } = useButton(0, {
activeLow: true, // pull-up wiring: pressed reads LOW
onPress: () => setLed(v => (v ? 0 : 1)),
});
const mv = useAnalogRead(4, { hz: 4 }); // reactive millivolts
return (
<View style={[styles.face, pressed() && styles.pressed]}>
<Text>{led() ? 'ON' : 'OFF'} · {(mv() / 1000).toFixed(2)} V</Text>
</View>
);
}
useDigitalRead(pin, { mode, edge, debounceMs })→ accessor() => 0|1(interrupt-driven; debounce happens natively, bounce never reaches JS)useDigitalWrite(pin)→[value, setValue]—valueis an accessor for the last-written level;setValuetakes a level or an updater and drives the pinuseButton(pin, { activeLow, debounceMs, onPress, onRelease })→{ pressed }useAnalogRead(pin, { hz })→ accessor() => number(calibrated mV)usePWM(pin, { freqHz, resBits })→{ duty },duty(0..1)
Everything cleans up on unmount: watches detach, PWM channels free, pin claims release.
v1 note — accessor returns. Hardware-hook reads (led, mv, pressed) are accessors: call
them (led(), mv()), Solid-style. They're naturally reactive wherever you read them in JSX. A
compiler whitelist that lets you write the plain value (led, not led()) — matching useState —
is on the roadmap.
One consequence while that's true: derive from an accessor where you read it, not in the
component body. const volts = mv() / 1000; at the top level freezes at its mount-time value —
the body runs once, and unlike a useState value the compiler cannot see that mv() is reactive
(an accessor call looks like any other function call, which is also why it can't fix or reject
this for you the way it does for state-derived consts). Write the division inline
({(mv() / 1000).toFixed(2)}) or as useMemo(() => mv() / 1000); the no-reactivity-loss lint
catches the frozen form at author time.
The power layer: @kog/hardware
Hooks are sugar over an imperative API with full control:
import { claim, digitalWrite, watch, analogRead, adc, pwm, i2c } from '@kog/hardware';
claim(5, 'output');
digitalWrite(5, 1);
const w = watch(6, 'both', (level) => { /* ... */ }, { debounceMs: 2 });
// w.stop() to detach
const sub = adc.subscribe(4, { hz: 10 }, (mv) => { /* ... */ });
const ch = pwm.open(9, { freqHz: 1000, resBits: 10 });
ch.setDuty(0.5); // 0..1
// I2C/SPI: bus transactions are Promise-based, on a native worker task that
// never blocks the UI. `writeRead` writes then reads back `readLen` bytes
// (sequential write-then-read) and resolves a Uint8Array; a bus/transfer error
// rejects with an HwError carrying a stable `.code`.
const bus = i2c.open({ sda: 8, scl: 9, freqHz: 400_000 });
const rx = await bus.writeRead(0x68, [0x30, 0xa2], 2); // device 0x68, write 2, read 2
// bus.write(addr, bytes) / bus.read(addr, len) for the one-way cases.
// spi.open({ sck, mosi, miso, cs }) → bus.transfer(tx, readLen) (no addr; cs selects).
Pin safety
Pins used by your board's display, touch panel, or backlight are declared in board.yaml and reserved. Claiming one fails at compile time and runtime:
- Compile time: the project generates a
Pintype from yourboard.yaml— autocomplete offers only free pins, anduseButton(21, …)on a display pin is a type error. - Runtime: dynamic pin values are validated with errors that name the owner:
GPIO 21 is reserved by the display (RGB DE), plus the list of free pins.
Input-only pins, strapping pins, and platform quirks (like ADC2's conflict with Wi-Fi) are flagged with specific, actionable errors.
In the simulator
All of this works without hardware: digital inputs toggle from the keyboard or scripts, ADC values can be set over the dev channel, and I2C/SPI can return mocked responses through the real async path. See The simulator.
GPIO expanders (MCP23017-class) are planned post-v1; the pin API already accepts expander-produced pins without breaking changes.