Skip to content

Instantly share code, notes, and snippets.

@ikeating
Created October 15, 2021 22:02
Show Gist options
  • Select an option

  • Save ikeating/32739f012db0c4a9d736fc942896b2dc to your computer and use it in GitHub Desktop.

Select an option

Save ikeating/32739f012db0c4a9d736fc942896b2dc to your computer and use it in GitHub Desktop.
// RH_RF95.cpp
//
// Copyright (C) 2011 Mike McCauley
// $Id: RH_RF95.cpp,v 1.27 2020/07/05 08:52:21 mikem Exp $
#include <RH_RF95.h>
// Maybe a mutex for multithreading on Raspberry Pi?
#ifdef RH_USE_MUTEX
RH_DECLARE_MUTEX(lock);
#endif
// Interrupt vectors for the 3 Arduino interrupt pins
// Each interrupt can be handled by a different instance of RH_RF95, allowing you to have
// 2 or more LORAs per Arduino
RH_RF95* RH_RF95::_deviceForInterrupt[RH_RF95_NUM_INTERRUPTS] = {0, 0, 0};
uint8_t RH_RF95::_interruptCount = 0; // Index into _deviceForInterrupt for next device
// These are indexed by the values of ModemConfigChoice
// Stored in flash (program) memory to save SRAM
PROGMEM static const RH_RF95::ModemConfig MODEM_CONFIG_TABLE[] =
{
// 1d, 1e, 26
{ 0x72, 0x74, 0x04}, // Bw125Cr45Sf128 (the chip default), AGC enabled
{ 0x92, 0x74, 0x04}, // Bw500Cr45Sf128, AGC enabled
{ 0x48, 0x94, 0x04}, // Bw31_25Cr48Sf512, AGC enabled
{ 0x78, 0xc4, 0x0c}, // Bw125Cr48Sf4096, AGC enabled
{ 0x72, 0xb4, 0x04}, // Bw125Cr45Sf2048, AGC enabled
};
RH_RF95::RH_RF95(uint8_t slaveSelectPin, uint8_t interruptPin, RHGenericSPI& spi)
:
RHSPIDriver(slaveSelectPin, spi),
_rxBufValid(0)
{
_interruptPin = interruptPin;
_myInterruptIndex = 0xff; // Not allocated yet
_enableCRC = true;
_useRFO = false;
}
bool RH_RF95::init()
{
if (!RHSPIDriver::init())
return false;
#ifdef RH_USE_MUTEX
if (RH_MUTEX_INIT(lock) != 0)
{
Serial.println("\n mutex init has failed\n");
return false;
}
#endif
// For some subclasses (eg RH_ABZ) we dont want to set up interrupt
int interruptNumber = NOT_AN_INTERRUPT;
if (_interruptPin != RH_INVALID_PIN)
{
// Determine the interrupt number that corresponds to the interruptPin
interruptNumber = digitalPinToInterrupt(_interruptPin);
if (interruptNumber == NOT_AN_INTERRUPT)
return false;
#ifdef RH_ATTACHINTERRUPT_TAKES_PIN_NUMBER
interruptNumber = _interruptPin;
#endif
// Tell the low level SPI interface we will use SPI within this interrupt
spiUsingInterrupt(interruptNumber);
}
// No way to check the device type :-(
// Set sleep mode, so we can also set LORA mode:
spiWrite(RH_RF95_REG_01_OP_MODE, RH_RF95_MODE_SLEEP | RH_RF95_LONG_RANGE_MODE);
delay(10); // Wait for sleep mode to take over from say, CAD
// Check we are in sleep mode, with LORA set
if (spiRead(RH_RF95_REG_01_OP_MODE) != (RH_RF95_MODE_SLEEP | RH_RF95_LONG_RANGE_MODE))
{
// Serial.println(spiRead(RH_RF95_REG_01_OP_MODE), HEX);
return false; // No device present?
}
if (_interruptPin != RH_INVALID_PIN)
{
// Add by Adrien van den Bossche <vandenbo@univ-tlse2.fr> for Teensy
// ARM M4 requires the below. else pin interrupt doesn't work properly.
// On all other platforms, its innocuous, belt and braces
pinMode(_interruptPin, INPUT);
// Set up interrupt handler
// Since there are a limited number of interrupt glue functions isr*() available,
// we can only support a limited number of devices simultaneously
// ON some devices, notably most Arduinos, the interrupt pin passed in is actually the
// interrupt number. You have to figure out the interruptnumber-to-interruptpin mapping
// yourself based on knwledge of what Arduino board you are running on.
if (_myInterruptIndex == 0xff)
{
// First run, no interrupt allocated yet
if (_interruptCount <= RH_RF95_NUM_INTERRUPTS)
_myInterruptIndex = _interruptCount++;
else
return false; // Too many devices, not enough interrupt vectors
}
_deviceForInterrupt[_myInterruptIndex] = this;
if (_myInterruptIndex == 0)
attachInterrupt(interruptNumber, isr0, RISING);
else if (_myInterruptIndex == 1)
attachInterrupt(interruptNumber, isr1, RISING);
else if (_myInterruptIndex == 2)
attachInterrupt(interruptNumber, isr2, RISING);
else
return false; // Too many devices, not enough interrupt vectors
}
// Set up FIFO
// We configure so that we can use the entire 256 byte FIFO for either receive
// or transmit, but not both at the same time
spiWrite(RH_RF95_REG_0E_FIFO_TX_BASE_ADDR, 0);
spiWrite(RH_RF95_REG_0F_FIFO_RX_BASE_ADDR, 0);
// Packet format is preamble + explicit-header + payload + crc
// Explicit Header Mode
// payload is TO + FROM + ID + FLAGS + message data
// RX mode is implmented with RXCONTINUOUS
// max message data length is 255 - 4 = 251 octets
setModeIdle();
// Set up default configuration
// No Sync Words in LORA mode.
setModemConfig(Bw125Cr45Sf128); // Radio default
// setModemConfig(Bw125Cr48Sf4096); // slow and reliable?
setPreambleLength(8); // Default is 8
// An innocuous ISM frequency, same as RF22's
setFrequency(434.0);
// Lowish power
setTxPower(13);
return true;
}
// C++ level interrupt handler for this instance
// LORA is unusual in that it has several interrupt lines, and not a single, combined one.
// On MiniWirelessLoRa, only one of the several interrupt lines (DI0) from the RFM95 is usefuly
// connnected to the processor.
// We use this to get RxDone and TxDone interrupts
void RH_RF95::handleInterrupt()
{
RH_MUTEX_LOCK(lock); // Multithreading support
// we need the RF95 IRQ to be level triggered, or we ……have slim chance of missing events
// https://github.com/geeksville/Meshtastic-esp32/commit/78470ed3f59f5c84fbd1325bcff1fd95b2b20183
// Read the interrupt register
uint8_t irq_flags = spiRead(RH_RF95_REG_12_IRQ_FLAGS);
// Read the RegHopChannel register to check if CRC presence is signalled
// in the header. If not it might be a stray (noise) packet.*
uint8_t hop_channel = spiRead(RH_RF95_REG_1C_HOP_CHANNEL);
// Serial.println(irq_flags, HEX);
// Serial.println(_mode, HEX);
// Serial.println(hop_channel, HEX);
// Serial.println(_enableCRC, HEX);
// ack all interrupts,
// Sigh: on some processors, for some unknown reason, doing this only once does not actually
// clear the radio's interrupt flag. So we do it twice. Why? (kevinh - I think the root cause we want level
// triggered interrupts here - not edge. Because edge allows us to miss handling secondard interrupts that occurred
// while this ISR was running. Better to instead, configure the interrupts as level triggered and clear pending
// at the _beginning_ of the ISR. If any interrupts occur while handling the ISR, the signal will remain asserted and
// our ISR will be reinvoked to handle that case)
// kevinh: turn this off until root cause is known, because it can cause missed interrupts!
// spiWrite(RH_RF95_REG_12_IRQ_FLAGS, 0xff); // Clear all IRQ flags
spiWrite(RH_RF95_REG_12_IRQ_FLAGS, 0xff); // Clear all IRQ flags
// error if:
// timeout
// bad CRC
// CRC is required but it is not present
if (_mode == RHModeRx
&& ( (irq_flags & (RH_RF95_RX_TIMEOUT | RH_RF95_PAYLOAD_CRC_ERROR))
|| (_enableCRC && !(hop_channel & RH_RF95_RX_PAYLOAD_CRC_IS_ON)) ))
// if (_mode == RHModeRx && irq_flags & (RH_RF95_RX_TIMEOUT | RH_RF95_PAYLOAD_CRC_ERROR))
{
// Serial.println("E");
_rxBad++;
clearRxBuf();
}
// It is possible to get RX_DONE and CRC_ERROR and VALID_HEADER all at once
// so this must be an else
else if (_mode == RHModeRx && irq_flags & RH_RF95_RX_DONE)
{
// Packet received, no CRC error
// Serial.println("R");
// Have received a packet
uint8_t len = spiRead(RH_RF95_REG_13_RX_NB_BYTES);
// Reset the fifo read ptr to the beginning of the packet
spiWrite(RH_RF95_REG_0D_FIFO_ADDR_PTR, spiRead(RH_RF95_REG_10_FIFO_RX_CURRENT_ADDR));
spiBurstRead(RH_RF95_REG_00_FIFO, _buf, len);
_bufLen = len;
// Remember the last signal to noise ratio, LORA mode
// Per page 111, SX1276/77/78/79 datasheet
_lastSNR = (int8_t)spiRead(RH_RF95_REG_19_PKT_SNR_VALUE) / 4;
// Remember the RSSI of this packet, LORA mode
// this is according to the doc, but is it really correct?
// weakest receiveable signals are reported RSSI at about -66
_lastRssi = spiRead(RH_RF95_REG_1A_PKT_RSSI_VALUE);
// Adjust the RSSI, datasheet page 87
if (_lastSNR < 0)
_lastRssi = _lastRssi + _lastSNR;
else
_lastRssi = (int)_lastRssi * 16 / 15;
if (_usingHFport)
_lastRssi -= 157;
else
_lastRssi -= 164;
// We have received a message.
validateRxBuf();
if (_rxBufValid)
setModeIdle(); // Got one
}
else if (_mode == RHModeTx && irq_flags & RH_RF95_TX_DONE)
{
// Serial.println("T");
_txGood++;
setModeIdle();
}
else if (_mode == RHModeCad && irq_flags & RH_RF95_CAD_DONE)
{
// Serial.println("C");
_cad = irq_flags & RH_RF95_CAD_DETECTED;
setModeIdle();
}
else
{
// Serial.println("?");
}
// Sigh: on some processors, for some unknown reason, doing this only once does not actually
// clear the radio's interrupt flag. So we do it twice. Why?
// spiWrite(RH_RF95_REG_12_IRQ_FLAGS, 0xff); // Clear all IRQ flags
// spiWrite(RH_RF95_REG_12_IRQ_FLAGS, 0xff); // Clear all IRQ flags
RH_MUTEX_UNLOCK(lock);
}
// These are low level functions that call the interrupt handler for the correct
// instance of RH_RF95.
// 3 interrupts allows us to have 3 different devices
void RH_INTERRUPT_ATTR RH_RF95::isr0()
{
if (_deviceForInterrupt[0])
_deviceForInterrupt[0]->handleInterrupt();
}
void RH_INTERRUPT_ATTR RH_RF95::isr1()
{
if (_deviceForInterrupt[1])
_deviceForInterrupt[1]->handleInterrupt();
}
void RH_INTERRUPT_ATTR RH_RF95::isr2()
{
if (_deviceForInterrupt[2])
_deviceForInterrupt[2]->handleInterrupt();
}
// Check whether the latest received message is complete and uncorrupted
void RH_RF95::validateRxBuf()
{
if (_bufLen < 4)
return; // Too short to be a real message
// Extract the 4 headers
_rxHeaderTo = _buf[0];
_rxHeaderFrom = _buf[1];
_rxHeaderId = _buf[2];
_rxHeaderFlags = _buf[3];
if (_promiscuous ||
_rxHeaderTo == _thisAddress ||
_rxHeaderTo == RH_BROADCAST_ADDRESS)
{
_rxGood++;
_rxBufValid = true;
}
}
bool RH_RF95::available()
{
RH_MUTEX_LOCK(lock); // Multithreading support
if (_mode == RHModeTx)
{
RH_MUTEX_UNLOCK(lock);
return false;
}
setModeRx();
RH_MUTEX_UNLOCK(lock);
return _rxBufValid; // Will be set by the interrupt handler when a good message is received
}
void RH_RF95::clearRxBuf()
{
ATOMIC_BLOCK_START;
_rxBufValid = false;
_bufLen = 0;
ATOMIC_BLOCK_END;
}
bool RH_RF95::recv(uint8_t* buf, uint8_t* len)
{
if (!available())
return false;
RH_MUTEX_LOCK(lock); // Multithread support
if (buf && len)
{
ATOMIC_BLOCK_START;
// Skip the 4 headers that are at the beginning of the rxBuf
if (*len > _bufLen-RH_RF95_HEADER_LEN)
*len = _bufLen-RH_RF95_HEADER_LEN;
memcpy(buf, _buf+RH_RF95_HEADER_LEN, *len);
ATOMIC_BLOCK_END;
}
clearRxBuf(); // This message accepted and cleared
RH_MUTEX_UNLOCK(lock);
return true;
}
bool RH_RF95::send(const uint8_t* data, uint8_t len)
{
if (len > RH_RF95_MAX_MESSAGE_LEN)
return false;
waitPacketSent(); // Make sure we dont interrupt an outgoing message
setModeIdle();
if (!waitCAD())
return false; // Check channel activity
// Position at the beginning of the FIFO
spiWrite(RH_RF95_REG_0D_FIFO_ADDR_PTR, 0);
// The headers
spiWrite(RH_RF95_REG_00_FIFO, _txHeaderTo);
spiWrite(RH_RF95_REG_00_FIFO, _txHeaderFrom);
spiWrite(RH_RF95_REG_00_FIFO, _txHeaderId);
spiWrite(RH_RF95_REG_00_FIFO, _txHeaderFlags);
// The message data
spiBurstWrite(RH_RF95_REG_00_FIFO, data, len);
spiWrite(RH_RF95_REG_22_PAYLOAD_LENGTH, len + RH_RF95_HEADER_LEN);
RH_MUTEX_LOCK(lock); // Multithreading support
setModeTx(); // Start the transmitter
RH_MUTEX_UNLOCK(lock);
// when Tx is done, interruptHandler will fire and radio mode will return to STANDBY
return true;
}
bool RH_RF95::printRegisters()
{
#ifdef RH_HAVE_SERIAL
uint8_t registers[] = { 0x01, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x014, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x4b};
uint8_t i;
for (i = 0; i < sizeof(registers); i++)
{
Serial.print(registers[i], HEX);
Serial.print(": ");
Serial.println(spiRead(registers[i]), HEX);
}
#endif
return true;
}
uint8_t RH_RF95::maxMessageLength()
{
return RH_RF95_MAX_MESSAGE_LEN;
}
bool RH_RF95::setFrequency(float centre)
{
// Frf = FRF / FSTEP
uint32_t frf = (centre * 1000000.0) / RH_RF95_FSTEP;
spiWrite(RH_RF95_REG_06_FRF_MSB, (frf >> 16) & 0xff);
spiWrite(RH_RF95_REG_07_FRF_MID, (frf >> 8) & 0xff);
spiWrite(RH_RF95_REG_08_FRF_LSB, frf & 0xff);
_usingHFport = (centre >= 779.0);
return true;
}
void RH_RF95::setModeIdle()
{
if (_mode != RHModeIdle)
{
modeWillChange(RHModeIdle);
spiWrite(RH_RF95_REG_01_OP_MODE, RH_RF95_MODE_STDBY);
_mode = RHModeIdle;
}
}
bool RH_RF95::sleep()
{
if (_mode != RHModeSleep)
{
modeWillChange(RHModeSleep);
spiWrite(RH_RF95_REG_01_OP_MODE, RH_RF95_MODE_SLEEP);
_mode = RHModeSleep;
}
return true;
}
void RH_RF95::setModeRx()
{
if (_mode != RHModeRx)
{
modeWillChange(RHModeRx);
spiWrite(RH_RF95_REG_01_OP_MODE, RH_RF95_MODE_RXCONTINUOUS);
spiWrite(RH_RF95_REG_40_DIO_MAPPING1, 0x00); // Interrupt on RxDone
_mode = RHModeRx;
}
}
void RH_RF95::setModeTx()
{
if (_mode != RHModeTx)
{
modeWillChange(RHModeTx);
spiWrite(RH_RF95_REG_01_OP_MODE, RH_RF95_MODE_TX);
spiWrite(RH_RF95_REG_40_DIO_MAPPING1, 0x40); // Interrupt on TxDone
_mode = RHModeTx;
}
}
void RH_RF95::setTxPower(int8_t power, bool useRFO)
{
_useRFO = useRFO;
// Sigh, different behaviours depending on whether the module use PA_BOOST or the RFO pin
// for the transmitter output
if (useRFO)
{
if (power > 15)
power = 15;
if (power < 0)
power = 0;
// Set the MaxPower register to 0x7 => MaxPower = 10.8 + 0.6 * 7 = 15dBm
// So Pout = Pmax - (15 - power) = 15 - 15 + power
spiWrite(RH_RF95_REG_09_PA_CONFIG, RH_RF95_MAX_POWER | power);
spiWrite(RH_RF95_REG_4D_PA_DAC, RH_RF95_PA_DAC_DISABLE);
}
else
{
if (power > 20)
power = 20;
if (power < 2)
power = 2;
// For RH_RF95_PA_DAC_ENABLE, manual says '+20dBm on PA_BOOST when OutputPower=0xf'
// RH_RF95_PA_DAC_ENABLE actually adds about 3dBm to all power levels. We will use it
// for 8, 19 and 20dBm
if (power > 17)
{
spiWrite(RH_RF95_REG_4D_PA_DAC, RH_RF95_PA_DAC_ENABLE);
power -= 3;
}
else
{
spiWrite(RH_RF95_REG_4D_PA_DAC, RH_RF95_PA_DAC_DISABLE);
}
// RFM95/96/97/98 does not have RFO pins connected to anything. Only PA_BOOST
// pin is connected, so must use PA_BOOST
// Pout = 2 + OutputPower (+3dBm if DAC enabled)
spiWrite(RH_RF95_REG_09_PA_CONFIG, RH_RF95_PA_SELECT | (power-2));
}
}
// Sets registers from a canned modem configuration structure
void RH_RF95::setModemRegisters(const ModemConfig* config)
{
spiWrite(RH_RF95_REG_1D_MODEM_CONFIG1, config->reg_1d);
spiWrite(RH_RF95_REG_1E_MODEM_CONFIG2, config->reg_1e);
spiWrite(RH_RF95_REG_26_MODEM_CONFIG3, config->reg_26);
}
// Set one of the canned FSK Modem configs
// Returns true if its a valid choice
bool RH_RF95::setModemConfig(ModemConfigChoice index)
{
if (index > (signed int)(sizeof(MODEM_CONFIG_TABLE) / sizeof(ModemConfig)))
return false;
ModemConfig cfg;
memcpy_P(&cfg, &MODEM_CONFIG_TABLE[index], sizeof(RH_RF95::ModemConfig));
setModemRegisters(&cfg);
return true;
}
void RH_RF95::setPreambleLength(uint16_t bytes)
{
spiWrite(RH_RF95_REG_20_PREAMBLE_MSB, bytes >> 8);
spiWrite(RH_RF95_REG_21_PREAMBLE_LSB, bytes & 0xff);
}
bool RH_RF95::isChannelActive()
{
// Set mode RHModeCad
if (_mode != RHModeCad)
{
modeWillChange(RHModeCad);
spiWrite(RH_RF95_REG_01_OP_MODE, RH_RF95_MODE_CAD);
spiWrite(RH_RF95_REG_40_DIO_MAPPING1, 0x80); // Interrupt on CadDone
_mode = RHModeCad;
}
while (_mode == RHModeCad)
YIELD;
return _cad;
}
void RH_RF95::enableTCXO(bool on)
{
if (on)
{
while ((spiRead(RH_RF95_REG_4B_TCXO) & RH_RF95_TCXO_TCXO_INPUT_ON) != RH_RF95_TCXO_TCXO_INPUT_ON)
{
sleep();
spiWrite(RH_RF95_REG_4B_TCXO, (spiRead(RH_RF95_REG_4B_TCXO) | RH_RF95_TCXO_TCXO_INPUT_ON));
}
}
else
{
while ((spiRead(RH_RF95_REG_4B_TCXO) & RH_RF95_TCXO_TCXO_INPUT_ON))
{
sleep();
spiWrite(RH_RF95_REG_4B_TCXO, (spiRead(RH_RF95_REG_4B_TCXO) & ~RH_RF95_TCXO_TCXO_INPUT_ON));
}
}
}
// From section 4.1.5 of SX1276/77/78/79
// Ferror = FreqError * 2**24 * BW / Fxtal / 500
int RH_RF95::frequencyError()
{
int32_t freqerror = 0;
// Convert 2.5 bytes (5 nibbles, 20 bits) to 32 bit signed int
// Caution: some C compilers make errors with eg:
// freqerror = spiRead(RH_RF95_REG_28_FEI_MSB) << 16
// so we go more carefully.
freqerror = spiRead(RH_RF95_REG_28_FEI_MSB);
freqerror <<= 8;
freqerror |= spiRead(RH_RF95_REG_29_FEI_MID);
freqerror <<= 8;
freqerror |= spiRead(RH_RF95_REG_2A_FEI_LSB);
// Sign extension into top 3 nibbles
if (freqerror & 0x80000)
freqerror |= 0xfff00000;
int error = 0; // In hertz
float bw_tab[] = {7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250, 500};
uint8_t bwindex = spiRead(RH_RF95_REG_1D_MODEM_CONFIG1) >> 4;
if (bwindex < (sizeof(bw_tab) / sizeof(float)))
error = (float)freqerror * bw_tab[bwindex] * ((float)(1L << 24) / (float)RH_RF95_FXOSC / 500.0);
// else not defined
return error;
}
int RH_RF95::lastSNR()
{
return _lastSNR;
}
///////////////////////////////////////////////////
//
// additions below by Brian Norman 9th Nov 2018
// brian.n.norman@gmail.com
//
// Routines intended to make changing BW, SF and CR
// a bit more intuitive
//
///////////////////////////////////////////////////
void RH_RF95::setSpreadingFactor(uint8_t sf)
{
if (sf <= 6)
sf = RH_RF95_SPREADING_FACTOR_64CPS;
else if (sf == 7)
sf = RH_RF95_SPREADING_FACTOR_128CPS;
else if (sf == 8)
sf = RH_RF95_SPREADING_FACTOR_256CPS;
else if (sf == 9)
sf = RH_RF95_SPREADING_FACTOR_512CPS;
else if (sf == 10)
sf = RH_RF95_SPREADING_FACTOR_1024CPS;
else if (sf == 11)
sf = RH_RF95_SPREADING_FACTOR_2048CPS;
else if (sf >= 12)
sf = RH_RF95_SPREADING_FACTOR_4096CPS;
// set the new spreading factor
spiWrite(RH_RF95_REG_1E_MODEM_CONFIG2, (spiRead(RH_RF95_REG_1E_MODEM_CONFIG2) & ~RH_RF95_SPREADING_FACTOR) | sf);
// check if Low data Rate bit should be set or cleared
setLowDatarate();
}
void RH_RF95::setSignalBandwidth(long sbw)
{
uint8_t bw; //register bit pattern
if (sbw <= 7800)
bw = RH_RF95_BW_7_8KHZ;
else if (sbw <= 10400)
bw = RH_RF95_BW_10_4KHZ;
else if (sbw <= 15600)
bw = RH_RF95_BW_15_6KHZ ;
else if (sbw <= 20800)
bw = RH_RF95_BW_20_8KHZ;
else if (sbw <= 31250)
bw = RH_RF95_BW_31_25KHZ;
else if (sbw <= 41700)
bw = RH_RF95_BW_41_7KHZ;
else if (sbw <= 62500)
bw = RH_RF95_BW_62_5KHZ;
else if (sbw <= 125000)
bw = RH_RF95_BW_125KHZ;
else if (sbw <= 250000)
bw = RH_RF95_BW_250KHZ;
else
bw = RH_RF95_BW_500KHZ;
// top 4 bits of reg 1D control bandwidth
spiWrite(RH_RF95_REG_1D_MODEM_CONFIG1, (spiRead(RH_RF95_REG_1D_MODEM_CONFIG1) & ~RH_RF95_BW) | bw);
// check if low data rate bit should be set or cleared
setLowDatarate();
}
void RH_RF95::setCodingRate4(uint8_t denominator)
{
int cr = RH_RF95_CODING_RATE_4_5;
// if (denominator <= 5)
// cr = RH_RF95_CODING_RATE_4_5;
if (denominator == 6)
cr = RH_RF95_CODING_RATE_4_6;
else if (denominator == 7)
cr = RH_RF95_CODING_RATE_4_7;
else if (denominator >= 8)
cr = RH_RF95_CODING_RATE_4_8;
// CR is bits 3..1 of RH_RF95_REG_1D_MODEM_CONFIG1
spiWrite(RH_RF95_REG_1D_MODEM_CONFIG1, (spiRead(RH_RF95_REG_1D_MODEM_CONFIG1) & ~RH_RF95_CODING_RATE) | cr);
}
void RH_RF95::setLowDatarate()
{
// called after changing bandwidth and/or spreading factor
// Semtech modem design guide AN1200.13 says
// "To avoid issues surrounding drift of the crystal reference oscillator due to either temperature change
// or motion,the low data rate optimization bit is used. Specifically for 125 kHz bandwidth and SF = 11 and 12,
// this adds a small overhead to increase robustness to reference frequency variations over the timescale of the LoRa packet."
// read current value for BW and SF
uint8_t BW = spiRead(RH_RF95_REG_1D_MODEM_CONFIG1) >> 4; // bw is in bits 7..4
uint8_t SF = spiRead(RH_RF95_REG_1E_MODEM_CONFIG2) >> 4; // sf is in bits 7..4
// calculate symbol time (see Semtech AN1200.22 section 4)
float bw_tab[] = {7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000, 500000};
float bandwidth = bw_tab[BW];
float symbolTime = 1000.0 * pow(2, SF) / bandwidth; // ms
// the symbolTime for SF 11 BW 125 is 16.384ms.
// and, according to this :-
// https://www.thethingsnetwork.org/forum/t/a-point-to-note-lora-low-data-rate-optimisation-flag/12007
// the LDR bit should be set if the Symbol Time is > 16ms
// So the threshold used here is 16.0ms
// the LDR is bit 3 of RH_RF95_REG_26_MODEM_CONFIG3
uint8_t current = spiRead(RH_RF95_REG_26_MODEM_CONFIG3) & ~RH_RF95_LOW_DATA_RATE_OPTIMIZE; // mask off the LDR bit
if (symbolTime > 16.0)
spiWrite(RH_RF95_REG_26_MODEM_CONFIG3, current | RH_RF95_LOW_DATA_RATE_OPTIMIZE);
else
spiWrite(RH_RF95_REG_26_MODEM_CONFIG3, current);
}
void RH_RF95::setPayloadCRC(bool on)
{
// Payload CRC is bit 2 of register 1E
uint8_t current = spiRead(RH_RF95_REG_1E_MODEM_CONFIG2) & ~RH_RF95_PAYLOAD_CRC_ON; // mask off the CRC
if (on)
spiWrite(RH_RF95_REG_1E_MODEM_CONFIG2, current | RH_RF95_PAYLOAD_CRC_ON);
else
spiWrite(RH_RF95_REG_1E_MODEM_CONFIG2, current);
_enableCRC = on;
}
uint8_t RH_RF95::getDeviceVersion()
{
_deviceVersion = spiRead(RH_RF95_REG_42_VERSION);
return _deviceVersion;
}
// RH_RF95.h
//
// Definitions for HopeRF LoRa radios per:
// http://www.hoperf.com/upload/rf/RFM95_96_97_98W.pdf
// http://www.hoperf.cn/upload/rfchip/RF96_97_98.pdf
//
// Author: Mike McCauley (mikem@airspayce.com)
// Copyright (C) 2014 Mike McCauley
// $Id: RH_RF95.h,v 1.26 2020/06/15 23:39:39 mikem Exp $
//
#ifndef RH_RF95_h
#define RH_RF95_h
#include <RHSPIDriver.h>
// This is the maximum number of interrupts the driver can support
// Most Arduinos can handle 2, Megas can handle more
#define RH_RF95_NUM_INTERRUPTS 3
// Max number of octets the LORA Rx/Tx FIFO can hold
#define RH_RF95_FIFO_SIZE 255
// This is the maximum number of bytes that can be carried by the LORA.
// We use some for headers, keeping fewer for RadioHead messages
#define RH_RF95_MAX_PAYLOAD_LEN RH_RF95_FIFO_SIZE
// The length of the headers we add.
// The headers are inside the LORA's payload
#define RH_RF95_HEADER_LEN 4
// This is the maximum message length that can be supported by this driver.
// Can be pre-defined to a smaller size (to save SRAM) prior to including this header
// Here we allow for 1 byte message length, 4 bytes headers, user data and 2 bytes of FCS
#ifndef RH_RF95_MAX_MESSAGE_LEN
#define RH_RF95_MAX_MESSAGE_LEN (RH_RF95_MAX_PAYLOAD_LEN - RH_RF95_HEADER_LEN)
#endif
// The crystal oscillator frequency of the module
#define RH_RF95_FXOSC 32000000.0
// The Frequency Synthesizer step = RH_RF95_FXOSC / 2^^19
#define RH_RF95_FSTEP (RH_RF95_FXOSC / 524288)
// Register names (LoRa Mode, from table 85)
#define RH_RF95_REG_00_FIFO 0x00
#define RH_RF95_REG_01_OP_MODE 0x01
#define RH_RF95_REG_02_RESERVED 0x02
#define RH_RF95_REG_03_RESERVED 0x03
#define RH_RF95_REG_04_RESERVED 0x04
#define RH_RF95_REG_05_RESERVED 0x05
#define RH_RF95_REG_06_FRF_MSB 0x06
#define RH_RF95_REG_07_FRF_MID 0x07
#define RH_RF95_REG_08_FRF_LSB 0x08
#define RH_RF95_REG_09_PA_CONFIG 0x09
#define RH_RF95_REG_0A_PA_RAMP 0x0a
#define RH_RF95_REG_0B_OCP 0x0b
#define RH_RF95_REG_0C_LNA 0x0c
#define RH_RF95_REG_0D_FIFO_ADDR_PTR 0x0d
#define RH_RF95_REG_0E_FIFO_TX_BASE_ADDR 0x0e
#define RH_RF95_REG_0F_FIFO_RX_BASE_ADDR 0x0f
#define RH_RF95_REG_10_FIFO_RX_CURRENT_ADDR 0x10
#define RH_RF95_REG_11_IRQ_FLAGS_MASK 0x11
#define RH_RF95_REG_12_IRQ_FLAGS 0x12
#define RH_RF95_REG_13_RX_NB_BYTES 0x13
#define RH_RF95_REG_14_RX_HEADER_CNT_VALUE_MSB 0x14
#define RH_RF95_REG_15_RX_HEADER_CNT_VALUE_LSB 0x15
#define RH_RF95_REG_16_RX_PACKET_CNT_VALUE_MSB 0x16
#define RH_RF95_REG_17_RX_PACKET_CNT_VALUE_LSB 0x17
#define RH_RF95_REG_18_MODEM_STAT 0x18
#define RH_RF95_REG_19_PKT_SNR_VALUE 0x19
#define RH_RF95_REG_1A_PKT_RSSI_VALUE 0x1a
#define RH_RF95_REG_1B_RSSI_VALUE 0x1b
#define RH_RF95_REG_1C_HOP_CHANNEL 0x1c
#define RH_RF95_REG_1D_MODEM_CONFIG1 0x1d
#define RH_RF95_REG_1E_MODEM_CONFIG2 0x1e
#define RH_RF95_REG_1F_SYMB_TIMEOUT_LSB 0x1f
#define RH_RF95_REG_20_PREAMBLE_MSB 0x20
#define RH_RF95_REG_21_PREAMBLE_LSB 0x21
#define RH_RF95_REG_22_PAYLOAD_LENGTH 0x22
#define RH_RF95_REG_23_MAX_PAYLOAD_LENGTH 0x23
#define RH_RF95_REG_24_HOP_PERIOD 0x24
#define RH_RF95_REG_25_FIFO_RX_BYTE_ADDR 0x25
#define RH_RF95_REG_26_MODEM_CONFIG3 0x26
#define RH_RF95_REG_27_PPM_CORRECTION 0x27
#define RH_RF95_REG_28_FEI_MSB 0x28
#define RH_RF95_REG_29_FEI_MID 0x29
#define RH_RF95_REG_2A_FEI_LSB 0x2a
#define RH_RF95_REG_2C_RSSI_WIDEBAND 0x2c
#define RH_RF95_REG_31_DETECT_OPTIMIZE 0x31
#define RH_RF95_REG_33_INVERT_IQ 0x33
#define RH_RF95_REG_37_DETECTION_THRESHOLD 0x37
#define RH_RF95_REG_39_SYNC_WORD 0x39
#define RH_RF95_REG_40_DIO_MAPPING1 0x40
#define RH_RF95_REG_41_DIO_MAPPING2 0x41
#define RH_RF95_REG_42_VERSION 0x42
#define RH_RF95_REG_4B_TCXO 0x4b
#define RH_RF95_REG_4D_PA_DAC 0x4d
#define RH_RF95_REG_5B_FORMER_TEMP 0x5b
#define RH_RF95_REG_61_AGC_REF 0x61
#define RH_RF95_REG_62_AGC_THRESH1 0x62
#define RH_RF95_REG_63_AGC_THRESH2 0x63
#define RH_RF95_REG_64_AGC_THRESH3 0x64
// RH_RF95_REG_01_OP_MODE 0x01
#define RH_RF95_LONG_RANGE_MODE 0x80
#define RH_RF95_ACCESS_SHARED_REG 0x40
#define RH_RF95_LOW_FREQUENCY_MODE 0x08
#define RH_RF95_MODE 0x07
#define RH_RF95_MODE_SLEEP 0x00
#define RH_RF95_MODE_STDBY 0x01
#define RH_RF95_MODE_FSTX 0x02
#define RH_RF95_MODE_TX 0x03
#define RH_RF95_MODE_FSRX 0x04
#define RH_RF95_MODE_RXCONTINUOUS 0x05
#define RH_RF95_MODE_RXSINGLE 0x06
#define RH_RF95_MODE_CAD 0x07
// RH_RF95_REG_09_PA_CONFIG 0x09
#define RH_RF95_PA_SELECT 0x80
#define RH_RF95_MAX_POWER 0x70
#define RH_RF95_OUTPUT_POWER 0x0f
// RH_RF95_REG_0A_PA_RAMP 0x0a
#define RH_RF95_LOW_PN_TX_PLL_OFF 0x10
#define RH_RF95_PA_RAMP 0x0f
#define RH_RF95_PA_RAMP_3_4MS 0x00
#define RH_RF95_PA_RAMP_2MS 0x01
#define RH_RF95_PA_RAMP_1MS 0x02
#define RH_RF95_PA_RAMP_500US 0x03
#define RH_RF95_PA_RAMP_250US 0x04
#define RH_RF95_PA_RAMP_125US 0x05
#define RH_RF95_PA_RAMP_100US 0x06
#define RH_RF95_PA_RAMP_62US 0x07
#define RH_RF95_PA_RAMP_50US 0x08
#define RH_RF95_PA_RAMP_40US 0x09
#define RH_RF95_PA_RAMP_31US 0x0a
#define RH_RF95_PA_RAMP_25US 0x0b
#define RH_RF95_PA_RAMP_20US 0x0c
#define RH_RF95_PA_RAMP_15US 0x0d
#define RH_RF95_PA_RAMP_12US 0x0e
#define RH_RF95_PA_RAMP_10US 0x0f
// RH_RF95_REG_0B_OCP 0x0b
#define RH_RF95_OCP_ON 0x20
#define RH_RF95_OCP_TRIM 0x1f
// RH_RF95_REG_0C_LNA 0x0c
#define RH_RF95_LNA_GAIN 0xe0
#define RH_RF95_LNA_GAIN_G1 0x20
#define RH_RF95_LNA_GAIN_G2 0x40
#define RH_RF95_LNA_GAIN_G3 0x60
#define RH_RF95_LNA_GAIN_G4 0x80
#define RH_RF95_LNA_GAIN_G5 0xa0
#define RH_RF95_LNA_GAIN_G6 0xc0
#define RH_RF95_LNA_BOOST_LF 0x18
#define RH_RF95_LNA_BOOST_LF_DEFAULT 0x00
#define RH_RF95_LNA_BOOST_HF 0x03
#define RH_RF95_LNA_BOOST_HF_DEFAULT 0x00
#define RH_RF95_LNA_BOOST_HF_150PC 0x03
// RH_RF95_REG_11_IRQ_FLAGS_MASK 0x11
#define RH_RF95_RX_TIMEOUT_MASK 0x80
#define RH_RF95_RX_DONE_MASK 0x40
#define RH_RF95_PAYLOAD_CRC_ERROR_MASK 0x20
#define RH_RF95_VALID_HEADER_MASK 0x10
#define RH_RF95_TX_DONE_MASK 0x08
#define RH_RF95_CAD_DONE_MASK 0x04
#define RH_RF95_FHSS_CHANGE_CHANNEL_MASK 0x02
#define RH_RF95_CAD_DETECTED_MASK 0x01
// RH_RF95_REG_12_IRQ_FLAGS 0x12
#define RH_RF95_RX_TIMEOUT 0x80
#define RH_RF95_RX_DONE 0x40
#define RH_RF95_PAYLOAD_CRC_ERROR 0x20
#define RH_RF95_VALID_HEADER 0x10
#define RH_RF95_TX_DONE 0x08
#define RH_RF95_CAD_DONE 0x04
#define RH_RF95_FHSS_CHANGE_CHANNEL 0x02
#define RH_RF95_CAD_DETECTED 0x01
// RH_RF95_REG_18_MODEM_STAT 0x18
#define RH_RF95_RX_CODING_RATE 0xe0
#define RH_RF95_MODEM_STATUS_CLEAR 0x10
#define RH_RF95_MODEM_STATUS_HEADER_INFO_VALID 0x08
#define RH_RF95_MODEM_STATUS_RX_ONGOING 0x04
#define RH_RF95_MODEM_STATUS_SIGNAL_SYNCHRONIZED 0x02
#define RH_RF95_MODEM_STATUS_SIGNAL_DETECTED 0x01
// RH_RF95_REG_1C_HOP_CHANNEL 0x1c
#define RH_RF95_PLL_TIMEOUT 0x80
#define RH_RF95_RX_PAYLOAD_CRC_IS_ON 0x40
#define RH_RF95_FHSS_PRESENT_CHANNEL 0x3f
// RH_RF95_REG_1D_MODEM_CONFIG1 0x1d
#define RH_RF95_BW 0xf0
#define RH_RF95_BW_7_8KHZ 0x00
#define RH_RF95_BW_10_4KHZ 0x10
#define RH_RF95_BW_15_6KHZ 0x20
#define RH_RF95_BW_20_8KHZ 0x30
#define RH_RF95_BW_31_25KHZ 0x40
#define RH_RF95_BW_41_7KHZ 0x50
#define RH_RF95_BW_62_5KHZ 0x60
#define RH_RF95_BW_125KHZ 0x70
#define RH_RF95_BW_250KHZ 0x80
#define RH_RF95_BW_500KHZ 0x90
#define RH_RF95_CODING_RATE 0x0e
#define RH_RF95_CODING_RATE_4_5 0x02
#define RH_RF95_CODING_RATE_4_6 0x04
#define RH_RF95_CODING_RATE_4_7 0x06
#define RH_RF95_CODING_RATE_4_8 0x08
#define RH_RF95_IMPLICIT_HEADER_MODE_ON 0x01
// RH_RF95_REG_1E_MODEM_CONFIG2 0x1e
#define RH_RF95_SPREADING_FACTOR 0xf0
#define RH_RF95_SPREADING_FACTOR_64CPS 0x60
#define RH_RF95_SPREADING_FACTOR_128CPS 0x70
#define RH_RF95_SPREADING_FACTOR_256CPS 0x80
#define RH_RF95_SPREADING_FACTOR_512CPS 0x90
#define RH_RF95_SPREADING_FACTOR_1024CPS 0xa0
#define RH_RF95_SPREADING_FACTOR_2048CPS 0xb0
#define RH_RF95_SPREADING_FACTOR_4096CPS 0xc0
#define RH_RF95_TX_CONTINUOUS_MODE 0x08
#define RH_RF95_PAYLOAD_CRC_ON 0x04
#define RH_RF95_SYM_TIMEOUT_MSB 0x03
// RH_RF95_REG_26_MODEM_CONFIG3
#define RH_RF95_MOBILE_NODE 0x08 // HopeRF term
#define RH_RF95_LOW_DATA_RATE_OPTIMIZE 0x08 // Semtechs term
#define RH_RF95_AGC_AUTO_ON 0x04
// RH_RF95_REG_4B_TCXO 0x4b
#define RH_RF95_TCXO_TCXO_INPUT_ON 0x10
// RH_RF95_REG_4D_PA_DAC 0x4d
#define RH_RF95_PA_DAC_DISABLE 0x04
#define RH_RF95_PA_DAC_ENABLE 0x07
/////////////////////////////////////////////////////////////////////
/// \class RH_RF95 RH_RF95.h <RH_RF95.h>
/// \brief Driver to send and receive unaddressed, unreliable datagrams via a LoRa
/// capable radio transceiver.
///
/// For an excellent discussion of LoRa range and modulations, see
/// https://medium.com/home-wireless/testing-lora-radios-with-the-limesdr-mini-part-2-37fa481217ff
///
/// For Semtech SX1276/77/78/79 and HopeRF RF95/96/97/98 and other similar LoRa capable radios.
/// Based on http://www.hoperf.com/upload/rf/RFM95_96_97_98W.pdf
/// and http://www.hoperf.cn/upload/rfchip/RF96_97_98.pdf
/// and http://www.semtech.com/images/datasheet/LoraDesignGuide_STD.pdf
/// and http://www.semtech.com/images/datasheet/sx1276.pdf
/// and http://www.semtech.com/images/datasheet/sx1276_77_78_79.pdf
/// FSK/GFSK/OOK modes are not (yet) supported.
///
/// Works with
/// - the excellent MiniWirelessLoRa from Anarduino http://www.anarduino.com/miniwireless
/// - The excellent Modtronix inAir4 http://modtronix.com/inair4.html
/// and inAir9 modules http://modtronix.com/inair9.html.
/// - the excellent Rocket Scream Mini Ultra Pro with the RFM95W
/// http://www.rocketscream.com/blog/product/mini-ultra-pro-with-radio/
/// - Lora1276 module from NiceRF http://www.nicerf.com/product_view.aspx?id=99
/// - Adafruit Feather M0 with RFM95
/// - The very fine Talk2 Whisper Node LoRa boards https://wisen.com.au/store/products/whisper-node-lora
/// an Arduino compatible board, which include an on-board RFM95/96 LoRa Radio (Semtech SX1276), external antenna,
/// run on 2xAAA batteries and support low power operations. RF95 examples work without modification.
/// Use Arduino Board Manager to install the Talk2 code support. Upload the code with an FTDI adapter set to 5V.
/// - heltec / TTGO ESP32 LoRa OLED https://www.aliexpress.com/item/Internet-Development-Board-SX1278-ESP32-WIFI-chip-0-96-inch-OLED-Bluetooth-WIFI-Lora-Kit-32/32824535649.html
///
/// \par Overview
///
/// This class provides basic functions for sending and receiving unaddressed,
/// unreliable datagrams of arbitrary length to 251 octets per packet.
///
/// Manager classes may use this class to implement reliable, addressed datagrams and streams,
/// mesh routers, repeaters, translators etc.
///
/// Naturally, for any 2 radios to communicate that must be configured to use the same frequency and
/// modulation scheme.
///
/// This Driver provides an object-oriented interface for sending and receiving data messages with Hope-RF
/// RFM95/96/97/98(W), Semtech SX1276/77/78/79 and compatible radio modules in LoRa mode.
///
/// The Hope-RF (http://www.hoperf.com) RFM95/96/97/98(W) and Semtech SX1276/77/78/79 is a low-cost ISM transceiver
/// chip. It supports FSK, GFSK, OOK over a wide range of frequencies and
/// programmable data rates, and it also supports the proprietary LoRA (Long Range) mode, which
/// is the only mode supported in this RadioHead driver.
///
/// This Driver provides functions for sending and receiving messages of up
/// to 251 octets on any frequency supported by the radio, in a range of
/// predefined Bandwidths, Spreading Factors and Coding Rates. Frequency can be set with
/// 61Hz precision to any frequency from 240.0MHz to 960.0MHz. Caution: most modules only support a more limited
/// range of frequencies due to antenna tuning.
///
/// Up to 2 modules can be connected to an Arduino (3 on a Mega),
/// permitting the construction of translators and frequency changers, etc.
///
/// Support for other features such as transmitter power control etc is
/// also provided.
///
/// Tested on MinWirelessLoRa with arduino-1.0.5
/// on OpenSuSE 13.1.
/// Also tested with Teensy3.1, Modtronix inAir4 and Arduino 1.6.5 on OpenSuSE 13.1
///
/// \par Packet Format
///
/// All messages sent and received by this RH_RF95 Driver conform to this packet format:
///
/// - LoRa mode:
/// - 8 symbol PREAMBLE
/// - Explicit header with header CRC (default CCITT, handled internally by the radio)
/// - 4 octets HEADER: (TO, FROM, ID, FLAGS)
/// - 0 to 251 octets DATA
/// - CRC (default CCITT, handled internally by the radio)
///
/// \par Connecting RFM95/96/97/98 and Semtech SX1276/77/78/79 to Arduino
///
/// We tested with Anarduino MiniWirelessLoRA, which is an Arduino Duemilanove compatible with a RFM96W
/// module on-board. Therefore it needs no connections other than the USB
/// programming connection and an antenna to make it work.
///
/// If you have a bare RFM95/96/97/98 that you want to connect to an Arduino, you
/// might use these connections (untested): CAUTION: you must use a 3.3V type
/// Arduino, otherwise you will also need voltage level shifters between the
/// Arduino and the RFM95. CAUTION, you must also ensure you connect an
/// antenna.
///
/// \code
/// Arduino RFM95/96/97/98
/// GND----------GND (ground in)
/// 3V3----------3.3V (3.3V in)
/// interrupt 0 pin D2-----------DIO0 (interrupt request out)
/// SS pin D10----------NSS (CS chip select in)
/// SCK pin D13----------SCK (SPI clock in)
/// MOSI pin D11----------MOSI (SPI Data in)
/// MISO pin D12----------MISO (SPI Data out)
/// \endcode
/// With these connections, you can then use the default constructor RH_RF95().
/// You can override the default settings for the SS pin and the interrupt in
/// the RH_RF95 constructor if you wish to connect the slave select SS to other
/// than the normal one for your Arduino (D10 for Diecimila, Uno etc and D53
/// for Mega) or the interrupt request to other than pin D2 (Caution,
/// different processors have different constraints as to the pins available
/// for interrupts).
///
/// You can connect a Modtronix inAir4 or inAir9 directly to a 3.3V part such as a Teensy 3.1 like
/// this (tested).
/// \code
/// Teensy inAir4 inAir9
/// GND----------0V (ground in)
/// 3V3----------3.3V (3.3V in)
/// interrupt 0 pin D2-----------D0 (interrupt request out)
/// SS pin D10----------CS (CS chip select in)
/// SCK pin D13----------CK (SPI clock in)
/// MOSI pin D11----------SI (SPI Data in)
/// MISO pin D12----------SO (SPI Data out)
/// \endcode
/// With these connections, you can then use the default constructor RH_RF95().
/// you must also set the transmitter power with useRFO:
/// driver.setTxPower(13, true);
///
/// Note that if you are using Modtronix inAir4 or inAir9,or any other module which uses the
/// transmitter RFO pins and not the PA_BOOST pins
/// that you must configure the power transmitter power for -1 to 14 dBm and with useRFO true.
/// Failure to do that will result in extremely low transmit powers.
///
/// If you have an Arduino M0 Pro from arduino.org,
/// you should note that you cannot use Pin 2 for the interrupt line
/// (Pin 2 is for the NMI only). The same comments apply to Pin 4 on Arduino Zero from arduino.cc.
/// Instead you can use any other pin (we use Pin 3) and initialise RH_RF69 like this:
/// \code
/// // Slave Select is pin 10, interrupt is Pin 3
/// RH_RF95 driver(10, 3);
/// \endcode
/// You can use the same constructor for Arduino Due, and this pinout diagram may be useful:
/// http://www.robgray.com/temp/Due-pinout-WEB.png
///
/// If you have a Rocket Scream Mini Ultra Pro with the RFM95W:
/// - Ensure you have Arduino SAMD board support 1.6.5 or later in Arduino IDE 1.6.8 or later.
/// - The radio SS is hardwired to pin D5 and the DIO0 interrupt to pin D2,
/// so you need to initialise the radio like this:
/// \code
/// RH_RF95 driver(5, 2);
/// \endcode
/// - The name of the serial port on that board is 'SerialUSB', not 'Serial', so this may be helpful at the top of our
/// sample sketches:
/// \code
/// #define Serial SerialUSB
/// \endcode
/// - You also need this in setup before radio initialisation
/// \code
/// // Ensure serial flash is not interfering with radio communication on SPI bus
/// pinMode(4, OUTPUT);
/// digitalWrite(4, HIGH);
/// \endcode
/// - and if you have a 915MHz part, you need this after driver/manager intitalisation:
/// \code
/// rf95.setFrequency(915.0);
/// \endcode
/// which adds up to modifying sample sketches something like:
/// \code
/// #include <SPI.h>
/// #include <RH_RF95.h>
/// RH_RF95 rf95(5, 2); // Rocket Scream Mini Ultra Pro with the RFM95W
/// #define Serial SerialUSB
///
/// void setup()
/// {
/// // Ensure serial flash is not interfering with radio communication on SPI bus
/// pinMode(4, OUTPUT);
/// digitalWrite(4, HIGH);
///
/// Serial.begin(9600);
/// while (!Serial) ; // Wait for serial port to be available
/// if (!rf95.init())
/// Serial.println("init failed");
/// rf95.setFrequency(915.0);
/// }
/// ...
/// \endcode
///
/// For Adafruit Feather M0 with RFM95, construct the driver like this:
/// \code
/// RH_RF95 rf95(8, 3);
/// \endcode
///
/// If you have a talk2 Whisper Node LoRa board with on-board RF95 radio,
/// the example rf95_* sketches work without modification. Initialise the radio like
/// with the default constructor:
/// \code
/// RH_RF95 driver;
/// \endcode
///
/// It is possible to have 2 or more radios connected to one Arduino, provided
/// each radio has its own SS and interrupt line (SCK, SDI and SDO are common
/// to all radios)
///
/// Caution: on some Arduinos such as the Mega 2560, if you set the slave
/// select pin to be other than the usual SS pin (D53 on Mega 2560), you may
/// need to set the usual SS pin to be an output to force the Arduino into SPI
/// master mode.
///
/// Caution: Power supply requirements of the RFM module may be relevant in some circumstances:
/// RFM95/96/97/98 modules are capable of pulling 120mA+ at full power, where Arduino's 3.3V line can
/// give 50mA. You may need to make provision for alternate power supply for
/// the RFM module, especially if you wish to use full transmit power, and/or you have
/// other shields demanding power. Inadequate power for the RFM is likely to cause symptoms such as:
/// - reset's/bootups terminate with "init failed" messages
/// - random termination of communication after 5-30 packets sent/received
/// - "fake ok" state, where initialization passes fluently, but communication doesn't happen
/// - shields hang Arduino boards, especially during the flashing
///
/// \par Interrupts
///
/// The RH_RF95 driver uses interrupts to react to events in the RFM module,
/// such as the reception of a new packet, or the completion of transmission
/// of a packet. The driver configures the radio so the required interrupt is generated by the radio's DIO0 pin.
/// The RH_RF95 driver interrupt service routine reads status from
/// and writes data to the the RFM module via the SPI interface. It is very
/// important therefore, that if you are using the RH_RF95 driver with another
/// SPI based deviced, that you disable interrupts while you transfer data to
/// and from that other device. Use cli() to disable interrupts and sei() to
/// reenable them.
///
/// \par Memory
///
/// The RH_RF95 driver requires non-trivial amounts of memory. The sample
/// programs all compile to about 8kbytes each, which will fit in the
/// flash proram memory of most Arduinos. However, the RAM requirements are
/// more critical. Therefore, you should be vary sparing with RAM use in
/// programs that use the RH_RF95 driver.
///
/// It is often hard to accurately identify when you are hitting RAM limits on Arduino.
/// The symptoms can include:
/// - Mysterious crashes and restarts
/// - Changes in behaviour when seemingly unrelated changes are made (such as adding print() statements)
/// - Hanging
/// - Output from Serial.print() not appearing
///
/// \par Range
///
/// We have made some simple range tests under the following conditions:
/// - rf95_client base station connected to a VHF discone antenna at 8m height above ground
/// - rf95_server mobile connected to 17.3cm 1/4 wavelength antenna at 1m height, no ground plane.
/// - Both configured for 13dBm, 434MHz, Bw = 125 kHz, Cr = 4/8, Sf = 4096chips/symbol, CRC on. Slow+long range
/// - Minimum reported RSSI seen for successful comms was about -91
/// - Range over flat ground through heavy trees and vegetation approx 2km.
/// - At 20dBm (100mW) otherwise identical conditions approx 3km.
/// - At 20dBm, along salt water flat sandy beach, 3.2km.
///
/// It should be noted that at this data rate, a 12 octet message takes 2 seconds to transmit.
///
/// At 20dBm (100mW) with Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on.
/// (Default medium range) in the conditions described above.
/// - Range over flat ground through heavy trees and vegetation approx 2km.
///
/// Caution: the performance of this radio, especially with narrow bandwidths is strongly dependent on the
/// accuracy and stability of the chip clock. HopeRF and Semtech do not appear to
/// recommend bandwidths of less than 62.5 kHz
/// unless you have the optional Temperature Compensated Crystal Oscillator (TCXO) installed and
/// enabled on your radio module. See the refernece manual for more data.
/// Also https://lowpowerlab.com/forum/rf-range-antennas-rfm69-library/lora-library-experiences-range/15/
/// and http://www.semtech.com/images/datasheet/an120014-xo-guidance-lora-modulation.pdf
///
/// \par Transmitter Power
///
/// You can control the transmitter power on the RF transceiver
/// with the RH_RF95::setTxPower() function. The argument can be any of
/// +2 to +20 (for modules that use PA_BOOST)
/// 0 to +15 (for modules that use RFO transmitter pin)
/// The default is 13. Eg:
/// \code
/// driver.setTxPower(10); // use PA_BOOST transmitter pin
/// driver.setTxPower(10, true); // use PA_RFO pin transmitter pin instead of PA_BOOST
/// \endcode
///
/// We have made some actual power measurements against
/// programmed power for Anarduino MiniWirelessLoRa (which has RFM96W-433Mhz installed, and which includes an RF power
/// amp for addition 3dBm of power
/// - MiniWirelessLoRa RFM96W-433Mhz, USB power
/// - 30cm RG316 soldered direct to RFM96W module ANT and GND
/// - SMA connector
/// - 12db attenuator
/// - SMA connector
/// - MiniKits AD8307 HF/VHF Power Head (calibrated against Rohde&Schwartz 806.2020 test set)
/// - Tektronix TDS220 scope to measure the Vout from power head
/// \code
/// Program power Measured Power
/// dBm dBm
/// 2 5
/// 4 7
/// 6 8
/// 8 11
/// 10 13
/// 12 15
/// 14 16
/// 16 18
/// 17 20
/// 18 21
/// 19 22
/// 20 23
/// \endcode
///
/// We have also measured the actual power output from a Modtronix inAir4 http://modtronix.com/inair4.html
/// connected to a Teensy 3.1:
/// Teensy 3.1 this is a 3.3V part, connected directly to:
/// Modtronix inAir4 with SMA antenna connector, connected as above:
/// 10cm SMA-SMA cable
/// - MiniKits AD8307 HF/VHF Power Head (calibrated against Rohde&Schwartz 806.2020 test set)
/// - Tektronix TDS220 scope to measure the Vout from power head
/// \code
/// Program power Measured Power
/// dBm dBm
/// 0 0
/// 2 2
/// 3 4
/// 6 7
/// 8 10
/// 10 13
/// 12 14.2
/// 14 15
/// 15 16
/// \endcode
/// (Caution: we dont claim laboratory accuracy for these power measurements)
/// You would not expect to get anywhere near these powers to air with a simple 1/4 wavelength wire antenna.
class RH_RF95 : public RHSPIDriver
{
public:
/// \brief Defines register values for a set of modem configuration registers
///
/// Defines register values for a set of modem configuration registers
/// that can be passed to setModemRegisters() if none of the choices in
/// ModemConfigChoice suit your need setModemRegisters() writes the
/// register values from this structure to the appropriate registers
/// to set the desired spreading factor, coding rate and bandwidth
typedef struct
{
uint8_t reg_1d; ///< Value for register RH_RF95_REG_1D_MODEM_CONFIG1
uint8_t reg_1e; ///< Value for register RH_RF95_REG_1E_MODEM_CONFIG2
uint8_t reg_26; ///< Value for register RH_RF95_REG_26_MODEM_CONFIG3
} ModemConfig;
/// Choices for setModemConfig() for a selected subset of common
/// data rates. If you need another configuration,
/// determine the necessary settings and call setModemRegisters() with your
/// desired settings. It might be helpful to use the LoRa calculator mentioned in
/// http://www.semtech.com/images/datasheet/LoraDesignGuide_STD.pdf
/// These are indexes into MODEM_CONFIG_TABLE. We strongly recommend you use these symbolic
/// definitions and not their integer equivalents: its possible that new values will be
/// introduced in later versions (though we will try to avoid it).
/// Caution: if you are using slow packet rates and long packets with RHReliableDatagram or subclasses
/// you may need to change the RHReliableDatagram timeout for reliable operations.
/// Caution: for some slow rates nad with ReliableDatagrams you may need to increase the reply timeout
/// with manager.setTimeout() to
/// deal with the long transmission times.
/// Caution: SX1276 family errata suggests alternate settings for some LoRa registers when 500kHz bandwidth
/// is in use. See the Semtech SX1276/77/78 Errata Note. These are not implemented by RH_RF95.
typedef enum
{
Bw125Cr45Sf128 = 0, ///< Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on. Default medium range
Bw500Cr45Sf128, ///< Bw = 500 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on. Fast+short range
Bw31_25Cr48Sf512, ///< Bw = 31.25 kHz, Cr = 4/8, Sf = 512chips/symbol, CRC on. Slow+long range
Bw125Cr48Sf4096, ///< Bw = 125 kHz, Cr = 4/8, Sf = 4096chips/symbol, low data rate, CRC on. Slow+long range
Bw125Cr45Sf2048, ///< Bw = 125 kHz, Cr = 4/5, Sf = 2048chips/symbol, CRC on. Slow+long range
} ModemConfigChoice;
/// Constructor. You can have multiple instances, but each instance must have its own
/// interrupt and slave select pin. After constructing, you must call init() to initialise the interface
/// and the radio module. A maximum of 3 instances can co-exist on one processor, provided there are sufficient
/// distinct interrupt lines, one for each instance.
/// \param[in] slaveSelectPin the Arduino pin number of the output to use to select the RH_RF22 before
/// accessing it. Defaults to the normal SS pin for your Arduino (D10 for Diecimila, Uno etc, D53 for Mega, D10 for Maple)
/// \param[in] interruptPin The interrupt Pin number that is connected to the RFM DIO0 interrupt line.
/// Defaults to pin 2, as required by Anarduino MinWirelessLoRa module.
/// Caution: You must specify an interrupt capable pin.
/// On many Arduino boards, there are limitations as to which pins may be used as interrupts.
/// On Leonardo pins 0, 1, 2 or 3. On Mega2560 pins 2, 3, 18, 19, 20, 21. On Due and Teensy, any digital pin.
/// On Arduino Zero from arduino.cc, any digital pin other than 4.
/// On Arduino M0 Pro from arduino.org, any digital pin other than 2.
/// On other Arduinos pins 2 or 3.
/// See http://arduino.cc/en/Reference/attachInterrupt for more details.
/// On Chipkit Uno32, pins 38, 2, 7, 8, 35.
/// On other boards, any digital pin may be used.
/// \param[in] spi Pointer to the SPI interface object to use.
/// Defaults to the standard Arduino hardware SPI interface
RH_RF95(uint8_t slaveSelectPin = SS, uint8_t interruptPin = 2, RHGenericSPI& spi = hardware_spi);
/// Initialise the Driver transport hardware and software.
/// Leaves the radio in idle mode,
/// with default configuration of: 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on
/// \return true if initialisation succeeded.
virtual bool init();
/// Prints the value of all chip registers
/// to the Serial device if RH_HAVE_SERIAL is defined for the current platform
/// For debugging purposes only.
/// \return true on success
bool printRegisters();
/// Sets all the registers required to configure the data modem in the radio, including the bandwidth,
/// spreading factor etc. You can use this to configure the modem with custom configurations if none of the
/// canned configurations in ModemConfigChoice suit you.
/// \param[in] config A ModemConfig structure containing values for the modem configuration registers.
void setModemRegisters(const ModemConfig* config);
/// Select one of the predefined modem configurations. If you need a modem configuration not provided
/// here, use setModemRegisters() with your own ModemConfig.
/// Caution: the slowest protocols may require a radio module with TCXO temperature controlled oscillator
/// for reliable operation.
/// \param[in] index The configuration choice.
/// \return true if index is a valid choice.
bool setModemConfig(ModemConfigChoice index);
/// Tests whether a new message is available from the Driver.
/// On most drivers, this will also put the Driver into RHModeRx mode until
/// a message is actually received by the transport, when it will be returned to RHModeIdle.
/// This can be called multiple times in a timeout loop
/// \return true if a new, complete, error-free uncollected message is available to be retreived by recv()
virtual bool available();
/// Turns the receiver on if it not already on.
/// If there is a valid message available, copy it to buf and return true
/// else return false.
/// If a message is copied, *len is set to the length (Caution, 0 length messages are permitted).
/// You should be sure to call this function frequently enough to not miss any messages
/// It is recommended that you call it in your main loop.
/// \param[in] buf Location to copy the received message
/// \param[in,out] len Pointer to available space in buf. Set to the actual number of octets copied.
/// \return true if a valid message was copied to buf
virtual bool recv(uint8_t* buf, uint8_t* len);
/// Waits until any previous transmit packet is finished being transmitted with waitPacketSent().
/// Then optionally waits for Channel Activity Detection (CAD)
/// to show the channnel is clear (if the radio supports CAD) by calling waitCAD().
/// Then loads a message into the transmitter and starts the transmitter. Note that a message length
/// of 0 is permitted.
/// \param[in] data Array of data to be sent
/// \param[in] len Number of bytes of data to send
/// specify the maximum time in ms to wait. If 0 (the default) do not wait for CAD before transmitting.
/// \return true if the message length was valid and it was correctly queued for transmit. Return false
/// if CAD was requested and the CAD timeout timed out before clear channel was detected.
virtual bool send(const uint8_t* data, uint8_t len);
/// Sets the length of the preamble
/// in bytes.
/// Caution: this should be set to the same
/// value on all nodes in your network. Default is 8.
/// Sets the message preamble length in RH_RF95_REG_??_PREAMBLE_?SB
/// \param[in] bytes Preamble length in bytes.
void setPreambleLength(uint16_t bytes);
/// Returns the maximum message length
/// available in this Driver.
/// \return The maximum legal message length
virtual uint8_t maxMessageLength();
/// Sets the transmitter and receiver
/// centre frequency.
/// \param[in] centre Frequency in MHz. 137.0 to 1020.0. Caution: RFM95/96/97/98 comes in several
/// different frequency ranges, and setting a frequency outside that range of your radio will probably not work
/// \return true if the selected frquency centre is within range
bool setFrequency(float centre);
/// If current mode is Rx or Tx changes it to Idle. If the transmitter or receiver is running,
/// disables them.
void setModeIdle();
/// If current mode is Tx or Idle, changes it to Rx.
/// Starts the receiver in the RF95/96/97/98.
void setModeRx();
/// If current mode is Rx or Idle, changes it to Rx. F
/// Starts the transmitter in the RF95/96/97/98.
void setModeTx();
/// Sets the transmitter power output level, and configures the transmitter pin.
/// Be a good neighbour and set the lowest power level you need.
/// Some SX1276/77/78/79 and compatible modules (such as RFM95/96/97/98)
/// use the PA_BOOST transmitter pin for high power output (and optionally the PA_DAC)
/// while some (such as the Modtronix inAir4 and inAir9)
/// use the RFO transmitter pin for lower power but higher efficiency.
/// You must set the appropriate power level and useRFO argument for your module.
/// Check with your module manufacturer which transmtter pin is used on your module
/// to ensure you are setting useRFO correctly.
/// Failure to do so will result in very low
/// transmitter power output.
/// Caution: legal power limits may apply in certain countries.
/// After init(), the power will be set to 13dBm, with useRFO false (ie PA_BOOST enabled).
/// \param[in] power Transmitter power level in dBm. For RFM95/96/97/98 LORA with useRFO false,
/// valid values are from +2 to +20. For 18, 19 and 20, PA_DAC is enabled,
/// For Modtronix inAir4 and inAir9 with useRFO true (ie RFO pins in use),
/// valid values are from 0 to 15.
/// \param[in] useRFO If true, enables the use of the RFO transmitter pins instead of
/// the PA_BOOST pin (false). Choose the correct setting for your module.
void setTxPower(int8_t power, bool useRFO = false);
/// Sets the radio into low-power sleep mode.
/// If successful, the transport will stay in sleep mode until woken by
/// changing mode it idle, transmit or receive (eg by calling send(), recv(), available() etc)
/// Caution: there is a time penalty as the radio takes a finite time to wake from sleep mode.
/// \return true if sleep mode was successfully entered.
virtual bool sleep();
// Bent G Christensen (bentor@gmail.com), 08/15/2016
/// Use the radio's Channel Activity Detect (CAD) function to detect channel activity.
/// Sets the RF95 radio into CAD mode and waits until CAD detection is complete.
/// To be used in a listen-before-talk mechanism (Collision Avoidance)
/// with a reasonable time backoff algorithm.
/// This is called automatically by waitCAD().
/// \return true if channel is in use.
virtual bool isChannelActive();
/// Enable TCXO mode
/// Call this immediately after init(), to force your radio to use an external
/// frequency source, such as a Temperature Compensated Crystal Oscillator (TCXO), if available.
/// See the comments in the main documentation about the sensitivity of this radio to
/// clock frequency especially when using narrow bandwidths.
/// Leaves the module in sleep mode.
/// Caution: the TCXO model radios are not low power when in sleep (consuming
/// about ~600 uA, reported by Phang Moh Lim.<br>
/// Caution: if you enable TCXO and there is no exernal TCXO signal connected to the radio
/// or if the exerrnal TCXO is not
/// powered up, the radio <b>will not work<\b>
/// \param[in] on If true (the default) enables the radio to use the external TCXO.
void enableTCXO(bool on = true);
/// Returns the last measured frequency error.
/// The LoRa receiver estimates the frequency offset between the receiver centre frequency
/// and that of the received LoRa signal. This function returns the estimates offset (in Hz)
/// of the last received message. Caution: this measurement is not absolute, but is measured
/// relative to the local receiver's oscillator.
/// Apparent errors may be due to the transmitter, the receiver or both.
/// \return The estimated centre frequency offset in Hz of the last received message.
/// If the modem bandwidth selector in
/// register RH_RF95_REG_1D_MODEM_CONFIG1 is invalid, returns 0.
int frequencyError();
/// Returns the Signal-to-noise ratio (SNR) of the last received message, as measured
/// by the receiver.
/// \return SNR of the last received message in dB
int lastSNR();
/// brian.n.norman@gmail.com 9th Nov 2018
/// Sets the radio spreading factor.
/// valid values are 6 through 12.
/// Out of range values below 6 are clamped to 6
/// Out of range values above 12 are clamped to 12
/// See Semtech DS SX1276/77/78/79 page 27 regarding SF6 configuration.
///
/// \param[in] uint8_t sf (spreading factor 6..12)
/// \return nothing
void setSpreadingFactor(uint8_t sf);
/// brian.n.norman@gmail.com 9th Nov 2018
/// Sets the radio signal bandwidth
/// sbw ranges and resultant settings are as follows:-
/// sbw range actual bw (kHz)
/// 0-7800 7.8
/// 7801-10400 10.4
/// 10401-15600 15.6
/// 15601-20800 20.8
/// 20801-31250 31.25
/// 31251-41700 41.7
/// 41701-62500 62.5
/// 62501-12500 125.0
/// 12501-250000 250.0
/// >250000 500.0
/// NOTE caution Earlier - Semtech do not recommend BW below 62.5 although, in testing
/// I managed 31.25 with two devices in close proximity.
/// \param[in] sbw long, signal bandwidth e.g. 125000
void setSignalBandwidth(long sbw);
/// brian.n.norman@gmail.com 9th Nov 2018
/// Sets the coding rate to 4/5, 4/6, 4/7 or 4/8.
/// Valid denominator values are 5, 6, 7 or 8. A value of 5 sets the coding rate to 4/5 etc.
/// Values below 5 are clamped at 5
/// values above 8 are clamped at 8.
/// Default for all standard modem config options is 4/5.
/// \param[in] denominator uint8_t range 5..8
void setCodingRate4(uint8_t denominator);
/// brian.n.norman@gmail.com 9th Nov 2018
/// sets the low data rate flag if symbol time exceeds 16ms
/// ref: https://www.thethingsnetwork.org/forum/t/a-point-to-note-lora-low-data-rate-optimisation-flag/12007
/// called by setBandwidth() and setSpreadingfactor() since these affect the symbol time.
void setLowDatarate();
/// brian.n.norman@gmail.com 9th Nov 2018
/// Allows the CRC to be turned on/off. Default is true (enabled)
/// When true, RH_RF95 sends a CRC in outgoing packets and requires a valid CRC to be
/// present and correct on incoming packets.
/// When false, does not send CRC in outgoing packets and does not require a CRC to be
/// present on incoming packets. However if a CRC is present, it must be correct.
/// Normally this should be left on (the default)
/// so that packets with a bad CRC are rejected. If turned off you wil be much more likely to receive
/// false noise packets.
/// \param[in] on bool, true enables CRCs in incoming and outgoing packets, false disables them
void setPayloadCRC(bool on);
/// tilman_1@gloetzner.net
/// Returns device version from register 42
/// \param none
/// \return uint8_t deviceID
uint8_t getDeviceVersion();
protected:
/// This is a low level function to handle the interrupts for one instance of RH_RF95.
/// Called automatically by isr*()
/// Should not need to be called by user code.
void handleInterrupt();
/// Examine the revceive buffer to determine whether the message is for this node
void validateRxBuf();
/// Clear our local receive buffer
void clearRxBuf();
/// Called by RH_RF95 when the radio mode is about to change to a new setting.
/// Can be used by subclasses to implement antenna switching etc.
/// \param[in] mode RHMode the new mode about to take effect
/// \return true if the subclasses changes successful
virtual bool modeWillChange(RHMode) {return true;}
/// False if the PA_BOOST transmitter output pin is to be used.
/// True if the RFO transmitter output pin is to be used.
bool _useRFO;
private:
/// Low level interrupt service routine for device connected to interrupt 0
static void isr0();
/// Low level interrupt service routine for device connected to interrupt 1
static void isr1();
/// Low level interrupt service routine for device connected to interrupt 1
static void isr2();
/// Array of instances connected to interrupts 0 and 1
static RH_RF95* _deviceForInterrupt[];
/// Index of next interrupt number to use in _deviceForInterrupt
static uint8_t _interruptCount;
/// The configured interrupt pin connected to this instance
uint8_t _interruptPin;
/// The index into _deviceForInterrupt[] for this device (if an interrupt is already allocated)
/// else 0xff
uint8_t _myInterruptIndex;
/// Number of octets in the buffer
volatile uint8_t _bufLen;
/// The receiver/transmitter buffer
uint8_t _buf[RH_RF95_MAX_PAYLOAD_LEN];
/// True when there is a valid message in the buffer
volatile bool _rxBufValid;
/// True if we are using the HF port (779.0 MHz and above)
bool _usingHFport;
/// Last measured SNR, dB
int8_t _lastSNR;
/// If true, sends CRCs in every packet and requires a valid CRC in every received packet
bool _enableCRC;
/// device ID
uint8_t _deviceVersion = 0x00;
};
/// @example rf95_client.pde
/// @example rf95_client.pde
/// @example rf95_server.pde
/// @example rf95_encrypted_client.pde
/// @example rf95_encrypted_server.pde
/// @example rf95_reliable_datagram_client.pde
/// @example rf95_reliable_datagram_server.pde
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment