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.
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.