Mirror of espurna firmware for wireless switches and more
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

478 lines
12 KiB

/*
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