/*
|
|
|
|
RFM69 MODULE
|
|
|
|
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
|
|
|
|
*/
|
|
|
|
#include "espurna.h"
|
|
|
|
#if RFM69_SUPPORT
|
|
|
|
#define RFM69_PACKET_SEPARATOR ':'
|
|
|
|
#include <RFM69.h>
|
|
#include <RFM69_ATC.h>
|
|
#include <SPI.h>
|
|
|
|
#include "rfm69.h"
|
|
#include "mqtt.h"
|
|
#include "ws.h"
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// PRIVATE
|
|
// -----------------------------------------------------------------------------
|
|
|
|
namespace rfm69 {
|
|
|
|
struct Message {
|
|
unsigned long id;
|
|
unsigned char packetID;
|
|
unsigned char senderID;
|
|
unsigned char targetID;
|
|
String key;
|
|
String value;
|
|
int16_t rssi;
|
|
};
|
|
|
|
struct Node {
|
|
unsigned long count = 0;
|
|
unsigned long missing = 0;
|
|
unsigned long duplicates = 0;
|
|
unsigned char lastPacketID = 0;
|
|
};
|
|
|
|
struct Mapping {
|
|
size_t node;
|
|
String key;
|
|
String topic;
|
|
};
|
|
|
|
namespace build {
|
|
|
|
constexpr size_t maxTopics() {
|
|
return RFM69_MAX_TOPICS;
|
|
}
|
|
|
|
constexpr size_t maxNodes() {
|
|
return RFM69_MAX_NODES;
|
|
}
|
|
|
|
constexpr uint8_t cs() {
|
|
return RFM69_CS_PIN;
|
|
}
|
|
|
|
constexpr uint8_t irq() {
|
|
return RFM69_IRQ_PIN;
|
|
}
|
|
|
|
constexpr bool hardware() {
|
|
return 1 == RFM69_IS_RFM69HW;
|
|
}
|
|
|
|
constexpr uint8_t frequency() {
|
|
return RFM69_FREQUENCY;
|
|
}
|
|
|
|
constexpr uint16_t nodeId() {
|
|
return RFM69_NODE_ID;
|
|
}
|
|
|
|
constexpr uint8_t networkId() {
|
|
return RFM69_NETWORK_ID;
|
|
}
|
|
|
|
constexpr uint8_t gatewayId() {
|
|
return RFM69_GATEWAY_ID;
|
|
}
|
|
|
|
const char* const encryptionKey() {
|
|
return RFM69_ENCRYPTKEY;
|
|
}
|
|
|
|
constexpr bool promiscuous() {
|
|
return 1 == RFM69_PROMISCUOUS;
|
|
}
|
|
|
|
constexpr bool promiscuousSends() {
|
|
return 1 == RFM69_PROMISCUOUS_SENDS;
|
|
}
|
|
|
|
const __FlashStringHelper* rootTopic() {
|
|
return F(RFM69_DEFAULT_TOPIC);
|
|
}
|
|
|
|
constexpr size_t node(size_t) {
|
|
return 0;
|
|
}
|
|
|
|
} // namespace build
|
|
|
|
namespace settings {
|
|
|
|
String rootTopic() {
|
|
return getSetting("rfm69Topic", build::rootTopic());
|
|
}
|
|
|
|
String topic(size_t index) {
|
|
return getSetting({"rfm69Topic", index});
|
|
}
|
|
|
|
String key(size_t index) {
|
|
return getSetting({"rfm69Key", index});
|
|
}
|
|
|
|
size_t node(size_t index) {
|
|
return getSetting({"rfm69Node", index}, build::node(index));
|
|
}
|
|
|
|
template <typename T>
|
|
void foreachMapping(T&& callback) {
|
|
for (size_t index = 0; index < build::maxTopics(); ++index) {
|
|
auto currentNode = node(index);
|
|
if (0 == currentNode) {
|
|
break;
|
|
}
|
|
|
|
Mapping entry{currentNode, key(index), topic(index)};
|
|
if (!entry.key.length() || !entry.topic.length()) {
|
|
break;
|
|
}
|
|
|
|
if (!callback(std::move(entry))) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<Mapping> mapping() {
|
|
std::vector<Mapping> out;
|
|
foreachMapping([&](rfm69::Mapping&& entry) {
|
|
out.emplace_back(std::move(entry));
|
|
return true;
|
|
});
|
|
|
|
return out;
|
|
}
|
|
|
|
} // namespace settings
|
|
} // namespace rfm69
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
class RFM69Wrap: public RFM69_ATC {
|
|
public:
|
|
using RFM69_ATC::RFM69_ATC;
|
|
|
|
protected:
|
|
// overriding SPI_CLOCK for ESP8266
|
|
void select() {
|
|
noInterrupts();
|
|
|
|
#if defined (SPCR) && defined (SPSR)
|
|
// save current SPI settings
|
|
_SPCR = SPCR;
|
|
_SPSR = SPSR;
|
|
#endif
|
|
|
|
// set RFM69 SPI settings
|
|
SPI.setDataMode(SPI_MODE0);
|
|
SPI.setBitOrder(MSBFIRST);
|
|
|
|
#if defined(__arm__)
|
|
SPI.setClockDivider(SPI_CLOCK_DIV16);
|
|
#elif defined(ARDUINO_ARCH_ESP8266)
|
|
SPI.setClockDivider(SPI_CLOCK_DIV2); // speeding it up for the ESP8266
|
|
#else
|
|
SPI.setClockDivider(SPI_CLOCK_DIV4);
|
|
#endif
|
|
|
|
digitalWrite(_slaveSelectPin, LOW);
|
|
}
|
|
};
|
|
|
|
namespace {
|
|
|
|
std::unique_ptr<RFM69Wrap> _rfm69_radio;
|
|
rfm69::Node _rfm69_node_info[rfm69::build::maxNodes()];
|
|
size_t _rfm69_node_count;
|
|
size_t _rfm69_packet_count;
|
|
|
|
void _rfm69Clear() {
|
|
for (auto& info : _rfm69_node_info) {
|
|
info.duplicates = 0;
|
|
info.missing = 0;
|
|
info.count = 0;
|
|
}
|
|
_rfm69_node_count = 0;
|
|
_rfm69_packet_count = 0;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// WEB
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#if WEB_SUPPORT
|
|
|
|
void _rfm69WebSocketOnVisible(JsonObject& root) {
|
|
wsPayloadModule(root, PSTR("rfm69"));
|
|
}
|
|
|
|
void _rfm69WebSocketOnConnected(JsonObject& root) {
|
|
root["rfm69Topic"] = rfm69::settings::rootTopic();
|
|
|
|
JsonObject& rfm69 = root.createNestedObject("rfm69");
|
|
rfm69["packets"] = _rfm69_packet_count; // TODO: unused?
|
|
rfm69["nodes"] = _rfm69_node_count; // TODO: unused?
|
|
|
|
static const char* const keys[] {
|
|
"rfm69Node", "rfm69Key", "rfm69Topic"
|
|
};
|
|
JsonArray& schema = rfm69.createNestedArray("schema");
|
|
schema.copyFrom(keys, sizeof(keys) / sizeof(*keys));
|
|
|
|
JsonArray& mappings = rfm69.createNestedArray("mapping");
|
|
for (auto& mapping : rfm69::settings::mapping()) {
|
|
JsonArray& entry = mappings.createNestedArray();
|
|
entry.add(mapping.node);
|
|
entry.add(mapping.key);
|
|
entry.add(mapping.topic);
|
|
}
|
|
}
|
|
|
|
bool _rfm69WebSocketOnKeyCheck(const char * key, JsonVariant& value) {
|
|
return (strncmp(key, "rfm69", 5) == 0);
|
|
}
|
|
|
|
void _rfm69WebSocketOnAction(uint32_t client_id, const char* action, JsonObject& data) {
|
|
if (strcmp(action, "rfm69Clear") == 0) {
|
|
_rfm69Clear();
|
|
}
|
|
}
|
|
|
|
#endif // WEB_SUPPORT
|
|
|
|
void _rfm69CleanNodes(size_t max) {
|
|
size_t id { 0 };
|
|
rfm69::settings::foreachMapping([&](rfm69::Mapping&&) {
|
|
if (id < max) {
|
|
++id;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
while (id < max) {
|
|
delSetting({"rfm69Node", id});
|
|
delSetting({"rfm69Key", id});
|
|
delSetting({"rfm69Topic", id});
|
|
++id;
|
|
}
|
|
}
|
|
|
|
void _rfm69Configure() {
|
|
_rfm69CleanNodes(rfm69::build::maxTopics());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Radio
|
|
// -----------------------------------------------------------------------------
|
|
|
|
void _rfm69Debug(const char* prefix, const rfm69::Message& message) {
|
|
DEBUG_MSG_P(PSTR("[RFM69] %s: message ID:%05u sender:%03hhu target:%03hhu packet:%03hhu rssi:%-04hd key:%s value:%s\n"),
|
|
prefix,
|
|
message.id, message.senderID, message.targetID, message.packetID,
|
|
message.rssi, message.key.c_str(), message.value.c_str());
|
|
}
|
|
|
|
void _rfm69Process(const rfm69::Message& message) {
|
|
// Is node beyond RFM69_MAX_NODES?
|
|
if (message.senderID >= rfm69::build::maxNodes()) {
|
|
return;
|
|
}
|
|
|
|
// Count seen nodes
|
|
if (_rfm69_node_info[message.senderID].count == 0) {
|
|
++_rfm69_node_count;
|
|
}
|
|
|
|
// Detect duplicates and missing messages
|
|
// message ID==0 means device is not sending this info
|
|
if (message.id > 0) {
|
|
if (_rfm69_node_info[message.senderID].count > 0) {
|
|
auto gap = message.packetID - _rfm69_node_info[message.senderID].lastPacketID;
|
|
if (gap == 0) {
|
|
_rfm69_node_info[message.senderID].duplicates = _rfm69_node_info[message.senderID].duplicates + 1;
|
|
return;
|
|
}
|
|
|
|
constexpr decltype(gap) Offset { 1 };
|
|
if ((gap > Offset) && (message.id > Offset)) {
|
|
_rfm69_node_info[message.senderID].missing = _rfm69_node_info[message.senderID].missing + gap - Offset;
|
|
DEBUG_MSG_P(PSTR("[RFM69] %hhu missing messages detected\n"), gap - Offset);
|
|
}
|
|
}
|
|
}
|
|
|
|
_rfm69Debug("OK", message);
|
|
|
|
_rfm69_node_info[message.senderID].lastPacketID = message.packetID;
|
|
_rfm69_node_info[message.senderID].count += 1;
|
|
|
|
#if WEB_SUPPORT
|
|
{
|
|
auto& info = _rfm69_node_info[message.senderID];
|
|
|
|
auto duplicates = info.duplicates;
|
|
auto missing = info.missing;
|
|
|
|
wsPost([message, duplicates, missing](JsonObject& root) {
|
|
JsonObject& rfm69 = root.createNestedObject("rfm69");
|
|
rfm69["packets"] = _rfm69_packet_count; // TODO: unused?
|
|
rfm69["nodes"] = _rfm69_node_count; // TODO: unused?
|
|
|
|
JsonArray& msg = rfm69.createNestedArray("message");
|
|
msg.add(message.packetID);
|
|
msg.add(message.senderID);
|
|
msg.add(message.targetID);
|
|
msg.add(message.key);
|
|
msg.add(message.value);
|
|
msg.add(message.rssi);
|
|
msg.add(duplicates);
|
|
msg.add(missing);
|
|
});
|
|
}
|
|
#endif
|
|
|
|
// If we are the target of the message, forward it via MQTT, otherwise quit
|
|
if (!rfm69::build::promiscuousSends() && (rfm69::build::gatewayId() != message.targetID)) {
|
|
return;
|
|
}
|
|
|
|
#if MQTT_SUPPORT
|
|
// Try to find a matching mapping
|
|
bool found { false };
|
|
rfm69::settings::foreachMapping([&](rfm69::Mapping&& mapping) {
|
|
if ((mapping.node == message.senderID) && (mapping.key == message.key)) {
|
|
found = true;
|
|
mqttSendRaw(mapping.topic.c_str(), message.value.c_str());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// Mapping not found, use default topic
|
|
if (!found) {
|
|
auto topic = rfm69::settings::rootTopic();
|
|
if (topic.length() > 0) {
|
|
topic.replace("{node}", String(message.senderID, 10));
|
|
topic.replace("{key}", message.key);
|
|
mqttSendRaw(topic.c_str(), message.value.c_str());
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void _rfm69Loop() {
|
|
if (_rfm69_radio->receiveDone()) {
|
|
uint8_t senderID = _rfm69_radio->SENDERID;
|
|
uint8_t targetID = _rfm69_radio->TARGETID;
|
|
int16_t rssi = _rfm69_radio->RSSI;
|
|
uint8_t length = _rfm69_radio->DATALEN;
|
|
char buffer[length + 1];
|
|
strncpy(buffer, (const char *) _rfm69_radio->DATA, length);
|
|
buffer[length] = 0;
|
|
|
|
// Do not send ACKs in promiscuous mode,
|
|
// we want to listen without being heard
|
|
if (!rfm69::build::promiscuous()) {
|
|
if (_rfm69_radio->ACKRequested()) {
|
|
_rfm69_radio->sendACK();
|
|
}
|
|
}
|
|
|
|
uint8_t parts = 1;
|
|
for (uint8_t i = 0; i < length; ++i) {
|
|
if (buffer[i] == RFM69_PACKET_SEPARATOR) {
|
|
++parts;
|
|
}
|
|
}
|
|
|
|
if (parts > 1) {
|
|
char sep[2] = {RFM69_PACKET_SEPARATOR, 0};
|
|
|
|
uint8_t packetID = 0;
|
|
char* key = strtok(buffer, sep);
|
|
char* value = strtok(NULL, sep);
|
|
if (parts > 2) {
|
|
char * packet = strtok(NULL, sep);
|
|
packetID = atoi(packet);
|
|
}
|
|
|
|
_rfm69Process(rfm69::Message{
|
|
++_rfm69_packet_count,
|
|
packetID,
|
|
senderID,
|
|
targetID,
|
|
key,
|
|
value,
|
|
rssi
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _rfm69SettingsMigrate(int version) {
|
|
if (version < 8) {
|
|
moveSettings("node", "rfm69Node");
|
|
moveSettings("key", "rfm69Key");
|
|
moveSettings("topic", "rfm69Topic");
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// RFM69
|
|
// -----------------------------------------------------------------------------
|
|
|
|
void rfm69Setup() {
|
|
delay(10);
|
|
|
|
migrateVersion(_rfm69SettingsMigrate);
|
|
_rfm69Configure();
|
|
|
|
_rfm69_radio = std::make_unique<RFM69Wrap>(rfm69::build::cs(), rfm69::build::irq(), rfm69::build::hardware());
|
|
_rfm69_radio->initialize(rfm69::build::frequency(), rfm69::build::nodeId(), rfm69::build::networkId());
|
|
_rfm69_radio->encrypt(rfm69::build::encryptionKey());
|
|
_rfm69_radio->spyMode(rfm69::build::promiscuous());
|
|
_rfm69_radio->enableAutoPower(0);
|
|
if (rfm69::build::hardware()) {
|
|
_rfm69_radio->setHighPower();
|
|
}
|
|
|
|
DEBUG_MSG_P(PSTR("[RFM69] Working at %d MHz\n"),
|
|
(rfm69::build::frequency() == RF69_433MHZ) ? 433 :
|
|
(rfm69::build::frequency() == RF69_868MHZ) ? 868 : 915);
|
|
DEBUG_MSG_P(PSTR("[RFM69] Node %u\n"), rfm69::build::nodeId());
|
|
DEBUG_MSG_P(PSTR("[RFM69] Network %u\n"), rfm69::build::networkId());
|
|
if (rfm69::build::promiscuous()) {
|
|
DEBUG_MSG_P(PSTR("[RFM69] Promiscuous mode\n"));
|
|
}
|
|
|
|
#if WEB_SUPPORT
|
|
wsRegister()
|
|
.onVisible(_rfm69WebSocketOnVisible)
|
|
.onConnected(_rfm69WebSocketOnConnected)
|
|
.onAction(_rfm69WebSocketOnAction)
|
|
.onKeyCheck(_rfm69WebSocketOnKeyCheck);
|
|
#endif
|
|
|
|
espurnaRegisterLoop(_rfm69Loop);
|
|
espurnaRegisterReload(_rfm69Configure);
|
|
}
|
|
|
|
#endif // RFM69_SUPPORT
|