Skip to content

Instantly share code, notes, and snippets.

@cmidgley
Last active November 25, 2021 01:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cmidgley/10e4635599178d698f88ec23a07c0988 to your computer and use it in GitHub Desktop.
Save cmidgley/10e4635599178d698f88ec23a07c0988 to your computer and use it in GitHub Desktop.
LoRa-ST127x.js (moddable JS)
/*
* This is a Moddable JS implementation of a LoRa driver for the ST127x chipset,
* such as found on the Heltec Wifi LoRa 32 V2 board. It is a port of the C++
* ESP-IDF driver (https://github.com/Inteform/esp32-lora-library, has no copyright
* or license), which is a port of an Arduino C++ LoRa driver (which does have a
* copyright and license, included herein, https://github.com/sandeepmistry/arduino-LoRa)
*
* MIT License
*
* Copyright (c) 2021 Christopher W. Midgley
* Copyright (c) 2016 Sandeep Mistry
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* This is a driver that implements support for LoRa using an ST127x chip (over SPI). This provides for broadcast (no
* error recovery, no sequencing) packet delivery (similar to UDP) without any enforcement of compliance to regulatory
* requirements. The reference implementation was on a Heltec ESP32 Lora V2 dev board.
*
* LoRa has strict regional requirements on frequency, per-packet on-air-time as well as total airtime (duty cycle or
* percent of air time). You are responsible to ensure you comply with the regulations (serious fines can be levied,
* region specific, for violations).
*
* You are usually limited to a maximum of 400ms of on-air time, so make sure to use a calculator (such as
* https://avbentem.github.io/airtime-calculator/ttn/au915/222) to determine your per-packet air time. The key settings
* for controlling per-packet air time are:
*
* - Packet size (max 222 bytes)
* - Bandwidth (higher is less air time, but also lower distance coverage)
* - Spreading factor (chirp rate, lower is faster, each level is 2x slower)
* - Coding rate (error correct, small is less correction but smaller packets)
*
* Both the sender and the receiver must have the same settings, or the packets will not be received.
*
* The other critical factor you must manage is your duty cycle, which is also region specific and very restricted to
* ensure the airspace can be shared with others. For example, in the US there is no duty cycle but there is a limit of
* 400ms of transmission per 20 second period of time. Therefore, if a packet consumes 200ms of airtime, it can be sent
* twice each 20 seconds. In the EU, depending on the channel, there is a 0.1% or 1% duty cycle. A packet consuming
* 200ms of airtime on a 1% channel would be limited to one packet each 20 seconds (or one packet every 3.3 minutes on
* the 0.1% channel). You are allowed to burst, as long as your overall duty cycle (over an unspecified time period)
* remains within guidlines. It's your responsibility to research your region specific regulations to ensure compliance.
*
* Note that LoRa is not the same as LoRaWAN, which is a network that operates across multiple channels / frequencies
* and gateways the messages using a standard protocol onto the Internet. See https://www.thethingsnetwork.org/ for a
* very popular free network, widely deployed. This driver could be used as a basis to implement a LoRaWAN client (but
* not a server, as the ST127x chip can not operate across multiple frequencies) in another project. Note that these
* networks often enforce even stricter regulations on airtime usage.
*
* A datasheet on the Semtech SX127x chips can be found here: https://cdn-shop.adafruit.com/product-files/3179/sx1276_77_78_79.pdf
*/
import Timer from "timer";
// max retries during init to look for the chip; takes 2ms per lookup so 100 = 200ms delay before failing
const MAX_INIT_RETRIES = 100;
// ST127x registers
const REG_FIFO = 0x00;
const REG_OP_MODE = 0x01;
const REG_FRF_MSB = 0x06;
const REG_FRF_MID = 0x07;
const REG_FRF_LSB = 0x08;
const REG_PA_CONFIG = 0x09;
const REG_FIFO_RX_CURRENT_ADDR = 0x10;
const REG_OCP = 0x0b;
const REG_LNA = 0x0c;
const REG_FIFO_ADDR_PTR = 0x0d;
const REG_FIFO_TX_BASE_ADDR = 0x0e;
const REG_FIFO_RX_BASE_ADDR = 0x0f;
const REG_IRQ_FLAGS = 0x12;
const REG_RX_NB_BYTES = 0x13;
const REG_PKT_SNR_VALUE = 0x19;
const REG_PKT_RSSI_VALUE = 0x1a;
const REG_RSSI_VALUE = 0x1b;
const REG_MODEM_CONFIG_1 = 0x1d;
const REG_MODEM_CONFIG_2 = 0x1e;
const REG_PREAMBLE_MSB = 0x20;
const REG_PREAMBLE_LSB = 0x21;
const REG_PAYLOAD_LENGTH = 0x22;
const REG_MODEM_CONFIG_3 = 0x26;
const REG_FREQ_ERROR_MSB = 0x28;
const REG_FREQ_ERROR_MID = 0x29;
const REG_FREQ_ERROR_LSB = 0x2a;
const REG_DETECTION_OPTIMIZE = 0x31;
const REG_DETECTION_THRESHOLD = 0x37;
const REG_SYNC_WORD = 0x39;
const REG_DIO_MAPPING_1 = 0x40;
const REG_VERSION = 0x42;
const REG_PA_DAC = 0x4d;
// ST127x transceiver modes
const MODE_SLEEP = 0x00;
const MODE_STDBY = 0x01;
const MODE_TX = 0x03;
const MODE_RX_CONTINUOUS = 0x05;
const MODE_LONG_RANGE_MODE = 0x80;
// ST127x PA configuration
const PA_BOOST = 0x80;
// ST127x IRQ masks
const IRQ_TX_DONE_MASK = 0x08;
const IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20;
const IRQ_RX_DONE_MASK = 0x40;
// RSI values
const RF_MID_BAND_THRESHOLD = 525;
const RSSI_OFFSET_HF_PORT = 157;
const RSSI_OFFSET_LF_PORT = 164;
// Heltec Wifi Lora 32 V2 was the reference implementation; this is the pins definition for that board
export const LoRa_Heltec_Wifi_Lora_32_v2_pins = {
spi: {
io: device.io.SPI,
clock: 5,
in: 19,
out: 27,
select: 18,
active: 0,
hz: 9_000_000,
port: 1,
mode: 0,
},
reset: {
io: device.io.Digital,
pin: 14,
mode: device.io.Digital.Output,
},
interrupt: {
io: device.io.Digital,
pin: 26,
mode: device.io.Digital.Input,
},
};
export class LoRa_ST127x {
#options; // copy of the dictionary passed to the constructor
#runtimeOptions = {}; // runtime options, from constructor(options) and set(options)
#spi; // embedded:io/spi instance
#interruptPin; // embedded:io/digital instance for the interrupt pin (if defined)
#selectPin; // embedded:io/digital instance for the chip select pin
#resetPin; // embedded:io/digital instance for the chip reset pin
#buffer16; // a 16-bit (two byte) buffer used for sending/receiving SPI register to the chip
#radioTransmitting = false; // true when busy transmitting a packet
#packetAvailable = false; // true when a read packet is waiting
/**
* set up driver for the ST127x LoRa chip (SPI)
*
* The parameter options is a dictionary and has the following members:
* - spi: Device settings dictionary for the LoRa chip on SPI (see below) [REQUIRED]
* - reset: Device settings dictionary for the LoRa RESET pin [REQUIRED]
* - interrupt: Device settings dictionary for the LoRa INTERRUPT pin [REQUIRED]
* - frequency: Frequency for transmission (see #setFrequency, defaults to 915Mhz for US) [OPTIONAL]
* - codingRate: Coding rate (default to 5; see #setCodingRate) [OPTIONAL]
* - transmitPower: Transmit power (see set_tx_power, defaults to full power or 17) [OPTIONAL]
* - crc: true if use CRC, false if not (default enabled or true) [OPTIONAL]
* - spreadingFactor: Spreading factor to use (default to 7; see #setSpreadingFactor) [OPTIONAL]
* - bandwidth: Bandwidth rate (default to 500; see #setBandwidth) [OPTIONAL]
* - gain: Gain (0=automatic gain control, 1-6=specific gain levels) [OPTIONAL]
* - implicitHeader: true if implicit, false if explicit packet size (see #setExplicitHeader and #setImplicitHeader) [OPTIONAL]
* - implicitHeaderSize: size of header, must be set if implicitHeader is true [OPTIONAL]
* - preambleLength: Preamble bytes prior to packet (default to 8) [OPTIONAL]
* - syncWord: Sync word to use in preamble (default to 0x12, private networks) [OPTIONAL]
* - enableReceiver: The receiver is normally enabled, but can be disabled to save power (default true) [OPTIONAL]
* - onReadable: Callback when data arrives for async operations (requires int to be defined) [OPTIONAL]
* - onWritable: Callback when data arrives for async operations (requires int to be defined) [OPTIONAL]
*
* Typical members of spi:
* - io: device.io.* of the device (always device.io.SPI)
* - clock: pin number of the clock (SCK)
* - in: pin number of SPI in (MISO)
* - out: pin number of SPI out (MOSI)
* - select: pin number for chip select (SS or CS) [REQUIRED: does not use SPI driver for chip select]
* - active: state of select pin when active; optional: defaults to 0)
* - hz: frequency of SPI in hz
* - port: SPI port
* - mode: SPI mode
*
* Typical members of reset (defines the RESET pin):
* - io: Usually device.io.DEVICE
* - pin: Pin number to reset the ST127x chip
* - mode: Usually device.io.Digital.Output
*
* Typical members of interrupt (defines the INTERRUPT pin):
* - io: Usually device.io.DEVICE
* - pin: Pin number to receive interrupts from the ST127x chip
* - mode: Usually device.io.Digital.Input
*/
constructor(options) {
this.#options = options;
// the default SPI chip select (options.select) doesn't work with ST127x so we remove that here, along with the
// other unwanted options dictionary members for SPI, and implement it via a Digital pin
let lora_options = { ...options.spi };
delete lora_options.select;
delete lora_options.active;
this.#spi = new options.spi.io(lora_options);
// define the interrupt pin
this.#interruptPin = new options.interrupt.io({
...options.interrupt,
edge: device.io.Digital.Rising,
onReadable: this.#interruptCompletion.bind(this),
});
// set up pin for chip select, and set to not selected
this.#selectPin = new device.io.Digital({
pin: options.spi.select,
mode: device.io.Digital.Output,
});
this.#selectPin.write(!options.active);
// set up pin to reset the LoRa chip
this.#resetPin = new options.reset.io(options.reset);
// define a 16 bit buffer for SPI communications
this.#buffer16 = new Uint8Array(2);
// reset hardware (takes about 12ms)
this.#reset();
// check version of chip, retrying for a while; this basically ensures we have access to our ST127x chip
let i = 0;
while (i++ < MAX_INIT_RETRIES) {
let version = this.#readRegister(REG_VERSION);
if (version == 0x12) break;
Timer.delay(2);
}
if (i >= MAX_INIT_RETRIES + 1) throw Error("Not Found");
// set up the chip
this.#sleep();
this.#writeRegister(REG_FIFO_RX_BASE_ADDR, 0);
this.#writeRegister(REG_FIFO_TX_BASE_ADDR, 0);
// set operational values based on options settings and defaults
let runtimeOptions = {};
runtimeOptions.frequency = this.#options.frequency ?? 915;
runtimeOptions.transmitPower = this.#options.transmitPower ?? 17;
runtimeOptions.crc = this.#options.crc ?? true;
runtimeOptions.implicitHeader = this.#options.implicitHeader ?? false;
runtimeOptions.spreadingFactor = this.#options.spreadingFactor ?? 7;
runtimeOptions.bandwidth = this.#options.bandwidth ?? 500;
runtimeOptions.codingRate = this.#options.codingRate ?? 5;
runtimeOptions.preambleLength = this.#options.preambleLength ?? 8;
runtimeOptions.syncWord = this.#options.syncWord ?? 0x12;
runtimeOptions.enableReceiver = this.#options.enableReceiver ?? true;
runtimeOptions.gain = this.#options.gain ?? 0;
this.configure(runtimeOptions);
// set the radio into receive or idle based on the options.enableReceiver mode
this.#updateRadioMode();
}
/**
* Change the operational parameters of the LoRa chip. These options can all be originally set on the constructor options
* dictionary, but can be adjusted here at runtime.
*
* Members include (see constructor for documentation)
* - frequency
* - transmitPower
* - crc
* - implicitHeader
* - implicitHeaderSize (must be set if implicitHeader is true)
* - spreadingFactor
* - bandwidth
* - codingRate
* - preambleLength
* - syncWord
* - enableReceiver
* - gain
*
* @param options Dictionary of options to set
*/
configure(options) {
if (undefined !== options.frequency && options.frequency != this.#runtimeOptions.frequency)
this.#setFrequency(options.frequency);
if (undefined !== options.transmitPower && options.transmitPower != this.#runtimeOptions.transmitPower)
this.#setTransmitPower(options.transmitPower);
if (undefined !== options.crc && options.crc != this.#runtimeOptions.crc) this.#setCRC(options.crc);
if (undefined !== options.implicitHeader && options.implicitHeader != this.#runtimeOptions.implicitHeader)
if (options.implicitHeader) this.#setImplicitHeader(this.#runtimeOptions.implicitHeaderSize);
else this.#setExplicitHeader(this.#runtimeOptions.implicitHeaderSize);
if (undefined !== options.spreadingFactor && options.spreadingFactor != this.#runtimeOptions.spreadingFactor)
this.#setSpreadingFactor(options.spreadingFactor);
if (undefined !== options.bandwidth && options.bandwidth != this.#runtimeOptions.bandwidth)
this.#setBandwidth(options.bandwidth);
if (undefined !== options.codingRate && options.codingRate != this.#runtimeOptions.codingRate)
this.#setCodingRate(options.codingRate);
if (undefined !== options.preambleLength && options.preambleLength != this.#runtimeOptions.preambleLength)
this.#setPreambleLength(options.preambleLength);
if (undefined !== options.syncWord && options.syncWord != this.#runtimeOptions.syncWord)
this.#setSyncWord(options.syncWord);
if (undefined !== options.enableReceiver) this.#setEnableReceiver(options.enableReceiver);
if (undefined !== options.gain) this.#setGain(options.gain);
}
/**
* Get the current operational values in use for the LoRa chip (from constructor and set operation). Responses match
* dictionary requirements for the configure() call.
*
* @returns Dictionary of all values
*/
get() {
return { ...this.#runtimeOptions };
}
/**
* Shutdown hardware and release associated resources
*/
close() {
if (this.#spi) this.#sleep();
this.#spi?.close();
this.#selectPin?.close();
this.#resetPin?.close();
this.#interruptPin?.close();
this.#spi = undefined;
this.#selectPin = undefined;
this.#resetPin = undefined;
this.#interruptPin = undefined;
this.#radioTransmitting = false;
}
/**
* Set data format for write and read. Just verifies the value is "buffer" as that is the only format supported.
*/
set format(value) {
if ("buffer" != value) throw new RangeError();
}
/**
* Return the format used for read/write (always "buffer")
*/
get format() {
return "buffer";
}
/**
* Read a received packet. Normally called after received() indicates a packet is available.
*
* @param arg Buffer to fill with data, or Number to allocate a buffer. Data must fit or packet is dropped.
* @return If Buffer passed, returns bytes received. If Number, returns ArrayBuffer with data. undefined when no packet.
*/
read(arg) {
// is a packet waiting?
if (!this.#packetAvailable) return undefined;
// determine packet size (based on implicit vs. explict headers)
let len = 0;
if (this.#runtimeOptions.implicitHeader) len = this.#readRegister(REG_PAYLOAD_LENGTH);
else len = this.#readRegister(REG_RX_NB_BYTES);
// pause the radio
this.#idle();
// set up our buffer (if arg is number, allocate it; otherwise assume it's a buffer)
let buf;
if ("number" == typeof arg) buf = new ArrayBuffer(len);
else buf = arg;
// transfer the packet from the radio (if we have sufficient room in our buffer)
this.#writeRegister(REG_FIFO_ADDR_PTR, this.#readRegister(REG_FIFO_RX_CURRENT_ADDR));
if (len > buf.byteLength) {
trace("Pkt too big");
return undefined;
}
let view = new Uint8Array(buf);
for (let i = 0; i < len; i++) view[i] = this.#readRegister(REG_FIFO);
// null terminate, so String.fromArrayBuffer is clean
if (len < buf.byteLength) view[len] = 0;
// enable the radio if wanted based on options
this.#packetAvailable = false;
this.#updateRadioMode();
// return buffer (if dynamically allocated) or length received (if buffer provided)
if ("number" == typeof arg) return buf;
return len;
}
/**
* Send a packet. If onWritable is set, will callback when the write is complete and the driver is ready to
* accept another packet.
*
* @param buf Data to be sent (buffer, such as ArrayBuffer)
* @param size Size of data (optional, if not supplied uses buf.byteLength)
*/
write(buf, size = 0) {
if (this.#radioTransmitting) throw Error("Xmit Overlap");
// determine size of packet to send
if (size == 0) size = buf.byteLength;
// prepare radio for transmission
this.#idle();
this.#writeRegister(REG_FIFO_ADDR_PTR, 0);
// track that we are transmitting
this.#radioTransmitting = true;
// send data to radio
let view = new Uint8Array(buf);
for (let i = 0; i < size; ++i) this.#writeRegister(REG_FIFO, view[i]);
this.#writeRegister(REG_PAYLOAD_LENGTH, size);
// tell the chip to interrupt on transmit complete
this.#writeRegister(REG_DIO_MAPPING_1, 0x40);
// tell radio to start tranmission, and let completion be handled by the ISR
this.#writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_TX);
}
#writeCompletion() {
// reset chip back to our standard radio mode (idle or receive)
this.#writeRegister(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK);
this.#updateRadioMode();
// track that we are done transmitting, and handle the completion
this.#radioTransmitting = false;
this.#options?.onWritable();
}
/**
* Returns statistics information about the radio communications
*
* Returns an object containing:
*
* rssi: The Received Signal Strength Indicator of the radio. RSSI range is -120dBm to 0dBm, with 0db being perfect
* (and never occurs). RSSI of -30dBm is very good, whereas -100dBm is quite poor.
*
* packetRssi: Same as Rssi, but for the last received packet
*
* snr: SNR (Signal to Noise Ratio), or the ratio of the signal and the radio noise floor. An SNR greater than 0
* means the received signal operates above the noise floor, and less than means the signal is below the noise
* floor. Typical values are -20dB and +10dB, though around -20dB LoRa is unable to decode the signal.
*
* packetFrequencyError: The frequency error of the received packet in Hz. The frequency error is the
* frequency offset between the receiver centre frequency and that of an incoming LoRa signal
*
* @returns Object with statistics (see above)
*/
statistics() {
return {
rssi: this.#rssi(),
packetRssi: this.#packetRssi(),
snr: this.#snr(),
packetFrequencyError: this.#packetFrequencyError(),
};
}
/***
*** PRIVATE METHODS
***/
/**
* SNR (Signal to Noise Ratio), or the ratio of the signal and the radio noise floor. An SNR greater than 0 means
* the received signal operates above the noise floor, and less than means the signal is below the noise floor.
* Typical values are -20dB and +10dB, though around -20dB LoRa is unable to decode the signal.
*
* @returns Signal to Noise Ratio
*/
#snr() {
return this.#readRegister(REG_PKT_SNR_VALUE) * 0.25;
}
/**
* The Received Signal Strength Indicator of the last received packet. RSSI range is -120dBm to 0dBm, with 0db
* being perfect (and never occurs). RSSI of -30dBm is very good, whereas -100dBm is quite poor.
*
* @returns RSSI for last received packet
*/
#packetRssi() {
return (
this.#readRegister(REG_PKT_RSSI_VALUE) -
(this.#runtimeOptions.frequency < RF_MID_BAND_THRESHOLD ? RSSI_OFFSET_LF_PORT : RSSI_OFFSET_HF_PORT)
);
}
/**
* The Received Signal Strength Indicator of the radio. RSSI range is -120dBm to 0dBm, with 0db being perfect
* (and never occurs). RSSI of -30dBm is very good, whereas -100dBm is quite poor.
*
* @returns RSSI
*/
#rssi() {
return (
this.#readRegister(REG_RSSI_VALUE) -
(this.#runtimeOptions.frequency < RF_MID_BAND_THRESHOLD ? RSSI_OFFSET_LF_PORT : RSSI_OFFSET_HF_PORT)
);
}
/**
* Returns the frequency error of the received packet in Hz. The frequency error is the
* frequency offset between the receiver centre frequency and that of an incoming LoRa signal
*
* @returns Frequency error
*/
#packetFrequencyError() {
let freqError = 0;
freqError = this.#readRegister(REG_FREQ_ERROR_MSB) & 0x7;
freqError <<= 8;
freqError += this.#readRegister(REG_FREQ_ERROR_MID);
freqError <<= 8;
freqError += this.#readRegister(REG_FREQ_ERROR_LSB);
if (this.#readRegister(REG_FREQ_ERROR_MSB) & 0x8) {
// Sign bit is on
freqError -= 524288; // B1000'0000'0000'0000'0000
}
let fXtal = 32e6; // FXOSC: crystal oscillator (XTAL) frequency (2.5. Chip Specification, p. 14)
let fError = ((freqError * (1 << 24)) / fXtal) * (this.#runtimeOptions.bandwidth / 500000.0); // p. 37
return fError;
}
/**
* Handle the interrupt completion for both transmit and receive
*/
#interruptCompletion() {
if (this.#radioTransmitting) this.#writeCompletion();
else {
// reset IRQ
let irq = this.#readRegister(REG_IRQ_FLAGS);
this.#writeRegister(REG_IRQ_FLAGS, irq);
// is a packet waiting and without CRC error?
if ((irq & IRQ_RX_DONE_MASK) != 0 && !(irq & IRQ_PAYLOAD_CRC_ERROR_MASK)) {
this.#packetAvailable = true;
this.#options?.onReadable();
}
}
}
/**
* Configure power level for transmission
*
* @param level 2-17, from least to most power
*/
#OLDsetTransmitPower(level) {
// RF9x module uses PA_BOOST pin
if (level < 2) level = 2;
else if (level > 17) level = 17;
this.#runtimeOptions.transmitPower = level;
this.#writeRegister(REG_PA_CONFIG, PA_BOOST | (level - 2));
}
#setTransmitPower(level) {
if (level > 17) {
if (level > 20) level = 20;
// subtract 3 from level, so 18 - 20 maps to 15 - 17
level -= 3;
// High Power +20 dBm Operation (Semtech SX1276/77/78/79 5.4.3.)
this.#writeRegister(REG_PA_DAC, 0x87);
this.#setOCP(140);
} else {
if (level < 2) {
level = 2;
}
//Default value PA_HF/LF or +17dBm
this.#writeRegister(REG_PA_DAC, 0x84);
this.#setOCP(100);
}
this.#writeRegister(REG_PA_CONFIG, PA_BOOST | (level - 2));
}
/**
* Set over current protection (OCP) on radio
*
* @param mA milliamps current
*/
#setOCP(mA) {
let ocpTrim = 27;
if (mA <= 120) {
ocpTrim = (mA - 45) / 5;
} else if (mA <= 240) {
ocpTrim = (mA + 30) / 10;
}
this.#writeRegister(REG_OCP, 0x20 | (0x1f & ocpTrim));
}
/**
* Set gain (0=auto-gain control (AGC), 1-6 for specific gain control)
*
* @param gain Gain (0-6) with 0=AGC
*/
#setGain(gain) {
// check allowed range and stash the value
if (gain > 6) gain = 6;
this.#runtimeOptions.gain = gain;
// set to standby
this.#idle();
// set gain
if (gain == 0) {
// if gain = 0, enable AGC
this.#writeRegister(REG_MODEM_CONFIG_3, 0x04);
} else {
// disable AGC
this.#writeRegister(REG_MODEM_CONFIG_3, 0x00);
// clear Gain and set LNA boost
this.#writeRegister(REG_LNA, 0x03);
// set gain
writeRegister(REG_LNA, readRegister(REG_LNA) | (gain << 5));
}
}
/**
* Set the LDO (Low Datarate Optimizing) flag when less than 16ms
*/
#setLdoFlag() {
// Section 4.1.1.5
let symbolDuration = 1000 / (this.#runtimeOptions.bandwidth / (1 << this.#runtimeOptions.spreadingFactor));
// Section 4.1.1.6
let ldoOn = (symbolDuration > 16) & 0x1;
let config3 = this.#readRegister(REG_MODEM_CONFIG_3);
// set bit 3 of REG_MODEM_CONFIG_3 to ldoOn
config3 = (config3 && 0xfc) || ldoOn << 3;
this.#writeRegister(REG_MODEM_CONFIG_3, config3);
}
/**
* Set carrier frequency (mandiated by regional regulatory requiremenst)
*
* @param frequency Frequency in MHz (such as 915 for us, or 868 for EU)
*/
#setFrequency(frequency) {
this.#runtimeOptions.frequency = frequency;
let frf = (BigInt(frequency * 1e6) << 19n) / 32000000n;
let msb = Number((frf >> 16n) & 0xffn);
let mid = Number((frf >> 8n) & 0xffn);
let lsb = Number(frf & 0xffn);
this.#writeRegister(REG_FRF_MSB, msb);
this.#writeRegister(REG_FRF_MID, mid);
this.#writeRegister(REG_FRF_LSB, lsb);
}
/**
* Set spreading factor
*
* @param sf Spreading factor (6-12, lower=faster, higher=more reliable)
*/
#setSpreadingFactor(sf) {
this.#runtimeOptions.spreadingFactor = this.#clamp(sf, 6, 12);
if (this.#runtimeOptions.spreadingFactor == 6) {
this.#writeRegister(REG_DETECTION_OPTIMIZE, 0xc5);
this.#writeRegister(REG_DETECTION_THRESHOLD, 0x0c);
} else {
this.#writeRegister(REG_DETECTION_OPTIMIZE, 0xc3);
this.#writeRegister(REG_DETECTION_THRESHOLD, 0x0a);
}
this.#writeRegister(
REG_MODEM_CONFIG_2,
(this.#readRegister(REG_MODEM_CONFIG_2) & 0x0f) | ((this.#runtimeOptions.spreadingFactor << 4) & 0xf0)
);
this.#setLdoFlag();
}
/**
* Set bandwidth (bit rate); regulated by region (for example, US allows 500 and 125, EU allows 250 and 125)
*
* @param bandwidth Bandwidth (in kHz (up to 500))
*/
#setBandwidth(sbw) {
this.#runtimeOptions.bandwidth = sbw;
let bw;
if (sbw <= 7.8) bw = 0;
else if (sbw <= 10.4) bw = 1;
else if (sbw <= 15.6) bw = 2;
else if (sbw <= 20.8) bw = 3;
else if (sbw <= 31.25) bw = 4;
else if (sbw <= 41.7) bw = 5;
else if (sbw <= 62.5) bw = 6;
else if (sbw <= 125) bw = 7;
else if (sbw <= 250) bw = 8;
else bw = 9;
this.#writeRegister(REG_MODEM_CONFIG_1, (this.#readRegister(REG_MODEM_CONFIG_1) & 0x0f) | (bw << 4));
this.#setLdoFlag();
}
/**
* Set coding rate
*
* @param denominator 5-8 (denominator for the coding rate 4/x, lower=less air time, higher=more reliable)
*/
#setCodingRate(denominator) {
this.#runtimeOptions.codingRate = denominator;
var cr = this.#clamp(denominator, 5, 8) - 4;
this.#writeRegister(REG_MODEM_CONFIG_1, (this.#readRegister(REG_MODEM_CONFIG_1) & 0xf1) | (cr << 1));
}
/**
* Set the size of preamble, used in front of packet for packet detection. Rarely needs to change, especially in
* peer-to-peer configurations.
*
* @param length Preamble length in symbols
*/
#setPreambleLength(length) {
this.#runtimeOptions.preambleLength = length;
this.#writeRegister(REG_PREAMBLE_MSB, (length >> 8) & 0xff);
this.#writeRegister(REG_PREAMBLE_LSB, length & 0xff);
}
/**
* Change radio sync word (used in preamble to identify). Defaults to 0x12 for private networks, can be changed
* such as to 0x34 for public networks (such as LoRaWAN)
* )
* @param sw New sync word to use
*/
#setSyncWord(sw) {
this.#runtimeOptions.syncWord = sw;
this.#writeRegister(REG_SYNC_WORD, sw);
}
/**
* Change radio usage of CRC (append/verify CRC if true)
*
* @param enable True to enable CRC, false to disable
*/
#setCRC(enable) {
this.#runtimeOptions.crc = enable;
if (enable) this.#writeRegister(REG_MODEM_CONFIG_2, this.#readRegister(REG_MODEM_CONFIG_2) | 0x04);
else this.#writeRegister(REG_MODEM_CONFIG_2, this.#readRegister(REG_MODEM_CONFIG_2) & 0xfb);
}
/**
* Configure explicit header mode, which allows for variable packet sizes where the size is included in the packet header
* (takes additional air time for size, but allows for variable airtime packets)
*/
#setExplicitHeader() {
this.#runtimeOptions.implicitHeader = false;
this.#writeRegister(REG_MODEM_CONFIG_1, this.#readRegister(REG_MODEM_CONFIG_1) & 0xfe);
}
/**
* Configure implicit header mode, which sets an implicit packet size and does not include packet size in the header
* (reducing air time slightly). All packets must be of the exact same size.
*
* @param size Size of the packets.
*/
#setImplicitHeader(size) {
this.#runtimeOptions.implicitHeader = true;
this.#writeRegister(REG_MODEM_CONFIG_1, this.#readRegister(REG_MODEM_CONFIG_1) | 0x01);
this.#writeRegister(REG_PAYLOAD_LENGTH, size);
}
/**
* The radio can be disabled to save power. When a new packet is transmitted it is automatically turned on and off
* needed. If you disable the radio, receive callbacks will not operate until it is enabled again.
*
* @param enable true to enable the radio, false to turn it off
*/
#setEnableReceiver(enable) {
this.#runtimeOptions.enableReceiver = enable;
if (!this.#radioTransmitting) this.#updateRadioMode();
}
/**
* Adjust the radio to turn on/off the receiver
*/
#updateRadioMode() {
if (this.#runtimeOptions.enableReceiver) {
// enable receive
this.#writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_RX_CONTINUOUS);
// tell the chip to interrupt on receive complete
this.#writeRegister(REG_DIO_MAPPING_1, 0x00);
} else {
// idle the radio
this.#idle();
}
}
/**
* Idle the radio
*/
#idle() {
this.#writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_STDBY);
}
/**
* Perform physical reset on the Lora chip
*/
#reset() {
this.#resetPin.write(0);
Timer.delay(10);
this.#resetPin.write(1);
Timer.delay(10);
}
/**
* Sets the radio transceiver in sleep mode (low power consumption)
*/
#sleep() {
this.#writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_SLEEP);
}
/**
* Private method to write a value directly to an ST127x register (over SPI)
*
* @param reg Register to write (see REG_* constants)
* @param val Value to write
*/
#writeRegister(reg, val) {
const buffer = this.#buffer16;
this.#selectPin.write(0);
buffer[0] = 0x80 | reg;
buffer[1] = val;
this.#spi.write(buffer);
this.#selectPin.write(1);
}
/**
* Private method to read a register directly from an ST127x register (over SPI)
*
* @param reg Register to write (see REG_* constants)
* @return Value of the register
*/
#readRegister(reg) {
const buffer = this.#buffer16;
this.#selectPin.write(0);
buffer[0] = reg;
buffer[1] = 0xff;
this.#spi.transfer(buffer);
let response = buffer[1];
this.#selectPin.write(1);
return response;
}
/**
* Helper method to clamp a value between min and max
*
* @param num Number of clamp
* @param min Min value of num
* @param max Max value of num
* @return Number guaranteed to be min <= num <= max
*/
#clamp(num, min, max) {
return Math.min(Math.max(num, min), max);
}
/**
* Internal debugging method that shows all st127x registers to the debugger
*
* @params msg Optional message to include with the dump
*/
// #lora_dump_registers(msg = "") {
// trace(`STM127x register dump: ${msg}`);
// trace("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\n");
// for (let i = 0; i < 0x80; i++) {
// trace(`${this.#read_reg(i).toString(16).padStart(2, "0")} `);
// if ((i & 0x0f) == 0x0f) trace("\n");
// }
// }
}
export default LoRa_ST127x;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment