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.
While hooks return accessors, 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. For the same reason, the compiler 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 reserved by the firmware's board configuration. Claiming one fails at runtime with an HwError whose .code is PIN_RESERVED.
Platform constraints are reported the same way, with a stable .code: PIN_CAPABILITY for a pin that can't do what you asked (for example, an output on an input-only pin) and ADC2_WIFI for an ADC2 read while Wi-Fi is up. The error doesn't name the pin's owner.
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.