The Semtech SX126x and SX127x LoRa radios and the SX1302 and SX1303 gateway concentrators: their commands, registers, and decoders, the amplifier setting a regional EIRP ceiling allows, a duty-cycle guard, and simulated chips that stand in for a module. One capability of pamoja, one memory-safe Rust core with bindings for TypeScript, Python, and C#.
npm install @pamoja/radios
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/radios.ts:
import { LoraRegion, linkBudget, planFor } from '@pamoja/lora'
import { DutyCycle, SimulatedLoraChip, sx126x } from '@pamoja/radios'
async function onTheBench() {
// An SX1262 node sends a ten-byte reading on 868.1 MHz at DR3, SF9 at 125 kHz, through a
// 2.15 dBi whip on half a decibel of pigtail. The plan caps the EIRP there, and the
// antenna and pigtail decide how hard the amplifier may drive under that cap.
const eu868 = planFor(LoraRegion.Eu868)
const frequency = 868_100_000
const link = eu868.linkSettings(3)!
const whip = linkBudget({ transmitAntennaGainDbi: 2.15, transmitCableLossDb: 0.5 })
const ceiling = eu868.maxEirpDbm(frequency)
const power = sx126x.txPowerUnderCeiling(sx126x.Amplifier.HighPower, whip, ceiling)
console.log(`power ${power.settingDbm} dBm under a ${ceiling} dBm EIRP ceiling`)
// A simulated SX1262 stands in for the chip on the node's board, driven by the same code
// that drives a real one, and it reports what that code told it.
const chip = SimulatedLoraChip.sx126x({ amplifier: sx126x.Amplifier.HighPower })
const radio = chip.radio()
await radio.configure({ frequencyHz: frequency, link, outputDbm: power.settingDbm })
const tuned = chip.tuning()
console.log(
`tuned ${(tuned.frequencyHz / 1e6).toFixed(1)} MHz, SF${tuned.link.spreadingFactor} ` +
`at ${tuned.link.bandwidthHz / 1000} kHz, ${tuned.outputDbm} dBm`,
)
// The reading goes out, and the airtime comes back for the duty-cycle guard. The sub-band
// that holds 868.1 MHz allows 1% of the time, so the frame buys ninety-nine times as long
// in silence before the next.
const reading = Buffer.from('level=0.42')
const airtime = await radio.transmit(reading)
console.log(`sent ${chip.sent()[0].payload.length} bytes, ${airtime} us on air`)
const guard = new DutyCycle(eu868.dutyCyclePermille(frequency)!)
guard.transmitted(0, link, reading.length)
console.log(`silence the next frame starts ${guard.waitUs(0)} us after this one did`)
// A gateway's answer arrives from the edge of range, 2.5 dB under the noise.
chip.hear(Buffer.from('ack'), -109, -2.5)
const heard = await radio.receive(1_000_000)
if (heard.outcome === 'Frame') {
console.log(
`received ${heard.payload!.toString()} at ${heard.rssiDbm!.toFixed(2)} dBm, ` +
`SNR ${heard.snrDb!.toFixed(2)} dB`,
)
}
// With nothing on the air the reception times out, and a frame whose CRC fails is dropped
// rather than handed over.
const quiet = (await radio.receive(1_000_000)).outcome
chip.hearCorrupt(-121, -12)
const broken = (await radio.receive(1_000_000)).outcome
console.log(`then ${quiet}, then ${broken}`)
radio.close()
return { power, tuned, airtime, link, reading, guard, chip, quiet, broken }
}
| Language | Package | Reference |
|---|---|---|
| Rust | pamoja-radios |
reference, docs.rs, install |
| TypeScript | @pamoja/radios |
reference, install |
| Python | pamoja-radios |
reference, install |
| C# | Pamoja.Radios |
reference, install |
@pamoja/radios reference, every class, function, and type this package exports.MIT
Ergonomic facade over the generated LoRa radio binding.
@pamoja/loraworks out what a link costs and how far it reaches; this puts a radio chip on the air to match. For the Semtech SX1261, SX1262, and LLCC68 it builds the bytes of every command and decodes every answer, and for the SX1276 family it gives the register values and decodes the readings. It chooses the amplifier setting a regional EIRP ceiling allows behind an antenna, and holds the radio silent for the off time a duty-cycle limit owes after each frame.