mirror of
https://github.com/meshtastic/firmware.git
synced 2026-02-06 00:52:01 +00:00
Compare commits
9 Commits
ble-Banner
...
zps-module
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0060b61fe | ||
|
|
538a5f0dfc | ||
|
|
b7db22055d | ||
|
|
0703e0e6d7 | ||
|
|
f514bc230b | ||
|
|
a7435b85a1 | ||
|
|
729d6c576f | ||
|
|
15b84fca01 | ||
|
|
285b30dff0 |
@@ -50,6 +50,8 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DRADIOLIB_EXCLUDE_APRS=1
|
||||
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||
-DMESHTASTIC_EXCLUDE_ZPS=1
|
||||
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
|
||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||
|
||||
@@ -459,6 +459,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
// if it's not HIGH - check the battery
|
||||
#endif
|
||||
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
|
||||
return false;
|
||||
|
||||
// technically speaking this should work for all(?) NRF52 boards
|
||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include "graphics/niche/InkHUD/Tile.h"
|
||||
#include <cstdint>
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./Applet.h"
|
||||
@@ -785,6 +787,16 @@ void InkHUD::Applet::drawHeader(std::string text)
|
||||
drawPixel(x, 0, BLACK);
|
||||
drawPixel(x, headerDivY, BLACK); // Dotted 50%
|
||||
}
|
||||
|
||||
// Dither near battery
|
||||
if (settings->optionalFeatures.batteryIcon) {
|
||||
constexpr uint16_t ditherSizePx = 4;
|
||||
Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile();
|
||||
const uint16_t batteryTileLeft = batteryTile->getLeft();
|
||||
const uint16_t batteryTileTop = batteryTile->getTop();
|
||||
const uint16_t batteryTileHeight = batteryTile->getHeight();
|
||||
hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the height of the standard applet header
|
||||
|
||||
@@ -48,37 +48,27 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
||||
|
||||
void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
{
|
||||
// Fill entire tile
|
||||
// - size of icon controlled by size of tile
|
||||
int16_t l = 0;
|
||||
int16_t t = 0;
|
||||
uint16_t w = width();
|
||||
int16_t h = height();
|
||||
|
||||
// Clear the region beneath the tile
|
||||
// Clear the region beneath the tile, including the border
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
// We do need to do this with the battery though, as it is an "overlay"
|
||||
fillRect(l, t, w, h, WHITE);
|
||||
|
||||
// Vertical centerline
|
||||
const int16_t m = t + (h / 2);
|
||||
fillRect(0, 0, width(), height(), WHITE);
|
||||
|
||||
// =====================
|
||||
// Draw battery outline
|
||||
// =====================
|
||||
|
||||
// Positive terminal "bump"
|
||||
const int16_t &bumpL = l;
|
||||
const uint16_t bumpH = h / 2;
|
||||
const int16_t bumpT = m - (bumpH / 2);
|
||||
constexpr uint16_t bumpW = 2;
|
||||
const int16_t &bumpL = 1;
|
||||
const uint16_t bumpH = (height() - 2) / 2;
|
||||
const int16_t bumpT = (1 + ((height() - 2) / 2)) - (bumpH / 2);
|
||||
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
||||
|
||||
// Main body of battery
|
||||
const int16_t bodyL = bumpL + bumpW;
|
||||
const int16_t &bodyT = t;
|
||||
const int16_t &bodyH = h;
|
||||
const int16_t bodyW = w - bumpW;
|
||||
const int16_t bodyL = 1 + bumpW;
|
||||
const int16_t &bodyT = 1;
|
||||
const int16_t &bodyH = height() - 2; // Handle top/bottom padding
|
||||
const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad
|
||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||
|
||||
// Erase join between bump and body
|
||||
@@ -89,12 +79,13 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
// ===================
|
||||
|
||||
constexpr int16_t slicePad = 2;
|
||||
const int16_t sliceL = bodyL + slicePad;
|
||||
int16_t sliceL = bodyL + slicePad;
|
||||
const int16_t sliceT = bodyT + slicePad;
|
||||
const uint16_t sliceH = bodyH - (slicePad * 2);
|
||||
uint16_t sliceW = bodyW - (slicePad * 2);
|
||||
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction
|
||||
|
||||
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||
|
||||
@@ -510,10 +510,10 @@ void InkHUD::WindowManager::placeSystemTiles()
|
||||
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
|
||||
inkhud->getSystemApplet("BatteryIcon")
|
||||
->getTile()
|
||||
->setRegion(inkhud->width() - batteryIconWidth, // x
|
||||
2, // y
|
||||
batteryIconWidth, // width
|
||||
batteryIconHeight); // height
|
||||
->setRegion(inkhud->width() - batteryIconWidth - 1, // x
|
||||
1, // y
|
||||
batteryIconWidth + 1, // width
|
||||
batteryIconHeight + 2); // height
|
||||
|
||||
// Note: the tiles of placeholder and menu applets are manipulated specially
|
||||
// - menuApplet borrows user tiles
|
||||
|
||||
@@ -2223,8 +2223,8 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location,
|
||||
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
|
||||
// TODO: After more mainline SD card support
|
||||
}
|
||||
return success;
|
||||
#endif
|
||||
return success;
|
||||
}
|
||||
|
||||
/// Record an error that should be reported via analytics
|
||||
|
||||
@@ -94,6 +94,10 @@
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ZPS
|
||||
#include "modules/esp32/ZPSModule.h"
|
||||
#endif
|
||||
|
||||
#if defined(HAS_HARDWARE_WATCHDOG)
|
||||
#include "watchdog/watchdogThread.h"
|
||||
#endif
|
||||
@@ -156,6 +160,9 @@ void setupModules()
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
statusMessageModule = new StatusMessageModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ZPS
|
||||
zpsModule = new ZPSModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
|
||||
new GenericThreadModule();
|
||||
#endif
|
||||
|
||||
183
src/modules/ReplyBotModule.cpp
Normal file
183
src/modules/ReplyBotModule.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
/*
|
||||
* ReplyBotModule.cpp
|
||||
*
|
||||
* This module implements a simple reply bot for the Meshtastic firmware. It listens for
|
||||
* specific text commands ("/ping", "/hello" and "/test") delivered either via a direct
|
||||
* message (DM) or a broadcast on the primary channel. When a supported command is
|
||||
* received the bot responds with a short status message that includes the hop count
|
||||
* (minimum number of relays), RSSI and SNR of the received packet. To avoid spamming
|
||||
* the network it enforces a per‑sender cooldown between responses. By default the
|
||||
* module is enabled; define MESHTASTIC_EXCLUDE_REPLYBOT at build time to exclude it
|
||||
* entirely. See the official firmware documentation for guidance on adding modules.
|
||||
*/
|
||||
|
||||
#include "ReplyBotModule.h"
|
||||
#include "Channels.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "configuration.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
|
||||
//
|
||||
// Rate limiting data structures
|
||||
//
|
||||
// Each sender is tracked in a small ring buffer. When a message arrives from a
|
||||
// sender we check the last time we responded to them. If the difference is
|
||||
// less than the configured cooldown (different values for DM vs broadcast)
|
||||
// the message is ignored; otherwise we update the last response time and
|
||||
// proceed with replying.
|
||||
|
||||
struct ReplyBotCooldownEntry {
|
||||
uint32_t from = 0;
|
||||
uint32_t lastMs = 0;
|
||||
};
|
||||
|
||||
static constexpr uint8_t REPLYBOT_COOLDOWN_SLOTS = 8; // ring buffer size
|
||||
static constexpr uint32_t REPLYBOT_DM_COOLDOWN_MS = 15 * 1000; // 15 seconds for DMs
|
||||
static constexpr uint32_t REPLYBOT_LF_COOLDOWN_MS = 60 * 1000; // 60 seconds for LongFast broadcasts
|
||||
|
||||
static ReplyBotCooldownEntry replybotCooldown[REPLYBOT_COOLDOWN_SLOTS];
|
||||
static uint8_t replybotCooldownIdx = 0;
|
||||
|
||||
// Return true if a reply should be rate‑limited for this sender, updating the
|
||||
// entry table as needed.
|
||||
static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs)
|
||||
{
|
||||
const uint32_t now = millis();
|
||||
for (auto &e : replybotCooldown) {
|
||||
if (e.from == from) {
|
||||
// Found existing entry; check if cooldown expired
|
||||
if ((uint32_t)(now - e.lastMs) < cooldownMs) {
|
||||
return true;
|
||||
}
|
||||
e.lastMs = now;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// No entry found – insert new sender into the ring
|
||||
replybotCooldown[replybotCooldownIdx].from = from;
|
||||
replybotCooldown[replybotCooldownIdx].lastMs = now;
|
||||
replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Constructor – registers a single text port and marks the module promiscuous
|
||||
// so that broadcast messages on the primary channel are visible.
|
||||
ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP)
|
||||
{
|
||||
isPromiscuous = true;
|
||||
}
|
||||
|
||||
void ReplyBotModule::setup()
|
||||
{
|
||||
// In future we may add a protobuf configuration; for now the module is
|
||||
// always enabled when compiled in.
|
||||
}
|
||||
|
||||
// Determine whether we want to process this packet. We only care about
|
||||
// plain text messages addressed to our port.
|
||||
bool ReplyBotModule::wantPacket(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
return (p && p->decoded.portnum == ourPortNum);
|
||||
}
|
||||
|
||||
ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// Accept only direct messages to us or broadcasts on the Primary channel
|
||||
// (regardless of modem preset: LongFast, MediumFast, etc).
|
||||
|
||||
const uint32_t ourNode = nodeDB->getNodeNum();
|
||||
const bool isDM = (mp.to == ourNode);
|
||||
const bool isPrimaryChannel = (mp.channel == channels.getPrimaryIndex()) && isBroadcast(mp.to);
|
||||
if (!isDM && !isPrimaryChannel) {
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Ignore empty payloads
|
||||
if (mp.decoded.payload.size == 0) {
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Copy payload into a null‑terminated buffer
|
||||
char buf[260];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
size_t n = mp.decoded.payload.size;
|
||||
if (n > sizeof(buf) - 1)
|
||||
n = sizeof(buf) - 1;
|
||||
memcpy(buf, mp.decoded.payload.bytes, n);
|
||||
|
||||
// React only to supported slash commands
|
||||
if (!isCommand(buf)) {
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Apply rate limiting per sender depending on DM/broadcast
|
||||
const uint32_t cooldownMs = isDM ? REPLYBOT_DM_COOLDOWN_MS : REPLYBOT_LF_COOLDOWN_MS;
|
||||
if (replybotRateLimited(mp.from, cooldownMs)) {
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Compute hop count indicator – if the relay_node is non‑zero we know
|
||||
// there was at least one relay. Some firmware builds support a hop_start
|
||||
// field which could be used for more accurate counts, but here we use
|
||||
// the available relay_node flag only.
|
||||
// int hopsAway = mp.hop_start - mp.hop_limit;
|
||||
int hopsAway = getHopsAway(mp);
|
||||
|
||||
// Normalize RSSI: if positive adjust down by 200 to align with typical values
|
||||
int rssi = mp.rx_rssi;
|
||||
if (rssi > 0) {
|
||||
rssi -= 200;
|
||||
}
|
||||
float snr = mp.rx_snr;
|
||||
|
||||
// Build the reply message and send it back via DM
|
||||
char reply[96];
|
||||
snprintf(reply, sizeof(reply), "🎙️ Mic Check : %d Hops away | RSSI %d | SNR %.1f", hopsAway, rssi, snr);
|
||||
sendDm(mp, reply);
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Check if the message starts with one of the supported commands. Leading
|
||||
// whitespace is skipped and commands must be followed by end‑of‑string or
|
||||
// whitespace.
|
||||
bool ReplyBotModule::isCommand(const char *msg) const
|
||||
{
|
||||
if (!msg)
|
||||
return false;
|
||||
while (*msg == ' ' || *msg == '\t')
|
||||
msg++;
|
||||
auto isEndOrSpace = [](char c) { return c == '\0' || std::isspace(static_cast<unsigned char>(c)); };
|
||||
if (strncmp(msg, "/ping", 5) == 0 && isEndOrSpace(msg[5]))
|
||||
return true;
|
||||
if (strncmp(msg, "/hello", 6) == 0 && isEndOrSpace(msg[6]))
|
||||
return true;
|
||||
if (strncmp(msg, "/test", 5) == 0 && isEndOrSpace(msg[5]))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send a direct message back to the originating node.
|
||||
void ReplyBotModule::sendDm(const meshtastic_MeshPacket &rx, const char *text)
|
||||
{
|
||||
if (!text)
|
||||
return;
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->to = rx.from;
|
||||
p->channel = rx.channel;
|
||||
p->want_ack = false;
|
||||
p->decoded.want_response = false;
|
||||
size_t len = strlen(text);
|
||||
if (len > sizeof(p->decoded.payload.bytes)) {
|
||||
len = sizeof(p->decoded.payload.bytes);
|
||||
}
|
||||
p->decoded.payload.size = len;
|
||||
memcpy(p->decoded.payload.bytes, text, len);
|
||||
service->sendToMesh(p);
|
||||
}
|
||||
#endif // MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
18
src/modules/ReplyBotModule.h
Normal file
18
src/modules/ReplyBotModule.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
#include "SinglePortModule.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
|
||||
class ReplyBotModule : public SinglePortModule
|
||||
{
|
||||
public:
|
||||
ReplyBotModule();
|
||||
void setup() override;
|
||||
bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
protected:
|
||||
bool isCommand(const char *msg) const;
|
||||
void sendDm(const meshtastic_MeshPacket &rx, const char *text);
|
||||
};
|
||||
#endif // MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
419
src/modules/esp32/ZPSModule.cpp
Normal file
419
src/modules/esp32/ZPSModule.cpp
Normal file
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* ZPS - Zero-GPS Positioning System for standalone Meshtastic devices
|
||||
* - experimental tools for estimating own position without a GPS -
|
||||
*
|
||||
* Copyright 2021 all rights reserved by https://github.com/a-f-G-U-C
|
||||
* Released under GPL v3 (see LICENSE file for details)
|
||||
*/
|
||||
|
||||
#include "ZPSModule.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeStatus.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <WiFi.h>
|
||||
|
||||
#if !defined(MESHTASTIC_EXCLUDE_BLUETOOTH)
|
||||
|
||||
#include "NimBLEDevice.h"
|
||||
|
||||
#define BLE_MAX_REC 15
|
||||
#define BLE_NO_RESULTS -1 // Indicates a BLE scan is in progress
|
||||
|
||||
uint8_t bleCounter = 0; // used internally by the ble scanner
|
||||
uint64_t bleResult[BLE_MAX_REC + 1];
|
||||
int bleResSize = BLE_NO_RESULTS;
|
||||
|
||||
uint64_t scanStart = 0;
|
||||
|
||||
ZPSModule *zpsModule;
|
||||
|
||||
// Mini BLE scanner, NIMBLE based and modelled loosely after the Wifi scanner
|
||||
static int ble_scan(uint32_t duration, bool passive = true, bool dedup = true);
|
||||
|
||||
// ZPSModule::ZPSModule()
|
||||
// : ProtobufModule("ZPS", ZPS_PORTNUM, Position_fields), concurrency::OSThread("ZPSModule")
|
||||
ZPSModule::ZPSModule() : SinglePortModule("ZPS", ZPS_PORTNUM), concurrency::OSThread("ZPSModule")
|
||||
{
|
||||
setIntervalFromNow(ZPS_STARTUP_DELAY); // Delay startup by 10 seconds, no need to race :)
|
||||
|
||||
wantBSS = true;
|
||||
wantBLE = true;
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
WiFi.scanNetworks(true, true); // nonblock, showhidden
|
||||
scanState = SCAN_BSS_RUN;
|
||||
}
|
||||
|
||||
ProcessMessage ZPSModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
meshtastic_Position pos = meshtastic_Position_init_default;
|
||||
|
||||
auto &pd = mp.decoded;
|
||||
uint8_t nRecs = pd.payload.size >> 3;
|
||||
|
||||
LOG_DEBUG("handleReceived %s 0x%0x->0x%0x, id=0x%x, port=%d, len=%d, rec=%d\n", name, mp.from, mp.to, mp.id, pd.portnum,
|
||||
pd.payload.size, nRecs);
|
||||
if (nRecs > ZPS_DATAPKT_MAXITEMS)
|
||||
nRecs = ZPS_DATAPKT_MAXITEMS;
|
||||
memcpy(&netData, pd.payload.bytes, nRecs << 3);
|
||||
|
||||
// Currently we are unable to act as a position server, so we're
|
||||
// not interested in broadcasts (this will change later)
|
||||
if (mp.to != nodeDB->getNodeNum()) {
|
||||
// Message is not for us, won't process
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
for (int i = 0; i < nRecs; i++) {
|
||||
LOG_DEBUG("ZPS[%d]: %08x"
|
||||
"%08x\n",
|
||||
i, (uint32_t)(netData[i] >> 32), (uint32_t)netData[i]);
|
||||
}
|
||||
#endif
|
||||
|
||||
if ((netData[0] & 0x800000000000) && (nRecs >= 2)) {
|
||||
// message contains a position
|
||||
pos.PDOP = (netData[0] >> 40) & 0x7f;
|
||||
pos.timestamp = netData[0] & 0xffffffff;
|
||||
// second int64 encodes lat and lon
|
||||
pos.longitude_i = (int32_t)(netData[1] & 0xffffffff);
|
||||
pos.latitude_i = (int32_t)((netData[1] >> 32) & 0xffffffff);
|
||||
|
||||
// FIXME should be conditional, to ensure we don't overwrite a good GPS fix!
|
||||
LOG_DEBUG("ZPS lat/lon/dop/pts %d/%d/%d/%d\n", pos.latitude_i, pos.longitude_i, pos.PDOP, pos.timestamp);
|
||||
|
||||
// Some required fields
|
||||
pos.time = getTime();
|
||||
pos.location_source = meshtastic_Position_LocSource_LOC_EXTERNAL;
|
||||
|
||||
// don't update position if my gps fix is valid
|
||||
if (nodeDB->hasValidPosition(nodeDB->getMeshNode(nodeDB->getNodeNum()))) {
|
||||
LOG_DEBUG("ZPSModule::handleReceived: ignoring position update, GPS is valid\n");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
nodeDB->updatePosition(nodeDB->getNodeNum(), pos);
|
||||
} else {
|
||||
// nothing we can do - for now
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *ZPSModule::allocReply()
|
||||
{
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->decoded.payload.size = (netRecs + 2) << 3; // actually can be only +1 if no GPS data
|
||||
|
||||
LOG_DEBUG("Allocating dataPacket for %d items, %d bytes\n", netRecs, p->decoded.payload.size);
|
||||
memcpy(p->decoded.payload.bytes, &netData, p->decoded.payload.size);
|
||||
|
||||
return (p);
|
||||
}
|
||||
|
||||
void ZPSModule::sendDataPacket(NodeNum dest, bool wantReplies)
|
||||
{
|
||||
// cancel any not yet sent (now stale) position packets
|
||||
if (prevPacketId)
|
||||
service->cancelSending(prevPacketId);
|
||||
|
||||
meshtastic_MeshPacket *p = allocReply();
|
||||
p->to = dest;
|
||||
p->decoded.portnum = meshtastic_PortNum_ZPS_APP;
|
||||
p->decoded.want_response = wantReplies;
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
prevPacketId = p->id;
|
||||
|
||||
service->sendToMesh(p, RX_SRC_LOCAL);
|
||||
}
|
||||
|
||||
int32_t ZPSModule::runOnce()
|
||||
{
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
assert(node);
|
||||
|
||||
// LOG_DEBUG("ZPSModule::runOnce() START, scanState: %d\n", (int) scanState);
|
||||
|
||||
int numWifi = 0;
|
||||
|
||||
if (scanState == SCAN_BSS_RUN) {
|
||||
// check completion status of any running Wifi scan
|
||||
numWifi = WiFi.scanComplete();
|
||||
|
||||
if (numWifi >= 0) {
|
||||
// scan is complete
|
||||
LOG_DEBUG("%d BSS found\n", numWifi);
|
||||
LOG_DEBUG("BSS scan done in %d millis\n", millis() - scanStart);
|
||||
|
||||
if (wantBSS && haveBSS) {
|
||||
// old data exists, overwrite it
|
||||
netRecs = 0;
|
||||
haveBSS = haveBLE = false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < numWifi; i++) {
|
||||
// pack each Wifi network record into a 64-bit int
|
||||
uint64_t netBytes = encodeBSS(WiFi.BSSID(i), WiFi.channel(i), abs(WiFi.RSSI(i)));
|
||||
|
||||
if (wantBSS) {
|
||||
// load into outbound array if needed
|
||||
outBufAdd(netBytes);
|
||||
haveBSS = true;
|
||||
}
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
LOG_DEBUG("BSS[%02d]: %08x"
|
||||
"%08x\n",
|
||||
i, (uint32_t)(netBytes >> 32), (uint32_t)netBytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
WiFi.scanDelete();
|
||||
scanState = SCAN_BSS_DONE;
|
||||
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
} else if (numWifi == -1) {
|
||||
// LOG_DEBUG("BSS scan in-progress\n");
|
||||
} else {
|
||||
LOG_DEBUG("BSS scan state=%d\n", numWifi);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if ((scanState == SCAN_BLE_RUN) && (bleResSize >= 0)) {
|
||||
// completion status checked above (bleResSize >= 0)
|
||||
LOG_DEBUG("BLE scan done in %d millis\n", millis() - scanStart);
|
||||
scanState = SCAN_BLE_DONE;
|
||||
|
||||
if (wantBLE && haveBLE) {
|
||||
// old data exists, overwrite it
|
||||
netRecs = 0;
|
||||
haveBSS = haveBLE = false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < bleResSize; i++) {
|
||||
// load data into output array if needed
|
||||
if (wantBLE) {
|
||||
outBufAdd(bleResult[i]);
|
||||
haveBLE = true;
|
||||
}
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
LOG_DEBUG("BLE[%d]: %08x"
|
||||
"%08x\n",
|
||||
i, (uint32_t)(bleResult[i] >> 32), (uint32_t)bleResult[i]);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reset the counter once we're done with the dataset
|
||||
bleResSize = BLE_NO_RESULTS;
|
||||
}
|
||||
|
||||
// Are we finished assembling that packet? Then send it out
|
||||
if ((wantBSS == haveBSS) && (wantBLE == haveBLE) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil() &&
|
||||
(lastSend == 0 || millis() - lastSend >= Default::getConfiguredOrDefaultMsScaled(config.position.position_broadcast_secs,
|
||||
default_broadcast_interval_secs,
|
||||
nodeStatus->getNumOnline()))) {
|
||||
|
||||
haveBSS = haveBLE = false;
|
||||
sendDataPacket(NODENUM_BROADCAST, false); // no replies
|
||||
lastSend = millis();
|
||||
netRecs = 0; // reset packet
|
||||
}
|
||||
|
||||
/*
|
||||
* State machine transitions
|
||||
*
|
||||
* FIXME could be managed better, for example: check if we require
|
||||
* each type of scan (wantBSS/wantBLE), and if not, don't start it!
|
||||
*/
|
||||
if (scanState == SCAN_BLE_DONE) {
|
||||
// BLE done, transition to BSS scanning
|
||||
scanStart = millis();
|
||||
LOG_DEBUG("BSS scan start t=%d\n", scanStart);
|
||||
if (WiFi.scanNetworks(true, true) == WIFI_SCAN_RUNNING) // nonblock, showhidden
|
||||
scanState = SCAN_BSS_RUN;
|
||||
|
||||
} else if (scanState == SCAN_BSS_DONE) {
|
||||
// BSS done, transition to BLE scanning
|
||||
scanStart = millis();
|
||||
LOG_DEBUG("BLE scan start t=%d\n", scanStart);
|
||||
if (ble_scan(ZPS_BLE_SCANTIME) == 0)
|
||||
scanState = SCAN_BLE_RUN;
|
||||
}
|
||||
|
||||
// LOG_DEBUG("ZPSModule::runOnce() DONE, scanState=%d\n", scanState);
|
||||
if ((scanState == SCAN_BSS_RUN) || (scanState == SCAN_BLE_RUN)) {
|
||||
return 1000; // scan in progress, re-check soon
|
||||
}
|
||||
|
||||
return 5000;
|
||||
}
|
||||
|
||||
uint64_t encodeBSS(uint8_t *bssid, uint8_t chan, uint8_t absRSSI)
|
||||
{
|
||||
uint64_t netBytes = absRSSI & 0xff;
|
||||
netBytes <<= 8;
|
||||
netBytes |= (chan & 0xff);
|
||||
|
||||
for (uint8_t b = 0; b < 6; b++) {
|
||||
netBytes <<= 8;
|
||||
netBytes |= bssid[b];
|
||||
}
|
||||
|
||||
return netBytes;
|
||||
}
|
||||
|
||||
uint64_t encodeBLE(uint8_t *addr, uint8_t absRSSI)
|
||||
{
|
||||
uint64_t netBytes = absRSSI & 0xff;
|
||||
netBytes <<= 8;
|
||||
netBytes |= 0xff; // "channel" byte reserved in BLE records
|
||||
|
||||
for (uint8_t b = 0; b < 6; b++) {
|
||||
netBytes <<= 8;
|
||||
netBytes |= addr[5 - b] & 0xff;
|
||||
}
|
||||
|
||||
return netBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler
|
||||
*/
|
||||
static int ble_gap_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
// Adverts matching certain patterns are useless for positioning purposes
|
||||
// (ephemeral MAC etc), so try excluding them if possible
|
||||
//
|
||||
// TODO: Expand the list of reject patterns for BLE adverts.
|
||||
// There are likely more than 10 patterns to test and reject, including most Apple devices and others.
|
||||
//
|
||||
// TODO: Implement full packet search for reject patterns (use memmem() or similar),
|
||||
// not just at the beginning (currently uses memcmp()).
|
||||
|
||||
const uint8_t rejPat[] = {0x1e, 0xff, 0x06, 0x00, 0x01}; // one of many
|
||||
|
||||
struct ble_hs_adv_fields fields;
|
||||
int rc;
|
||||
int i = 0;
|
||||
|
||||
uint64_t netBytes = 0;
|
||||
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_DISC:
|
||||
// called once for every BLE advert received
|
||||
rc = ble_hs_adv_parse_fields(&fields, event->disc.data, event->disc.length_data);
|
||||
if (rc != 0)
|
||||
return 0;
|
||||
|
||||
if (bleResSize != BLE_NO_RESULTS)
|
||||
// as far as we know, we're not in the middle of a BLE scan!
|
||||
LOG_DEBUG("Unexpected BLE_GAP_EVENT_DISC!\n");
|
||||
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
// Dump the advertisement packet
|
||||
DEBUG_PORT.hexDump("DEBUG", (unsigned char *)event->disc.data, event->disc.length_data);
|
||||
#endif
|
||||
// Reject beacons known to be unreliable (ephemeral etc)
|
||||
if (memcmp(event->disc.data, rejPat, sizeof(rejPat)) == 0) {
|
||||
LOG_DEBUG("(BLE item filtered by pattern)\n");
|
||||
return 0; // Processing-wise, it's still a success
|
||||
}
|
||||
|
||||
//
|
||||
// STORE THE RESULTS IN A SORTED LIST
|
||||
//
|
||||
|
||||
// first, pack each BLE item reading into a 64-bit int
|
||||
netBytes = encodeBLE(event->disc.addr.val, abs(event->disc.rssi));
|
||||
|
||||
// SOME DUPLICATES SURVIVE through filter_duplicates = 1, catch them here
|
||||
// Duplicate filtering is now handled in the sorting loop below,
|
||||
// but right now we write for clarity not optimization
|
||||
for (i = 0; i < bleCounter; i++) {
|
||||
if ((bleResult[i] & 0xffffffffffff) == (netBytes & 0xffffffffffff)) {
|
||||
LOG_DEBUG("(BLE duplicate filtered)\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
// redundant extraverbosity, but I need it for duplicate hunting
|
||||
LOG_DEBUG("BL_[%02d]: %08x"
|
||||
"%08x\n",
|
||||
bleCounter, (uint32_t)(netBytes >> 32), (uint32_t)netBytes);
|
||||
#endif
|
||||
// then insert item into a list (up to BLE_MAX_REC records), sorted by RSSI
|
||||
for (i = 0; i < bleCounter; i++) {
|
||||
// find first element greater than ours, that will be our insertion point
|
||||
if (bleResult[i] > netBytes)
|
||||
break;
|
||||
}
|
||||
// any other records move down one position to vacate res[i]
|
||||
for (int j = bleCounter; j > i; j--)
|
||||
bleResult[j] = bleResult[j - 1];
|
||||
// write new element at insertion point
|
||||
bleResult[i] = netBytes;
|
||||
|
||||
// advance tail of list, but not beyond limit
|
||||
if (bleCounter < BLE_MAX_REC)
|
||||
bleCounter++;
|
||||
|
||||
return 0; // SUCCESS
|
||||
|
||||
case BLE_GAP_EVENT_DISC_COMPLETE:
|
||||
LOG_DEBUG("EVENT_DISC_COMPLETE in %d millis\n", (millis() - scanStart));
|
||||
LOG_DEBUG("%d BLE found\n", bleCounter);
|
||||
bleResSize = bleCounter;
|
||||
|
||||
bleCounter = 0; // reset counter
|
||||
return 0; // SUCCESS
|
||||
|
||||
default:
|
||||
return 0; // SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the GAP general discovery procedure (non-blocking)
|
||||
*/
|
||||
static int ble_scan(uint32_t duration, bool passive, bool dedup)
|
||||
{
|
||||
uint8_t own_addr_type;
|
||||
struct ble_gap_disc_params disc_params;
|
||||
int rc;
|
||||
|
||||
// Figure out address type to use
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
LOG_DEBUG("error determining address type; rc=%d\n", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Scanning parameters, these are mostly default
|
||||
disc_params.itvl = 0;
|
||||
disc_params.window = 0;
|
||||
disc_params.filter_policy = 0;
|
||||
disc_params.limited = 0;
|
||||
|
||||
// These two params are the more interesting ones
|
||||
disc_params.filter_duplicates = dedup; // self-explanatory
|
||||
disc_params.passive = passive; // passive uses less power
|
||||
|
||||
// Start scanning process (non-blocking) and return
|
||||
rc = ble_gap_disc(own_addr_type, duration, &disc_params, ble_gap_event, NULL);
|
||||
if (rc != 0) {
|
||||
LOG_DEBUG("error initiating GAP discovery; rc=%d\n", rc);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
#endif // MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
86
src/modules/esp32/ZPSModule.h
Normal file
86
src/modules/esp32/ZPSModule.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#include "SinglePortModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#define ZPS_PORTNUM meshtastic_PortNum_ZPS_APP
|
||||
|
||||
#define ZPS_DATAPKT_MAXITEMS 20 // max number of records to pack in an outbound packet (~10)
|
||||
#define ZPS_STARTUP_DELAY 10000 // Module startup delay in millis
|
||||
|
||||
// Duration of a BLE scan in millis.
|
||||
// We want this number to be SLIGHTLY UNDER an integer number of seconds,
|
||||
// to be able to catch the result as fresh as possible on a 1-second polling loop
|
||||
#define ZPS_BLE_SCANTIME 2900 // millis
|
||||
|
||||
enum SCANSTATE { SCAN_NONE, SCAN_BSS_RUN, SCAN_BSS_DONE, SCAN_BLE_RUN, SCAN_BLE_DONE };
|
||||
|
||||
/*
|
||||
* Data packing "compression" functions
|
||||
* Ingest a WiFi BSSID, channel and RSSI (or BLE address and RSSI)
|
||||
* and encode them into a packed uint64
|
||||
*/
|
||||
uint64_t encodeBSS(uint8_t *bssid, uint8_t chan, uint8_t absRSSI);
|
||||
uint64_t encodeBLE(uint8_t *addr, uint8_t absRSSI);
|
||||
|
||||
class ZPSModule : public SinglePortModule, private concurrency::OSThread
|
||||
{
|
||||
/// The id of the last packet we sent, to allow us to cancel it if we make something fresher
|
||||
PacketId prevPacketId = 0;
|
||||
|
||||
/// We limit our broadcasts to a max rate
|
||||
uint32_t lastSend = 0;
|
||||
|
||||
bool wantBSS = true;
|
||||
bool haveBSS = false;
|
||||
|
||||
bool wantBLE = true;
|
||||
bool haveBLE = false;
|
||||
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
ZPSModule();
|
||||
|
||||
/**
|
||||
* Send our radio environment data into the mesh
|
||||
*/
|
||||
void sendDataPacket(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
@return true if you've guaranteed you've handled this message and no other handlers should be considered for it
|
||||
*/
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp);
|
||||
|
||||
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked
|
||||
* so that subclasses can (optionally) send a response back to the original sender. */
|
||||
virtual meshtastic_MeshPacket *allocReply();
|
||||
|
||||
/** Does our periodic broadcast */
|
||||
virtual int32_t runOnce();
|
||||
|
||||
private:
|
||||
// outbound data packet staging buffer and record counter
|
||||
uint64_t netData[ZPS_DATAPKT_MAXITEMS + 2] = {0};
|
||||
uint8_t netRecs = 0;
|
||||
|
||||
// mini state machine to alternate between BSS(Wifi) and BLE scanning
|
||||
SCANSTATE scanState = SCAN_NONE;
|
||||
|
||||
inline void outBufAdd(uint64_t netBytes)
|
||||
{
|
||||
// If this is the first record, initialize the header with the current time and reset the record count.
|
||||
if (!netRecs) {
|
||||
netData[0] = getTime();
|
||||
netData[1] = 0;
|
||||
}
|
||||
|
||||
// push to buffer and update counter
|
||||
if (netRecs < ZPS_DATAPKT_MAXITEMS)
|
||||
netData[2 + (netRecs++)] = netBytes;
|
||||
}
|
||||
};
|
||||
|
||||
extern ZPSModule *zpsModule;
|
||||
Reference in New Issue
Block a user