mesh sending and receving now much more real

1.2-legacy
geeksville 2020-02-02 12:45:32 -08:00
rodzic 4051bf8465
commit 9aed5056ad
10 zmienionych plików z 352 dodań i 268 usunięć

Wyświetl plik

@ -5,6 +5,7 @@
* solder debug headers to board
* make message send from android go to service, then to mesh radio
* make message receive from radio go through to android
* test loopback tx/rx path code without using radio
* have MeshService keep a node DB by sniffing user messages
* have meshservice send location data on mesh (if device has a GPS)
@ -20,6 +21,8 @@
* sendToMesh can currently block for a long time, instead have it just queue a packet for a radio freertos thread
* see section 7.3 of https://cdn.sparkfun.com/assets/learn_tutorials/8/0/4/RFM95_96_97_98W.pdf and have hope radio wake only when a valid packet is received. Possibly even wake the ESP32 from deep sleep via GPIO.
* fix the logo
* do debug logging to android over bluetooth
* break out my bluetooth OTA software as a seperate library so others can use it
# Pre-beta priority

Wyświetl plik

@ -3,6 +3,7 @@
#include <Arduino.h>
#include <assert.h>
#include "PointerQueue.h"
/**
* A pool based allocator
@ -10,12 +11,13 @@
* Eventually this routine will even be safe for ISR use...
*/
template <class T> class MemoryPool {
TypedQueue<T *> dead;
PointerQueue<T> dead;
T *buf; // our large raw block of memory
size_t maxElements;
public:
MemoryPool(int maxElements): queued(maxElements), dead(maxElements) {
MemoryPool(size_t _maxElements): dead(_maxElements), maxElements(_maxElements) {
buf = new T[maxElements];
// prefill dead
@ -29,19 +31,28 @@ public:
/// Return a queable object which has been prefilled with zeros
T *allocZeroed(TickType_t maxWait = portMAX_DELAY) {
T *p;
if(dead.dequeue(&p, maxWait) != pdTRUE)
return NULL;
memset(p, 0, sizeof(T));
T *p = dead.dequeuePtr(maxWait);
if(p)
memset(p, 0, sizeof(T));
return p;
}
/// Return a queable object which is a copy of some other object
T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) {
T *p = dead.dequeuePtr(maxWait);
if(p)
memcpy(p, &src, sizeof(T));
return p;
}
/// Return a buffer for use by others
void free(T *p) {
void release(T *p) {
int res = dead.enqueue(p, 0);
assert(res == pdTRUE);
assert(p >= buf && (p - buf) < maxElements); // sanity check to make sure a programmer didn't free something that didn't come from this pool
}
};

Wyświetl plik

@ -8,148 +8,8 @@
#include <pb_encode.h>
#include <pb_decode.h>
#include "mesh.pb.h"
#include "MeshRadio.h"
#include "TypedQueue.h"
#include "MemoryPool.h"
#include "MeshService.h"
/*
receivedPacketQueue - this is a queue of messages we've received from the mesh, which we are keeping to deliver to the phone.
It is implemented with a FreeRTos queue (wrapped with a little RTQueue class) of pointers to MeshPacket protobufs (which were alloced with new).
After a packet ptr is removed from the queue and processed it should be deleted. (eventually we should move sent packets into a 'sentToPhone' queue
of packets we can delete just as soon as we are sure the phone has acked those packets - when the phone writes to FromNum)
mesh - an instance of Mesh class. Which manages the interface to the mesh radio library, reception of packets from other nodes, arbitrating to select
a node number and keeping the current nodedb.
typedef in32_t NodeNum;
class NodeInfo {
position;
last_seen
user
};
class NodeDB {
NodeNum provisionalNodeNum; // if we are trying to find a node num this is our current attempt
NodeNum ourNodeNum; // -1 if not yet found
HashMap<NodeNum, NodeInfo> nodes;
public:
/// don't do mesh based algoritm for node id assignment (initially) - instead just store in flash - possibly even in the initial alpha release do this hack
/// if returns false, that means our node should send a DenyNodeNum response. If true, we think the number is okay for use
// bool handleWantNodeNum(NodeNum n);
void handleDenyNodeNum(NodeNum FIXME read mesh proto docs, perhaps picking a random node num is not a great idea
and instead we should use a special 'im unconfigured node number' and include our desired node number in the wantnum message. the
unconfigured node num would only be used while initially joining the mesh so low odds of conflicting (especially if we randomly select
from a small number of nodenums which can be used temporarily for this operation). figure out what the lower level
mesh sw does if it does conflict? would it be better for people who are replying with denynode num to just broadcast their denial?)
};
*/
#define MAX_PACKETS 32 // max number of packets which can be in flight (either queued from reception or queued for sending)
#define MAX_RX_TOPHONE 16 // max number of packets which can be waiting for delivery to android
/// A temporary buffer used for sending packets, sized to hold the biggest buffer we might need
static uint8_t outbuf[MeshPacket_size];
/**
* Top level app for this service. keeps the mesh, the radio config and the queue of received packets.
*
*/
class MeshService
{
MemoryPool<MeshPacket> packetPool;
/// received packets waiting for the phone to process them
/// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure
/// we never hang because android hasn't been there in a while
PointerQueue<MeshPacket> toPhoneQueue;
/// Packets which have just arrived from the radio, ready to be processed by this service and possibly
/// forwarded to the phone. Note: not using yet - seeing if I can just handle everything asap in handleFromRadio
// PointerQueue<MeshPacket> fromRadioQueue;
public:
MeshService() : packetPool(MAX_PACKETS), toPhoneQueue(MAX_RX_TOPHONE) {
}
/// Do idle processing (mostly processing messages which have been queued from the radio)
// void loop() { }
/**
* handle an incoming MeshPacket from the radio, update DB state and queue it for the phone
*/
void handleFromRadio(NodeNum from, NodeNum to, const uint8_t *buf, size_t len) {
MeshPacket *p = packetPool.allocZeroed();
assert(p);
pb_istream_t stream = pb_istream_from_buffer(buf, len);
if (!pb_decode(&stream, MeshPacket_fields, p) || !p->has_payload)
{
Serial.printf("Error: can't decode MeshPacket %s\n", PB_GET_ERROR(&stream));
}
else
{
// FIXME - update DB state based on payload and show recevied texts
toPhoneQueue.enqueue(p);
}
}
/// Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh)
void handleToRadio(std::string s)
{
static ToRadio r; // new ToRadio(); FIXME dynamically allocate
pb_istream_t stream = pb_istream_from_buffer((const uint8_t *)s.c_str(), s.length());
if (!pb_decode(&stream, ToRadio_fields, &r))
{
Serial.printf("Error: can't decode ToRadio %s\n", PB_GET_ERROR(&stream));
}
else
{
switch (r.which_variant)
{
case ToRadio_packet_tag:
sendToMesh(r.variant.packet);
break;
default:
Serial.println("Error: unexpected ToRadio variant");
break;
}
}
}
private:
/// Send a packet into the mesh
void sendToMesh(const MeshPacket &p)
{
assert(p.has_payload);
pb_ostream_t stream = pb_ostream_from_buffer(outbuf, sizeof(outbuf));
if (!pb_encode(&stream, MeshPacket_fields, &p))
{
Serial.printf("Error: can't encode MeshPacket %s\n", PB_GET_ERROR(&stream));
}
else
{
assert(radio.sendTo(p.to, outbuf, stream.bytes_written) == ERRNO_OK);
}
}
};
MeshService service;
static BLECharacteristic meshFromRadioCharacteristic("8ba2bcc2-ee02-4a55-a531-c525c5e454d5", BLECharacteristic::PROPERTY_READ);
static BLECharacteristic meshToRadioCharacteristic("f75c76d2-129e-4dad-a1dd-7866124401e7", BLECharacteristic::PROPERTY_WRITE);

Wyświetl plik

@ -3,103 +3,127 @@
#include <RHMesh.h>
#include <assert.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "MeshRadio.h"
#include "configuration.h"
// Change to 434.0 or other frequency, must match RX's freq!
#define RF95_FREQ 915.0
MeshRadio radio;
/**
* get our starting (provisional) nodenum from flash. But check first if anyone else is using it, by trying to send a message to it (arping)
*/
NodeNum getDesiredNodeNum() {
uint8_t dmac[6];
esp_efuse_mac_get_default(dmac);
// FIXME not the right way to guess node numes
uint8_t r = dmac[5];
assert(r != 0xff); // It better not be the broadcast address
return r;
}
MeshRadio::MeshRadio() : rf95(NSS_GPIO, DIO0_GPIO), manager(rf95, getDesiredNodeNum()) {
}
bool MeshRadio::init() {
pinMode(RESET_GPIO, OUTPUT); // Deassert reset
digitalWrite(RESET_GPIO, HIGH);
// pulse reset
digitalWrite(RESET_GPIO, LOW);
delay(10);
digitalWrite(RESET_GPIO, HIGH);
delay(10);
if (!manager.init()) {
Serial.println("LoRa radio init failed");
Serial.println("Uncomment '#define SERIAL_DEBUG' in RH_RF95.cpp for detailed debug info");
return false;
}
Serial.println("LoRa radio init OK!");
// Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
if (!rf95.setFrequency(RF95_FREQ)) {
Serial.println("setFrequency failed");
while (1);
}
Serial.print("Set Freq to: "); Serial.println(RF95_FREQ);
// Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on
// The default transmitter power is 13dBm, using PA_BOOST.
// If you are using RFM95/96/97/98 modules which uses the PA_BOOST transmitter pin, then
// you can set transmitter powers from 5 to 23 dBm:
// FIXME - can we do this? It seems to be in the Heltec board.
rf95.setTxPower(23, false);
return true;
}
ErrorCode MeshRadio::sendTo(NodeNum dest, const uint8_t *buf, size_t len) {
Serial.printf("mesh sendTo %d bytes to %d\n", len, dest);
// FIXME - for now we do all packets as broadcast
dest = NODENUM_BROADCAST;
// Note: we don't use sendToWait here because we don't want to wait and for the time being don't require
// reliable delivery
// return manager.sendtoWait((uint8_t *) buf, len, dest);
return manager.sendto((uint8_t *) buf, len, dest) ? ERRNO_OK : ERRNO_UNKNOWN;
}
void MeshRadio::loop() {
// FIXME read from radio with recvfromAckTimeout
}
void mesh_init() {
while (!radio.init()) {
Serial.println("radio init failed");
while (1);
}
}
int16_t packetnum = 0; // packet counter, we increment per xmission
void mesh_loop()
NodeNum getDesiredNodeNum()
{
radio.loop();
uint8_t dmac[6];
esp_efuse_mac_get_default(dmac);
delay(1000); // Wait 1 second between transmits, could also 'sleep' here!
// FIXME not the right way to guess node numes
uint8_t r = dmac[5];
assert(r != 0xff); // It better not be the broadcast address
return r;
}
MeshRadio::MeshRadio(MemoryPool<MeshPacket> &_pool, PointerQueue<MeshPacket> &_rxDest)
: rf95(NSS_GPIO, DIO0_GPIO),
manager(rf95, getDesiredNodeNum()),
pool(_pool),
rxDest(_rxDest),
txQueue(MAX_TX_QUEUE)
{
}
bool MeshRadio::init()
{
pinMode(RESET_GPIO, OUTPUT); // Deassert reset
digitalWrite(RESET_GPIO, HIGH);
// pulse reset
digitalWrite(RESET_GPIO, LOW);
delay(10);
digitalWrite(RESET_GPIO, HIGH);
delay(10);
if (!manager.init())
{
Serial.println("LoRa radio init failed");
Serial.println("Uncomment '#define SERIAL_DEBUG' in RH_RF95.cpp for detailed debug info");
return false;
}
Serial.println("LoRa radio init OK!");
// Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
if (!rf95.setFrequency(RF95_FREQ))
{
Serial.println("setFrequency failed");
while (1)
;
}
Serial.print("Set Freq to: ");
Serial.println(RF95_FREQ);
// Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on
// The default transmitter power is 13dBm, using PA_BOOST.
// If you are using RFM95/96/97/98 modules which uses the PA_BOOST transmitter pin, then
// you can set transmitter powers from 5 to 23 dBm:
// FIXME - can we do this? It seems to be in the Heltec board.
rf95.setTxPower(23, false);
return true;
}
ErrorCode MeshRadio::send(MeshPacket *p)
{
int res = ERRNO_UNKNOWN;
/// A temporary buffer used for sending packets, sized to hold the biggest buffer we might need
static uint8_t outbuf[SubPacket_size];
assert(p->has_payload);
pb_ostream_t stream = pb_ostream_from_buffer(outbuf, sizeof(outbuf));
if (!pb_encode(&stream, SubPacket_fields, &p->payload))
{
Serial.printf("Error: can't encode SubPacket %s\n", PB_GET_ERROR(&stream));
}
else
{
res = sendTo(p->to, outbuf, stream.bytes_written);
}
pool.release(p);
return res;
}
ErrorCode MeshRadio::sendTo(NodeNum dest, const uint8_t *buf, size_t len)
{
Serial.printf("mesh sendTo %d bytes to %d\n", len, dest);
// FIXME - for now we do all packets as broadcast
dest = NODENUM_BROADCAST;
// Note: we don't use sendToWait here because we don't want to wait and for the time being don't require
// reliable delivery
// return manager.sendtoWait((uint8_t *) buf, len, dest);
return manager.sendto((uint8_t *)buf, len, dest) ? ERRNO_OK : ERRNO_UNKNOWN;
}
void MeshRadio::loop()
{
// FIXME read from radio with recvfromAckTimeout
#if 0
static int16_t packetnum = 0; // packet counter, we increment per xmission
char radiopacket[20] = "Hello World # ";
sprintf(radiopacket, "hello %d", packetnum++);
assert(radio.sendTo(NODENUM_BROADCAST, (uint8_t *)radiopacket, sizeof(radiopacket)) == ERRNO_OK);
assert(sendTo(NODENUM_BROADCAST, (uint8_t *)radiopacket, sizeof(radiopacket)) == ERRNO_OK);
#endif
// manager.recvfromAckTimeout()
}

Wyświetl plik

@ -2,6 +2,9 @@
#include <RH_RF95.h>
#include <RHMesh.h>
#include "MemoryPool.h"
#include "mesh.pb.h"
#include "PointerQueue.h"
#define NODENUM_BROADCAST 255
#define ERRNO_OK 0
@ -10,37 +13,42 @@
typedef int ErrorCode;
typedef uint8_t NodeNum;
/// Callback for a receive packet, the callee must copy/queue the payload elsewhere before returning
typedef void (*MeshRXHandler)(NodeNum from, NodeNum to, const uint8_t *buf, size_t len);
#define MAX_TX_QUEUE 4 // max number of packets which can be waiting for transmission
/**
* A raw low level interface to our mesh. Only understands nodenums and bytes (not protobufs or node ids)
*/
class MeshRadio {
public:
MeshRadio();
/** pool is the pool we will alloc our rx packets from
* rxDest is where we will send any rx packets, it becomes receivers responsibility to return packet to the pool
*/
MeshRadio(MemoryPool<MeshPacket> &pool, PointerQueue<MeshPacket> &rxDest);
bool init();
/// Prepare the radio to enter sleep mode, where it should draw only 0.2 uA
void sleep() { rf95.sleep(); }
/// Send a packet - the current implementation blocks for a while possibly (FIXME)
ErrorCode sendTo(NodeNum dest, const uint8_t *buf, size_t len);
/// Send a packet (possibly by enquing in a private fifo). This routine will
/// later free() the packet to pool.
ErrorCode send(MeshPacket *p);
/// Do loop callback operations (we currently FIXME poll the receive mailbox here)
/// for received packets it will call the rx handler
void loop();
void setRXHandler(MeshRXHandler h) { rxHandler = h; }
private:
RH_RF95 rf95; // the raw radio interface
RHMesh manager;
MeshRXHandler rxHandler;
// MeshRXHandler rxHandler;
MemoryPool<MeshPacket> &pool;
PointerQueue<MeshPacket> &rxDest;
PointerQueue<MeshPacket> txQueue;
/// low level send, might block for mutiple seconds
ErrorCode sendTo(NodeNum dest, const uint8_t *buf, size_t len);
};
extern MeshRadio radio;
void mesh_init();
void mesh_loop();

103
src/MeshService.cpp 100644
Wyświetl plik

@ -0,0 +1,103 @@
#include <Arduino.h>
#include <assert.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "mesh.pb.h"
#include "MeshService.h"
/*
receivedPacketQueue - this is a queue of messages we've received from the mesh, which we are keeping to deliver to the phone.
It is implemented with a FreeRTos queue (wrapped with a little RTQueue class) of pointers to MeshPacket protobufs (which were alloced with new).
After a packet ptr is removed from the queue and processed it should be deleted. (eventually we should move sent packets into a 'sentToPhone' queue
of packets we can delete just as soon as we are sure the phone has acked those packets - when the phone writes to FromNum)
mesh - an instance of Mesh class. Which manages the interface to the mesh radio library, reception of packets from other nodes, arbitrating to select
a node number and keeping the current nodedb.
typedef in32_t NodeNum;
class NodeInfo {
position;
last_seen
user
};
class NodeDB {
NodeNum provisionalNodeNum; // if we are trying to find a node num this is our current attempt
NodeNum ourNodeNum; // -1 if not yet found
HashMap<NodeNum, NodeInfo> nodes;
public:
/// don't do mesh based algoritm for node id assignment (initially) - instead just store in flash - possibly even in the initial alpha release do this hack
/// if returns false, that means our node should send a DenyNodeNum response. If true, we think the number is okay for use
// bool handleWantNodeNum(NodeNum n);
void handleDenyNodeNum(NodeNum FIXME read mesh proto docs, perhaps picking a random node num is not a great idea
and instead we should use a special 'im unconfigured node number' and include our desired node number in the wantnum message. the
unconfigured node num would only be used while initially joining the mesh so low odds of conflicting (especially if we randomly select
from a small number of nodenums which can be used temporarily for this operation). figure out what the lower level
mesh sw does if it does conflict? would it be better for people who are replying with denynode num to just broadcast their denial?)
};
*/
MeshService service;
#define MAX_PACKETS 32 // max number of packets which can be in flight (either queued from reception or queued for sending)
#define MAX_RX_TOPHONE 16 // max number of packets which can be waiting for delivery to android
MeshService::MeshService() : packetPool(MAX_PACKETS), toPhoneQueue(MAX_RX_TOPHONE), radio(packetPool, toPhoneQueue)
{
}
void MeshService::init()
{
if (!radio.init())
Serial.println("radio init failed");
}
/// Do idle processing (mostly processing messages which have been queued from the radio)
void MeshService::loop()
{
radio.loop(); // FIXME, possibly move radio interaction to own thread
}
/// Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh)
void MeshService::handleToRadio(std::string s)
{
static ToRadio r; // this is a static scratch object, any data must be copied elsewhere before returning
pb_istream_t stream = pb_istream_from_buffer((const uint8_t *)s.c_str(), s.length());
if (!pb_decode(&stream, ToRadio_fields, &r))
{
Serial.printf("Error: can't decode ToRadio %s\n", PB_GET_ERROR(&stream));
}
else
{
switch (r.which_variant)
{
case ToRadio_packet_tag:
sendToMesh(r.variant.packet);
break;
default:
Serial.println("Error: unexpected ToRadio variant");
break;
}
}
}
/// Send a packet into the mesh - note p is read only and should be copied into a pool based MeshPacket before
/// sending.
void MeshService::sendToMesh(const MeshPacket &pIn)
{
MeshPacket *pOut = packetPool.allocCopy(pIn);
assert(pOut); // FIXME
assert(radio.send(pOut) == pdTRUE);
}

73
src/MeshService.h 100644
Wyświetl plik

@ -0,0 +1,73 @@
#pragma once
#include <Arduino.h>
#include <assert.h>
#include "mesh.pb.h"
#include "MeshRadio.h"
#include "PointerQueue.h"
#include "MemoryPool.h"
/**
* Top level app for this service. keeps the mesh, the radio config and the queue of received packets.
*
*/
class MeshService
{
MemoryPool<MeshPacket> packetPool;
/// received packets waiting for the phone to process them
/// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure
/// we never hang because android hasn't been there in a while
PointerQueue<MeshPacket> toPhoneQueue;
/// Packets which have just arrived from the radio, ready to be processed by this service and possibly
/// forwarded to the phone. Note: not using yet - seeing if I can just handle everything asap in handleFromRadio
// PointerQueue<MeshPacket> fromRadioQueue;
public:
MeshRadio radio;
MeshService();
void init();
/// Do idle processing (mostly processing messages which have been queued from the radio)
void loop();
#if 0
/**
* handle an incoming MeshPacket from the radio, update DB state and queue it for the phone
*/
void handleFromRadio(NodeNum from, NodeNum to, const uint8_t *buf, size_t len) {
MeshPacket *p = packetPool.allocZeroed();
assert(p);
pb_istream_t stream = pb_istream_from_buffer(buf, len);
if (!pb_decode(&stream, MeshPacket_fields, p) || !p->has_payload)
{
Serial.printf("Error: can't decode MeshPacket %s\n", PB_GET_ERROR(&stream));
}
else
{
// FIXME - update DB state based on payload and show recevied texts
toPhoneQueue.enqueue(p);
}
}
#endif
/// Given a ToRadio buffer (from bluetooth) parse it and properly handle it (setup radio, owner or send packet into the mesh)
void handleToRadio(std::string s);
private:
/// Send a packet into the mesh - note p is read only and should be copied into a pool based MeshPacket before
/// sending.
void sendToMesh(const MeshPacket &p);
};
extern MeshService service;

21
src/PointerQueue.h 100644
Wyświetl plik

@ -0,0 +1,21 @@
#pragma once
#include "TypedQueue.h"
/**
* A wrapper for freertos queues that assumes each element is a pointer
*/
template <class T> class PointerQueue: public TypedQueue<T *> {
public:
PointerQueue(int maxElements) : TypedQueue<T *>(maxElements) {
}
// preturns a ptr or null if the queue was empty
T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) {
T *p;
return this->dequeue(&p, maxWait) == pdTRUE ? p : NULL;
}
};

Wyświetl plik

@ -29,22 +29,3 @@ public:
return xQueueReceive(h, p, maxWait);
}
};
/**
* A wrapper for freertos queues that assumes each element is a pointer
*/
template <class T> class PointerQueue: public TypedQueue<T *> {
TypedQueue h;
public:
PointerQueue(int maxElements) : TypedQueue(maxElements) {
}
// preturns a ptr or null if the queue was empty
T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) {
T *p;
return dequeue(&p, maxWait) == pdTRUE ? p : NULL;
}
};

Wyświetl plik

@ -28,7 +28,7 @@
#include <Wire.h>
#include "BluetoothUtil.h"
#include "MeshBluetoothService.h"
#include "MeshRadio.h"
#include "MeshService.h"
#ifdef T_BEAM_V10
#include "axp20x.h"
@ -62,7 +62,7 @@ void doDeepSleep(uint64_t msecToWake)
screen_off(); // datasheet says this will draw only 10ua
// Put radio in sleep mode (will still draw power but only 0.2uA)
radio.sleep();
service.radio.sleep();
#ifdef RESET_OLED
digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power
@ -345,7 +345,7 @@ void setup()
delay(LOGO_DELAY);
//}
mesh_init();
service.init();
BLEServer *serve = initBLE("KHBT Test"); // FIXME, use a real name based on the macaddr
BLEService *bts = createMeshBluetoothService(serve);
bts->start();
@ -356,7 +356,7 @@ void loop()
{
gps_loop();
screen_loop();
mesh_loop();
service.loop();
loopBLE();
#ifdef LED_PIN