I2C address frames with reserved-range checks, the four SPI clock modes, active-high or active-low switches and contacts, and GPIO lines opened on a Linux board. One capability of pamoja, one memory-safe Rust core with bindings for TypeScript, Python, and C#.
npm install @pamoja/gpio
This pulls in @pamoja/native, the compiled engine. npm install pamoja is the whole framework in one package.
The test that runs in CI, spliced here as it ran.
From bindings/node/guides/gpio.ts:
import { Contact, GpioLine, PinEdge, PinLevel, PinScript, Switch, i2c, pin, spi } from '@pamoja/gpio'
// Most relay boards energize when their input is pulled low, and a float switch wired to
// ground closes the same way. Saying "active low" once, here, is what keeps the inversion
// out of every line below it.
const pump = Switch.activeLow(new PinScript())
const float = Contact.activeLow(new PinScript([PinLevel.High, PinLevel.Low]))
const runsOn = pin.levelFor(pump.polarity, true)
console.log(`a pump on an active-low relay runs when its line is ${runsOn}`)
// The pump runs while the tank fills. The scripted line answers open and then closed, so
// this is the real loop with nothing plugged in; on a board the same two lines take a pin
// from the board's GPIO library instead.
pump.set(true)
const whileFilling = float.isAsserted()
const onceFilled = float.isAsserted()
const full = (closed: boolean) => (closed ? 'full' : 'not full')
console.log(`the float reads ${full(whileFilling)}, then ${full(onceFilled)}`)
// The moment the float closes is that line going low, which is a falling edge. A watch
// armed for the rising one would sleep through the tank filling.
const closing = pin.triggers(PinEdge.Falling, PinLevel.High, PinLevel.Low)
console.log(`the float closing is ${closing ? 'a falling edge' : 'not a falling edge'} on that line`)
// Full, so the pump stops. Releasing the switch hands the line back, and the levels it was
// driven to are the whole conversation the board saw.
pump.set(false)
const [ran, stopped] = pump.release().driven
console.log(`running drove the line ${ran} and stopping drove it ${stopped}`)
// A part on a shared bus answers to an address, and the byte on the wire is not the
// address the datasheet prints: it shifts up one and the low bit says read or write.
const hex = (byte: number) => `0x${byte.toString(16).toUpperCase().padStart(2, '0')}`
const toWrite = i2c.addressFrame(0x76)[0]!
const toRead = i2c.addressFrame(0x76, { read: true })[0]!
console.log(`a part at 0x76 is written to as ${hex(toWrite)} and read from as ${hex(toRead)}`)
// Two ranges belong to the specification itself, so a part answering in either is a wiring
// mistake rather than a device.
const reserved = i2c.isReserved(i2c.RESERVED_FROM)
console.log(`${hex(i2c.RESERVED_FROM)} is ${reserved ? 'reserved by the specification' : 'free for a device'}`)
// And a datasheet quotes SPI's clock polarity and phase as one mode number.
const clock = spi.clockFor(3)
const idle = clock.cpol ? 'high' : 'low'
const edge = clock.cpha ? 'trailing' : 'leading'
console.log(`SPI mode 3 idles ${idle} and samples on the ${edge} edge`)
| Language | Package | Reference |
|---|---|---|
| Rust | pamoja-gpio |
reference, docs.rs, install |
| TypeScript | @pamoja/gpio |
reference, install |
| Python | pamoja-gpio |
reference, install |
| C# | Pamoja.Gpio |
reference, install |
@pamoja/gpio reference, every class, function, and type this package exports.MIT
Ergonomic facade over the generated on-board bus binding.
Before a node reaches any network it talks to the chips wired to its own board. Three interfaces cover almost everything cheap hardware uses, and each carries one small piece of logic that is a classic field bug when it is wrong: the I2C address byte, the SPI clock mode, and whether a relay is active high or active low.