diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..d1a923a --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,7 @@ +Thank you for your interest in improving this library. + +If you have any questions or have found a bug, please feel free to create an +Issue on this project's GitHub page. + +Any suggestions for improvements are welcome as well. Please bring them up as +an Issue or a Pull Request. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e5feb4f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Microfire LLC + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..44c1854 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +HABridge +====== + +> Create a Home Assistant sensor from any hardware and send measurements with ESPNow or LoRa. + +#### Summary ℹ️ + +This is a great solution for a send-only battery-operated Home Assistant device. + +This library works in conjuction with the MQTT-Bridge project described [here](https://microfire.co/articles/lora-with-espnow) and [here](https://microfire.co/articles/espnow-with-esphome). An ESP32 device is programmed to receive ESPNow or LoRa messages, processes them and then creates an MQTT-based sensor for display in Home Assistant. + +An ESP32 can be used for ESPNow messages, or any device with a LoRa radio can be used to send LoRa messages. + +Only one-way messaging works, sensor-device to Home Assistant. The following types are supported. + +| Home Assistant | | +| ------------------- | :--------: | +| Sensor | ✅ | +| Binary Sensor | ✅ | +| Text Sensor | ✅ | + +* * * + +### Use +1. Read about the Bridge portion of this from the links above +2. Create your bridge device +3. Use this library to create a sensor device +* * * + +### Ask a question 🤙 + +* [Discord](https://discord.gg/rAnZPdW) +* [questions@microfire.co](mailto:questions@microfire.co) + +* * * + +### Website +[microfire.co](https://microfire.co) diff --git a/examples/ESPNow/ESPNow.ino b/examples/ESPNow/ESPNow.ino new file mode 100644 index 0000000..b809f62 --- /dev/null +++ b/examples/ESPNow/ESPNow.ino @@ -0,0 +1,50 @@ +#include +#include +#include +#define DEVICE_NAME "test_device" + +// This library is intended to work in conjuction with the ESPNow-MQTT Bridge project +// as described here: https://microfire.co/articles/espnow-with-esphome + +// HABridgeSensor sends a measurement associated with a unit, like temperature in Celsius +// the parameters are: +// device name: a string that represents the device name (outside_temperature_box, livingroom_presence) +// sensor name: the name of the sensor (outside_temp_f) +// SensorClass: available SensorClass values are listed here: https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes +// SensorState: available SensorState values are here: https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes +// unit of measurement: a string that represents the unit being measured, (C, F) for display in Home Assistant +// (optional) HA icon: an explanation of icons can be found here: https://www.home-assistant.io/docs/frontend/icons/ +// (optional) hardware board name for display within Home Assistant +// (optional) board version for display in Home Assistant +HABridgeSensor sensor(DEVICE_NAME, "temperature_sensor", HABridge::SensorClass::TEMPERATURE, HABridge::SensorState::MEASUREMENT, "C", "mdi:temperature-celsius"); + +// HABridgeBinary sends a binary state, 1/0 +// the parameters are: device name, sensor name, (optional) icon, (optional) hardware board name, (optional) board version +HABridgeBinary binary(DEVICE_NAME, "binary_sensor"); + +// HABridgeText sends a text state, battery_charging, battery_hot, battery_discharging +// the parameters are: device name, sensor name, (optional) icon, (optional) hardware board name, (optional) board version +HABridgeText text(DEVICE_NAME, "text_sensor", "mdi:text"); + +ESPNowBridge now; + +void setup() +{ + // start the ESPNow library + now.begin(); +} + +void loop() +{ + // After getting your sensor measurement (this demo just uses rand numbers) call HABridgeSensor::line(float, precision) + // ::send it to the bridge device + now.send(sensor.line(rand() % 20, 2)); + + // HABridgeBinary::line(bool) + now.send(binary.line(rand() % 2)); + + // HABridgeText::line(std::string) + now.send(text.line(rand() % 2 ? "charging" : "discharging")); + + delay(5000); +} diff --git a/examples/LoRa/LoRa.ino b/examples/LoRa/LoRa.ino new file mode 100644 index 0000000..9a10ab9 --- /dev/null +++ b/examples/LoRa/LoRa.ino @@ -0,0 +1,62 @@ +#include +#include +#include +#define DEVICE_NAME "test_device" + +// This library is intended to work in conjuction with the ESPNow-MQTT Bridge project +// as described here: https://microfire.co/articles/lora-with-espnow + +// HABridgeSensor sends a measurement associated with a unit, like temperature in Celsius +// the parameters are: +// device name: a string that represents the device name (outside_temperature_box, livingroom_presence) +// sensor name: the name of the sensor (outside_temp_f) +// SensorClass: available SensorClass values are listed here: https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes +// SensorState: available SensorState values are here: https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes +// unit of measurement: a string that represents the unit being measured, (C, F) for display in Home Assistant +// (optional) HA icon: an explanation of icons can be found here: https://www.home-assistant.io/docs/frontend/icons/ +// (optional) hardware board name for display within Home Assistant +// (optional) board version for display in Home Assistant +HABridgeSensor sensor(DEVICE_NAME, "temperature_sensor", HABridge::SensorClass::TEMPERATURE, HABridge::SensorState::MEASUREMENT, "C", "mdi:temperature-celsius"); + +// HABridgeBinary sends a binary state, 1/0 +// the parameters are: device name, sensor name, (optional) icon, (optional) hardware board name, (optional) board version +HABridgeBinary binary(DEVICE_NAME, "binary_sensor"); + +// HABridgeText sends a text state, battery_charging, battery_hot, battery_discharging +// the parameters are: device name, sensor name, (optional) icon, (optional) hardware board name, (optional) board version +HABridgeText text(DEVICE_NAME, "text_sensor", "mdi:text"); + +// this is just a very thin wrapper of https://github.com/sandeepmistry/arduino-LoRa, all methods are exposed through LoRaBridge:: +LoRaBridge lora; + +void setup() +{ + // start SPI + SPI.begin(/*SCK*/ 5, /*MISO*/19, /*MOSI*/27, /*SS*/18); + + // start the LoRa library with the additional pins to use the radio + lora.begin(/*SS*/18, /*LoRa RESET*/14, /*LoRa DIO0*/35); + + // If you want to change any of these settings, be sure to read the datasheet for your radio module + // not all combinations work and it is hardware dependent + // additionally, some settings may not be permitted in your location + // lora.setSyncWord(0xF3); + // lora.setSpreadingFactor(12); + // lora.setCodingRate4(5); + // lora.setSignalBandwidth(250E3); +} + +void loop() +{ + // After getting your sensor measurement (this demo just uses rand numbers) call HABridgeSensor::line(float, precision) + // ::send it to the bridge device + lora.send(sensor.line(rand() % 20, 2)); + + // HABridgeBinary::line(bool) + lora.send(binary.line(rand() % 2)); + + // HABridgeText::line(std::string) + lora.send(text.line(rand() % 2 ? "charging" : "discharging")); + + delay(5000); +} diff --git a/library.json b/library.json new file mode 100644 index 0000000..c4f6c30 --- /dev/null +++ b/library.json @@ -0,0 +1,20 @@ +{ + "name": "Microfire_HABridge", + "keywords": "home assistant, esphome, espnow, lora", + "description": "Create a Home Assistant sensor from any hardware and send measurements with ESPNow or LoRa.", + "repository": { + "type": "git", + "url": "https://github.com/u-fire/HABridge.git" + }, + "authors": [ + { + "name": "Microfire LLC", + "email": "contact@microfire.co", + "url": "https://microfire.co", + "maintainer": true + } + ], + "version": "1.0.0", + "frameworks": "arduino", + "platforms": "*" +} diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..d9ee80f --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=Microfire_HABridge +version=1.0.0 +author=Microfire LLC +maintainer=contact@microfire.co +sentence=Create a Home Assistant sensor from any hardware and send measurements with ESPNow or LoRa. +paragraph=This library works in conjuction with the MQTT-ESPnow/LoRa bridge project (https://microfire.co/articles/lora-with-espnow). +category=Sensors +url=https://microfire.co +architectures=* diff --git a/src/ESPNowBridge.cpp b/src/ESPNowBridge.cpp new file mode 100644 index 0000000..e10ad9a --- /dev/null +++ b/src/ESPNowBridge.cpp @@ -0,0 +1,39 @@ +#include + +uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +bool ESPNowBridge::begin(uint8_t channel) +{ + esp_now_peer_info_t peerInfo = {}; + + esp_netif_init(); + esp_event_loop_create_default(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + esp_wifi_init(&cfg); + esp_wifi_set_storage(WIFI_STORAGE_RAM); + esp_wifi_set_mode(WIFI_MODE_STA); + esp_wifi_start(); + + if (esp_now_init() != ESP_OK) + { + return false; + } + + esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR); + memcpy(peerInfo.peer_addr, broadcastAddress, 6); + peerInfo.channel = channel; + peerInfo.encrypt = false; + + if (esp_now_add_peer(&peerInfo) != ESP_OK) + { + return false; + } + + return true; +} + +bool ESPNowBridge::send(std::string line) +{ + esp_now_send(broadcastAddress, reinterpret_cast(&line[0]), line.size()); + return true; +} \ No newline at end of file diff --git a/src/ESPNowBridge.h b/src/ESPNowBridge.h new file mode 100644 index 0000000..01b458f --- /dev/null +++ b/src/ESPNowBridge.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include + +#include +#include + +class ESPNowBridge +{ +public: + bool begin(uint8_t channel = 1); + bool send(std::string line); +}; \ No newline at end of file diff --git a/src/HABridge.cpp b/src/HABridge.cpp new file mode 100644 index 0000000..e46c514 --- /dev/null +++ b/src/HABridge.cpp @@ -0,0 +1,158 @@ +#include +#include + +HABridgeSensor::HABridgeSensor(std::string device_name, std::string measurement_name, SensorClass sensor_class, SensorState sensor_state, std::string unit, std::string icon, std::string board, std::string version) +{ + _device_name = device_name; + _sensor_class = sensor_class; + _sensor_state = sensor_state; + _measurement_name = measurement_name; + _unit = unit; + _icon = icon; + _version = version; + _board = board; +} + +std::string HABridgeSensor::line(float state, uint8_t accuracy) +{ + std::string line; + + line = _device_name; + line += ":"; + line += _SensorClassToString(_sensor_class); + line += ":"; + line += _SensorStateToString(_sensor_state); + line += ":"; + line += _measurement_name; + line += ":"; + line += _unit; + line += ":"; + line += _to_string_with_precision(state, accuracy); + line += ":"; + if (_icon.length() != 0) + { + line += _icon; + } + else + { + line += ":"; + } + line += ":"; + line += _version; + line += ":"; + line += _board; + line += ":sensor:"; + return line; +} + +HABridgeBinary::HABridgeBinary(std::string device_name, std::string measurement_name, std::string icon, std::string board, std::string version) +{ + _device_name = device_name; + _measurement_name = measurement_name; + _icon = icon; + _version = version; + _board = board; +} + +std::string HABridgeBinary::line(bool state) +{ + std::string line; + const char *state_s = state ? "ON" : "OFF"; + + line = _device_name; + line += ":"; + line += ":"; + line += "binary_sensor"; + line += ":"; + line += _measurement_name; + line += ":"; + line += ":"; + line += state_s; + line += ":"; + if (_icon.length() != 0) + { + line += _icon; + } + else + { + line += ":"; + } + line += ":"; + line += _version; + line += ":"; + line += _board; + line += "::"; + return line; +} + +HABridgeText::HABridgeText(std::string device_name, std::string measurement_name, std::string icon, std::string board, std::string version) +{ + _device_name = device_name; + _measurement_name = measurement_name; + _icon = icon; + _version = version; + _board = board; +} + +std::string HABridgeText::line(std::string state) +{ + std::string line; + + line = _device_name; + line += ":"; + line += ":"; + line += ":"; + line += _measurement_name; + line += ":"; + line += ":"; + line += state; + line += ":"; + if (_icon.length() != 0) + { + line += _icon; + } + else + { + line += ":"; + } + line += ":"; + line += _version; + line += ":"; + line += _board; + line += "::"; + return line; +} + +std::string HABridge::_SensorClassToString(SensorClass sensor_class) +{ + switch (sensor_class) + { + case SensorClass::TEMPERATURE: + return "temperature"; + case SensorClass::HUMIDITY: + return "humidity"; + default: + return ""; + } +} + +std::string HABridge::_SensorStateToString(SensorState sensor_state) +{ + switch (sensor_state) + { + case SensorState::MEASUREMENT: + return "measurement"; + case SensorState::TOTAL: + return "total"; + default: + return ""; + } +} + +std::string HABridge::_to_string_with_precision(float a_value, const int n) +{ + std::ostringstream out; + out.precision(n); + out << std::fixed << a_value; + return std::move(out).str(); +} \ No newline at end of file diff --git a/src/HABridge.h b/src/HABridge.h new file mode 100644 index 0000000..88e21e2 --- /dev/null +++ b/src/HABridge.h @@ -0,0 +1,104 @@ +#pragma once +#include +#include +#include +#define BOARD "esp32dev" +#define VERSION "HAB-0.0.1" +class HABridge +{ +public: + enum SensorClass + { + APPARENT_POWER, + AQI, + ATMOSPHERIC_PRESSURE, + BATTERY, + CO2, + CO, + CURRENT, + DATA_RATE, + DATA_SIZE, + DATE, + DISTANCE, + DURATION, + ENERGY, + ENERGY_STORAGE, + ENUM, + FREQUENCY, + GAS, + HUMIDITY, + ILLUMINANCE, + IRRADIANCE, + MOISTURE, + MONETARY, + NITROGEN_DIOXIDE, + NITROGEN_MONOXIDE, + NITROUS_OXIDE, + OZONE, + PH, + PM1, + PM25, + PM10, + POWER, + POWER_FACTOR, + PRECIPITATION, + PRECIPITATION_INTENSITY, + PRESSURE, + REACTIVE_POWER, + SIGNAL_STRENGTH, + SOUND_PRESSURE, + SPEED, + SULPHUR_DIOXIDE, + TEMPERATURE, + TIMESTAMP, + VOLATILE_ORGANIC_COMPOUNDS, + VOLATILE_ORGANIC_COMPOUNDS_PARTS, + VOLTAGE, + VOLUME, + VOLUME_STORAGE, + WATER, + WEIGHT, + WIND_SPEED + }; + enum SensorState + { + MEASUREMENT, + TOTAL, + TOTAL_INCREASING + }; + +protected: + std::string _device_name; + SensorClass _sensor_class; + SensorState _sensor_state; + std::string _measurement_name; + std::string _unit; + std::string _icon; + std::string _version; + std::string _board; + + std::string _SensorClassToString(SensorClass sensor_class); + std::string _SensorStateToString(SensorState sensor_state); + std::string _to_string_with_precision(const float a_value, const int n); +}; + +class HABridgeSensor : public HABridge +{ +public: + HABridgeSensor(std::string device_name, std::string measurement_name, SensorClass sensor_class, SensorState sensor_state, std::string unit, std::string icon = "", std::string board = BOARD, std::string version = VERSION); + std::string line(float state, uint8_t accuracy); +}; + +class HABridgeBinary : public HABridge +{ +public: + HABridgeBinary(std::string device_name, std::string measurement_name, std::string icon = "", std::string board = BOARD, std::string version = VERSION); + std::string line(bool state); +}; + +class HABridgeText : public HABridge +{ +public: + HABridgeText(std::string device_name, std::string measurement_name, std::string icon = "", std::string board = BOARD, std::string version = VERSION); + std::string line(std::string state); +}; \ No newline at end of file diff --git a/src/LoRa.cpp b/src/LoRa.cpp new file mode 100644 index 0000000..9bdb431 --- /dev/null +++ b/src/LoRa.cpp @@ -0,0 +1,785 @@ +// Copyright (c) Sandeep Mistry. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include "LoRa.h" + +// registers +#define REG_FIFO 0x00 +#define REG_OP_MODE 0x01 +#define REG_FRF_MSB 0x06 +#define REG_FRF_MID 0x07 +#define REG_FRF_LSB 0x08 +#define REG_PA_CONFIG 0x09 +#define REG_OCP 0x0b +#define REG_LNA 0x0c +#define REG_FIFO_ADDR_PTR 0x0d +#define REG_FIFO_TX_BASE_ADDR 0x0e +#define REG_FIFO_RX_BASE_ADDR 0x0f +#define REG_FIFO_RX_CURRENT_ADDR 0x10 +#define REG_IRQ_FLAGS 0x12 +#define REG_RX_NB_BYTES 0x13 +#define REG_PKT_SNR_VALUE 0x19 +#define REG_PKT_RSSI_VALUE 0x1a +#define REG_RSSI_VALUE 0x1b +#define REG_MODEM_CONFIG_1 0x1d +#define REG_MODEM_CONFIG_2 0x1e +#define REG_PREAMBLE_MSB 0x20 +#define REG_PREAMBLE_LSB 0x21 +#define REG_PAYLOAD_LENGTH 0x22 +#define REG_MODEM_CONFIG_3 0x26 +#define REG_FREQ_ERROR_MSB 0x28 +#define REG_FREQ_ERROR_MID 0x29 +#define REG_FREQ_ERROR_LSB 0x2a +#define REG_RSSI_WIDEBAND 0x2c +#define REG_DETECTION_OPTIMIZE 0x31 +#define REG_INVERTIQ 0x33 +#define REG_DETECTION_THRESHOLD 0x37 +#define REG_SYNC_WORD 0x39 +#define REG_INVERTIQ2 0x3b +#define REG_DIO_MAPPING_1 0x40 +#define REG_VERSION 0x42 +#define REG_PA_DAC 0x4d + +// modes +#define MODE_LONG_RANGE_MODE 0x80 +#define MODE_SLEEP 0x00 +#define MODE_STDBY 0x01 +#define MODE_TX 0x03 +#define MODE_RX_CONTINUOUS 0x05 +#define MODE_RX_SINGLE 0x06 +#define MODE_CAD 0x07 + +// PA config +#define PA_BOOST 0x80 + +// IRQ masks +#define IRQ_TX_DONE_MASK 0x08 +#define IRQ_PAYLOAD_CRC_ERROR_MASK 0x20 +#define IRQ_RX_DONE_MASK 0x40 +#define IRQ_CAD_DONE_MASK 0x04 +#define IRQ_CAD_DETECTED_MASK 0x01 + +#define RF_MID_BAND_THRESHOLD 525E6 +#define RSSI_OFFSET_HF_PORT 157 +#define RSSI_OFFSET_LF_PORT 164 + +#define MAX_PKT_LENGTH 255 + +#if (ESP8266 || ESP32) + #define ISR_PREFIX ICACHE_RAM_ATTR +#else + #define ISR_PREFIX +#endif + +LoRaClass::LoRaClass() : + _spiSettings(LORA_DEFAULT_SPI_FREQUENCY, MSBFIRST, SPI_MODE0), + _spi(&LORA_DEFAULT_SPI), + _ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN), + _frequency(0), + _packetIndex(0), + _implicitHeaderMode(0), + _onReceive(NULL), + _onCadDone(NULL), + _onTxDone(NULL) +{ + // overide Stream timeout value + setTimeout(0); +} + +int LoRaClass::begin(long frequency) +{ +#if defined(ARDUINO_SAMD_MKRWAN1300) || defined(ARDUINO_SAMD_MKRWAN1310) + pinMode(LORA_IRQ_DUMB, OUTPUT); + digitalWrite(LORA_IRQ_DUMB, LOW); + + // Hardware reset + pinMode(LORA_BOOT0, OUTPUT); + digitalWrite(LORA_BOOT0, LOW); + + pinMode(LORA_RESET, OUTPUT); + digitalWrite(LORA_RESET, HIGH); + delay(200); + digitalWrite(LORA_RESET, LOW); + delay(200); + digitalWrite(LORA_RESET, HIGH); + delay(50); +#endif + + // setup pins + pinMode(_ss, OUTPUT); + // set SS high + digitalWrite(_ss, HIGH); + + if (_reset != -1) { + pinMode(_reset, OUTPUT); + + // perform reset + digitalWrite(_reset, LOW); + delay(10); + digitalWrite(_reset, HIGH); + delay(10); + } + + // start SPI + _spi->begin(); + + // check version + uint8_t version = readRegister(REG_VERSION); + if (version != 0x12) { + return 0; + } + + // put in sleep mode + sleep(); + + // set frequency + setFrequency(frequency); + + // set base addresses + writeRegister(REG_FIFO_TX_BASE_ADDR, 0); + writeRegister(REG_FIFO_RX_BASE_ADDR, 0); + + // set LNA boost + writeRegister(REG_LNA, readRegister(REG_LNA) | 0x03); + + // set auto AGC + writeRegister(REG_MODEM_CONFIG_3, 0x04); + + // set output power to 17 dBm + setTxPower(17); + + // put in standby mode + idle(); + + return 1; +} + +void LoRaClass::end() +{ + // put in sleep mode + sleep(); + + // stop SPI + _spi->end(); +} + +int LoRaClass::beginPacket(int implicitHeader) +{ + if (isTransmitting()) { + return 0; + } + + // put in standby mode + idle(); + + if (implicitHeader) { + implicitHeaderMode(); + } else { + explicitHeaderMode(); + } + + // reset FIFO address and paload length + writeRegister(REG_FIFO_ADDR_PTR, 0); + writeRegister(REG_PAYLOAD_LENGTH, 0); + + return 1; +} + +int LoRaClass::endPacket(bool async) +{ + + if ((async) && (_onTxDone)) + writeRegister(REG_DIO_MAPPING_1, 0x40); // DIO0 => TXDONE + + // put in TX mode + writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_TX); + + if (!async) { + // wait for TX done + while ((readRegister(REG_IRQ_FLAGS) & IRQ_TX_DONE_MASK) == 0) { + yield(); + } + // clear IRQ's + writeRegister(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK); + } + + return 1; +} + +bool LoRaClass::isTransmitting() +{ + if ((readRegister(REG_OP_MODE) & MODE_TX) == MODE_TX) { + return true; + } + + if (readRegister(REG_IRQ_FLAGS) & IRQ_TX_DONE_MASK) { + // clear IRQ's + writeRegister(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK); + } + + return false; +} + +int LoRaClass::parsePacket(int size) +{ + int packetLength = 0; + int irqFlags = readRegister(REG_IRQ_FLAGS); + + if (size > 0) { + implicitHeaderMode(); + + writeRegister(REG_PAYLOAD_LENGTH, size & 0xff); + } else { + explicitHeaderMode(); + } + + // clear IRQ's + writeRegister(REG_IRQ_FLAGS, irqFlags); + + if ((irqFlags & IRQ_RX_DONE_MASK) && (irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK) == 0) { + // received a packet + _packetIndex = 0; + + // read packet length + if (_implicitHeaderMode) { + packetLength = readRegister(REG_PAYLOAD_LENGTH); + } else { + packetLength = readRegister(REG_RX_NB_BYTES); + } + + // set FIFO address to current RX address + writeRegister(REG_FIFO_ADDR_PTR, readRegister(REG_FIFO_RX_CURRENT_ADDR)); + + // put in standby mode + idle(); + } else if (readRegister(REG_OP_MODE) != (MODE_LONG_RANGE_MODE | MODE_RX_SINGLE)) { + // not currently in RX mode + + // reset FIFO address + writeRegister(REG_FIFO_ADDR_PTR, 0); + + // put in single RX mode + writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_RX_SINGLE); + } + + return packetLength; +} + +int LoRaClass::packetRssi() +{ + return (readRegister(REG_PKT_RSSI_VALUE) - (_frequency < RF_MID_BAND_THRESHOLD ? RSSI_OFFSET_LF_PORT : RSSI_OFFSET_HF_PORT)); +} + +float LoRaClass::packetSnr() +{ + return ((int8_t)readRegister(REG_PKT_SNR_VALUE)) * 0.25; +} + +long LoRaClass::packetFrequencyError() +{ + int32_t freqError = 0; + freqError = static_cast(readRegister(REG_FREQ_ERROR_MSB) & 0b111); + freqError <<= 8L; + freqError += static_cast(readRegister(REG_FREQ_ERROR_MID)); + freqError <<= 8L; + freqError += static_cast(readRegister(REG_FREQ_ERROR_LSB)); + + if (readRegister(REG_FREQ_ERROR_MSB) & 0b1000) { // Sign bit is on + freqError -= 524288; // 0b1000'0000'0000'0000'0000 + } + + const float fXtal = 32E6; // FXOSC: crystal oscillator (XTAL) frequency (2.5. Chip Specification, p. 14) + const float fError = ((static_cast(freqError) * (1L << 24)) / fXtal) * (getSignalBandwidth() / 500000.0f); // p. 37 + + return static_cast(fError); +} + +int LoRaClass::rssi() +{ + return (readRegister(REG_RSSI_VALUE) - (_frequency < RF_MID_BAND_THRESHOLD ? RSSI_OFFSET_LF_PORT : RSSI_OFFSET_HF_PORT)); +} + +size_t LoRaClass::write(uint8_t byte) +{ + return write(&byte, sizeof(byte)); +} + +size_t LoRaClass::write(const uint8_t *buffer, size_t size) +{ + int currentLength = readRegister(REG_PAYLOAD_LENGTH); + + // check size + if ((currentLength + size) > MAX_PKT_LENGTH) { + size = MAX_PKT_LENGTH - currentLength; + } + + // write data + for (size_t i = 0; i < size; i++) { + writeRegister(REG_FIFO, buffer[i]); + } + + // update length + writeRegister(REG_PAYLOAD_LENGTH, currentLength + size); + + return size; +} + +int LoRaClass::available() +{ + return (readRegister(REG_RX_NB_BYTES) - _packetIndex); +} + +int LoRaClass::read() +{ + if (!available()) { + return -1; + } + + _packetIndex++; + + return readRegister(REG_FIFO); +} + +int LoRaClass::peek() +{ + if (!available()) { + return -1; + } + + // store current FIFO address + int currentAddress = readRegister(REG_FIFO_ADDR_PTR); + + // read + uint8_t b = readRegister(REG_FIFO); + + // restore FIFO address + writeRegister(REG_FIFO_ADDR_PTR, currentAddress); + + return b; +} + +void LoRaClass::flush() +{ +} + +#ifndef ARDUINO_SAMD_MKRWAN1300 +void LoRaClass::onReceive(void(*callback)(int)) +{ + _onReceive = callback; + + if (callback) { + pinMode(_dio0, INPUT); +#ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.usingInterrupt(digitalPinToInterrupt(_dio0)); +#endif + attachInterrupt(digitalPinToInterrupt(_dio0), LoRaClass::onDio0Rise, RISING); + } else { + detachInterrupt(digitalPinToInterrupt(_dio0)); +#ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0)); +#endif + } +} + +void LoRaClass::onCadDone(void(*callback)(boolean)) +{ + _onCadDone = callback; + + if (callback) { + pinMode(_dio0, INPUT); +#ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.usingInterrupt(digitalPinToInterrupt(_dio0)); +#endif + attachInterrupt(digitalPinToInterrupt(_dio0), LoRaClass::onDio0Rise, RISING); + } else { + detachInterrupt(digitalPinToInterrupt(_dio0)); +#ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0)); +#endif + } +} + +void LoRaClass::onTxDone(void(*callback)()) +{ + _onTxDone = callback; + + if (callback) { + pinMode(_dio0, INPUT); +#ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.usingInterrupt(digitalPinToInterrupt(_dio0)); +#endif + attachInterrupt(digitalPinToInterrupt(_dio0), LoRaClass::onDio0Rise, RISING); + } else { + detachInterrupt(digitalPinToInterrupt(_dio0)); +#ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0)); +#endif + } +} + +void LoRaClass::receive(int size) +{ + + writeRegister(REG_DIO_MAPPING_1, 0x00); // DIO0 => RXDONE + + if (size > 0) { + implicitHeaderMode(); + + writeRegister(REG_PAYLOAD_LENGTH, size & 0xff); + } else { + explicitHeaderMode(); + } + + writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_RX_CONTINUOUS); +} + +void LoRaClass::channelActivityDetection(void) +{ + writeRegister(REG_DIO_MAPPING_1, 0x80);// DIO0 => CADDONE + writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_CAD); +} +#endif + +void LoRaClass::idle() +{ + writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_STDBY); +} + +void LoRaClass::sleep() +{ + writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_SLEEP); +} + +void LoRaClass::setTxPower(int level, int outputPin) +{ + if (PA_OUTPUT_RFO_PIN == outputPin) { + // RFO + if (level < 0) { + level = 0; + } else if (level > 14) { + level = 14; + } + + writeRegister(REG_PA_CONFIG, 0x70 | level); + } else { + // PA BOOST + 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.) + writeRegister(REG_PA_DAC, 0x87); + setOCP(140); + } else { + if (level < 2) { + level = 2; + } + //Default value PA_HF/LF or +17dBm + writeRegister(REG_PA_DAC, 0x84); + setOCP(100); + } + + writeRegister(REG_PA_CONFIG, PA_BOOST | (level - 2)); + } +} + +void LoRaClass::setFrequency(long frequency) +{ + _frequency = frequency; + + uint64_t frf = ((uint64_t)frequency << 19) / 32000000; + + writeRegister(REG_FRF_MSB, (uint8_t)(frf >> 16)); + writeRegister(REG_FRF_MID, (uint8_t)(frf >> 8)); + writeRegister(REG_FRF_LSB, (uint8_t)(frf >> 0)); +} + +int LoRaClass::getSpreadingFactor() +{ + return readRegister(REG_MODEM_CONFIG_2) >> 4; +} + +void LoRaClass::setSpreadingFactor(int sf) +{ + if (sf < 6) { + sf = 6; + } else if (sf > 12) { + sf = 12; + } + + if (sf == 6) { + writeRegister(REG_DETECTION_OPTIMIZE, 0xc5); + writeRegister(REG_DETECTION_THRESHOLD, 0x0c); + } else { + writeRegister(REG_DETECTION_OPTIMIZE, 0xc3); + writeRegister(REG_DETECTION_THRESHOLD, 0x0a); + } + + writeRegister(REG_MODEM_CONFIG_2, (readRegister(REG_MODEM_CONFIG_2) & 0x0f) | ((sf << 4) & 0xf0)); + setLdoFlag(); +} + +long LoRaClass::getSignalBandwidth() +{ + byte bw = (readRegister(REG_MODEM_CONFIG_1) >> 4); + + switch (bw) { + case 0: return 7.8E3; + case 1: return 10.4E3; + case 2: return 15.6E3; + case 3: return 20.8E3; + case 4: return 31.25E3; + case 5: return 41.7E3; + case 6: return 62.5E3; + case 7: return 125E3; + case 8: return 250E3; + case 9: return 500E3; + } + + return -1; +} + +void LoRaClass::setSignalBandwidth(long sbw) +{ + int bw; + + if (sbw <= 7.8E3) { + bw = 0; + } else if (sbw <= 10.4E3) { + bw = 1; + } else if (sbw <= 15.6E3) { + bw = 2; + } else if (sbw <= 20.8E3) { + bw = 3; + } else if (sbw <= 31.25E3) { + bw = 4; + } else if (sbw <= 41.7E3) { + bw = 5; + } else if (sbw <= 62.5E3) { + bw = 6; + } else if (sbw <= 125E3) { + bw = 7; + } else if (sbw <= 250E3) { + bw = 8; + } else /*if (sbw <= 250E3)*/ { + bw = 9; + } + + writeRegister(REG_MODEM_CONFIG_1, (readRegister(REG_MODEM_CONFIG_1) & 0x0f) | (bw << 4)); + setLdoFlag(); +} + +void LoRaClass::setLdoFlag() +{ + // Section 4.1.1.5 + long symbolDuration = 1000 / ( getSignalBandwidth() / (1L << getSpreadingFactor()) ) ; + + // Section 4.1.1.6 + boolean ldoOn = symbolDuration > 16; + + uint8_t config3 = readRegister(REG_MODEM_CONFIG_3); + bitWrite(config3, 3, ldoOn); + writeRegister(REG_MODEM_CONFIG_3, config3); +} + +void LoRaClass::setCodingRate4(int denominator) +{ + if (denominator < 5) { + denominator = 5; + } else if (denominator > 8) { + denominator = 8; + } + + int cr = denominator - 4; + + writeRegister(REG_MODEM_CONFIG_1, (readRegister(REG_MODEM_CONFIG_1) & 0xf1) | (cr << 1)); +} + +void LoRaClass::setPreambleLength(long length) +{ + writeRegister(REG_PREAMBLE_MSB, (uint8_t)(length >> 8)); + writeRegister(REG_PREAMBLE_LSB, (uint8_t)(length >> 0)); +} + +void LoRaClass::setSyncWord(int sw) +{ + writeRegister(REG_SYNC_WORD, sw); +} + +void LoRaClass::enableCrc() +{ + writeRegister(REG_MODEM_CONFIG_2, readRegister(REG_MODEM_CONFIG_2) | 0x04); +} + +void LoRaClass::disableCrc() +{ + writeRegister(REG_MODEM_CONFIG_2, readRegister(REG_MODEM_CONFIG_2) & 0xfb); +} + +void LoRaClass::enableInvertIQ() +{ + writeRegister(REG_INVERTIQ, 0x66); + writeRegister(REG_INVERTIQ2, 0x19); +} + +void LoRaClass::disableInvertIQ() +{ + writeRegister(REG_INVERTIQ, 0x27); + writeRegister(REG_INVERTIQ2, 0x1d); +} + +void LoRaClass::setOCP(uint8_t mA) +{ + uint8_t ocpTrim = 27; + + if (mA <= 120) { + ocpTrim = (mA - 45) / 5; + } else if (mA <=240) { + ocpTrim = (mA + 30) / 10; + } + + writeRegister(REG_OCP, 0x20 | (0x1F & ocpTrim)); +} + +void LoRaClass::setGain(uint8_t gain) +{ + // check allowed range + if (gain > 6) { + gain = 6; + } + + // set to standby + idle(); + + // set gain + if (gain == 0) { + // if gain = 0, enable AGC + writeRegister(REG_MODEM_CONFIG_3, 0x04); + } else { + // disable AGC + writeRegister(REG_MODEM_CONFIG_3, 0x00); + + // clear Gain and set LNA boost + writeRegister(REG_LNA, 0x03); + + // set gain + writeRegister(REG_LNA, readRegister(REG_LNA) | (gain << 5)); + } +} + +byte LoRaClass::random() +{ + return readRegister(REG_RSSI_WIDEBAND); +} + +void LoRaClass::setPins(int ss, int reset, int dio0) +{ + _ss = ss; + _reset = reset; + _dio0 = dio0; +} + +void LoRaClass::setSPI(SPIClass& spi) +{ + _spi = &spi; +} + +void LoRaClass::setSPIFrequency(uint32_t frequency) +{ + _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); +} + +void LoRaClass::dumpRegisters(Stream& out) +{ + for (int i = 0; i < 128; i++) { + out.print("0x"); + out.print(i, HEX); + out.print(": 0x"); + out.println(readRegister(i), HEX); + } +} + +void LoRaClass::explicitHeaderMode() +{ + _implicitHeaderMode = 0; + + writeRegister(REG_MODEM_CONFIG_1, readRegister(REG_MODEM_CONFIG_1) & 0xfe); +} + +void LoRaClass::implicitHeaderMode() +{ + _implicitHeaderMode = 1; + + writeRegister(REG_MODEM_CONFIG_1, readRegister(REG_MODEM_CONFIG_1) | 0x01); +} + +void LoRaClass::handleDio0Rise() +{ + int irqFlags = readRegister(REG_IRQ_FLAGS); + + // clear IRQ's + writeRegister(REG_IRQ_FLAGS, irqFlags); + + if ((irqFlags & IRQ_CAD_DONE_MASK) != 0) { + if (_onCadDone) { + _onCadDone((irqFlags & IRQ_CAD_DETECTED_MASK) != 0); + } + } else if ((irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK) == 0) { + + if ((irqFlags & IRQ_RX_DONE_MASK) != 0) { + // received a packet + _packetIndex = 0; + + // read packet length + int packetLength = _implicitHeaderMode ? readRegister(REG_PAYLOAD_LENGTH) : readRegister(REG_RX_NB_BYTES); + + // set FIFO address to current RX address + writeRegister(REG_FIFO_ADDR_PTR, readRegister(REG_FIFO_RX_CURRENT_ADDR)); + + if (_onReceive) { + _onReceive(packetLength); + } + } else if ((irqFlags & IRQ_TX_DONE_MASK) != 0) { + if (_onTxDone) { + _onTxDone(); + } + } + } +} + +uint8_t LoRaClass::readRegister(uint8_t address) +{ + return singleTransfer(address & 0x7f, 0x00); +} + +void LoRaClass::writeRegister(uint8_t address, uint8_t value) +{ + singleTransfer(address | 0x80, value); +} + +uint8_t LoRaClass::singleTransfer(uint8_t address, uint8_t value) +{ + uint8_t response; + + digitalWrite(_ss, LOW); + + _spi->beginTransaction(_spiSettings); + _spi->transfer(address); + response = _spi->transfer(value); + _spi->endTransaction(); + + digitalWrite(_ss, HIGH); + + return response; +} + +ISR_PREFIX void LoRaClass::onDio0Rise() +{ + LoRa.handleDio0Rise(); +} + +LoRaClass LoRa; diff --git a/src/LoRa.h b/src/LoRa.h new file mode 100644 index 0000000..bad87da --- /dev/null +++ b/src/LoRa.h @@ -0,0 +1,133 @@ +// Copyright (c) Sandeep Mistry. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#ifndef LORA_H +#define LORA_H + +#include +#include + +#if defined(ARDUINO_SAMD_MKRWAN1300) +#define LORA_DEFAULT_SPI SPI1 +#define LORA_DEFAULT_SPI_FREQUENCY 200000 +#define LORA_DEFAULT_SS_PIN LORA_IRQ_DUMB +#define LORA_DEFAULT_RESET_PIN -1 +#define LORA_DEFAULT_DIO0_PIN -1 +#elif defined(ARDUINO_SAMD_MKRWAN1310) +#define LORA_DEFAULT_SPI SPI1 +#define LORA_DEFAULT_SPI_FREQUENCY 200000 +#define LORA_DEFAULT_SS_PIN LORA_IRQ_DUMB +#define LORA_DEFAULT_RESET_PIN -1 +#define LORA_DEFAULT_DIO0_PIN LORA_IRQ +#else +#define LORA_DEFAULT_SPI SPI +#define LORA_DEFAULT_SPI_FREQUENCY 8E6 +#define LORA_DEFAULT_SS_PIN 10 +#define LORA_DEFAULT_RESET_PIN 9 +#define LORA_DEFAULT_DIO0_PIN 2 +#endif + +#define PA_OUTPUT_RFO_PIN 0 +#define PA_OUTPUT_PA_BOOST_PIN 1 + +class LoRaClass : public Stream { +public: + LoRaClass(); + + int begin(long frequency); + void end(); + + int beginPacket(int implicitHeader = false); + int endPacket(bool async = false); + + int parsePacket(int size = 0); + int packetRssi(); + float packetSnr(); + long packetFrequencyError(); + + int rssi(); + + // from Print + virtual size_t write(uint8_t byte); + virtual size_t write(const uint8_t *buffer, size_t size); + + // from Stream + virtual int available(); + virtual int read(); + virtual int peek(); + virtual void flush(); + +#ifndef ARDUINO_SAMD_MKRWAN1300 + void onReceive(void(*callback)(int)); + void onCadDone(void(*callback)(boolean)); + void onTxDone(void(*callback)()); + + void receive(int size = 0); + void channelActivityDetection(void); +#endif + void idle(); + void sleep(); + + void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN); + void setFrequency(long frequency); + void setSpreadingFactor(int sf); + void setSignalBandwidth(long sbw); + void setCodingRate4(int denominator); + void setPreambleLength(long length); + void setSyncWord(int sw); + void enableCrc(); + void disableCrc(); + void enableInvertIQ(); + void disableInvertIQ(); + + void setOCP(uint8_t mA); // Over Current Protection control + + void setGain(uint8_t gain); // Set LNA gain + + // deprecated + void crc() { enableCrc(); } + void noCrc() { disableCrc(); } + + byte random(); + + void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN); + void setSPI(SPIClass& spi); + void setSPIFrequency(uint32_t frequency); + + void dumpRegisters(Stream& out); + +private: + void explicitHeaderMode(); + void implicitHeaderMode(); + + void handleDio0Rise(); + bool isTransmitting(); + + int getSpreadingFactor(); + long getSignalBandwidth(); + + void setLdoFlag(); + + uint8_t readRegister(uint8_t address); + void writeRegister(uint8_t address, uint8_t value); + uint8_t singleTransfer(uint8_t address, uint8_t value); + + static void onDio0Rise(); + +private: + SPISettings _spiSettings; + SPIClass* _spi; + int _ss; + int _reset; + int _dio0; + long _frequency; + int _packetIndex; + int _implicitHeaderMode; + void (*_onReceive)(int); + void (*_onCadDone)(boolean); + void (*_onTxDone)(); +}; + +extern LoRaClass LoRa; + +#endif diff --git a/src/LoRaBridge.cpp b/src/LoRaBridge.cpp new file mode 100644 index 0000000..b468d39 --- /dev/null +++ b/src/LoRaBridge.cpp @@ -0,0 +1,22 @@ +#include + +bool LoRaBridge::begin(int cs_pin, int reset_pin, int dio0_pin, long frequency) +{ + LoRa.enableCrc(); + LoRa.setPins(cs_pin, reset_pin, dio0_pin); + if (!LoRa.begin(frequency)) + { + return false; + } + + return true; +} + +bool LoRaBridge::send(std::string line) +{ + LoRa.beginPacket(); + LoRa.print(line.c_str()); + LoRa.endPacket(); + + return true; +} \ No newline at end of file diff --git a/src/LoRaBridge.h b/src/LoRaBridge.h new file mode 100644 index 0000000..bfabf06 --- /dev/null +++ b/src/LoRaBridge.h @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include +#include "LoRa.h" + +class LoRaBridge: public LoRaClass +{ +public: + bool begin(int cs_pin = 18, int reset_pin = 14, int dio0_pin = 26, long frequency = 915000000); + bool send(std::string line); +}; \ No newline at end of file