Compare commits

...
Author SHA1 Message Date
Jonathan Bennett abd11ad0bc Use upstream fusion library 2026-06-15 13:48:12 -05:00
43d485dd76 Add IIS2MDCTR and ISM330DHCX to ScanI2C (#10723)
* add ScanI2C for IIS2MDCTR and ISM330DHCX

* Trunk format

* Minor cleanup

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-15 13:39:57 -05:00
Ben MeadorsGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Copilot Autofix powered by AI
d878c81ce8 Clamp position precision on public / known-keys (#10665)
* Clamp position precision on public / known-keys (Compliance)

* Fix review comments: bounds check, all-channel precision clamp, disabled channel guard

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Refactor position precision comments for clarity and update channel configuration handling in AdminModule

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-14 18:53:18 -05:00
oscgonferandGitHub 8a0c7592cc Remove duplicate code from AQ telemetry, probably from merge conflict (#10708) 2026-06-14 06:38:49 -05:00
Benjamin FaershteinandGitHub 882ca0a216 Improve GPS stale probe recovery (#10714)
* Improve GPS stale probe recovery

* Address GPS review feedback
2026-06-14 06:30:20 -05:00
Benjamin FaershteinandGitHub 5d1c4f15b7 Make nRF52 lockdown support opt-in (#10712)
* Make nRF52 lockdown support opt-in

* Scope lockdown opt-in normalization to nRF52
2026-06-13 21:14:47 -05:00
745b53698a Mesh node t1 fixes (#10602)
* Fixes

* Remove BATTERY_LPCOMP_THRESHOLD

BATTERY_LPCOMP_THRESHOLD is dead code — in main-nrf52.cpp it's inside #ifdef BATTERY_LPCOMP_INPUT, which this board intentionally doesn't define. The threshold value is never reached.

* Trunk fix

* Update MotionSensor.cpp

* fix

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-06-13 19:57:28 -05:00
b938b63e8a fix a long-running CI bug that overran a lot (#10707)
* fix the fix

* Address Copilot review: add EXIT trap and clarify PKC comment

Add `trap` to kill meshtasticd on any early exit (python harness
failure, socket timeout) so CI never leaks a background process.

Reword the ARCH_PORTDUINO comment to make explicit that pki_encrypted=true
causes the from==0 plain-admin branch to be skipped, routing into the
PKC key-check — the underlying logic was correct all along.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update PORTDUINO comment to reflect from==0 auth fix

The from==0 branch no longer requires !pki_encrypted (fixed upstream
in this branch), so update the simulator comment to reflect the actual
remaining reason for the early intercept: is_managed could still block
exit_simulator even for local packets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 18:46:29 -05:00
TomandGitHub b76e5e6ba4 removes NRF52832 from codebase - it is vestigal at best (#10709) 2026-06-13 18:46:08 -05:00
bbcc35e209 Stm32 general (#10700)
* Attempt to generalize ARCH_STM32

* Trunk

* One More ARCH_STM32

* Whoops, one snuck in there

* Fix comment to reflect define change

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-13 12:21:56 -05:00
Jonathan BennettandBen Meadors bede05356d Lora led rx (#10674)
* add optional LED_LORA to indicate LoRa TX

* Briefly flash LED_LORA on packet RX

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-13 12:18:22 -05:00
Jonathan BennettandBen Meadors 031e73bbe6 Use standard GPS enable pin, for smarter power control on M3 (#10671)
* Use standard GPS enable pin, for smarter power control on M3

* Enable GPS pin in variant.cpp initialization

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-13 12:05:24 -05:00
Ben Meadors 9ea1f0065a Update protos 2026-06-13 07:30:56 -05:00
Jonathan BennettGitHubBen MeadorsCopilotWesselcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>thebentern
8267bb22bd Packet Signing via XEdDSA (#10478)
* Test commit for XEdDSA support

* Update to Crypto lib in Meshtatic org

* Generate a new node identity on key generation (#7628)

* Generate a new node identity on key generation

* Fixes

* Fixes

* Fixes

* Messed up

* Fixes

* Update src/modules/AdminModule.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Figured it out!

* Cleanup

* Update src/mesh/NodeDB.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/AdminModule.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update crypto commit hash

* Some fixes for xeddsa pr (#9610)

* fix: add null check for getMeshNode() in NodeInfoModule

getMeshNode() can return nullptr for unknown nodes. Dereferencing
without a check crashes the firmware when receiving NodeInfo from
a node not yet in the database.

* fix: enforce XEdDSA signature verification and prevent stripping

Previously, failed signature verification still allowed the packet
through, making signatures purely cosmetic. Now:

- Failed verification drops the packet (DECODE_FAILURE)
- Successfully verified nodes get HAS_XEDDSA_SIGNED bitfield set
- Unsigned packets from previously-signing nodes are rejected
- Log levels reduced from WARN/ERROR to DEBUG/WARN as appropriate

* fix: include packet metadata in XEdDSA signature

The signature now covers [fromNode | packetId | portnum | payload]
instead of just the payload bytes. This prevents:
- Replay attacks (different packetId fails verification)
- Reattribution (different fromNode fails verification)
- Portnum redirection (different portnum fails verification)

Also adds a key initialization check to xeddsa_sign (returns false
if XEdDSA keys are all zeros) and checks the return value in the
encode path.

* fix: handle existing key pair in AdminModule security config

When a user provides both a valid private key and public key via
admin config, the crypto engine's DH private key and owner public
key were never loaded. DMs and XEdDSA signing would silently break.

Add an else branch to load both keys into the crypto engine.

* perf: cache Ed25519 public key conversion in xeddsa_verify

curve_to_ed_pub() performs field element parsing, inversion, and
multiplication on every call. Since packets from the same node
tend to arrive in bursts, a single-entry cache avoids repeating
this expensive conversion for consecutive packets from one sender.

* fix: skip identity cleanup when node number is unchanged

createNewIdentity() was called on every generateCryptoKeyPair(),
including normal boots where the same key is regenerated. This
caused unnecessary NodeDB writes and old-node cleanup logic to
run when the node number hadn't actually changed.

Also fixes only zeroing byte[0] of the old node's public key
instead of clearing the entire array.

* fix: replace hardcoded 120 with derived XEDDSA_SIGNATURE_SIZE constant

The payload size check for XEdDSA signing used a magic number (120).
Replace with a derivation from DATA_PAYLOAD_LEN and XEDDSA_SIGNATURE_SIZE
so the limit adjusts automatically if constants change. This also
increases the max signable payload from 120 to 169 bytes, which is
still safe since the actual encoded size is checked after pb_encode.

* fix: add const qualifiers to XEdDSA verify and curve_to_ed_pub inputs

pubKey, payload, and signature parameters in xeddsa_verify are
input-only and should not be modified. Same for curve_pubkey in
curve_to_ed_pub.

* chore: remove commented-out old Crypto dependency in portduino.ini

* Leave out the admin module change for now

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>

* trunk

* protobuf re-update

* Protobufs

* Merge resolution fix

* Put XEDDSA on the right bit

* NodeDB update to new nodeInfoLite accessors, etc

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Refine unsigned packet rejection logic in Router (#10534)

* use hardware random to fill the first 32 signature bytes with entropy prior to signing.

* Add XEdDSA packet-signing policy tests and update dependencies for macos

* Minor fixes

* integrate XEdDSA support and update dependencies across multiple modules

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Wessel <github@weebl.me>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-06-13 06:45:56 -05:00
82 changed files with 1520 additions and 1988 deletions
+21 -1
View File
@@ -37,13 +37,33 @@ jobs:
sed -i -e "s#${PWD}#.#" coverage_base.info # Make paths relative.
- name: Integration test
# Cap the whole step: if the simulator ever fails to exit (e.g. the
# exit_simulator admin path regresses again) the job must fail fast,
# not run to GitHub's 6-hour limit.
timeout-minutes: 5
run: |
.pio/build/coverage/meshtasticd -s &
PID=$!
trap 'kill "$PID" 2>/dev/null || true' EXIT
timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done"
echo "Simulator started, launching python test..."
python3 -c 'from meshtastic.test import testSimulator; testSimulator()'
wait
# The Python harness sends exit_simulator and exits; the simulator is
# expected to terminate on its own. Give it a moment, then verify.
# If it is still alive the exit handshake is broken — fail loudly and
# do NOT fall through to `wait`, which would otherwise block until the
# job's hard timeout.
for i in $(seq 1 10); do
kill -0 "$PID" 2>/dev/null || break
sleep 1
done
if kill -0 "$PID" 2>/dev/null; then
echo "::error title=Simulator did not exit::meshtasticd ignored exit_simulator and is still running after the integration test. The exit_simulator admin path is broken (see AdminModule::handleReceivedProtobuf, ARCH_PORTDUINO bypass). Killing it to avoid a 6-hour CI overrun."
kill -9 "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true
exit 1
fi
wait "$PID" 2>/dev/null || true
- name: Capture coverage information
if: always() # run this step even if previous step failed
-50
View File
@@ -1,50 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52832_s132_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52832_XXAA -DNRF52",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"]
],
"usb_product": "Feather nRF52832 Express",
"mcu": "nrf52832",
"variant": "WisCore_RAK4600_Board",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS132",
"sd_name": "s132",
"sd_version": "6.1.1",
"sd_fwid": "0x00B7"
},
"zephyr": {
"variant": "nrf52_adafruit_feather"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52832_xxAA",
"svd_path": "nrf52.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino", "zephyr"],
"name": "Adafruit Bluefruit nRF52832 Feather",
"upload": {
"maximum_ram_size": 65536,
"maximum_size": 524288,
"require_upload_port": true,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"]
},
"url": "https://www.adafruit.com/product/3406",
"vendor": "Adafruit"
}
+1 -1
View File
@@ -44,7 +44,7 @@ _ESP32_ARCHES = {
"esp32-c6",
"esp32c6",
}
_NRF52_ARCHES = {"nrf52", "nrf52840", "nrf52832"}
_NRF52_ARCHES = {"nrf52", "nrf52840"}
def _wait_port_free(port: str, *, timeout_s: float = 15.0, role: str = "") -> None:
+1
View File
@@ -200,6 +200,7 @@ lib_deps =
https://github.com/adafruit/Adafruit_TSL2561/archive/refs/tags/1.1.3.zip
# renovate: datasource=github-tags depName=BH1750_WE packageName=wollewald/BH1750_WE
https://github.com/wollewald/BH1750_WE/archive/refs/tags/1.1.10.zip
https://github.com/xioTechnologies/Fusion/archive/a93c0dc83ce3ab65246f63ba134d3c2a15d6cabf.zip
; Common environmental sensor libraries (not included in native / portduino)
[environmental_extra_common]
+2 -2
View File
@@ -14,8 +14,8 @@
#define FILE_O_READ "r"
#endif
#if defined(ARCH_STM32WL)
// STM32WL
#if defined(ARCH_STM32)
// STM32
#include "LittleFS.h"
#define FSCom InternalFS
#define FSBegin() FSCom.begin()
-32
View File
@@ -1,32 +0,0 @@
/**
* @file Fusion.h
* @author Seb Madgwick
* @brief Main header file for the Fusion library. This is the only file that
* needs to be included when using the library.
*/
#ifndef FUSION_H
#define FUSION_H
//------------------------------------------------------------------------------
// Includes
#ifdef __cplusplus
extern "C" {
#endif
#include "FusionAhrs.h"
#include "FusionAxes.h"
#include "FusionCalibration.h"
#include "FusionCompass.h"
#include "FusionConvention.h"
#include "FusionMath.h"
#include "FusionOffset.h"
#ifdef __cplusplus
}
#endif
#endif
//------------------------------------------------------------------------------
// End of file
-542
View File
@@ -1,542 +0,0 @@
/**
* @file FusionAhrs.c
* @author Seb Madgwick
* @brief AHRS algorithm to combine gyroscope, accelerometer, and magnetometer
* measurements into a single measurement of orientation relative to the Earth.
*/
//------------------------------------------------------------------------------
// Includes
#include "FusionAhrs.h"
#include <float.h> // FLT_MAX
#include <math.h> // atan2f, cosf, fabsf, powf, sinf
//------------------------------------------------------------------------------
// Definitions
/**
* @brief Initial gain used during the initialisation.
*/
#define INITIAL_GAIN (10.0f)
/**
* @brief Initialisation period in seconds.
*/
#define INITIALISATION_PERIOD (3.0f)
//------------------------------------------------------------------------------
// Function declarations
static inline FusionVector HalfGravity(const FusionAhrs *const ahrs);
static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs);
static inline FusionVector Feedback(const FusionVector sensor, const FusionVector reference);
static inline int Clamp(const int value, const int min, const int max);
//------------------------------------------------------------------------------
// Functions
/**
* @brief Initialises the AHRS algorithm structure.
* @param ahrs AHRS algorithm structure.
*/
void FusionAhrsInitialise(FusionAhrs *const ahrs)
{
const FusionAhrsSettings settings = {
.convention = FusionConventionNwu,
.gain = 0.5f,
.gyroscopeRange = 0.0f,
.accelerationRejection = 90.0f,
.magneticRejection = 90.0f,
.recoveryTriggerPeriod = 0,
};
FusionAhrsSetSettings(ahrs, &settings);
FusionAhrsReset(ahrs);
}
/**
* @brief Resets the AHRS algorithm. This is equivalent to reinitialising the
* algorithm while maintaining the current settings.
* @param ahrs AHRS algorithm structure.
*/
void FusionAhrsReset(FusionAhrs *const ahrs)
{
ahrs->quaternion = FUSION_IDENTITY_QUATERNION;
ahrs->accelerometer = FUSION_VECTOR_ZERO;
ahrs->initialising = true;
ahrs->rampedGain = INITIAL_GAIN;
ahrs->angularRateRecovery = false;
ahrs->halfAccelerometerFeedback = FUSION_VECTOR_ZERO;
ahrs->halfMagnetometerFeedback = FUSION_VECTOR_ZERO;
ahrs->accelerometerIgnored = false;
ahrs->accelerationRecoveryTrigger = 0;
ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
ahrs->magnetometerIgnored = false;
ahrs->magneticRecoveryTrigger = 0;
ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
}
/**
* @brief Sets the AHRS algorithm settings.
* @param ahrs AHRS algorithm structure.
* @param settings Settings.
*/
void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings)
{
ahrs->settings.convention = settings->convention;
ahrs->settings.gain = settings->gain;
ahrs->settings.gyroscopeRange = settings->gyroscopeRange == 0.0f ? FLT_MAX : 0.98f * settings->gyroscopeRange;
ahrs->settings.accelerationRejection = settings->accelerationRejection == 0.0f
? FLT_MAX
: powf(0.5f * sinf(FusionDegreesToRadians(settings->accelerationRejection)), 2);
ahrs->settings.magneticRejection =
settings->magneticRejection == 0.0f ? FLT_MAX : powf(0.5f * sinf(FusionDegreesToRadians(settings->magneticRejection)), 2);
ahrs->settings.recoveryTriggerPeriod = settings->recoveryTriggerPeriod;
ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
if ((settings->gain == 0.0f) ||
(settings->recoveryTriggerPeriod == 0)) { // disable acceleration and magnetic rejection features if gain is zero
ahrs->settings.accelerationRejection = FLT_MAX;
ahrs->settings.magneticRejection = FLT_MAX;
}
if (ahrs->initialising == false) {
ahrs->rampedGain = ahrs->settings.gain;
}
ahrs->rampedGainStep = (INITIAL_GAIN - ahrs->settings.gain) / INITIALISATION_PERIOD;
}
/**
* @brief Updates the AHRS algorithm using the gyroscope, accelerometer, and
* magnetometer measurements.
* @param ahrs AHRS algorithm structure.
* @param gyroscope Gyroscope measurement in degrees per second.
* @param accelerometer Accelerometer measurement in g.
* @param magnetometer Magnetometer measurement in arbitrary units.
* @param deltaTime Delta time in seconds.
*/
void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer,
const FusionVector magnetometer, const float deltaTime)
{
#define Q ahrs->quaternion.element
// Store accelerometer
ahrs->accelerometer = accelerometer;
// Reinitialise if gyroscope range exceeded
if ((fabsf(gyroscope.axis.x) > ahrs->settings.gyroscopeRange) || (fabsf(gyroscope.axis.y) > ahrs->settings.gyroscopeRange) ||
(fabsf(gyroscope.axis.z) > ahrs->settings.gyroscopeRange)) {
const FusionQuaternion quaternion = ahrs->quaternion;
FusionAhrsReset(ahrs);
ahrs->quaternion = quaternion;
ahrs->angularRateRecovery = true;
}
// Ramp down gain during initialisation
if (ahrs->initialising) {
ahrs->rampedGain -= ahrs->rampedGainStep * deltaTime;
if ((ahrs->rampedGain < ahrs->settings.gain) || (ahrs->settings.gain == 0.0f)) {
ahrs->rampedGain = ahrs->settings.gain;
ahrs->initialising = false;
ahrs->angularRateRecovery = false;
}
}
// Calculate direction of gravity indicated by algorithm
const FusionVector halfGravity = HalfGravity(ahrs);
// Calculate accelerometer feedback
FusionVector halfAccelerometerFeedback = FUSION_VECTOR_ZERO;
ahrs->accelerometerIgnored = true;
if (FusionVectorIsZero(accelerometer) == false) {
// Calculate accelerometer feedback scaled by 0.5
ahrs->halfAccelerometerFeedback = Feedback(FusionVectorNormalise(accelerometer), halfGravity);
// Don't ignore accelerometer if acceleration error below threshold
if (ahrs->initialising ||
((FusionVectorMagnitudeSquared(ahrs->halfAccelerometerFeedback) <= ahrs->settings.accelerationRejection))) {
ahrs->accelerometerIgnored = false;
ahrs->accelerationRecoveryTrigger -= 9;
} else {
ahrs->accelerationRecoveryTrigger += 1;
}
// Don't ignore accelerometer during acceleration recovery
if (ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout) {
ahrs->accelerationRecoveryTimeout = 0;
ahrs->accelerometerIgnored = false;
} else {
ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
}
ahrs->accelerationRecoveryTrigger = Clamp(ahrs->accelerationRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod);
// Apply accelerometer feedback
if (ahrs->accelerometerIgnored == false) {
halfAccelerometerFeedback = ahrs->halfAccelerometerFeedback;
}
}
// Calculate magnetometer feedback
FusionVector halfMagnetometerFeedback = FUSION_VECTOR_ZERO;
ahrs->magnetometerIgnored = true;
if (FusionVectorIsZero(magnetometer) == false) {
// Calculate direction of magnetic field indicated by algorithm
const FusionVector halfMagnetic = HalfMagnetic(ahrs);
// Calculate magnetometer feedback scaled by 0.5
ahrs->halfMagnetometerFeedback =
Feedback(FusionVectorNormalise(FusionVectorCrossProduct(halfGravity, magnetometer)), halfMagnetic);
// Don't ignore magnetometer if magnetic error below threshold
if (ahrs->initialising ||
((FusionVectorMagnitudeSquared(ahrs->halfMagnetometerFeedback) <= ahrs->settings.magneticRejection))) {
ahrs->magnetometerIgnored = false;
ahrs->magneticRecoveryTrigger -= 9;
} else {
ahrs->magneticRecoveryTrigger += 1;
}
// Don't ignore magnetometer during magnetic recovery
if (ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout) {
ahrs->magneticRecoveryTimeout = 0;
ahrs->magnetometerIgnored = false;
} else {
ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod;
}
ahrs->magneticRecoveryTrigger = Clamp(ahrs->magneticRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod);
// Apply magnetometer feedback
if (ahrs->magnetometerIgnored == false) {
halfMagnetometerFeedback = ahrs->halfMagnetometerFeedback;
}
}
// Convert gyroscope to radians per second scaled by 0.5
const FusionVector halfGyroscope = FusionVectorMultiplyScalar(gyroscope, FusionDegreesToRadians(0.5f));
// Apply feedback to gyroscope
const FusionVector adjustedHalfGyroscope = FusionVectorAdd(
halfGyroscope,
FusionVectorMultiplyScalar(FusionVectorAdd(halfAccelerometerFeedback, halfMagnetometerFeedback), ahrs->rampedGain));
// Integrate rate of change of quaternion
ahrs->quaternion = FusionQuaternionAdd(
ahrs->quaternion,
FusionQuaternionMultiplyVector(ahrs->quaternion, FusionVectorMultiplyScalar(adjustedHalfGyroscope, deltaTime)));
// Normalise quaternion
ahrs->quaternion = FusionQuaternionNormalise(ahrs->quaternion);
#undef Q
}
/**
* @brief Returns the direction of gravity scaled by 0.5.
* @param ahrs AHRS algorithm structure.
* @return Direction of gravity scaled by 0.5.
*/
static inline FusionVector HalfGravity(const FusionAhrs *const ahrs)
{
#define Q ahrs->quaternion.element
switch (ahrs->settings.convention) {
case FusionConventionNwu:
case FusionConventionEnu: {
const FusionVector halfGravity = {.axis = {
.x = Q.x * Q.z - Q.w * Q.y,
.y = Q.y * Q.z + Q.w * Q.x,
.z = Q.w * Q.w - 0.5f + Q.z * Q.z,
}}; // third column of transposed rotation matrix scaled by 0.5
return halfGravity;
}
case FusionConventionNed: {
const FusionVector halfGravity = {.axis = {
.x = Q.w * Q.y - Q.x * Q.z,
.y = -1.0f * (Q.y * Q.z + Q.w * Q.x),
.z = 0.5f - Q.w * Q.w - Q.z * Q.z,
}}; // third column of transposed rotation matrix scaled by -0.5
return halfGravity;
}
}
return FUSION_VECTOR_ZERO; // avoid compiler warning
#undef Q
}
/**
* @brief Returns the direction of the magnetic field scaled by 0.5.
* @param ahrs AHRS algorithm structure.
* @return Direction of the magnetic field scaled by 0.5.
*/
static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs)
{
#define Q ahrs->quaternion.element
switch (ahrs->settings.convention) {
case FusionConventionNwu: {
const FusionVector halfMagnetic = {.axis = {
.x = Q.x * Q.y + Q.w * Q.z,
.y = Q.w * Q.w - 0.5f + Q.y * Q.y,
.z = Q.y * Q.z - Q.w * Q.x,
}}; // second column of transposed rotation matrix scaled by 0.5
return halfMagnetic;
}
case FusionConventionEnu: {
const FusionVector halfMagnetic = {.axis = {
.x = 0.5f - Q.w * Q.w - Q.x * Q.x,
.y = Q.w * Q.z - Q.x * Q.y,
.z = -1.0f * (Q.x * Q.z + Q.w * Q.y),
}}; // first column of transposed rotation matrix scaled by -0.5
return halfMagnetic;
}
case FusionConventionNed: {
const FusionVector halfMagnetic = {.axis = {
.x = -1.0f * (Q.x * Q.y + Q.w * Q.z),
.y = 0.5f - Q.w * Q.w - Q.y * Q.y,
.z = Q.w * Q.x - Q.y * Q.z,
}}; // second column of transposed rotation matrix scaled by -0.5
return halfMagnetic;
}
}
return FUSION_VECTOR_ZERO; // avoid compiler warning
#undef Q
}
/**
* @brief Returns the feedback.
* @param sensor Sensor.
* @param reference Reference.
* @return Feedback.
*/
static inline FusionVector Feedback(const FusionVector sensor, const FusionVector reference)
{
if (FusionVectorDotProduct(sensor, reference) < 0.0f) { // if error is >90 degrees
return FusionVectorNormalise(FusionVectorCrossProduct(sensor, reference));
}
return FusionVectorCrossProduct(sensor, reference);
}
/**
* @brief Returns a value limited to maximum and minimum.
* @param value Value.
* @param min Minimum value.
* @param max Maximum value.
* @return Value limited to maximum and minimum.
*/
static inline int Clamp(const int value, const int min, const int max)
{
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
/**
* @brief Updates the AHRS algorithm using the gyroscope and accelerometer
* measurements only.
* @param ahrs AHRS algorithm structure.
* @param gyroscope Gyroscope measurement in degrees per second.
* @param accelerometer Accelerometer measurement in g.
* @param deltaTime Delta time in seconds.
*/
void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer,
const float deltaTime)
{
// Update AHRS algorithm
FusionAhrsUpdate(ahrs, gyroscope, accelerometer, FUSION_VECTOR_ZERO, deltaTime);
// Zero heading during initialisation
if (ahrs->initialising) {
FusionAhrsSetHeading(ahrs, 0.0f);
}
}
/**
* @brief Updates the AHRS algorithm using the gyroscope, accelerometer, and
* heading measurements.
* @param ahrs AHRS algorithm structure.
* @param gyroscope Gyroscope measurement in degrees per second.
* @param accelerometer Accelerometer measurement in g.
* @param heading Heading measurement in degrees.
* @param deltaTime Delta time in seconds.
*/
void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer,
const float heading, const float deltaTime)
{
#define Q ahrs->quaternion.element
// Calculate roll
const float roll = atan2f(Q.w * Q.x + Q.y * Q.z, 0.5f - Q.y * Q.y - Q.x * Q.x);
// Calculate magnetometer
const float headingRadians = FusionDegreesToRadians(heading);
const float sinHeadingRadians = sinf(headingRadians);
const FusionVector magnetometer = {.axis = {
.x = cosf(headingRadians),
.y = -1.0f * cosf(roll) * sinHeadingRadians,
.z = sinHeadingRadians * sinf(roll),
}};
// Update AHRS algorithm
FusionAhrsUpdate(ahrs, gyroscope, accelerometer, magnetometer, deltaTime);
#undef Q
}
/**
* @brief Returns the quaternion describing the sensor relative to the Earth.
* @param ahrs AHRS algorithm structure.
* @return Quaternion describing the sensor relative to the Earth.
*/
FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs)
{
return ahrs->quaternion;
}
/**
* @brief Sets the quaternion describing the sensor relative to the Earth.
* @param ahrs AHRS algorithm structure.
* @param quaternion Quaternion describing the sensor relative to the Earth.
*/
void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quaternion)
{
ahrs->quaternion = quaternion;
}
/**
* @brief Returns the linear acceleration measurement equal to the accelerometer
* measurement with the 1 g of gravity removed.
* @param ahrs AHRS algorithm structure.
* @return Linear acceleration measurement in g.
*/
FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs)
{
#define Q ahrs->quaternion.element
// Calculate gravity in the sensor coordinate frame
const FusionVector gravity = {.axis = {
.x = 2.0f * (Q.x * Q.z - Q.w * Q.y),
.y = 2.0f * (Q.y * Q.z + Q.w * Q.x),
.z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z),
}}; // third column of transposed rotation matrix
// Remove gravity from accelerometer measurement
switch (ahrs->settings.convention) {
case FusionConventionNwu:
case FusionConventionEnu: {
return FusionVectorSubtract(ahrs->accelerometer, gravity);
}
case FusionConventionNed: {
return FusionVectorAdd(ahrs->accelerometer, gravity);
}
}
return FUSION_VECTOR_ZERO; // avoid compiler warning
#undef Q
}
/**
* @brief Returns the Earth acceleration measurement equal to accelerometer
* measurement in the Earth coordinate frame with the 1 g of gravity removed.
* @param ahrs AHRS algorithm structure.
* @return Earth acceleration measurement in g.
*/
FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs)
{
#define Q ahrs->quaternion.element
#define A ahrs->accelerometer.axis
// Calculate accelerometer measurement in the Earth coordinate frame
const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations
const float qwqx = Q.w * Q.x;
const float qwqy = Q.w * Q.y;
const float qwqz = Q.w * Q.z;
const float qxqy = Q.x * Q.y;
const float qxqz = Q.x * Q.z;
const float qyqz = Q.y * Q.z;
FusionVector accelerometer = {.axis = {
.x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z),
.y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z),
.z = 2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z),
}}; // rotation matrix multiplied with the accelerometer
// Remove gravity from accelerometer measurement
switch (ahrs->settings.convention) {
case FusionConventionNwu:
case FusionConventionEnu:
accelerometer.axis.z -= 1.0f;
break;
case FusionConventionNed:
accelerometer.axis.z += 1.0f;
break;
}
return accelerometer;
#undef Q
#undef A
}
/**
* @brief Returns the AHRS algorithm internal states.
* @param ahrs AHRS algorithm structure.
* @return AHRS algorithm internal states.
*/
FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs)
{
const FusionAhrsInternalStates internalStates = {
.accelerationError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfAccelerometerFeedback))),
.accelerometerIgnored = ahrs->accelerometerIgnored,
.accelerationRecoveryTrigger =
ahrs->settings.recoveryTriggerPeriod == 0
? 0.0f
: (float)ahrs->accelerationRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod,
.magneticError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfMagnetometerFeedback))),
.magnetometerIgnored = ahrs->magnetometerIgnored,
.magneticRecoveryTrigger = ahrs->settings.recoveryTriggerPeriod == 0
? 0.0f
: (float)ahrs->magneticRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod,
};
return internalStates;
}
/**
* @brief Returns the AHRS algorithm flags.
* @param ahrs AHRS algorithm structure.
* @return AHRS algorithm flags.
*/
FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs)
{
const FusionAhrsFlags flags = {
.initialising = ahrs->initialising,
.angularRateRecovery = ahrs->angularRateRecovery,
.accelerationRecovery = ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout,
.magneticRecovery = ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout,
};
return flags;
}
/**
* @brief Sets the heading of the orientation measurement provided by the AHRS
* algorithm. This function can be used to reset drift in heading when the AHRS
* algorithm is being used without a magnetometer.
* @param ahrs AHRS algorithm structure.
* @param heading Heading angle in degrees.
*/
void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading)
{
#define Q ahrs->quaternion.element
const float yaw = atan2f(Q.w * Q.z + Q.x * Q.y, 0.5f - Q.y * Q.y - Q.z * Q.z);
const float halfYawMinusHeading = 0.5f * (yaw - FusionDegreesToRadians(heading));
const FusionQuaternion rotation = {.element = {
.w = cosf(halfYawMinusHeading),
.x = 0.0f,
.y = 0.0f,
.z = -1.0f * sinf(halfYawMinusHeading),
}};
ahrs->quaternion = FusionQuaternionMultiply(rotation, ahrs->quaternion);
#undef Q
}
//------------------------------------------------------------------------------
// End of file
-112
View File
@@ -1,112 +0,0 @@
/**
* @file FusionAhrs.h
* @author Seb Madgwick
* @brief AHRS algorithm to combine gyroscope, accelerometer, and magnetometer
* measurements into a single measurement of orientation relative to the Earth.
*/
#ifndef FUSION_AHRS_H
#define FUSION_AHRS_H
//------------------------------------------------------------------------------
// Includes
#include "FusionConvention.h"
#include "FusionMath.h"
#include <stdbool.h>
//------------------------------------------------------------------------------
// Definitions
/**
* @brief AHRS algorithm settings.
*/
typedef struct {
FusionConvention convention;
float gain;
float gyroscopeRange;
float accelerationRejection;
float magneticRejection;
unsigned int recoveryTriggerPeriod;
} FusionAhrsSettings;
/**
* @brief AHRS algorithm structure. Structure members are used internally and
* must not be accessed by the application.
*/
typedef struct {
FusionAhrsSettings settings;
FusionQuaternion quaternion;
FusionVector accelerometer;
bool initialising;
float rampedGain;
float rampedGainStep;
bool angularRateRecovery;
FusionVector halfAccelerometerFeedback;
FusionVector halfMagnetometerFeedback;
bool accelerometerIgnored;
int accelerationRecoveryTrigger;
int accelerationRecoveryTimeout;
bool magnetometerIgnored;
int magneticRecoveryTrigger;
int magneticRecoveryTimeout;
} FusionAhrs;
/**
* @brief AHRS algorithm internal states.
*/
typedef struct {
float accelerationError;
bool accelerometerIgnored;
float accelerationRecoveryTrigger;
float magneticError;
bool magnetometerIgnored;
float magneticRecoveryTrigger;
} FusionAhrsInternalStates;
/**
* @brief AHRS algorithm flags.
*/
typedef struct {
bool initialising;
bool angularRateRecovery;
bool accelerationRecovery;
bool magneticRecovery;
} FusionAhrsFlags;
//------------------------------------------------------------------------------
// Function declarations
void FusionAhrsInitialise(FusionAhrs *const ahrs);
void FusionAhrsReset(FusionAhrs *const ahrs);
void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings);
void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer,
const FusionVector magnetometer, const float deltaTime);
void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer,
const float deltaTime);
void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer,
const float heading, const float deltaTime);
FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs);
void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quaternion);
FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs);
FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs);
FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs);
FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs);
void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading);
#endif
//------------------------------------------------------------------------------
// End of file
-188
View File
@@ -1,188 +0,0 @@
/**
* @file FusionAxes.h
* @author Seb Madgwick
* @brief Swaps sensor axes for alignment with the body axes.
*/
#ifndef FUSION_AXES_H
#define FUSION_AXES_H
//------------------------------------------------------------------------------
// Includes
#include "FusionMath.h"
//------------------------------------------------------------------------------
// Definitions
/**
* @brief Axes alignment describing the sensor axes relative to the body axes.
* For example, if the body X axis is aligned with the sensor Y axis and the
* body Y axis is aligned with sensor X axis but pointing the opposite direction
* then alignment is +Y-X+Z.
*/
typedef enum {
FusionAxesAlignmentPXPYPZ, /* +X+Y+Z */
FusionAxesAlignmentPXNZPY, /* +X-Z+Y */
FusionAxesAlignmentPXNYNZ, /* +X-Y-Z */
FusionAxesAlignmentPXPZNY, /* +X+Z-Y */
FusionAxesAlignmentNXPYNZ, /* -X+Y-Z */
FusionAxesAlignmentNXPZPY, /* -X+Z+Y */
FusionAxesAlignmentNXNYPZ, /* -X-Y+Z */
FusionAxesAlignmentNXNZNY, /* -X-Z-Y */
FusionAxesAlignmentPYNXPZ, /* +Y-X+Z */
FusionAxesAlignmentPYNZNX, /* +Y-Z-X */
FusionAxesAlignmentPYPXNZ, /* +Y+X-Z */
FusionAxesAlignmentPYPZPX, /* +Y+Z+X */
FusionAxesAlignmentNYPXPZ, /* -Y+X+Z */
FusionAxesAlignmentNYNZPX, /* -Y-Z+X */
FusionAxesAlignmentNYNXNZ, /* -Y-X-Z */
FusionAxesAlignmentNYPZNX, /* -Y+Z-X */
FusionAxesAlignmentPZPYNX, /* +Z+Y-X */
FusionAxesAlignmentPZPXPY, /* +Z+X+Y */
FusionAxesAlignmentPZNYPX, /* +Z-Y+X */
FusionAxesAlignmentPZNXNY, /* +Z-X-Y */
FusionAxesAlignmentNZPYPX, /* -Z+Y+X */
FusionAxesAlignmentNZNXPY, /* -Z-X+Y */
FusionAxesAlignmentNZNYNX, /* -Z-Y-X */
FusionAxesAlignmentNZPXNY, /* -Z+X-Y */
} FusionAxesAlignment;
//------------------------------------------------------------------------------
// Inline functions
/**
* @brief Swaps sensor axes for alignment with the body axes.
* @param sensor Sensor axes.
* @param alignment Axes alignment.
* @return Sensor axes aligned with the body axes.
*/
static inline FusionVector FusionAxesSwap(const FusionVector sensor, const FusionAxesAlignment alignment)
{
FusionVector result;
switch (alignment) {
case FusionAxesAlignmentPXPYPZ:
break;
case FusionAxesAlignmentPXNZPY:
result.axis.x = +sensor.axis.x;
result.axis.y = -sensor.axis.z;
result.axis.z = +sensor.axis.y;
return result;
case FusionAxesAlignmentPXNYNZ:
result.axis.x = +sensor.axis.x;
result.axis.y = -sensor.axis.y;
result.axis.z = -sensor.axis.z;
return result;
case FusionAxesAlignmentPXPZNY:
result.axis.x = +sensor.axis.x;
result.axis.y = +sensor.axis.z;
result.axis.z = -sensor.axis.y;
return result;
case FusionAxesAlignmentNXPYNZ:
result.axis.x = -sensor.axis.x;
result.axis.y = +sensor.axis.y;
result.axis.z = -sensor.axis.z;
return result;
case FusionAxesAlignmentNXPZPY:
result.axis.x = -sensor.axis.x;
result.axis.y = +sensor.axis.z;
result.axis.z = +sensor.axis.y;
return result;
case FusionAxesAlignmentNXNYPZ:
result.axis.x = -sensor.axis.x;
result.axis.y = -sensor.axis.y;
result.axis.z = +sensor.axis.z;
return result;
case FusionAxesAlignmentNXNZNY:
result.axis.x = -sensor.axis.x;
result.axis.y = -sensor.axis.z;
result.axis.z = -sensor.axis.y;
return result;
case FusionAxesAlignmentPYNXPZ:
result.axis.x = +sensor.axis.y;
result.axis.y = -sensor.axis.x;
result.axis.z = +sensor.axis.z;
return result;
case FusionAxesAlignmentPYNZNX:
result.axis.x = +sensor.axis.y;
result.axis.y = -sensor.axis.z;
result.axis.z = -sensor.axis.x;
return result;
case FusionAxesAlignmentPYPXNZ:
result.axis.x = +sensor.axis.y;
result.axis.y = +sensor.axis.x;
result.axis.z = -sensor.axis.z;
return result;
case FusionAxesAlignmentPYPZPX:
result.axis.x = +sensor.axis.y;
result.axis.y = +sensor.axis.z;
result.axis.z = +sensor.axis.x;
return result;
case FusionAxesAlignmentNYPXPZ:
result.axis.x = -sensor.axis.y;
result.axis.y = +sensor.axis.x;
result.axis.z = +sensor.axis.z;
return result;
case FusionAxesAlignmentNYNZPX:
result.axis.x = -sensor.axis.y;
result.axis.y = -sensor.axis.z;
result.axis.z = +sensor.axis.x;
return result;
case FusionAxesAlignmentNYNXNZ:
result.axis.x = -sensor.axis.y;
result.axis.y = -sensor.axis.x;
result.axis.z = -sensor.axis.z;
return result;
case FusionAxesAlignmentNYPZNX:
result.axis.x = -sensor.axis.y;
result.axis.y = +sensor.axis.z;
result.axis.z = -sensor.axis.x;
return result;
case FusionAxesAlignmentPZPYNX:
result.axis.x = +sensor.axis.z;
result.axis.y = +sensor.axis.y;
result.axis.z = -sensor.axis.x;
return result;
case FusionAxesAlignmentPZPXPY:
result.axis.x = +sensor.axis.z;
result.axis.y = +sensor.axis.x;
result.axis.z = +sensor.axis.y;
return result;
case FusionAxesAlignmentPZNYPX:
result.axis.x = +sensor.axis.z;
result.axis.y = -sensor.axis.y;
result.axis.z = +sensor.axis.x;
return result;
case FusionAxesAlignmentPZNXNY:
result.axis.x = +sensor.axis.z;
result.axis.y = -sensor.axis.x;
result.axis.z = -sensor.axis.y;
return result;
case FusionAxesAlignmentNZPYPX:
result.axis.x = -sensor.axis.z;
result.axis.y = +sensor.axis.y;
result.axis.z = +sensor.axis.x;
return result;
case FusionAxesAlignmentNZNXPY:
result.axis.x = -sensor.axis.z;
result.axis.y = -sensor.axis.x;
result.axis.z = +sensor.axis.y;
return result;
case FusionAxesAlignmentNZNYNX:
result.axis.x = -sensor.axis.z;
result.axis.y = -sensor.axis.y;
result.axis.z = -sensor.axis.x;
return result;
case FusionAxesAlignmentNZPXNY:
result.axis.x = -sensor.axis.z;
result.axis.y = +sensor.axis.x;
result.axis.z = -sensor.axis.y;
return result;
}
return sensor; // avoid compiler warning
}
#endif
//------------------------------------------------------------------------------
// End of file
-49
View File
@@ -1,49 +0,0 @@
/**
* @file FusionCalibration.h
* @author Seb Madgwick
* @brief Gyroscope, accelerometer, and magnetometer calibration models.
*/
#ifndef FUSION_CALIBRATION_H
#define FUSION_CALIBRATION_H
//------------------------------------------------------------------------------
// Includes
#include "FusionMath.h"
//------------------------------------------------------------------------------
// Inline functions
/**
* @brief Gyroscope and accelerometer calibration model.
* @param uncalibrated Uncalibrated measurement.
* @param misalignment Misalignment matrix.
* @param sensitivity Sensitivity.
* @param offset Offset.
* @return Calibrated measurement.
*/
static inline FusionVector FusionCalibrationInertial(const FusionVector uncalibrated, const FusionMatrix misalignment,
const FusionVector sensitivity, const FusionVector offset)
{
return FusionMatrixMultiplyVector(misalignment,
FusionVectorHadamardProduct(FusionVectorSubtract(uncalibrated, offset), sensitivity));
}
/**
* @brief Magnetometer calibration model.
* @param uncalibrated Uncalibrated measurement.
* @param softIronMatrix Soft-iron matrix.
* @param hardIronOffset Hard-iron offset.
* @return Calibrated measurement.
*/
static inline FusionVector FusionCalibrationMagnetic(const FusionVector uncalibrated, const FusionMatrix softIronMatrix,
const FusionVector hardIronOffset)
{
return FusionMatrixMultiplyVector(softIronMatrix, FusionVectorSubtract(uncalibrated, hardIronOffset));
}
#endif
//------------------------------------------------------------------------------
// End of file
-51
View File
@@ -1,51 +0,0 @@
/**
* @file FusionCompass.c
* @author Seb Madgwick
* @brief Tilt-compensated compass to calculate the magnetic heading using
* accelerometer and magnetometer measurements.
*/
//------------------------------------------------------------------------------
// Includes
#include "FusionCompass.h"
#include "FusionAxes.h"
#include <math.h> // atan2f
//------------------------------------------------------------------------------
// Functions
/**
* @brief Calculates the magnetic heading.
* @param convention Earth axes convention.
* @param accelerometer Accelerometer measurement in any calibrated units.
* @param magnetometer Magnetometer measurement in any calibrated units.
* @return Heading angle in degrees.
*/
float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer,
const FusionVector magnetometer)
{
switch (convention) {
case FusionConventionNwu: {
const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer));
const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer));
return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x));
}
case FusionConventionEnu: {
const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer));
const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer));
const FusionVector east = FusionVectorMultiplyScalar(west, -1.0f);
return FusionRadiansToDegrees(atan2f(north.axis.x, east.axis.x));
}
case FusionConventionNed: {
const FusionVector up = FusionVectorMultiplyScalar(accelerometer, -1.0f);
const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(up, magnetometer));
const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, up));
return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x));
}
}
return 0; // avoid compiler warning
}
//------------------------------------------------------------------------------
// End of file
-26
View File
@@ -1,26 +0,0 @@
/**
* @file FusionCompass.h
* @author Seb Madgwick
* @brief Tilt-compensated compass to calculate the magnetic heading using
* accelerometer and magnetometer measurements.
*/
#ifndef FUSION_COMPASS_H
#define FUSION_COMPASS_H
//------------------------------------------------------------------------------
// Includes
#include "FusionConvention.h"
#include "FusionMath.h"
//------------------------------------------------------------------------------
// Function declarations
float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer,
const FusionVector magnetometer);
#endif
//------------------------------------------------------------------------------
// End of file
-25
View File
@@ -1,25 +0,0 @@
/**
* @file FusionConvention.h
* @author Seb Madgwick
* @brief Earth axes convention.
*/
#ifndef FUSION_CONVENTION_H
#define FUSION_CONVENTION_H
//------------------------------------------------------------------------------
// Definitions
/**
* @brief Earth axes convention.
*/
typedef enum {
FusionConventionNwu, /* North-West-Up */
FusionConventionEnu, /* East-North-Up */
FusionConventionNed, /* North-East-Down */
} FusionConvention;
#endif
//------------------------------------------------------------------------------
// End of file
-503
View File
@@ -1,503 +0,0 @@
/**
* @file FusionMath.h
* @author Seb Madgwick
* @brief Math library.
*/
#ifndef FUSION_MATH_H
#define FUSION_MATH_H
//------------------------------------------------------------------------------
// Includes
#include <math.h> // M_PI, sqrtf, atan2f, asinf
#include <stdbool.h>
#include <stdint.h>
//------------------------------------------------------------------------------
// Definitions
/**
* @brief 3D vector.
*/
typedef union {
float array[3];
struct {
float x;
float y;
float z;
} axis;
} FusionVector;
/**
* @brief Quaternion.
*/
typedef union {
float array[4];
struct {
float w;
float x;
float y;
float z;
} element;
} FusionQuaternion;
/**
* @brief 3x3 matrix in row-major order.
* See http://en.wikipedia.org/wiki/Row-major_order
*/
typedef union {
float array[3][3];
struct {
float xx;
float xy;
float xz;
float yx;
float yy;
float yz;
float zx;
float zy;
float zz;
} element;
} FusionMatrix;
/**
* @brief Euler angles. Roll, pitch, and yaw correspond to rotations around
* X, Y, and Z respectively.
*/
typedef union {
float array[3];
struct {
float roll;
float pitch;
float yaw;
} angle;
} FusionEuler;
/**
* @brief Vector of zeros.
*/
#define FUSION_VECTOR_ZERO ((FusionVector){.array = {0.0f, 0.0f, 0.0f}})
/**
* @brief Vector of ones.
*/
#define FUSION_VECTOR_ONES ((FusionVector){.array = {1.0f, 1.0f, 1.0f}})
/**
* @brief Identity quaternion.
*/
#define FUSION_IDENTITY_QUATERNION ((FusionQuaternion){.array = {1.0f, 0.0f, 0.0f, 0.0f}})
/**
* @brief Identity matrix.
*/
#define FUSION_IDENTITY_MATRIX ((FusionMatrix){.array = {{1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}}})
/**
* @brief Euler angles of zero.
*/
#define FUSION_EULER_ZERO ((FusionEuler){.array = {0.0f, 0.0f, 0.0f}})
/**
* @brief Pi. May not be defined in math.h.
*/
#ifndef M_PI
#define M_PI (3.14159265358979323846)
#endif
/**
* @brief Include this definition or add as a preprocessor definition to use
* normal square root operations.
*/
// #define FUSION_USE_NORMAL_SQRT
//------------------------------------------------------------------------------
// Inline functions - Degrees and radians conversion
/**
* @brief Converts degrees to radians.
* @param degrees Degrees.
* @return Radians.
*/
static inline float FusionDegreesToRadians(const float degrees)
{
return degrees * ((float)M_PI / 180.0f);
}
/**
* @brief Converts radians to degrees.
* @param radians Radians.
* @return Degrees.
*/
static inline float FusionRadiansToDegrees(const float radians)
{
return radians * (180.0f / (float)M_PI);
}
//------------------------------------------------------------------------------
// Inline functions - Arc sine
/**
* @brief Returns the arc sine of the value.
* @param value Value.
* @return Arc sine of the value.
*/
static inline float FusionAsin(const float value)
{
if (value <= -1.0f) {
return (float)M_PI / -2.0f;
}
if (value >= 1.0f) {
return (float)M_PI / 2.0f;
}
return asinf(value);
}
//------------------------------------------------------------------------------
// Inline functions - Fast inverse square root
#ifndef FUSION_USE_NORMAL_SQRT
/**
* @brief Calculates the reciprocal of the square root.
* See https://pizer.wordpress.com/2008/10/12/fast-inverse-square-root/
* @param x Operand.
* @return Reciprocal of the square root of x.
*/
static inline float FusionFastInverseSqrt(const float x)
{
typedef union {
float f;
int32_t i;
} Union32;
Union32 union32 = {.f = x};
union32.i = 0x5F1F1412 - (union32.i >> 1);
return union32.f * (1.69000231f - 0.714158168f * x * union32.f * union32.f);
}
#endif
//------------------------------------------------------------------------------
// Inline functions - Vector operations
/**
* @brief Returns true if the vector is zero.
* @param vector Vector.
* @return True if the vector is zero.
*/
static inline bool FusionVectorIsZero(const FusionVector vector)
{
return (vector.axis.x == 0.0f) && (vector.axis.y == 0.0f) && (vector.axis.z == 0.0f);
}
/**
* @brief Returns the sum of two vectors.
* @param vectorA Vector A.
* @param vectorB Vector B.
* @return Sum of two vectors.
*/
static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const FusionVector vectorB)
{
const FusionVector result = {.axis = {
.x = vectorA.axis.x + vectorB.axis.x,
.y = vectorA.axis.y + vectorB.axis.y,
.z = vectorA.axis.z + vectorB.axis.z,
}};
return result;
}
/**
* @brief Returns vector B subtracted from vector A.
* @param vectorA Vector A.
* @param vectorB Vector B.
* @return Vector B subtracted from vector A.
*/
static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, const FusionVector vectorB)
{
const FusionVector result = {.axis = {
.x = vectorA.axis.x - vectorB.axis.x,
.y = vectorA.axis.y - vectorB.axis.y,
.z = vectorA.axis.z - vectorB.axis.z,
}};
return result;
}
/**
* @brief Returns the sum of the elements.
* @param vector Vector.
* @return Sum of the elements.
*/
static inline float FusionVectorSum(const FusionVector vector)
{
return vector.axis.x + vector.axis.y + vector.axis.z;
}
/**
* @brief Returns the multiplication of a vector by a scalar.
* @param vector Vector.
* @param scalar Scalar.
* @return Multiplication of a vector by a scalar.
*/
static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, const float scalar)
{
const FusionVector result = {.axis = {
.x = vector.axis.x * scalar,
.y = vector.axis.y * scalar,
.z = vector.axis.z * scalar,
}};
return result;
}
/**
* @brief Calculates the Hadamard product (element-wise multiplication).
* @param vectorA Vector A.
* @param vectorB Vector B.
* @return Hadamard product.
*/
static inline FusionVector FusionVectorHadamardProduct(const FusionVector vectorA, const FusionVector vectorB)
{
const FusionVector result = {.axis = {
.x = vectorA.axis.x * vectorB.axis.x,
.y = vectorA.axis.y * vectorB.axis.y,
.z = vectorA.axis.z * vectorB.axis.z,
}};
return result;
}
/**
* @brief Returns the cross product.
* @param vectorA Vector A.
* @param vectorB Vector B.
* @return Cross product.
*/
static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, const FusionVector vectorB)
{
#define A vectorA.axis
#define B vectorB.axis
const FusionVector result = {.axis = {
.x = A.y * B.z - A.z * B.y,
.y = A.z * B.x - A.x * B.z,
.z = A.x * B.y - A.y * B.x,
}};
return result;
#undef A
#undef B
}
/**
* @brief Returns the dot product.
* @param vectorA Vector A.
* @param vectorB Vector B.
* @return Dot product.
*/
static inline float FusionVectorDotProduct(const FusionVector vectorA, const FusionVector vectorB)
{
return FusionVectorSum(FusionVectorHadamardProduct(vectorA, vectorB));
}
/**
* @brief Returns the vector magnitude squared.
* @param vector Vector.
* @return Vector magnitude squared.
*/
static inline float FusionVectorMagnitudeSquared(const FusionVector vector)
{
return FusionVectorSum(FusionVectorHadamardProduct(vector, vector));
}
/**
* @brief Returns the vector magnitude.
* @param vector Vector.
* @return Vector magnitude.
*/
static inline float FusionVectorMagnitude(const FusionVector vector)
{
return sqrtf(FusionVectorMagnitudeSquared(vector));
}
/**
* @brief Returns the normalised vector.
* @param vector Vector.
* @return Normalised vector.
*/
static inline FusionVector FusionVectorNormalise(const FusionVector vector)
{
#ifdef FUSION_USE_NORMAL_SQRT
const float magnitudeReciprocal = 1.0f / sqrtf(FusionVectorMagnitudeSquared(vector));
#else
const float magnitudeReciprocal = FusionFastInverseSqrt(FusionVectorMagnitudeSquared(vector));
#endif
return FusionVectorMultiplyScalar(vector, magnitudeReciprocal);
}
//------------------------------------------------------------------------------
// Inline functions - Quaternion operations
/**
* @brief Returns the sum of two quaternions.
* @param quaternionA Quaternion A.
* @param quaternionB Quaternion B.
* @return Sum of two quaternions.
*/
static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB)
{
const FusionQuaternion result = {.element = {
.w = quaternionA.element.w + quaternionB.element.w,
.x = quaternionA.element.x + quaternionB.element.x,
.y = quaternionA.element.y + quaternionB.element.y,
.z = quaternionA.element.z + quaternionB.element.z,
}};
return result;
}
/**
* @brief Returns the multiplication of two quaternions.
* @param quaternionA Quaternion A (to be post-multiplied).
* @param quaternionB Quaternion B (to be pre-multiplied).
* @return Multiplication of two quaternions.
*/
static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB)
{
#define A quaternionA.element
#define B quaternionB.element
const FusionQuaternion result = {.element = {
.w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z,
.x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y,
.y = A.w * B.y - A.x * B.z + A.y * B.w + A.z * B.x,
.z = A.w * B.z + A.x * B.y - A.y * B.x + A.z * B.w,
}};
return result;
#undef A
#undef B
}
/**
* @brief Returns the multiplication of a quaternion with a vector. This is a
* normal quaternion multiplication where the vector is treated a
* quaternion with a W element value of zero. The quaternion is post-
* multiplied by the vector.
* @param quaternion Quaternion.
* @param vector Vector.
* @return Multiplication of a quaternion with a vector.
*/
static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuaternion quaternion, const FusionVector vector)
{
#define Q quaternion.element
#define V vector.axis
const FusionQuaternion result = {.element = {
.w = -Q.x * V.x - Q.y * V.y - Q.z * V.z,
.x = Q.w * V.x + Q.y * V.z - Q.z * V.y,
.y = Q.w * V.y - Q.x * V.z + Q.z * V.x,
.z = Q.w * V.z + Q.x * V.y - Q.y * V.x,
}};
return result;
#undef Q
#undef V
}
/**
* @brief Returns the normalised quaternion.
* @param quaternion Quaternion.
* @return Normalised quaternion.
*/
static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion quaternion)
{
#define Q quaternion.element
#ifdef FUSION_USE_NORMAL_SQRT
const float magnitudeReciprocal = 1.0f / sqrtf(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z);
#else
const float magnitudeReciprocal = FusionFastInverseSqrt(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z);
#endif
const FusionQuaternion result = {.element = {
.w = Q.w * magnitudeReciprocal,
.x = Q.x * magnitudeReciprocal,
.y = Q.y * magnitudeReciprocal,
.z = Q.z * magnitudeReciprocal,
}};
return result;
#undef Q
}
//------------------------------------------------------------------------------
// Inline functions - Matrix operations
/**
* @brief Returns the multiplication of a matrix with a vector.
* @param matrix Matrix.
* @param vector Vector.
* @return Multiplication of a matrix with a vector.
*/
static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, const FusionVector vector)
{
#define R matrix.element
const FusionVector result = {.axis = {
.x = R.xx * vector.axis.x + R.xy * vector.axis.y + R.xz * vector.axis.z,
.y = R.yx * vector.axis.x + R.yy * vector.axis.y + R.yz * vector.axis.z,
.z = R.zx * vector.axis.x + R.zy * vector.axis.y + R.zz * vector.axis.z,
}};
return result;
#undef R
}
//------------------------------------------------------------------------------
// Inline functions - Conversion operations
/**
* @brief Converts a quaternion to a rotation matrix.
* @param quaternion Quaternion.
* @return Rotation matrix.
*/
static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quaternion)
{
#define Q quaternion.element
const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations
const float qwqx = Q.w * Q.x;
const float qwqy = Q.w * Q.y;
const float qwqz = Q.w * Q.z;
const float qxqy = Q.x * Q.y;
const float qxqz = Q.x * Q.z;
const float qyqz = Q.y * Q.z;
const FusionMatrix matrix = {.element = {
.xx = 2.0f * (qwqw - 0.5f + Q.x * Q.x),
.xy = 2.0f * (qxqy - qwqz),
.xz = 2.0f * (qxqz + qwqy),
.yx = 2.0f * (qxqy + qwqz),
.yy = 2.0f * (qwqw - 0.5f + Q.y * Q.y),
.yz = 2.0f * (qyqz - qwqx),
.zx = 2.0f * (qxqz - qwqy),
.zy = 2.0f * (qyqz + qwqx),
.zz = 2.0f * (qwqw - 0.5f + Q.z * Q.z),
}};
return matrix;
#undef Q
}
/**
* @brief Converts a quaternion to ZYX Euler angles in degrees.
* @param quaternion Quaternion.
* @return Euler angles in degrees.
*/
static inline FusionEuler FusionQuaternionToEuler(const FusionQuaternion quaternion)
{
#define Q quaternion.element
const float halfMinusQySquared = 0.5f - Q.y * Q.y; // calculate common terms to avoid repeated operations
const FusionEuler euler = {.angle = {
.roll = FusionRadiansToDegrees(atan2f(Q.w * Q.x + Q.y * Q.z, halfMinusQySquared - Q.x * Q.x)),
.pitch = FusionRadiansToDegrees(FusionAsin(2.0f * (Q.w * Q.y - Q.z * Q.x))),
.yaw = FusionRadiansToDegrees(atan2f(Q.w * Q.z + Q.x * Q.y, halfMinusQySquared - Q.z * Q.z)),
}};
return euler;
#undef Q
}
#endif
//------------------------------------------------------------------------------
// End of file
-80
View File
@@ -1,80 +0,0 @@
/**
* @file FusionOffset.c
* @author Seb Madgwick
* @brief Gyroscope offset correction algorithm for run-time calibration of the
* gyroscope offset.
*/
//------------------------------------------------------------------------------
// Includes
#include "FusionOffset.h"
#include <math.h> // fabsf
//------------------------------------------------------------------------------
// Definitions
/**
* @brief Cutoff frequency in Hz.
*/
#define CUTOFF_FREQUENCY (0.02f)
/**
* @brief Timeout in seconds.
*/
#define TIMEOUT (5)
/**
* @brief Threshold in degrees per second.
*/
#define THRESHOLD (3.0f)
//------------------------------------------------------------------------------
// Functions
/**
* @brief Initialises the gyroscope offset algorithm.
* @param offset Gyroscope offset algorithm structure.
* @param sampleRate Sample rate in Hz.
*/
void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate)
{
offset->filterCoefficient = 2.0f * (float)M_PI * CUTOFF_FREQUENCY * (1.0f / (float)sampleRate);
offset->timeout = TIMEOUT * sampleRate;
offset->timer = 0;
offset->gyroscopeOffset = FUSION_VECTOR_ZERO;
}
/**
* @brief Updates the gyroscope offset algorithm and returns the corrected
* gyroscope measurement.
* @param offset Gyroscope offset algorithm structure.
* @param gyroscope Gyroscope measurement in degrees per second.
* @return Corrected gyroscope measurement in degrees per second.
*/
FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope)
{
// Subtract offset from gyroscope measurement
gyroscope = FusionVectorSubtract(gyroscope, offset->gyroscopeOffset);
// Reset timer if gyroscope not stationary
if ((fabsf(gyroscope.axis.x) > THRESHOLD) || (fabsf(gyroscope.axis.y) > THRESHOLD) || (fabsf(gyroscope.axis.z) > THRESHOLD)) {
offset->timer = 0;
return gyroscope;
}
// Increment timer while gyroscope stationary
if (offset->timer < offset->timeout) {
offset->timer++;
return gyroscope;
}
// Adjust offset if timer has elapsed
offset->gyroscopeOffset =
FusionVectorAdd(offset->gyroscopeOffset, FusionVectorMultiplyScalar(gyroscope, offset->filterCoefficient));
return gyroscope;
}
//------------------------------------------------------------------------------
// End of file
-40
View File
@@ -1,40 +0,0 @@
/**
* @file FusionOffset.h
* @author Seb Madgwick
* @brief Gyroscope offset correction algorithm for run-time calibration of the
* gyroscope offset.
*/
#ifndef FUSION_OFFSET_H
#define FUSION_OFFSET_H
//------------------------------------------------------------------------------
// Includes
#include "FusionMath.h"
//------------------------------------------------------------------------------
// Definitions
/**
* @brief Gyroscope offset algorithm structure. Structure members are used
* internally and must not be accessed by the application.
*/
typedef struct {
float filterCoefficient;
unsigned int timeout;
unsigned int timer;
FusionVector gyroscopeOffset;
} FusionOffset;
//------------------------------------------------------------------------------
// Function declarations
void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate);
FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope);
#endif
//------------------------------------------------------------------------------
// End of file
+6 -6
View File
@@ -48,7 +48,7 @@
#include "concurrency/LockGuard.h"
#endif
#if defined(ARCH_STM32WL) && defined(BATTERY_PIN)
#if defined(ARCH_STM32) && defined(BATTERY_PIN)
#include "stm32yyxx_ll_adc.h"
/* Analog read resolution */
@@ -431,7 +431,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
float scaled = 0;
battery_adcEnable();
#ifdef ARCH_STM32WL
#ifdef ARCH_STM32
// STM32 ADC with VREFINT runtime calibration
Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
raw = analogRead(BATTERY_PIN);
@@ -608,7 +608,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
bool initial_read_done = false;
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
uint32_t last_read_time_ms = 0;
#ifdef ARCH_STM32WL
#ifdef ARCH_STM32
// 3300mV placeholder for STM32 errata where VREFINT factory calibration may be missing
// (e.g. STM32U0, see DS14756 Rev 3 §2.4.1 "VREFINT offset")
uint32_t Vref = 3300;
@@ -718,7 +718,7 @@ bool Power::analogInit()
#define BATTERY_SENSE_RESOLUTION_BITS 10
#endif
#ifdef ARCH_STM32WL
#ifdef ARCH_STM32
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
adc_oneshot_unit_init_cfg_t init_config = {
@@ -749,7 +749,7 @@ bool Power::analogInit()
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
#if !defined(ARCH_ESP32) && !defined(ARCH_STM32WL)
#if !defined(ARCH_ESP32) && !defined(ARCH_STM32)
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
#endif
@@ -838,7 +838,7 @@ void Power::reboot()
}
LOG_DEBUG("final reboot!");
::reboot();
#elif defined(ARCH_STM32WL)
#elif defined(ARCH_STM32)
HAL_NVIC_SystemReset();
#else
rebootAtMsec = -1;
+25 -9
View File
@@ -576,9 +576,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// -----------------------------------------------------------------------------
// MESHTASTIC_LOCKDOWN — runtime, client-toggleable hardening (nRF52 only)
//
// There is NO build flag to turn lockdown on or off. On nRF52 (CC310 hardware
// crypto) the lockdown machinery is ALWAYS compiled in; whether it is ACTIVE
// is decided entirely at runtime by EncryptedStorage::isLockdownActive()
// Lockdown/protect support is opt-in at build time. Builds that need it pass
// -DMESHTASTIC_ENABLE_LOCKDOWN=1. When enabled on nRF52 (CC310 hardware
// crypto), whether it is ACTIVE is decided entirely at runtime by
// EncryptedStorage::isLockdownActive()
// (== a passphrase has been provisioned, i.e. /prefs/.dek exists). A device
// that has never been provisioned — or that the operator disabled from the
// client app — behaves exactly like stock firmware: plaintext storage, no
@@ -594,11 +595,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// reboots into normal mode. APPROTECT is the one thing that
// does NOT revert (see below).
//
// MESHTASTIC_LOCKDOWN here is an INTERNAL capability marker, auto-defined for
// nRF52. It gates the UI bits (lock screen, pairing-PIN handling). It is NOT
// something a variant sets. Flash-constrained nRF52 variants that genuinely
// cannot afford the ~tens-of-KB of crypto + access-control code may opt OUT
// with -DMESHTASTIC_EXCLUDE_LOCKDOWN=1.
// MESHTASTIC_LOCKDOWN here is an INTERNAL capability marker. It gates the UI
// bits (lock screen, pairing-PIN handling). Flash-constrained nRF52 variants
// that genuinely cannot afford the ~tens-of-KB of crypto + access-control code
// may also opt out with -DMESHTASTIC_EXCLUDE_LOCKDOWN=1.
//
// MESHTASTIC_PHONEAPI_ACCESS_CONTROL — per-connection auth + redaction,
// gated at runtime on isLockdownActive()
@@ -615,7 +615,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// -DMESHTASTIC_LOCKDOWN_DEBUG=1 keeps the irreversible APPROTECT burn disabled
// even when provisioned — for development so dev boards never lose SWD.
// -----------------------------------------------------------------------------
#if defined(ARCH_NRF52) && !defined(MESHTASTIC_EXCLUDE_LOCKDOWN)
#if defined(ARCH_NRF52)
#ifndef MESHTASTIC_ENABLE_LOCKDOWN
#define MESHTASTIC_ENABLE_LOCKDOWN 0
#endif
#if !MESHTASTIC_ENABLE_LOCKDOWN
#undef MESHTASTIC_LOCKDOWN
#undef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
#undef MESHTASTIC_ENCRYPTED_STORAGE
#undef MESHTASTIC_ENABLE_APPROTECT
#ifndef MESHTASTIC_EXCLUDE_LOCKDOWN
#define MESHTASTIC_EXCLUDE_LOCKDOWN 1
#endif
#endif
#if MESHTASTIC_ENABLE_LOCKDOWN && !defined(MESHTASTIC_EXCLUDE_LOCKDOWN)
#define MESHTASTIC_LOCKDOWN 1
#define MESHTASTIC_PHONEAPI_ACCESS_CONTROL 1
#define MESHTASTIC_ENCRYPTED_STORAGE 1
@@ -623,6 +638,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MESHTASTIC_ENABLE_APPROTECT 1
#endif
#endif
#endif
#ifdef MESHTASTIC_LOCKDOWN
+5 -5
View File
@@ -37,15 +37,15 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
{
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P};
return firstOfOrNONE(11, types);
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P, ISM330DHCX};
return firstOfOrNONE(12, types);
}
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
{
ScanI2C::DeviceType types[] = {MMC5983MA};
return firstOfOrNONE(1, types);
ScanI2C::DeviceType types[] = {MMC5983MA, IIS2MDCTR};
return firstOfOrNONE(2, types);
}
ScanI2C::FoundDevice ScanI2C::firstAQI() const
+2
View File
@@ -99,6 +99,8 @@ class ScanI2C
CW2015,
SCD30,
ADS1115,
IIS2MDCTR,
ISM330DHCX,
} DeviceType;
// typedef uint8_t DeviceAddress;
+15 -2
View File
@@ -8,7 +8,7 @@
#if defined(ARCH_PORTDUINO)
#include "linux/LinuxHardwareI2C.h"
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32)
#include "meshUtils.h" // vformat
#endif
@@ -584,6 +584,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
if (registerValue == 0x6A) {
type = LSM6DS3;
logFoundDevice("LSM6DS3", (uint8_t)addr.address);
} else if (registerValue == 0x6B) {
type = ISM330DHCX;
logFoundDevice("ISM330DHCX", (uint8_t)addr.address);
} else {
type = QMI8658;
logFoundDevice("QMI8658", (uint8_t)addr.address);
@@ -591,7 +594,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
SCAN_SIMPLE_CASE(QMC5883L_ADDR, QMC5883L, "QMC5883L", (uint8_t)addr.address)
SCAN_SIMPLE_CASE(HMC5883L_ADDR, HMC5883L, "HMC5883L", (uint8_t)addr.address)
case HMC5883L_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x4FU), 1); // get ID
if (registerValue == 0x40) {
type = IIS2MDCTR;
logFoundDevice("IIS2MDCTR", (uint8_t)addr.address);
break;
} else {
type = HMC5883L;
logFoundDevice("HMC5883L", (uint8_t)addr.address);
break;
}
#ifdef HAS_QMA6100P
SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address)
#else
+75 -21
View File
@@ -47,7 +47,7 @@ template <typename T, std::size_t N> std::size_t array_count(const T (&)[N])
#if defined(ARCH_NRF52)
Uart *GPS::_serial_gps = &GPS_SERIAL_PORT;
#elif defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
#elif defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32)
HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT;
#elif defined(ARCH_RP2040)
SerialUART *GPS::_serial_gps = &GPS_SERIAL_PORT;
@@ -80,6 +80,12 @@ namespace
constexpr uint32_t GPS_PROBE_CACHE_MAGIC = 0x47504348UL; // "GPCH"
constexpr uint16_t GPS_PROBE_CACHE_VERSION = 1;
constexpr const char *GPS_PROBE_CACHE_FILE = "/prefs/gps_probe_cache.dat";
constexpr int MIN_PLAUSIBLE_GPS_YEAR = 2020;
constexpr int MAX_PLAUSIBLE_GPS_YEAR = 2100;
#ifdef TRACKER_T1000_E
constexpr uint32_t T1000_E_AIROHA_WAKE_MS = 1000;
constexpr uint32_t T1000_E_AIROHA_WAKE_INTERVAL_MS = 40;
#endif
struct GPSProbeCacheRecord {
uint32_t magic;
@@ -101,6 +107,45 @@ bool isValidProbeBaud(uint32_t baud)
return baud >= 1200 && baud <= 921600;
}
template <typename T> void wakeAirohaForActiveProbe(T *serialGps)
{
#ifdef TRACKER_T1000_E
digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE);
digitalWrite(GPS_RTC_INT, HIGH);
delay(3);
digitalWrite(GPS_RTC_INT, LOW);
delay(50);
const uint32_t start = millis();
do {
serialGps->write("$PAIR382,1*2E\r\n");
delay(T1000_E_AIROHA_WAKE_INTERVAL_MS);
} while (Throttle::isWithinTimespanMs(start, T1000_E_AIROHA_WAKE_MS));
#elif defined(GNSS_AIROHA)
serialGps->write("$PAIR382,1*2E\r\n");
delay(20);
#else
(void)serialGps;
#endif
}
bool isPlausibleNmeaTime(const struct tm &t)
{
const int year = t.tm_year + 1900;
if (year < MIN_PLAUSIBLE_GPS_YEAR || year > MAX_PLAUSIBLE_GPS_YEAR) {
return false;
}
#ifdef BUILD_EPOCH
const int64_t candidate = static_cast<int64_t>(gm_mktime(&t));
const int64_t minEpoch = static_cast<int64_t>(BUILD_EPOCH);
const int64_t maxEpoch = minEpoch + static_cast<int64_t>(FORTY_YEARS);
return candidate >= minEpoch && candidate <= maxEpoch;
#else
return true;
#endif
}
template <typename T> bool sawNmeaSentenceAtBaud(T *serialGps, uint32_t timeoutMs)
{
// Lightweight passive check: look for at least one complete
@@ -642,7 +687,7 @@ bool GPS::verifyCachedProbePresence()
return false;
}
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32)
_serial_gps->end();
_serial_gps->begin(cachedProbeBaud);
#elif defined(ARCH_RP2040)
@@ -689,6 +734,7 @@ bool GPS::verifyCachedProbePresence()
case GNSS_MODEL_AG3352:
if (cachedProbeModel == GNSS_MODEL_AG3352)
cachedProbeModelName = "AG3352";
wakeAirohaForActiveProbe(_serial_gps);
_serial_gps->write("$PAIR021*39\r\n");
present = (getACK("$PAIR021,", 900) == GNSS_RESPONSE_OK);
break;
@@ -741,7 +787,6 @@ bool GPS::verifyCachedProbePresence()
if (!present) {
LOG_WARN("Cached GPS probe is stale (%s @ %d), clearing cache", cachedProbeModelName, cachedProbeBaud);
clearProbeCache();
cachedProbeFailedThisBoot = true;
return false;
}
@@ -762,13 +807,6 @@ bool GPS::setup()
{
if (!didSerialInit) {
int msglen = 0;
if (cachedProbeFailedThisBoot) {
// If cached verification failed, suppress further probing until
// reboot.
didSerialInit = true;
return true;
}
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
if (!hasProbeCache && !triedProbeCache) {
(void)loadProbeCache();
@@ -777,12 +815,13 @@ bool GPS::setup()
if (hasProbeCache && !triedProbeCache) {
triedProbeCache = true;
if (!verifyCachedProbePresence()) {
// Cache was stale and got wiped; skip scanning this boot
// and let next boot do a full probe.
didSerialInit = true;
return true;
currentStep = 0;
speedSelect = 0;
probeTries = 0;
}
} else if (probeTries < GPS_PROBETRIES) {
}
if (gnssModel == GNSS_MODEL_UNKNOWN && probeTries < GPS_PROBETRIES) {
// No usable cache: walk common baud rates first.
gnssModel = probe(serialSpeeds[speedSelect]);
if (gnssModel != GNSS_MODEL_UNKNOWN) {
@@ -794,7 +833,7 @@ bool GPS::setup()
}
// Rare Serial Speeds
#ifndef CONFIG_IDF_TARGET_ESP32C6
else if (probeTries == GPS_PROBETRIES) {
else if (gnssModel == GNSS_MODEL_UNKNOWN && probeTries == GPS_PROBETRIES) {
// Then try less common baud rates before giving up.
gnssModel = probe(rareSerialSpeeds[speedSelect]);
if (gnssModel != GNSS_MODEL_UNKNOWN) {
@@ -1125,6 +1164,15 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
break;
if (oldState != GPS_ACTIVE && oldState != GPS_IDLE) // If hardware just waking now, clear buffer
clearBuffer();
#ifdef TRACKER_T1000_E
pinMode(GPS_VRTC_EN, OUTPUT);
digitalWrite(GPS_VRTC_EN, HIGH);
pinMode(GPS_SLEEP_INT, OUTPUT);
digitalWrite(GPS_SLEEP_INT, HIGH);
pinMode(GPS_RTC_INT, OUTPUT);
digitalWrite(GPS_RTC_INT, LOW);
pinMode(GPS_RESETB_OUT, INPUT_PULLUP);
#endif
powerMon->setState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing)
writePinEN(true); // Power (EN pin): on
setPowerPMU(true); // Power (PMU): on
@@ -1383,9 +1431,8 @@ int32_t GPS::runOnce()
if (!setup())
return currentDelay; // Setup failed, re-run in two seconds
if (cachedProbeFailedThisBoot || gnssModel == GNSS_MODEL_UNKNOWN) {
LOG_WARN("GPS not detected at cached settings; marked not present "
"for this boot");
if (gnssModel == GNSS_MODEL_UNKNOWN) {
LOG_WARN("GPS not detected; marked not present for this boot");
return disable();
}
@@ -1564,7 +1611,7 @@ GnssModel_t GPS::probe(int serialSpeed)
switch (currentStep) {
case 0: {
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32)
_serial_gps->end();
_serial_gps->begin(serialSpeed);
#elif defined(ARCH_RP2040)
@@ -1585,6 +1632,9 @@ GnssModel_t GPS::probe(int serialSpeed)
digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms
delay(10);
digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE);
#ifdef TRACKER_T1000_E
delay(100);
#endif
// attempt to detect the chip based on boot messages
std::vector<ChipInfo> passive_detect = {
@@ -1637,6 +1687,7 @@ GnssModel_t GPS::probe(int serialSpeed)
}
case 3: {
/* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */
wakeAirohaForActiveProbe(_serial_gps);
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
@@ -1921,7 +1972,7 @@ std::unique_ptr<GPS> GPS::createGps()
#elif defined(ARCH_NRF52)
_serial_gps->setPins(new_gps->rx_gpio, new_gps->tx_gpio);
_serial_gps->begin(GPS_BAUDRATE);
#elif defined(ARCH_STM32WL)
#elif defined(ARCH_STM32)
_serial_gps->setTx(new_gps->tx_gpio);
_serial_gps->setRx(new_gps->rx_gpio);
_serial_gps->begin(GPS_BAUDRATE);
@@ -1968,6 +2019,9 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
t.tm_year = d.year() - 1900;
t.tm_isdst = false;
if (t.tm_mon > -1) {
if (!isPlausibleNmeaTime(t)) {
return false;
}
if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) {
LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour,
t.tm_min, t.tm_sec, ti.age());
-2
View File
@@ -193,8 +193,6 @@ class GPS : private concurrency::OSThread
bool hasProbeCache = false;
// Ensures cached probe is attempted once per boot.
bool triedProbeCache = false;
// Latched when cached presence check fails
bool cachedProbeFailedThisBoot = false;
/**
* hasValidLocation - indicates that the position variables contain a complete
+26 -23
View File
@@ -233,6 +233,16 @@ static inline float wrapHeading360(float heading)
return heading;
}
static inline float wrapDelta180(float delta)
{
if (delta > 180.0f) {
delta -= 360.0f;
} else if (delta < -180.0f) {
delta += 360.0f;
}
return delta;
}
void Screen::setHeading(float heading)
{
const float wrappedHeading = wrapHeading360(heading);
@@ -244,37 +254,30 @@ void Screen::setHeading(float heading)
}
// Interpolate using shortest-path angular delta to avoid jumps around 0/360.
float delta = wrappedHeading - compassHeading;
if (delta > 180.0f) {
delta -= 360.0f;
} else if (delta < -180.0f) {
delta += 360.0f;
}
float delta = wrapDelta180(wrappedHeading - compassHeading);
// Adaptive filtering:
// - Strong damping for tiny deltas (jitter)
// - Faster response for larger turns
const float absDelta = (delta >= 0.0f) ? delta : -delta;
if (absDelta < 1.0f) {
return;
}
if (absDelta >= 1.0f) {
float alpha = 0.35f;
if (absDelta > 25.0f) {
alpha = 0.85f;
} else if (absDelta > 10.0f) {
alpha = 0.65f;
}
float alpha = 0.35f;
if (absDelta > 25.0f) {
alpha = 0.85f;
} else if (absDelta > 10.0f) {
alpha = 0.65f;
}
float step = delta * alpha;
const float maxStep = 12.0f;
if (step > maxStep) {
step = maxStep;
} else if (step < -maxStep) {
step = -maxStep;
}
float step = delta * alpha;
const float maxStep = 12.0f;
if (step > maxStep) {
step = maxStep;
} else if (step < -maxStep) {
step = -maxStep;
compassHeading = wrapHeading360(compassHeading + step);
}
compassHeading = wrapHeading360(compassHeading + step);
}
// ==============================
+2 -4
View File
@@ -1533,8 +1533,7 @@ bool TFTDisplay::hasTouch(void)
{
#ifdef RAK14014
return true;
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1)
return tft->touch() != nullptr;
#else
return false;
@@ -1553,8 +1552,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
} else {
return false;
}
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1)
return tft->getTouch(x, y);
#else
return false;
+1 -1
View File
@@ -1191,7 +1191,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()
// No bluetooth on these targets (yet):
// Pico W / 2W may get it at some point
// Portduino and ESP32-C6 are excluded because we don't have a working bluetooth stacks integrated yet.
#if defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) || defined(CONFIG_IDF_TARGET_ESP32C6)
#if defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32) || defined(CONFIG_IDF_TARGET_ESP32C6)
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_BLUETOOTH_CONFIG;
#endif
+1 -1
View File
@@ -14,7 +14,7 @@
#include <malloc.h>
#include <unistd.h> // sbrk
#ifdef ARCH_STM32WL
#if defined(ARCH_STM32)
// Returns the uncommitted sbrk headroom: addressable space between the current heap
// break and the stack pointer that has not yet been committed to the arena.
static uint32_t sbrkHeadroom()
+36
View File
@@ -404,6 +404,42 @@ bool Channels::isDefaultChannel(ChannelIndex chIndex)
return false;
}
bool cryptoKeyIsPublic(const CryptoKey &key)
{
if (key.length == 0)
return true; // encryption disabled
// Match the defaultpsk family ignoring its last byte (getKey() bumps only that byte per 1-byte index).
if (key.length == (int)sizeof(defaultpsk) && memcmp(key.bytes, defaultpsk, sizeof(defaultpsk) - 1) == 0)
return true;
return false;
}
bool Channels::usesPublicKey(ChannelIndex chIndex)
{
const meshtastic_Channel &ch = getByIndex(chIndex);
if (!ch.has_settings || ch.role == meshtastic_Channel_Role_DISABLED)
return false;
const auto &psk = ch.settings.psk;
if (psk.size == 0) {
// Secondary channels inherit the primary key when unset; primary size==0 means encryption disabled.
if (ch.role == meshtastic_Channel_Role_SECONDARY) {
// Guard against malformed configs with no PRIMARY channel (primaryIndex could point back to us).
if (primaryIndex == chIndex)
return true; // fail closed: treat as public
return usesPublicKey(primaryIndex);
}
return true;
}
if (psk.size == 1) {
// Short PSK aliases: 0 disables encryption; 1..255 are the public defaultpsk family.
return true;
}
return (psk.size == sizeof(defaultpsk) && memcmp(psk.bytes, defaultpsk, sizeof(defaultpsk) - 1) == 0);
}
bool Channels::hasDefaultChannel()
{
// If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel
+6
View File
@@ -86,6 +86,9 @@ class Channels
// Returns true if the channel has the default name and PSK
bool isDefaultChannel(ChannelIndex chIndex);
// Returns true if this channel's effective key is publicly decryptable (open or well-known/default PSK).
bool usesPublicKey(ChannelIndex chIndex);
// Returns true if we can be reached via a channel with the default settings given a region and modem preset
bool hasDefaultChannel();
@@ -144,6 +147,9 @@ extern Channels channels;
static const uint8_t defaultpsk[] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59,
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01};
/// True if a getKey()-resolved key offers no privacy: length 0 (off) or the public defaultpsk family. Pure; for tests.
bool cryptoKeyIsPublic(const CryptoKey &key);
static const uint8_t eventpsk[] = {0x38, 0x4b, 0xbc, 0xc0, 0x1d, 0xc0, 0x22, 0xd1, 0x81, 0xbf, 0x36,
0xb8, 0x61, 0x21, 0xe1, 0xfb, 0x96, 0xb7, 0x2e, 0x55, 0xbf, 0x74,
0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1};
+108 -3
View File
@@ -12,10 +12,18 @@
#include <Curve25519.h>
#include <RNG.h>
#include <SHA256.h>
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN)
#if !defined(ARCH_STM32WL)
#define CryptRNG RNG
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
#include "XEdDSA.h"
#include <Ed25519.h>
#ifndef NUM_LIMBS_256BIT
#define NUM_LIMBS_BITS(n) (((n) + sizeof(limb_t) * 8 - 1) / (8 * sizeof(limb_t)))
#define NUM_LIMBS_256BIT NUM_LIMBS_BITS(256)
#endif
#endif
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN)
/**
* Create a public/private key pair with Curve25519.
@@ -46,6 +54,9 @@ void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey)
Curve25519::dh1(public_key, private_key);
memcpy(pubKey, public_key, sizeof(public_key));
memcpy(privKey, private_key, sizeof(private_key));
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
XEdDSA::priv_curve_to_ed_keys(private_key, xeddsa_private_key, xeddsa_public_key);
#endif
}
/**
@@ -65,6 +76,9 @@ bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey)
}
memcpy(private_key, privKey, sizeof(private_key));
memcpy(public_key, pubKey, sizeof(public_key));
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
XEdDSA::priv_curve_to_ed_keys(private_key, xeddsa_private_key, xeddsa_public_key);
#endif
} else {
LOG_WARN("X25519 key generation failed due to blank private key");
return false;
@@ -72,6 +86,97 @@ bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey)
return true;
}
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
/**
* Build a signing buffer that covers packet metadata and payload:
* [fromNode(4) | packetId(4) | portnum(4) | payload(N)]
* This prevents replay, reattribution, and portnum redirection attacks.
*/
static size_t buildSigningBuffer(uint8_t *buf, size_t bufSize, uint32_t fromNode, uint32_t packetId, uint32_t portnum,
const uint8_t *payload, size_t payloadLen)
{
const size_t headerLen = sizeof(uint32_t) * 3;
size_t totalLen = headerLen + payloadLen;
if (totalLen > bufSize)
return 0;
// May need endian conversion for oddball platforms.
memcpy(buf, &fromNode, sizeof(uint32_t));
memcpy(buf + sizeof(uint32_t), &packetId, sizeof(uint32_t));
memcpy(buf + sizeof(uint32_t) * 2, &portnum, sizeof(uint32_t));
memcpy(buf + headerLen, payload, payloadLen);
return totalLen;
}
bool CryptoEngine::xeddsa_sign(uint32_t fromNode, uint32_t packetId, uint32_t portnum, const uint8_t *payload, size_t payloadLen,
uint8_t *signature)
{
if (memfll(xeddsa_private_key, 0, sizeof(xeddsa_private_key)))
return false;
uint8_t sigBuf[MAX_BLOCKSIZE];
size_t sigLen = buildSigningBuffer(sigBuf, sizeof(sigBuf), fromNode, packetId, portnum, payload, payloadLen);
if (sigLen == 0)
return false;
// the XEdDSA::sign function requires at least the first 32 bytes of signature to be pre-filled with randomness
HardwareRNG::fill(signature, 32);
XEdDSA::sign(signature, xeddsa_private_key, xeddsa_public_key, sigBuf, sigLen);
return true;
}
bool CryptoEngine::xeddsa_verify(const uint8_t *pubKey, uint32_t fromNode, uint32_t packetId, uint32_t portnum,
const uint8_t *payload, size_t payloadLen, const uint8_t *signature)
{
// Use cached Ed25519 key if the Curve25519 key matches, avoiding expensive field inversion
if (memcmp(pubKey, cached_curve_pubkey, 32) != 0) {
curve_to_ed_pub(pubKey, cached_ed_pubkey);
memcpy(cached_curve_pubkey, pubKey, 32);
}
uint8_t sigBuf[MAX_BLOCKSIZE];
size_t sigLen = buildSigningBuffer(sigBuf, sizeof(sigBuf), fromNode, packetId, portnum, payload, payloadLen);
if (sigLen == 0)
return false;
return XEdDSA::verify(signature, cached_ed_pubkey, sigBuf, sigLen);
}
void CryptoEngine::curve_to_ed_pub(const uint8_t *curve_pubkey, uint8_t *ed_pubkey)
{
// Apply the birational map defined in RFC 7748, section 4.1 "Curve25519" to calculate an Ed25519 public
// key from a Curve25519 public key. Because the serialization format of Curve25519 public keys only
// contains the u coordinate, the x coordinate of the corresponding Ed25519 public key can't be uniquely
// calculated as defined by the birational map. The x coordinate is represented in the serialization
// format of Ed25519 public keys only in a single sign bit. XEdDSA always normalizes the Ed25519 public
// key to a sign bit of zero (the signer negates its key pair when needed), so this function clears the
// sign bit unconditionally below instead of taking it as an input.
fe u, y;
fe one;
fe u_minus_one, u_plus_one, u_plus_one_inv;
// Parse the Curve25519 public key input as a field element containing the u coordinate. RFC 7748,
// section 5 "The X25519 and X448 Functions", mandates that the most significant bit of the Curve25519
// public key has to be zeroized. This is handled by fe_frombytes internally.
fe_frombytes(u, curve_pubkey);
// Calculate the parameters (u - 1) and (u + 1)
fe_1(one);
fe_sub(u_minus_one, u, one);
fe_add(u_plus_one, u, one);
// Invert u + 1
fe_invert(u_plus_one_inv, u_plus_one);
// Calculate y = (u - 1) * inv(u + 1) (mod p)
fe_mul(y, u_minus_one, u_plus_one_inv);
// Serialize the field element containing the y coordinate to the Ed25519 public key output
fe_tobytes(ed_pubkey, y);
// Set the sign bit to zero
ed_pubkey[31] &= 0x7f;
// need to convert the pubkey y = ( u - 1) * inv( u + 1) (mod p).
}
#endif
bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user)
{
if (user.is_licensed) {
+15 -1
View File
@@ -23,6 +23,7 @@ struct CryptoKey {
#define MAX_BLOCKSIZE 256
#define TEST_CURVE25519_FIELD_OPS // Exposes Curve25519::isWeakPoint() for testing keys
#define XEDDSA_SIGNATURE_SIZE 64
class CryptoEngine
{
@@ -37,7 +38,12 @@ class CryptoEngine
virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey);
virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey);
virtual bool ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user);
#endif
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
bool xeddsa_sign(uint32_t fromNode, uint32_t packetId, uint32_t portnum, const uint8_t *payload, size_t payloadLen,
uint8_t *signature);
bool xeddsa_verify(const uint8_t *pubKey, uint32_t fromNode, uint32_t packetId, uint32_t portnum, const uint8_t *payload,
size_t payloadLen, const uint8_t *signature);
#endif
void setDHPrivateKey(uint8_t *_private_key);
// The remotePublic key parameter takes the public_key bytes container from
@@ -85,6 +91,14 @@ class CryptoEngine
#if !(MESHTASTIC_EXCLUDE_PKI)
uint8_t shared_key[32] = {0};
uint8_t private_key[32] = {0};
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
uint8_t xeddsa_public_key[32] = {0};
uint8_t xeddsa_private_key[32] = {0};
void curve_to_ed_pub(const uint8_t *curve_pubkey, uint8_t *ed_pubkey);
// Single-entry cache for curve_to_ed_pub conversion (avoids expensive field inversion per packet)
uint8_t cached_curve_pubkey[32] = {0};
uint8_t cached_ed_pubkey[32] = {0};
#endif
#endif
/**
* Init our 128 bit nonce for a new packet
+99 -21
View File
@@ -448,8 +448,6 @@ NodeDB::NodeDB()
// likewise - we always want the app requirements to come from the running appload
myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00
// Note! We do this after loading saved settings, so that if somehow an invalid nodenum was stored in preferences we won't
// keep using that nodenum forever. Crummy guess at our nodenum (but we will check against the nodedb to avoid conflicts)
pickNewNodeNum();
// Set our board type so we can share it with others
@@ -469,31 +467,18 @@ NodeDB::NodeDB()
}
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
bool keygenSuccess = false;
keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key);
if (config.security.private_key.size == 32 && !keyIsLowEntropy) {
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
keygenSuccess = true;
}
} else {
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
keygenSuccess = true;
}
if (keygenSuccess) {
config.security.public_key.size = 32;
config.security.private_key.size = 32;
owner.public_key.size = 32;
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
}
}
// Generate crypto keys if needed using consolidated function
// Set my node num uint32 value to bytes from the public key (if we have one)
// Generate identity and crypto keys if needed; this will create a new identity if one does not exist
generateCryptoKeyPair(nullptr);
#elif !(MESHTASTIC_EXCLUDE_PKI)
// Calculate Curve25519 public and private keys
if (config.security.private_key.size == 32 && config.security.public_key.size == 32) {
owner.public_key.size = config.security.public_key.size;
memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size);
crypto->setDHPrivateKey(config.security.private_key.bytes);
// Set my node num uint32 value to bytes from the new public key
myNodeInfo.my_node_num = crc32Buffer(config.security.public_key.bytes, config.security.public_key.size);
}
#endif
// Include our owner in the node db under our nodenum
@@ -3059,6 +3044,99 @@ bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_pub
}
#endif
bool NodeDB::generateCryptoKeyPair(const uint8_t *privateKey)
{
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
// Only generate keys for non-licensed users and if LoRa region is set
if (owner.is_licensed || config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
return false;
}
bool keygenSuccess = false;
// Record whether the stored key is a known compromised/low-entropy key so main.cpp can warn the
// user. A detected low-entropy key is regenerated below, but the flag stays set so the
// "Compromised keys were detected and regenerated" notification still fires.
keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key);
// If a specific private key was provided, use it
if (privateKey != nullptr) {
LOG_INFO("Using provided private key for PKI");
memcpy(config.security.private_key.bytes, privateKey, 32);
config.security.private_key.size = 32;
config.security.public_key.size = 32;
// Generate public key from the provided private key
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
keygenSuccess = true;
} else {
LOG_ERROR("Failed to generate public key from provided private key");
return false;
}
}
// Try to regenerate public key from existing private key if it's valid and not low entropy
else if (config.security.private_key.size == 32 && !keyIsLowEntropy) {
config.security.public_key.size = 32;
LOG_DEBUG("Regenerate PKI public key from existing private key");
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
keygenSuccess = true;
}
} else {
// Generate a new key pair
LOG_INFO("Generate new PKI keys");
config.security.public_key.size = 32;
config.security.private_key.size = 32;
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
keygenSuccess = true;
}
// Update sizes and copy to owner if successful
if (keygenSuccess) {
owner.public_key.size = 32;
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
// Set the DH private key for crypto operations
LOG_DEBUG("Set DH private key for crypto operations");
crypto->setDHPrivateKey(config.security.private_key.bytes);
// Conditionally create new identity based on parameter
createNewIdentity();
}
return keygenSuccess;
#else
return false;
#endif
}
bool NodeDB::createNewIdentity()
{
uint32_t oldNodeNum = getNodeNum();
uint32_t newNodeNum = crc32Buffer(config.security.public_key.bytes, config.security.public_key.size);
// If the key hasn't changed, nothing to do
if (newNodeNum == oldNodeNum)
return false;
// Retire the old node entry
meshtastic_NodeInfoLite *node = getMeshNode(oldNodeNum);
if (node != NULL) {
LOG_DEBUG("Old node num %u is now %u", oldNodeNum, newNodeNum);
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
node->public_key.size = 0;
memset(node->public_key.bytes, 0, sizeof(node->public_key.bytes));
}
// Drop satellite-store entries (position/telemetry/environment/status) keyed by the retired
// node number so stale data isn't left attached to the old identity.
eraseNodeSatellites(oldNodeNum);
myNodeInfo.my_node_num = newNodeNum;
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
TypeConversions::CopyUserToNodeInfoLite(info, owner);
return true;
}
bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location)
{
bool success = false;
+13 -1
View File
@@ -376,6 +376,12 @@ class NodeDB
bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest);
#endif
/// Consolidate crypto key generation logic used across multiple modules
/// @param privateKey Optional 32-byte private key to use. If nullptr, generates new random keys.
bool generateCryptoKeyPair(const uint8_t *privateKey = nullptr);
bool createNewIdentity();
bool backupPreferences(meshtastic_AdminMessage_BackupLocation location);
bool restorePreferences(meshtastic_AdminMessage_BackupLocation location,
int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS);
@@ -519,7 +525,9 @@ extern uint32_t error_address;
#define NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_IS_UNMESSAGABLE_SHIFT)
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT 8
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT)
// Bits 9..31 reserved for future single-bit flags.
#define NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_SHIFT 9
#define NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK (1u << NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_SHIFT)
// Bits 10..31 reserved for future single-bit flags.
// Convenience accessors so call sites read like the old struct fields.
inline bool nodeInfoLiteHasUser(const meshtastic_NodeInfoLite *n)
@@ -558,6 +566,10 @@ inline bool nodeInfoLiteIsKeyManuallyVerified(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK);
}
inline bool nodeInfoLiteHasXeddsaSigned(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK);
}
inline void nodeInfoLiteSetBit(meshtastic_NodeInfoLite *n, uint32_t mask, bool value)
{
+10 -1
View File
@@ -16,7 +16,16 @@ uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel)
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
{
return getPositionPrecisionForChannel(channels.getByIndex(channelIndex));
const meshtastic_Channel &ch = channels.getByIndex(channelIndex);
if (ch.role == meshtastic_Channel_Role_DISABLED)
return 0;
uint32_t precision = getPositionPrecisionForChannel(ch);
// Never send a precise position on a publicly-decryptable channel (key check is gated on > ceiling).
if (precision > MAX_POSITION_PRECISION_PUBLIC_KEY && channels.usesPublicKey(channelIndex)) {
precision = MAX_POSITION_PRECISION_PUBLIC_KEY;
}
return precision;
}
static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
+9
View File
@@ -4,7 +4,16 @@
#include "meshtastic/mesh.pb.h"
#include <stdint.h>
// Max precision on a publicly-decryptable channel. CCPA "precise geolocation" = within a ~564m (1,850ft) radius.
// Precision is bit-truncation of latitude_i/longitude_i: the latitude cell stays ~constant in meters worldwide
// (~700m at 15 bits), while only the longitude cell varies — widest at the equator, narrowing toward the poles.
// 15 also matches the MQTT map-report public precision ceiling.
#define MAX_POSITION_PRECISION_PUBLIC_KEY 15
// Configured precision as-is; does NOT apply the public-key clamp -- use the channelIndex overload for the on-wire value.
uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel);
// Configured precision, clamped to MAX_POSITION_PRECISION_PUBLIC_KEY when the channel's effective key is publicly decryptable.
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex);
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision);
bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision);
+3 -1
View File
@@ -63,6 +63,8 @@ const RegionProfile PROFILE_HAM_20KHZ = {PRESETS_TINY, 0, 0.0022f, false, true,
// Ham '100kHz' profile. 62.5kHz bandwidth coerced to 100kHz via padding.
const RegionProfile PROFILE_HAM_100KHZ = {PRESETS_NARROW, 0, 0.01875f, false, true, 0, 1, 1};
Observable<uint32_t> RadioInterface::loraRxPacketObservable;
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset, \
override_slot) \
{ \
@@ -1365,4 +1367,4 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
sendingPacket = p;
return p->encrypted.size + sizeof(PacketHeader);
}
}
+3
View File
@@ -128,6 +128,9 @@ class RadioInterface
virtual ~RadioInterface() {}
/// Fires once per valid received LoRa packet (arg = sender NodeNum). Used e.g. to flash LED_LORA.
static Observable<uint32_t> loraRxPacketObservable;
/**
* Coerce LoRa config fields (bandwidth/spread_factor) derived from presets.
* This is used during early bootstrapping so UIs that display these fields directly remain consistent.
+4
View File
@@ -614,6 +614,10 @@ void RadioLibInterface::handleReceiveInterrupt()
printPacket("Lora RX", mp);
#ifdef LED_LORA
loraRxPacketObservable.notifyObservers(mp->from);
#endif
airTime->logAirtime(RX_LOG, rxMsec);
deliverToReceiver(mp);
+44
View File
@@ -559,6 +559,38 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
if (p->decoded.has_bitfield)
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
if (p->decoded.xeddsa_signature.size == XEDDSA_SIGNATURE_SIZE) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->from);
if (node && node->public_key.size == 32) {
p->xeddsa_signed =
crypto->xeddsa_verify(node->public_key.bytes, p->from, p->id, p->decoded.portnum, p->decoded.payload.bytes,
p->decoded.payload.size, p->decoded.xeddsa_signature.bytes);
if (p->xeddsa_signed) {
// Mark this node as a signer so future unsigned packets from it are rejected
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, true);
LOG_DEBUG("Verified XEdDSA signature from 0x%08x", p->from);
} else {
LOG_WARN("XEdDSA signature verification failed from 0x%08x, dropping", p->from);
return DecodeState::DECODE_FAILURE;
}
} else {
LOG_DEBUG("No public key for 0x%08x, cannot verify XEdDSA signature", p->from);
}
} else {
// Unsigned packet — only reject the class of packet a signing node always signs:
// an unencrypted broadcast small enough to also carry a signature (see perhapsEncode()).
// Unicast packets and oversized broadcasts are never signed, so they must not be
// hard-failed here even if this node has signed before.
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->from);
if (node && nodeInfoLiteHasXeddsaSigned(node) && isBroadcast(p->to) &&
p->decoded.payload.size + XEDDSA_SIGNATURE_SIZE < meshtastic_Constants_DATA_PAYLOAD_LEN) {
LOG_WARN("Dropping unsigned broadcast from 0x%08x that previously signed", p->from);
return DecodeState::DECODE_FAILURE;
}
}
#endif
/* Not actually ever used.
// Decompress if needed. jm
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) {
@@ -629,6 +661,18 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
p->decoded.has_bitfield = true;
p->decoded.bitfield |= (config.lora.config_ok_to_mqtt << BITFIELD_OK_TO_MQTT_SHIFT);
p->decoded.bitfield |= (p->decoded.want_response << BITFIELD_WANT_RESPONSE_SHIFT);
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
// Sign broadcast packets if payload + signature fits within the max Data payload.
// The actual encoded size is checked after pb_encode (TOO_LARGE).
if (!p->pki_encrypted && isBroadcast(p->to) &&
p->decoded.payload.size + XEDDSA_SIGNATURE_SIZE < meshtastic_Constants_DATA_PAYLOAD_LEN) {
if (crypto->xeddsa_sign(p->from, p->id, p->decoded.portnum, p->decoded.payload.bytes, p->decoded.payload.size,
p->decoded.xeddsa_signature.bytes)) {
p->decoded.xeddsa_signature.size = XEDDSA_SIGNATURE_SIZE;
LOG_DEBUG("XEdDSA signed packet 0x%08x", p->id);
}
}
#endif
}
size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded);
+1
View File
@@ -18,6 +18,7 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo
info.is_ignored = nodeInfoLiteIsIgnored(lite);
info.is_key_manually_verified = nodeInfoLiteIsKeyManuallyVerified(lite);
info.is_muted = nodeInfoLiteIsMuted(lite);
info.has_xeddsa_signed = nodeInfoLiteHasXeddsaSigned(lite);
if (lite->has_hops_away) {
info.has_hops_away = true;
+9 -4
View File
@@ -234,6 +234,9 @@ typedef struct _meshtastic_HamParameters {
float frequency;
/* Optional short name of user */
char short_name[5];
/* Optional long name of user
Appended to callsign */
char long_name[15];
} meshtastic_HamParameters;
/* Response envelope for node_remote_hardware_pins */
@@ -544,7 +547,7 @@ extern "C" {
#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0}
#define meshtastic_AdminMessage_OTAEvent_init_default {_meshtastic_OTAMode_MIN, {0, {0}}}
#define meshtastic_LockdownAuth_init_default {{0, {0}}, 0, 0, 0, 0, 0}
#define meshtastic_HamParameters_init_default {"", 0, 0, ""}
#define meshtastic_HamParameters_init_default {"", 0, 0, "", ""}
#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0}
#define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
@@ -557,7 +560,7 @@ extern "C" {
#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0}
#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}}
#define meshtastic_LockdownAuth_init_zero {{0, {0}}, 0, 0, 0, 0, 0}
#define meshtastic_HamParameters_init_zero {"", 0, 0, ""}
#define meshtastic_HamParameters_init_zero {"", 0, 0, "", ""}
#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0}
#define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
@@ -584,6 +587,7 @@ extern "C" {
#define meshtastic_HamParameters_tx_power_tag 2
#define meshtastic_HamParameters_frequency_tag 3
#define meshtastic_HamParameters_short_name_tag 4
#define meshtastic_HamParameters_long_name_tag 5
#define meshtastic_NodeRemoteHardwarePinsResponse_node_remote_hardware_pins_tag 1
#define meshtastic_SharedContact_node_num_tag 1
#define meshtastic_SharedContact_user_tag 2
@@ -786,7 +790,8 @@ X(a, STATIC, SINGULAR, BOOL, disable, 6)
X(a, STATIC, SINGULAR, STRING, call_sign, 1) \
X(a, STATIC, SINGULAR, INT32, tx_power, 2) \
X(a, STATIC, SINGULAR, FLOAT, frequency, 3) \
X(a, STATIC, SINGULAR, STRING, short_name, 4)
X(a, STATIC, SINGULAR, STRING, short_name, 4) \
X(a, STATIC, SINGULAR, STRING, long_name, 5)
#define meshtastic_HamParameters_CALLBACK NULL
#define meshtastic_HamParameters_DEFAULT NULL
@@ -891,7 +896,7 @@ extern const pb_msgdesc_t meshtastic_SHTXX_config_msg;
#define meshtastic_AdminMessage_InputEvent_size 14
#define meshtastic_AdminMessage_OTAEvent_size 36
#define meshtastic_AdminMessage_size 511
#define meshtastic_HamParameters_size 31
#define meshtastic_HamParameters_size 47
#define meshtastic_KeyVerificationAdmin_size 25
#define meshtastic_LockdownAuth_size 56
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
+12 -6
View File
@@ -69,8 +69,8 @@ typedef PB_BYTES_ARRAY_T(32) meshtastic_NodeInfoLite_public_key_t;
typedef struct _meshtastic_NodeInfoLite {
/* The node number */
uint32_t num;
/* Returns the Signal-to-noise ratio (SNR) of the last received message,
as measured by the receiver. Return SNR of the last received message in dB */
/* In-memory SNR of the last received message in dB. Not serialised directly:
always zeroed before encode; persisted as snr_q4 = 19 below. */
float snr;
/* Set to indicate the last time we received a packet from this node */
uint32_t last_heard;
@@ -94,6 +94,10 @@ typedef struct _meshtastic_NodeInfoLite {
meshtastic_Config_DeviceConfig_Role role;
/* The public key of the user's device, for PKI-based encrypted DMs. */
meshtastic_NodeInfoLite_public_key_t public_key;
/* Q4-encoded SNR: dB × 4, sint32 zigzag. Matches RouteDiscovery convention.
Encode: snr_q4 = (int32_t)(snr * 4.0f). Decode: snr = snr_q4 / 4.0f.
float snr is always zeroed on disk; this field carries all persisted SNR. */
int32_t snr_q4;
} meshtastic_NodeInfoLite;
/* This message is never sent over the wire, but it is used for serializing DB
@@ -215,7 +219,7 @@ extern "C" {
/* Initializer values for message structs */
#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN, 0}
#define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
#define meshtastic_NodeInfoLite_init_default {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
#define meshtastic_NodeInfoLite_init_default {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, 0}
#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
#define meshtastic_NodePositionEntry_init_default {0, false, meshtastic_PositionLite_init_default}
#define meshtastic_NodeTelemetryEntry_init_default {0, false, meshtastic_DeviceMetrics_init_default}
@@ -226,7 +230,7 @@ extern "C" {
#define meshtastic_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default}
#define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN, 0}
#define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
#define meshtastic_NodeInfoLite_init_zero {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
#define meshtastic_NodeInfoLite_init_zero {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, 0}
#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
#define meshtastic_NodePositionEntry_init_zero {0, false, meshtastic_PositionLite_init_zero}
#define meshtastic_NodeTelemetryEntry_init_zero {0, false, meshtastic_DeviceMetrics_init_zero}
@@ -263,6 +267,7 @@ extern "C" {
#define meshtastic_NodeInfoLite_hw_model_tag 16
#define meshtastic_NodeInfoLite_role_tag 17
#define meshtastic_NodeInfoLite_public_key_tag 18
#define meshtastic_NodeInfoLite_snr_q4_tag 19
#define meshtastic_DeviceState_my_node_tag 2
#define meshtastic_DeviceState_owner_tag 3
#define meshtastic_DeviceState_receive_queue_tag 5
@@ -330,7 +335,8 @@ X(a, STATIC, SINGULAR, STRING, long_name, 14) \
X(a, STATIC, SINGULAR, STRING, short_name, 15) \
X(a, STATIC, SINGULAR, UENUM, hw_model, 16) \
X(a, STATIC, SINGULAR, UENUM, role, 17) \
X(a, STATIC, SINGULAR, BYTES, public_key, 18)
X(a, STATIC, SINGULAR, BYTES, public_key, 18) \
X(a, STATIC, SINGULAR, SINT32, snr_q4, 19)
#define meshtastic_NodeInfoLite_CALLBACK NULL
#define meshtastic_NodeInfoLite_DEFAULT NULL
@@ -450,7 +456,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
#define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1944
#define meshtastic_NodeEnvironmentEntry_size 170
#define meshtastic_NodeInfoLite_size 105
#define meshtastic_NodeInfoLite_size 112
#define meshtastic_NodePositionEntry_size 42
#define meshtastic_NodeStatusEntry_size 89
#define meshtastic_NodeTelemetryEntry_size 35
+8
View File
@@ -325,6 +325,14 @@ typedef enum _meshtastic_HardwareModel {
meshtastic_HardwareModel_T_IMPULSE_PLUS = 135,
/* Lilygo T-Echo Card */
meshtastic_HardwareModel_T_ECHO_CARD = 136,
/* Seeed Tracker L2 */
meshtastic_HardwareModel_SEEED_WIO_TRACKER_L2 = 137,
/* Elecrow CrowPanel Advance P4 models, ESP32-P4 and TFT with SX1262 radio plugin */
meshtastic_HardwareModel_CROWPANEL_P4 = 138,
/* Heltec Mesh Tower V2 */
meshtastic_HardwareModel_HELTEC_MESH_TOWER_V2 = 139,
/* Meshnology W10 */
meshtastic_HardwareModel_MESHNOLOGY_W10 = 140,
/* ------------------------------------------------------------------------------------------------------------------------------------------
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
------------------------------------------------------------------------------------------------------------------------------------------ */
+50 -21
View File
@@ -2,6 +2,7 @@
#include "Channels.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PositionPrecision.h"
#include "PowerFSM.h"
#include "RTC.h"
#include "SPILock.h"
@@ -19,6 +20,7 @@
#include "main.h"
#endif
#ifdef ARCH_PORTDUINO
#include "PortduinoGlue.h"
#include "unistd.h"
#endif
@@ -106,13 +108,29 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
return handled;
}
#ifdef ARCH_PORTDUINO
// Simulator only: honor exit_simulator unconditionally for the local client (from==0).
// The from==0 branch below now covers pki_encrypted local packets too, but is_managed
// can still block it. Rather than threading simulator awareness through the auth gates,
// intercept here before any auth logic runs. Local-origin + force_simradio only.
// TODO: should a local client bypass admin auth at all? Fenced to the simulator for now.
if (portduino_config.force_simradio && mp.from == 0 &&
r->which_payload_variant == meshtastic_AdminMessage_exit_simulator_tag) {
LOG_INFO("Exiting simulator");
exit(0);
}
#endif
meshtastic_Channel *ch = &channels.getByIndex(mp.channel);
// Could tighten this up further by tracking the last public_key we went an AdminMessage request to
// and only allowing responses from that remote.
if (messageIsResponse(r)) {
LOG_DEBUG("Allow admin response message");
} else if (mp.from == 0 && !mp.pki_encrypted) {
// Plain (non-PKC) local admin from BLE/USB client.
} else if (mp.from == 0) {
// Local admin from a BLE/USB/TCP client. from == 0 cannot arrive from the
// mesh: RF drops packets without a sender (RadioLibInterface) and MQTT treats
// from == 0 as our own downlink and ignores it. Clients may set pki_encrypted
// on self-addressed admin (the python CLI does), so don't use it to reroute
// local packets into the remote-PKC key check.
//
// Under MESHTASTIC_PHONEAPI_ACCESS_CONTROL, the per-connection auth
// gate lives in PhoneAPI::handleToRadioPacket — any local admin
@@ -542,7 +560,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
#if HAS_SCREEN
IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
#endif
#if defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)
#if defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32)
enterDfuMode();
#endif
break;
@@ -987,22 +1005,14 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
LOG_INFO("Set config: Security");
config.security = c.payload_variant.security;
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI)
// If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode
if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
if (config.security.private_key.size != 32) {
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
} else {
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
config.security.public_key.size = 32;
}
}
// Only regenerate keys if the private key is not 32 bytes
if (config.security.private_key.size != 32) {
nodeDB->generateCryptoKeyPair();
}
// If user provided a private key of correct size but no public key, generate the public key from private key
else if (config.security.private_key.size == 32 && config.security.public_key.size == 0) {
nodeDB->generateCryptoKeyPair(config.security.private_key.bytes);
}
#endif
owner.public_key.size = config.security.public_key.size;
memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size);
#if !MESHTASTIC_EXCLUDE_PKI
crypto->setDHPrivateKey(config.security.private_key.bytes);
#endif
if (config.security.is_managed && !(config.security.admin_key[0].size == 32 || config.security.admin_key[1].size == 32 ||
config.security.admin_key[2].size == 32)) {
@@ -1012,9 +1022,9 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
sendWarning(warning);
}
if (config.security.debug_log_api_enabled == c.payload_variant.security.debug_log_api_enabled &&
config.security.serial_enabled == c.payload_variant.security.serial_enabled)
requiresReboot = false;
changes = SEGMENT_CONFIG | SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE;
requiresReboot = true;
break;
case meshtastic_Config_device_ui_tag:
@@ -1148,7 +1158,26 @@ void AdminModule::handleSetChannel(const meshtastic_Channel &cc)
if (channels.ensureLicensedOperation()) {
sendWarning(licensedModeMessage);
}
// Refresh derived state (primaryIndex in particular) BEFORE the precision clamp below. usesPublicKey()
// resolves a secondary channel's key against the primary, so it must see the post-update primaryIndex;
// running the clamp first could evaluate secondaries against the previous primary and skip the clamp/warning.
channels.onConfigChanged(); // tell the radios about this change
// Persist the public-key precision clamp for all channels that may be affected (e.g. secondaries
// that inherit a now-public primary key) and warn the client once if anything was coarsened.
bool clamped = false;
for (uint8_t i = 0; i < channels.getNumChannels(); i++) {
meshtastic_Channel &ch = channels.getByIndex(i);
if (ch.role == meshtastic_Channel_Role_DISABLED || !ch.settings.has_module_settings)
continue;
uint32_t allowed = getPositionPrecisionForChannel(i);
if (allowed != ch.settings.module_settings.position_precision) {
ch.settings.module_settings.position_precision = allowed;
clamped = true;
}
}
if (clamped)
sendWarning(publicChannelPrecisionMessage);
saveChanges(SEGMENT_CHANNELS, false);
}
+3
View File
@@ -88,6 +88,9 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
static constexpr const char *licensedModeMessage =
"Licensed mode activated, removing admin channel and encryption from all channels";
static constexpr const char *publicChannelPrecisionMessage =
"Precise position is not allowed on a public (open / known-key) channel; reduced to coarse precision";
extern AdminModule *adminModule;
void disableBluetooth();
+6
View File
@@ -49,6 +49,12 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
return true;
}
NodeNum sourceNum = getFrom(&mp);
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(sourceNum);
if (node && nodeInfoLiteHasXeddsaSigned(node) && !mp.xeddsa_signed) {
LOG_WARN("Dropping unsigned NodeInfo from node 0x%08x that previously signed", sourceNum);
return true;
}
// Coerce user.id to be derived from the node number
snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp));
+30
View File
@@ -1,6 +1,7 @@
#include "StatusLEDModule.h"
#include "MeshService.h"
#include "configuration.h"
#include "mesh/RadioInterface.h"
#include <Arduino.h>
/*
@@ -17,6 +18,9 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule")
if (inputBroker)
inputObserver.observe(inputBroker);
#endif
#ifdef LED_LORA
loraRxObserver.observe(&RadioInterface::loraRxPacketObservable);
#endif
#ifdef NEOPIXEL_STATUS_POWER_PIN
powerPixel.begin();
powerPixel.clear();
@@ -90,6 +94,18 @@ int StatusLEDModule::handleInputEvent(const InputEvent *event)
return 0;
}
#endif
#ifdef LED_LORA
int StatusLEDModule::handleLoRaRx(uint32_t)
{
// Briefly flash LED_LORA on each received packet. Turn it on now (we share the main thread with
// the radio's receive handler, so this is safe) and wake runOnce() at flash end to turn it off.
digitalWrite(LED_LORA, LED_STATE_ON);
LORA_LED_state = LED_STATE_ON;
LORA_LED_starttime = millis();
setIntervalFromNow(LORA_RX_LED_FLASH_MS);
return 0;
}
#endif
int32_t StatusLEDModule::runOnce()
{
@@ -227,6 +243,20 @@ int32_t StatusLEDModule::runOnce()
digitalWrite(Battery_LED_4, chargeIndicatorLED4);
#endif
#ifdef LED_LORA
// End the LoRa-RX flash once its duration has elapsed; otherwise make sure we come back
// exactly at flash end (only ever clamp my_interval down, so other LED timing is preserved).
if (LORA_LED_state == LED_STATE_ON) {
uint32_t elapsed = millis() - LORA_LED_starttime;
if (elapsed >= LORA_RX_LED_FLASH_MS) {
digitalWrite(LED_LORA, LED_STATE_OFF);
LORA_LED_state = LED_STATE_OFF;
} else if ((uint32_t)my_interval > LORA_RX_LED_FLASH_MS - elapsed) {
my_interval = LORA_RX_LED_FLASH_MS - elapsed;
}
}
#endif
return (my_interval);
}
+12
View File
@@ -43,6 +43,9 @@ class StatusLEDModule : private concurrency::OSThread
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
int handleInputEvent(const InputEvent *arg);
#endif
#ifdef LED_LORA
int handleLoRaRx(uint32_t sender);
#endif
void setPowerLED(bool);
@@ -65,6 +68,10 @@ class StatusLEDModule : private concurrency::OSThread
CallbackObserver<StatusLEDModule, const InputEvent *> inputObserver =
CallbackObserver<StatusLEDModule, const InputEvent *>(this, &StatusLEDModule::handleInputEvent);
#endif
#ifdef LED_LORA
CallbackObserver<StatusLEDModule, uint32_t> loraRxObserver =
CallbackObserver<StatusLEDModule, uint32_t>(this, &StatusLEDModule::handleLoRaRx);
#endif
private:
bool CHARGE_LED_state = LED_STATE_OFF;
@@ -77,6 +84,11 @@ class StatusLEDModule : private concurrency::OSThread
uint32_t lastUserbuttonTime = 0;
uint32_t POWER_LED_starttime = 0;
bool doing_fast_blink = false;
#ifdef LED_LORA
static constexpr uint32_t LORA_RX_LED_FLASH_MS = 100;
bool LORA_LED_state = LED_STATE_OFF;
uint32_t LORA_LED_starttime = 0;
#endif
enum PowerState { discharging, charging, charged, critical };
@@ -428,20 +428,6 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
notification->level = meshtastic_LogRecord_Level_INFO;
notification->time = getValidTime(RTCQualityFromNet);
sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment",
Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs) /
1000U);
service->sendClientNotification(notification);
sleepOnNextExecution = true;
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
}
return true;
}
+13 -1
View File
@@ -15,8 +15,8 @@
#endif
#include "BMM150Sensor.h"
#include "BMX160Sensor.h"
#include "ICM42607PSensor.h"
#include "ICM20948Sensor.h"
#include "ICM42607PSensor.h"
#include "LIS3DHSensor.h"
#include "LSM6DS3Sensor.h"
#include "MPU6050Sensor.h"
@@ -92,32 +92,44 @@ class AccelerometerThread : public concurrency::OSThread
sensor = new BMA423Sensor(device);
break;
#endif
#if __has_include(<Adafruit_MPU6050.h>)
case ScanI2C::DeviceType::MPU6050:
sensor = new MPU6050Sensor(device);
break;
#endif
case ScanI2C::DeviceType::BMX160:
sensor = new BMX160Sensor(device);
break;
#if __has_include(<Adafruit_LIS3DH.h>)
case ScanI2C::DeviceType::LIS3DH:
sensor = new LIS3DHSensor(device);
break;
#endif
#if __has_include(<Adafruit_LSM6DS3TRC.h>)
case ScanI2C::DeviceType::LSM6DS3:
sensor = new LSM6DS3Sensor(device);
break;
#endif
#ifdef HAS_STK8XXX
case ScanI2C::DeviceType::STK8BAXX:
sensor = new STK8XXXSensor(device);
break;
#endif
#if __has_include(<ICM_20948.h>)
case ScanI2C::DeviceType::ICM20948:
sensor = new ICM20948Sensor(device);
break;
#endif
#if __has_include(<ICM42670P.h>)
case ScanI2C::DeviceType::ICM42607P:
sensor = new ICM42607PSensor(device);
break;
#endif
#if __has_include(<DFRobot_BMM150.h>)
case ScanI2C::DeviceType::BMM150:
sensor = new BMM150Sensor(device);
break;
#endif
#ifdef HAS_BMI270
case ScanI2C::DeviceType::BMI270:
sensor = new BMI270Sensor(device);
+3 -3
View File
@@ -53,11 +53,11 @@ int32_t BMX160Sensor::runOnce()
// If we're set to one of the inverted positions
if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) {
ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ);
ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ);
ma = FusionRemap(ma, FusionRemapAlignmentNXNYPZ);
ga = FusionRemap(ga, FusionRemapAlignmentNXNYPZ);
}
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
float heading = FusionCompass(ga, ma, FusionConventionNed);
heading = applyCompassOrientation(heading);
if (screen)
+3 -3
View File
@@ -93,11 +93,11 @@ int32_t ICM20948Sensor::runOnce()
// If we're set to one of the inverted positions
if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) {
ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ);
ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ);
ma = FusionRemap(ma, FusionRemapAlignmentNXNYPZ);
ga = FusionRemap(ga, FusionRemapAlignmentNXNYPZ);
}
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
float heading = FusionCompass(ga, ma, FusionConventionNed);
heading = applyCompassOrientation(heading);
if (screen)
+25 -7
View File
@@ -4,10 +4,16 @@
#include "detect/ScanI2CTwoWire.h"
#include <ICM42670P.h>
#include <math.h>
static constexpr uint16_t ICM42607P_ACCEL_ODR_HZ = 50;
static constexpr uint16_t ICM42607P_ACCEL_FSR_G = 2;
static constexpr float ICM42607P_COUNTS_PER_G = 32768.0f / ICM42607P_ACCEL_FSR_G;
static constexpr float ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG_VALUE =
#ifdef ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG
ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG;
#else
0.0f;
#endif
#ifdef ICM_42607P_INT_PIN
volatile static bool ICM42607P_IRQ = false;
@@ -18,10 +24,7 @@ void ICM42607PSetInterrupt()
}
#endif
ICM42607PSensor::ICM42607PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice)
{
wire = ScanI2CTwoWire::fetchI2CBus(foundDevice.address);
}
ICM42607PSensor::ICM42607PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
ICM42607PSensor::~ICM42607PSensor() = default;
@@ -30,6 +33,7 @@ bool ICM42607PSensor::init()
bool addressLsb = deviceAddress() == ICM42607P_ADDR_ALT;
LOG_DEBUG("ICM-42607-P begin on addr 0x%02X (port=%d)", deviceAddress(), devicePort());
TwoWire *wire = ScanI2CTwoWire::fetchI2CBus(device.address);
sensor.reset();
auto newSensor = std::make_unique<ICM42670>(*wire, addressLsb);
@@ -82,8 +86,22 @@ int32_t ICM42607PSensor::runOnce()
return MOTION_SENSOR_CHECK_INTERVAL_MS;
}
// LOG_DEBUG("ICM-42607-P accel read x=%.3fg y=%.3fg z=%.3fg", (float)event.accel[0] / ICM42607P_COUNTS_PER_G,
// (float)event.accel[1] / ICM42607P_COUNTS_PER_G, (float)event.accel[2] / ICM42607P_COUNTS_PER_G);
float ax = static_cast<float>(event.accel[0]);
float ay = static_cast<float>(event.accel[1]);
const float az = static_cast<float>(event.accel[2]);
if (ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG_VALUE != 0.0f) {
static const float rotRad = ICM42607P_ACCEL_TO_COMPASS_ROTATION_DEG_VALUE * DEG_TO_RAD;
static const float cosTheta = cosf(rotRad);
static const float sinTheta = sinf(rotRad);
const float rotatedX = (ax * cosTheta) - (ay * sinTheta);
const float rotatedY = (ax * sinTheta) + (ay * cosTheta);
ax = rotatedX;
ay = rotatedY;
}
// Match the accel sign convention used by other FusionCompass sensor paths.
publishCompassAccelSample(ax, -ay, -az);
return MOTION_SENSOR_CHECK_INTERVAL_MS;
#endif
-1
View File
@@ -14,7 +14,6 @@ class ICM42607PSensor : public MotionSensor
{
private:
std::unique_ptr<ICM42670> sensor;
TwoWire *wire = nullptr;
public:
explicit ICM42607PSensor(ScanI2C::FoundDevice foundDevice);
+63 -31
View File
@@ -2,6 +2,7 @@
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<SparkFun_MMC5983MA_Arduino_Library.h>)
#include "Fusion/Fusion.h"
#include "detect/ScanI2CTwoWire.h"
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
@@ -10,8 +11,11 @@ extern graphics::Screen *screen;
static constexpr float MMC5983MA_ZERO_FIELD = 131072.0f;
static constexpr float MMC5983MA_COUNTS_PER_GAUSS = 16384.0f;
static constexpr uint16_t MMC5983MA_CONTINUOUS_FREQUENCY_HZ = 10;
static constexpr uint16_t MMC5983MA_CONTINUOUS_FREQUENCY_HZ = 50;
static constexpr int32_t MMC5983MA_UPDATE_INTERVAL_MS = 20;
static constexpr float MMC5983MA_HEADING_OFFSET_DEG = 180.0f;
static constexpr uint32_t MMC5983MA_ACCEL_STALE_MS = 300;
static constexpr float MMC5983MA_MIN_AXIS_RADIUS = 1e-4f;
MMC5983MASensor::MMC5983MASensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
@@ -59,40 +63,68 @@ bool MMC5983MASensor::readMagnetometer(float &xGauss, float &yGauss, float &zGau
}
int32_t MMC5983MASensor::runOnce()
{
float magX = 0, magY = 0, magZ = 0;
if (!readMagnetometer(magX, magY, magZ)) {
return MOTION_SENSOR_CHECK_INTERVAL_MS;
{
float magX = 0, magY = 0, magZ = 0;
if (!readMagnetometer(magX, magY, magZ)) {
return MMC5983MA_UPDATE_INTERVAL_MS;
}
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
if (doCalibration) {
beginCalibrationDisplay(showingScreen);
updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
lowestZ);
}
#endif
// Hard-iron bias removal.
magX -= (highestX + lowestX) * 0.5f;
magY -= (highestY + lowestY) * 0.5f;
magZ -= (highestZ + lowestZ) * 0.5f;
// Soft-iron diagonal scaling from calibration extrema.
const float radiusX = (highestX - lowestX) * 0.5f;
const float radiusY = (highestY - lowestY) * 0.5f;
const float radiusZ = (highestZ - lowestZ) * 0.5f;
const float avgRadius = (radiusX + radiusY + radiusZ) / 3.0f;
magX *= (radiusX > MMC5983MA_MIN_AXIS_RADIUS) ? (avgRadius / radiusX) : 1.0f;
magY *= (radiusY > MMC5983MA_MIN_AXIS_RADIUS) ? (avgRadius / radiusY) : 1.0f;
magZ *= (radiusZ > MMC5983MA_MIN_AXIS_RADIUS) ? (avgRadius / radiusZ) : 1.0f;
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
float heading;
float accelX = 0.0f;
float accelY = 0.0f;
float accelZ = 0.0f;
uint32_t accelAgeMs = 0;
if (getLatestCompassAccelSample(accelX, accelY, accelZ, accelAgeMs) && accelAgeMs <= MMC5983MA_ACCEL_STALE_MS) {
FusionVector ga = {.axis = {accelX, accelY, accelZ}};
FusionVector ma = {.axis = {magX, magY, magZ}};
if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) {
ma = FusionRemap(ma, FusionRemapAlignmentNXNYPZ);
ga = FusionRemap(ga, FusionRemapAlignmentNXNYPZ);
}
heading = FusionCompass(ga, ma, FusionConventionNed) + MMC5983MA_HEADING_OFFSET_DEG;
} else {
heading = atan2f(magY, magX) * RAD_TO_DEG + MMC5983MA_HEADING_OFFSET_DEG;
}
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
if (doCalibration) {
beginCalibrationDisplay(showingScreen);
updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
lowestZ);
}
#endif
if (heading >= 360.0f)
heading -= 360.0f;
else if (heading < 0.0f)
heading += 360.0f;
heading = 360.0f - heading;
if (heading >= 360.0f)
heading -= 360.0f;
magX -= (highestX + lowestX) / 2;
magY -= (highestY + lowestY) / 2;
magZ -= (highestZ + lowestZ) / 2;
heading = applyCompassOrientation(heading);
if (screen)
screen->setHeading(heading);
#endif
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
float heading = atan2f(magY, magX) * RAD_TO_DEG + MMC5983MA_HEADING_OFFSET_DEG;
if (heading < 0.0f) {
heading += 360.0f;
} else if (heading >= 360.0f) {
heading -= 360.0f;
}
heading = applyCompassOrientation(heading);
if (screen) {
screen->setHeading(heading);
}
#endif
return MOTION_SENSOR_CHECK_INTERVAL_MS;
return MMC5983MA_UPDATE_INTERVAL_MS;
}
void MMC5983MASensor::calibrate(uint16_t forSeconds)
+1 -2
View File
@@ -47,9 +47,8 @@ class MagnetometerThread : public concurrency::OSThread
{
canSleep = true;
if (isInitialised) {
if (isInitialised)
return sensor->runOnce();
}
return MOTION_SENSOR_CHECK_INTERVAL_MS;
}
+41
View File
@@ -2,6 +2,7 @@
#include "FSCommon.h"
#include "SPILock.h"
#include "SafeFile.h"
#include "concurrency/LockGuard.h"
#include "graphics/draw/CompassRenderer.h"
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
@@ -30,6 +31,17 @@ bool isRangeValid(float highest, float lowest)
// NaN/Inf guard without pulling in extra math helpers.
return (highest == highest) && (lowest == lowest) && (highest > lowest);
}
struct CompassAccelSample {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
uint32_t sampledAtMs = 0;
bool valid = false;
};
concurrency::Lock latestCompassAccelLock;
CompassAccelSample latestCompassAccelSample;
} // namespace
// screen is defined in main.cpp
@@ -204,6 +216,35 @@ float MotionSensor::applyCompassOrientation(float heading)
}
}
void MotionSensor::publishCompassAccelSample(float x, float y, float z)
{
concurrency::LockGuard guard(&latestCompassAccelLock);
latestCompassAccelSample.x = x;
latestCompassAccelSample.y = y;
latestCompassAccelSample.z = z;
latestCompassAccelSample.sampledAtMs = millis();
latestCompassAccelSample.valid = true;
}
bool MotionSensor::getLatestCompassAccelSample(float &x, float &y, float &z, uint32_t &ageMs)
{
uint32_t sampledAtMs = 0;
{
concurrency::LockGuard guard(&latestCompassAccelLock);
if (!latestCompassAccelSample.valid) {
return false;
}
x = latestCompassAccelSample.x;
y = latestCompassAccelSample.y;
z = latestCompassAccelSample.z;
sampledAtMs = latestCompassAccelSample.sampledAtMs;
}
ageMs = millis() - sampledAtMs;
return true;
}
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
+2
View File
@@ -67,6 +67,8 @@ class MotionSensor
static void updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
float &lowestY, float &highestZ, float &lowestZ);
static float applyCompassOrientation(float heading);
static void publishCompassAccelSample(float x, float y, float z);
static bool getLatestCompassAccelSample(float &x, float &y, float &z, uint32_t &ageMs);
ScanI2C::FoundDevice device;
+2
View File
@@ -71,9 +71,11 @@ void onConnect(uint16_t conn_handle)
// the (single, reused) bluetoothPhoneAPI instance, so a prior session's
// authorization can otherwise survive a quick reconnect. handleStartConfig()
// re-locks on every want_config too; this closes the window before that.
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (bluetoothPhoneAPI) {
bluetoothPhoneAPI->setAdminAuthorized(false);
}
#endif
// Notify UI (or any other interested firmware components)
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
+1
View File
@@ -1,6 +1,7 @@
#pragma once
#define ARCH_STM32WL
#define ARCH_STM32
//
// defaults for STM32WL architecture
+1 -1
View File
@@ -17,7 +17,7 @@
// Device specific curves go in variant.h
#ifndef OCV_ARRAY
#if defined(ARCH_STM32WL) && BATTERY_PIN == AVBAT
#if defined(ARCH_STM32) && BATTERY_PIN == AVBAT
// STM32 VDD/VBAT absolute maximum is 4V so use an LFP curve
#define OCV_ARRAY 3650, 3400, 3340, 3320, 3300, 3280, 3270, 3260, 3240, 3200, 2500
#else
+135
View File
@@ -2,6 +2,7 @@
#include "CryptoEngine.h"
#include "TestUtil.h"
#include <XEdDSA.h>
#include <unity.h>
void HexToBytes(uint8_t *result, const std::string hex, size_t len = 0)
@@ -152,6 +153,134 @@ void test_PKC(void)
TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10);
}
void test_XEdDSA(void)
{
uint8_t private_key[32];
uint8_t x_public_key[32];
uint8_t ed_private_key[32];
uint8_t ed_public_key[32];
uint8_t ed_public_key2[32];
uint8_t message[] = "This is a test!";
uint8_t message2[] = "This is a test.";
uint8_t signature[64];
uint32_t fromNode = 0x1234;
uint32_t packetId = 0xDEADBEEF;
uint32_t portnum = 1;
for (int times = 0; times < 10; times++) {
printf("Start of time %u\n", times);
crypto->generateKeyPair(x_public_key, private_key);
XEdDSA::priv_curve_to_ed_keys(private_key, ed_private_key, ed_public_key);
crypto->curve_to_ed_pub(x_public_key, ed_public_key2);
TEST_ASSERT_EQUAL_MEMORY(ed_public_key, ed_public_key2, 32);
// Sign and verify with metadata
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), signature));
TEST_ASSERT(crypto->xeddsa_verify(x_public_key, fromNode, packetId, portnum, message, sizeof(message), signature));
// Different payload fails
TEST_ASSERT_FALSE(
crypto->xeddsa_verify(x_public_key, fromNode, packetId, portnum, message2, sizeof(message2), signature));
// Different fromNode fails
TEST_ASSERT_FALSE(
crypto->xeddsa_verify(x_public_key, fromNode + 1, packetId, portnum, message, sizeof(message), signature));
// Different packetId fails
TEST_ASSERT_FALSE(
crypto->xeddsa_verify(x_public_key, fromNode, packetId + 1, portnum, message, sizeof(message), signature));
// Different portnum fails
TEST_ASSERT_FALSE(
crypto->xeddsa_verify(x_public_key, fromNode, packetId, portnum + 1, message, sizeof(message), signature));
}
}
// A signature only verifies under the signer's own key; a different key (or an all-zero key) fails.
void test_XEdDSA_cross_key_reject(void)
{
uint8_t pubA[32], privA[32];
uint8_t pubB[32], privB[32];
uint8_t signature[64];
uint8_t message[] = "cross-key check";
uint32_t fromNode = 0x4242, packetId = 0xABCD1234, portnum = 7;
crypto->generateKeyPair(pubA, privA); // engine now holds key A
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), signature));
crypto->generateKeyPair(pubB, privB); // unrelated key pair
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pubA, fromNode, packetId, portnum, message, sizeof(message), signature));
TEST_ASSERT_FALSE(crypto->xeddsa_verify(pubB, fromNode, packetId, portnum, message, sizeof(message), signature));
uint8_t zeroKey[32] = {0};
TEST_ASSERT_FALSE(crypto->xeddsa_verify(zeroKey, fromNode, packetId, portnum, message, sizeof(message), signature));
}
// Signing with an unset (all-zero) private key must fail rather than emit a bogus signature.
void test_XEdDSA_empty_key_sign_fails(void)
{
CryptoEngine fresh; // freshly constructed: xeddsa_private_key is all zero
uint8_t signature[64];
uint8_t message[] = "no key";
TEST_ASSERT_FALSE(fresh.xeddsa_sign(0x1, 0x2, 0x3, message, sizeof(message), signature));
}
// curve_to_ed_pub caches the last converted key; verifying A, then B, then A must stay correct.
void test_XEdDSA_curve_to_ed_cache(void)
{
uint8_t pubA[32], privA[32], sigA[64];
uint8_t pubB[32], privB[32], sigB[64];
uint8_t message[] = "cache check";
uint32_t fromNode = 0x11, packetId = 0x22, portnum = 3;
crypto->generateKeyPair(pubA, privA);
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), sigA));
crypto->generateKeyPair(pubB, privB);
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), sigB));
// Interleave keys to exercise both cache hits and cache invalidation.
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pubA, fromNode, packetId, portnum, message, sizeof(message), sigA));
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pubB, fromNode, packetId, portnum, message, sizeof(message), sigB));
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pubA, fromNode, packetId, portnum, message, sizeof(message), sigA));
TEST_ASSERT_FALSE(crypto->xeddsa_verify(pubA, fromNode, packetId, portnum, message, sizeof(message), sigB));
}
// A payload at the maximum signable size (DATA_PAYLOAD_LEN - signature) round-trips and detects tampering.
void test_XEdDSA_max_payload(void)
{
const size_t len = meshtastic_Constants_DATA_PAYLOAD_LEN - XEDDSA_SIGNATURE_SIZE;
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN];
for (size_t i = 0; i < len; i++)
payload[i] = (uint8_t)(i * 7 + 1);
uint8_t pub[32], priv[32], signature[64];
crypto->generateKeyPair(pub, priv);
uint32_t fromNode = 0xFEED, packetId = 0xC0DE, portnum = 1;
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, payload, len, signature));
TEST_ASSERT(crypto->xeddsa_verify(pub, fromNode, packetId, portnum, payload, len, signature));
payload[0] ^= 0x01;
TEST_ASSERT_FALSE(crypto->xeddsa_verify(pub, fromNode, packetId, portnum, payload, len, signature));
}
// Signing the same message twice yields signatures that both verify. This XEdDSA implementation is
// deterministic in practice (the two signatures are typically byte-identical, even though
// HardwareRNG::fill provides real entropy on this platform), so we assert only the security-relevant
// property — every produced signature verifies — rather than asserting (non-)determinism.
void test_XEdDSA_repeated_sign_verifies(void)
{
uint8_t pub[32], priv[32], sig1[64], sig2[64];
uint8_t message[] = "same message";
uint32_t fromNode = 0x9, packetId = 0x9, portnum = 9;
crypto->generateKeyPair(pub, priv);
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), sig1));
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), sig2));
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pub, fromNode, packetId, portnum, message, sizeof(message), sig1));
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pub, fromNode, packetId, portnum, message, sizeof(message), sig2));
}
void test_AES_CTR(void)
{
uint8_t expected[32];
@@ -192,6 +321,12 @@ void setup()
RUN_TEST(test_DH25519);
RUN_TEST(test_AES_CTR);
RUN_TEST(test_PKC);
RUN_TEST(test_XEdDSA);
RUN_TEST(test_XEdDSA_cross_key_reject);
RUN_TEST(test_XEdDSA_empty_key_sign_fails);
RUN_TEST(test_XEdDSA_curve_to_ed_cache);
RUN_TEST(test_XEdDSA_max_payload);
RUN_TEST(test_XEdDSA_repeated_sign_verifies);
exit(UNITY_END()); // stop unit testing
}
+392
View File
@@ -0,0 +1,392 @@
// Tests for XEdDSA packet-signing *policy* — the receive-path accept/reject behavior and the
// send-path signing policy — as opposed to the raw sign/verify primitive (covered in test_crypto).
//
// The decision logic under test lives inside perhapsDecode()/perhapsEncode() (free functions in
// Router.cpp). It only runs after a packet is decrypted, so every case drives a real
// encode -> decode round-trip through the default channel (black-box, no production changes).
//
// Group A receive-side accept/reject matrix (verify, downgrade protection, signer-bit learning)
// Group B send-side signing policy (which outgoing packets perhapsEncode signs)
// Group C NodeInfoModule's stricter "drop unsigned NodeInfo from a known signer" rule
#include "MeshTypes.h" // include BEFORE TestUtil.h
#include "TestUtil.h"
#include <unity.h>
#if !(MESHTASTIC_EXCLUDE_PKI)
#include "mesh/Channels.h"
#include "mesh/CryptoEngine.h"
#include "mesh/NodeDB.h"
#include "mesh/Router.h"
#include "modules/NodeInfoModule.h"
#include <cstdio>
#include <cstring>
#include <memory>
#include <vector>
// ---------------------------------------------------------------------------
// Test fixture identifiers
// ---------------------------------------------------------------------------
static constexpr NodeNum LOCAL_NODE = 0x0A0A0A0A;
static constexpr NodeNum REMOTE_NODE = 0x0B0B0B0B;
// A "small" broadcast payload that leaves room for a 64-byte signature (payload + 64 < 233),
// and an "oversized" one that does not (payload + 64 >= 233) yet still encodes within a LoRa frame.
static constexpr size_t SMALL_PAYLOAD = 16;
static constexpr size_t OVERSIZED_PAYLOAD = 180;
// ---------------------------------------------------------------------------
// MockNodeDB — inject nodes with controlled public keys / signer bits.
// Mirrors the pattern in test/test_hop_scaling. meshNodes/numMeshNodes are public on NodeDB.
// ---------------------------------------------------------------------------
class MockNodeDB : public NodeDB
{
public:
void clearTestNodes()
{
testNodes.clear();
meshNodes = &testNodes;
numMeshNodes = 0;
}
// Add a bare node and return a stable handle (fetch via getMeshNode so the pointer stays valid
// even if the vector reallocates after later adds).
void addNode(NodeNum num)
{
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
node.num = num;
testNodes.push_back(node);
meshNodes = &testNodes;
numMeshNodes = testNodes.size();
}
void setPublicKey(NodeNum num, const uint8_t *pubKey)
{
meshtastic_NodeInfoLite *n = getMeshNode(num);
TEST_ASSERT_NOT_NULL(n);
n->public_key.size = 32;
memcpy(n->public_key.bytes, pubKey, 32);
}
void setSignerBit(NodeNum num, bool value)
{
meshtastic_NodeInfoLite *n = getMeshNode(num);
TEST_ASSERT_NOT_NULL(n);
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, value);
}
std::vector<meshtastic_NodeInfoLite> testNodes;
};
static MockNodeDB *mockNodeDB = nullptr;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
// Build a decoded packet with a deterministic payload of the requested size.
static meshtastic_MeshPacket makeDecoded(NodeNum from, NodeNum to, meshtastic_PortNum port, size_t payloadLen)
{
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
p.from = from;
p.to = to;
p.id = 0x12345678;
p.channel = 0; // primary channel index (perhapsEncode rewrites this to the channel hash)
p.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
p.decoded.portnum = port;
p.decoded.payload.size = payloadLen;
for (size_t i = 0; i < payloadLen; i++)
p.decoded.payload.bytes[i] = (uint8_t)(i & 0xff);
return p;
}
// Sign a decoded packet with the CryptoEngine's current key — used to simulate a *remote* signer,
// because perhapsEncode only auto-signs packets that originate from us.
static void signWithCurrentKey(meshtastic_MeshPacket *p)
{
bool ok = crypto->xeddsa_sign(p->from, p->id, p->decoded.portnum, p->decoded.payload.bytes, p->decoded.payload.size,
p->decoded.xeddsa_signature.bytes);
TEST_ASSERT_TRUE_MESSAGE(ok, "xeddsa_sign failed in test setup");
p->decoded.xeddsa_signature.size = XEDDSA_SIGNATURE_SIZE;
}
// Encrypt (perhapsEncode) then decrypt+evaluate (perhapsDecode) the same packet in place.
static DecodeState roundTrip(meshtastic_MeshPacket *p)
{
meshtastic_Routing_Error enc = perhapsEncode(p);
TEST_ASSERT_EQUAL_MESSAGE(meshtastic_Routing_Error_NONE, enc, "perhapsEncode did not succeed");
TEST_ASSERT_EQUAL_MESSAGE(meshtastic_MeshPacket_encrypted_tag, p->which_payload_variant,
"perhapsEncode left packet unencrypted");
return perhapsDecode(p);
}
static bool remoteSignerBit()
{
return nodeInfoLiteHasXeddsaSigned(mockNodeDB->getMeshNode(REMOTE_NODE));
}
// ---------------------------------------------------------------------------
// Unity lifecycle
// ---------------------------------------------------------------------------
void setUp(void)
{
// Clean global config/owner; zeroed config => rebroadcast ALL (no KNOWN_ONLY drop) and
// security.private_key.size == 0 (PKI encrypt path skipped => simple channel crypto).
config = meshtastic_LocalConfig_init_zero;
owner = meshtastic_User_init_zero;
mockNodeDB = new MockNodeDB();
mockNodeDB->clearTestNodes();
nodeDB = mockNodeDB;
myNodeInfo.my_node_num = LOCAL_NODE; // drives isFromUs()/getFrom()/isToUs()
// Working primary channel with the default PSK so encrypt/decrypt round-trips.
channels.initDefaults();
channels.onConfigChanged();
}
void tearDown(void)
{
delete mockNodeDB;
mockNodeDB = nullptr;
nodeDB = nullptr;
}
// ===========================================================================
// Group A — receive-side accept/reject matrix
// ===========================================================================
// A1: valid signature from a node whose key we know -> accepted, marked signed, signer bit learned.
void test_A1_valid_signature_accepted_and_learns_signer(void)
{
uint8_t pub[32], priv[32];
crypto->generateKeyPair(pub, priv); // engine now holds REMOTE's key
mockNodeDB->addNode(REMOTE_NODE);
mockNodeDB->setPublicKey(REMOTE_NODE, pub);
TEST_ASSERT_FALSE(remoteSignerBit()); // not known as a signer yet
meshtastic_MeshPacket p = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, meshtastic_PortNum_TEXT_MESSAGE_APP, SMALL_PAYLOAD);
signWithCurrentKey(&p);
TEST_ASSERT_EQUAL(DECODE_SUCCESS, roundTrip(&p));
TEST_ASSERT_TRUE(p.xeddsa_signed);
TEST_ASSERT_TRUE_MESSAGE(remoteSignerBit(), "verified signature must set the signer bit");
}
// A2: a tampered signature from a known key -> dropped.
void test_A2_bad_signature_dropped(void)
{
uint8_t pub[32], priv[32];
crypto->generateKeyPair(pub, priv);
mockNodeDB->addNode(REMOTE_NODE);
mockNodeDB->setPublicKey(REMOTE_NODE, pub);
meshtastic_MeshPacket p = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, meshtastic_PortNum_TEXT_MESSAGE_APP, SMALL_PAYLOAD);
signWithCurrentKey(&p);
p.decoded.xeddsa_signature.bytes[0] ^= 0xFF; // corrupt the signature
TEST_ASSERT_EQUAL(DECODE_FAILURE, roundTrip(&p));
}
// A3: signed packet but we have no key for the sender -> accepted unverified, signer bit NOT set.
void test_A3_signed_no_pubkey_accepted_unverified(void)
{
uint8_t pub[32], priv[32];
crypto->generateKeyPair(pub, priv);
mockNodeDB->addNode(REMOTE_NODE); // node exists, but no public key stored
meshtastic_MeshPacket p = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, meshtastic_PortNum_TEXT_MESSAGE_APP, SMALL_PAYLOAD);
signWithCurrentKey(&p);
TEST_ASSERT_EQUAL(DECODE_SUCCESS, roundTrip(&p));
TEST_ASSERT_FALSE_MESSAGE(p.xeddsa_signed, "cannot be marked verified without a key");
TEST_ASSERT_FALSE_MESSAGE(remoteSignerBit(), "must not learn signer without verifying");
}
// A4: downgrade protection — unsigned small broadcast from a known signer -> dropped.
void test_A4_downgrade_unsigned_broadcast_from_signer_dropped(void)
{
mockNodeDB->addNode(REMOTE_NODE);
mockNodeDB->setSignerBit(REMOTE_NODE, true); // we've seen this node sign before
meshtastic_MeshPacket p = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, meshtastic_PortNum_TEXT_MESSAGE_APP, SMALL_PAYLOAD);
// from != us, so perhapsEncode leaves it unsigned.
TEST_ASSERT_EQUAL(DECODE_FAILURE, roundTrip(&p));
}
// A5: no prior knowledge — unsigned small broadcast from a non-signer -> accepted.
void test_A5_unsigned_broadcast_from_nonsigner_accepted(void)
{
mockNodeDB->addNode(REMOTE_NODE); // signer bit clear
meshtastic_MeshPacket p = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, meshtastic_PortNum_TEXT_MESSAGE_APP, SMALL_PAYLOAD);
TEST_ASSERT_EQUAL(DECODE_SUCCESS, roundTrip(&p));
TEST_ASSERT_FALSE(p.xeddsa_signed);
}
// A6: unsigned UNICAST from a known signer -> accepted (unicasts are never signed).
void test_A6_unsigned_unicast_from_signer_accepted(void)
{
mockNodeDB->addNode(REMOTE_NODE);
mockNodeDB->setSignerBit(REMOTE_NODE, true);
// Unicast to us; PRIVATE_APP avoids the unrelated legacy-DM rejection for TEXT_MESSAGE_APP.
meshtastic_MeshPacket p = makeDecoded(REMOTE_NODE, LOCAL_NODE, meshtastic_PortNum_PRIVATE_APP, SMALL_PAYLOAD);
TEST_ASSERT_EQUAL(DECODE_SUCCESS, roundTrip(&p));
}
// A7: unsigned OVERSIZED broadcast from a known signer -> accepted (couldn't have carried a sig).
void test_A7_unsigned_oversized_broadcast_from_signer_accepted(void)
{
mockNodeDB->addNode(REMOTE_NODE);
mockNodeDB->setSignerBit(REMOTE_NODE, true);
meshtastic_MeshPacket p = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, meshtastic_PortNum_TEXT_MESSAGE_APP, OVERSIZED_PAYLOAD);
TEST_ASSERT_EQUAL(DECODE_SUCCESS, roundTrip(&p));
}
// ===========================================================================
// Group B — send-side signing policy (perhapsEncode)
// ===========================================================================
// B1: our own small broadcast is auto-signed (and verifies on the way back in).
void test_B1_local_broadcast_is_signed(void)
{
uint8_t pub[32], priv[32];
crypto->generateKeyPair(pub, priv); // engine signs with this; store the matching pubkey for us
mockNodeDB->addNode(LOCAL_NODE);
mockNodeDB->setPublicKey(LOCAL_NODE, pub);
meshtastic_MeshPacket p = makeDecoded(LOCAL_NODE, NODENUM_BROADCAST, meshtastic_PortNum_TEXT_MESSAGE_APP, SMALL_PAYLOAD);
TEST_ASSERT_EQUAL(DECODE_SUCCESS, roundTrip(&p));
TEST_ASSERT_EQUAL_MESSAGE(XEDDSA_SIGNATURE_SIZE, p.decoded.xeddsa_signature.size, "broadcast should be auto-signed");
TEST_ASSERT_TRUE(p.xeddsa_signed);
}
// B2: our own unicast is NOT signed.
void test_B2_local_unicast_not_signed(void)
{
mockNodeDB->addNode(REMOTE_NODE);
meshtastic_MeshPacket p = makeDecoded(LOCAL_NODE, REMOTE_NODE, meshtastic_PortNum_PRIVATE_APP, SMALL_PAYLOAD);
TEST_ASSERT_EQUAL(DECODE_SUCCESS, roundTrip(&p));
TEST_ASSERT_EQUAL_MESSAGE(0, p.decoded.xeddsa_signature.size, "unicast must not be signed");
}
// B3: our own oversized broadcast is NOT signed (signature wouldn't fit).
void test_B3_local_oversized_broadcast_not_signed(void)
{
meshtastic_MeshPacket p = makeDecoded(LOCAL_NODE, NODENUM_BROADCAST, meshtastic_PortNum_TEXT_MESSAGE_APP, OVERSIZED_PAYLOAD);
TEST_ASSERT_EQUAL(DECODE_SUCCESS, roundTrip(&p));
TEST_ASSERT_EQUAL_MESSAGE(0, p.decoded.xeddsa_signature.size, "oversized broadcast must not be signed");
}
// ===========================================================================
// Group C — NodeInfoModule downgrade drop (stricter: any unsigned NodeInfo from a known signer)
// ===========================================================================
class NodeInfoTestShim : public NodeInfoModule
{
public:
using NodeInfoModule::handleReceivedProtobuf; // protected virtual -> exposed for direct call
};
static meshtastic_MeshPacket makeNodeInfoPacket(bool signed_)
{
// Broadcast so the module's phone-forward path (which needs `service`) is skipped.
meshtastic_MeshPacket mp = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, meshtastic_PortNum_NODEINFO_APP, SMALL_PAYLOAD);
mp.xeddsa_signed = signed_;
return mp;
}
// C1: unsigned NodeInfo from a node that previously signed -> dropped.
void test_C1_unsigned_nodeinfo_from_signer_dropped(void)
{
mockNodeDB->addNode(REMOTE_NODE);
mockNodeDB->setSignerBit(REMOTE_NODE, true);
NodeInfoTestShim shim;
meshtastic_MeshPacket mp = makeNodeInfoPacket(/*signed_=*/false);
meshtastic_User user = meshtastic_User_init_zero;
user.is_licensed = owner.is_licensed;
TEST_ASSERT_TRUE_MESSAGE(shim.handleReceivedProtobuf(mp, &user), "unsigned NodeInfo from signer must be dropped");
}
// C2: signed NodeInfo from a known signer -> not dropped by this rule.
void test_C2_signed_nodeinfo_from_signer_not_dropped(void)
{
mockNodeDB->addNode(REMOTE_NODE);
mockNodeDB->setSignerBit(REMOTE_NODE, true);
NodeInfoTestShim shim;
meshtastic_MeshPacket mp = makeNodeInfoPacket(/*signed_=*/true);
meshtastic_User user = meshtastic_User_init_zero;
user.is_licensed = owner.is_licensed;
TEST_ASSERT_FALSE(shim.handleReceivedProtobuf(mp, &user));
}
// C3: unsigned NodeInfo from a node we've never seen sign -> not dropped.
void test_C3_unsigned_nodeinfo_from_nonsigner_not_dropped(void)
{
mockNodeDB->addNode(REMOTE_NODE); // signer bit clear
NodeInfoTestShim shim;
meshtastic_MeshPacket mp = makeNodeInfoPacket(/*signed_=*/false);
meshtastic_User user = meshtastic_User_init_zero;
user.is_licensed = owner.is_licensed;
TEST_ASSERT_FALSE(shim.handleReceivedProtobuf(mp, &user));
}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
printf("\n=== Group A: receive-side accept/reject ===\n");
RUN_TEST(test_A1_valid_signature_accepted_and_learns_signer);
RUN_TEST(test_A2_bad_signature_dropped);
RUN_TEST(test_A3_signed_no_pubkey_accepted_unverified);
RUN_TEST(test_A4_downgrade_unsigned_broadcast_from_signer_dropped);
RUN_TEST(test_A5_unsigned_broadcast_from_nonsigner_accepted);
RUN_TEST(test_A6_unsigned_unicast_from_signer_accepted);
RUN_TEST(test_A7_unsigned_oversized_broadcast_from_signer_accepted);
printf("\n=== Group B: send-side signing policy ===\n");
RUN_TEST(test_B1_local_broadcast_is_signed);
RUN_TEST(test_B2_local_unicast_not_signed);
RUN_TEST(test_B3_local_oversized_broadcast_not_signed);
printf("\n=== Group C: NodeInfoModule downgrade drop ===\n");
RUN_TEST(test_C1_unsigned_nodeinfo_from_signer_dropped);
RUN_TEST(test_C2_signed_nodeinfo_from_signer_not_dropped);
RUN_TEST(test_C3_unsigned_nodeinfo_from_nonsigner_not_dropped);
exit(UNITY_END());
}
void loop() {}
#else // MESHTASTIC_EXCLUDE_PKI
void setUp(void) {}
void tearDown(void) {}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
exit(UNITY_END());
}
void loop() {}
#endif
@@ -1,6 +1,8 @@
#include "Channels.h"
#include "PositionPrecision.h"
#include "TestUtil.h"
#include "mesh-pb-constants.h"
#include <cstring>
#include <unity.h>
static meshtastic_Position makePosition()
@@ -119,6 +121,92 @@ static void test_getPositionPrecisionForChannel_secondaryWithoutModuleSettingsFa
TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(channel));
}
// End-to-end via the channelIndex overload + live channels singleton, exercising getKey()'s 1-byte->16-byte expansion.
static void test_getPositionPrecisionForChannel_clampsPreciseOnDefaultKeyChannel()
{
channels.initDefaults(); // channel 0: primary, default key (psk {0x01}) -> publicly decryptable
uint8_t idx = 0;
meshtastic_Channel &ch = channels.getByIndex(idx);
ch.settings.has_module_settings = true;
ch.settings.module_settings.position_precision = 32; // user requests "Precise" on a public channel
TEST_ASSERT_EQUAL_UINT32(MAX_POSITION_PRECISION_PUBLIC_KEY, getPositionPrecisionForChannel(idx));
}
static void test_getPositionPrecisionForChannel_keepsPreciseOnStrongKeyChannel()
{
channels.initDefaults();
uint8_t idx = 0;
meshtastic_Channel &ch = channels.getByIndex(idx);
memset(ch.settings.psk.bytes, 0xAB, 16); // a private 128-bit key, not the defaultpsk family
ch.settings.psk.size = 16;
ch.settings.has_module_settings = true;
ch.settings.module_settings.position_precision = 32;
TEST_ASSERT_EQUAL_UINT32(32, getPositionPrecisionForChannel(idx));
}
static CryptoKey makeCryptoKey(const uint8_t *bytes, int length)
{
CryptoKey k;
memset(k.bytes, 0, sizeof(k.bytes));
// CryptoKey::length is int8_t and CryptoKey::bytes is 32 bytes; keep the helper consistent and overflow-safe.
int cappedLen = length;
if (cappedLen < 0)
cappedLen = -1;
else if (cappedLen > static_cast<int>(sizeof(k.bytes)))
cappedLen = static_cast<int>(sizeof(k.bytes));
if (cappedLen > 0 && bytes != nullptr) {
memcpy(k.bytes, bytes, static_cast<size_t>(cappedLen));
}
k.length = static_cast<int8_t>(cappedLen);
return k;
}
static void test_cryptoKeyIsPublic_openKeyIsPublic()
{
// length 0 == encryption disabled.
TEST_ASSERT_TRUE(cryptoKeyIsPublic(makeCryptoKey(nullptr, 0)));
}
static void test_cryptoKeyIsPublic_defaultKeyIsPublic()
{
// The expanded default PSK (the 16-byte defaultpsk) -- the case a key-length check misses.
TEST_ASSERT_TRUE(cryptoKeyIsPublic(makeCryptoKey(defaultpsk, sizeof(defaultpsk))));
}
static void test_cryptoKeyIsPublic_defaultKeyFamilyVariesLastByte()
{
// Higher indices (e.g. {0x02}) expand to defaultpsk with only the last byte bumped -- still public.
uint8_t key[sizeof(defaultpsk)];
memcpy(key, defaultpsk, sizeof(defaultpsk));
key[sizeof(defaultpsk) - 1] = static_cast<uint8_t>(key[sizeof(defaultpsk) - 1] + 1);
TEST_ASSERT_TRUE(cryptoKeyIsPublic(makeCryptoKey(key, sizeof(key))));
}
static void test_cryptoKeyIsPublic_strongKeyIsPrivate()
{
uint8_t key[16];
memset(key, 0xAB, sizeof(key)); // not the defaultpsk family
TEST_ASSERT_FALSE(cryptoKeyIsPublic(makeCryptoKey(key, sizeof(key))));
}
static void test_cryptoKeyIsPublic_aes256KeyIsPrivate()
{
uint8_t key[32];
memset(key, 0x11, sizeof(key));
TEST_ASSERT_FALSE(cryptoKeyIsPublic(makeCryptoKey(key, sizeof(key))));
}
static void test_cryptoKeyIsPublic_invalidKeyIsNotPublic()
{
// length < 0 == no/invalid key (e.g. a disabled channel); it carries no traffic to leak.
TEST_ASSERT_FALSE(cryptoKeyIsPublic(makeCryptoKey(nullptr, -1)));
}
void setUp(void) {}
void tearDown(void) {}
@@ -136,6 +224,14 @@ void setup()
RUN_TEST(test_getPositionPrecisionForChannel_explicitZeroDisablesPrimary);
RUN_TEST(test_getPositionPrecisionForChannel_primaryWithoutModuleSettingsFailsClosed);
RUN_TEST(test_getPositionPrecisionForChannel_secondaryWithoutModuleSettingsFailsClosed);
RUN_TEST(test_getPositionPrecisionForChannel_clampsPreciseOnDefaultKeyChannel);
RUN_TEST(test_getPositionPrecisionForChannel_keepsPreciseOnStrongKeyChannel);
RUN_TEST(test_cryptoKeyIsPublic_openKeyIsPublic);
RUN_TEST(test_cryptoKeyIsPublic_defaultKeyIsPublic);
RUN_TEST(test_cryptoKeyIsPublic_defaultKeyFamilyVariesLastByte);
RUN_TEST(test_cryptoKeyIsPublic_strongKeyIsPrivate);
RUN_TEST(test_cryptoKeyIsPublic_aes256KeyIsPrivate);
RUN_TEST(test_cryptoKeyIsPublic_invalidKeyIsNotPublic);
exit(UNITY_END());
}
+2 -2
View File
@@ -70,8 +70,8 @@ lib_deps =
https://github.com/mverch67/libpax/archive/6f52ee989301cdabaeef00bcbf93bff55708ce2f.zip
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
lewisxhe/XPowersLib@0.3.3
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
rweather/Crypto@0.4.0
# renovate: datasource=git-refs depName=meshtastic/Crypto packageName=https://github.com/meshtastic/Crypto gitBranch=master
https://github.com/meshtastic/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
lib_ignore =
segger_rtt
+2 -2
View File
@@ -55,5 +55,5 @@ lib_deps =
https://github.com/mverch67/libpax/archive/6f52ee989301cdabaeef00bcbf93bff55708ce2f.zip
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
lewisxhe/XPowersLib@0.3.3
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
rweather/Crypto@0.4.0
# renovate: datasource=git-refs depName=meshtastic/Crypto packageName=https://github.com/meshtastic/Crypto gitBranch=master
https://github.com/meshtastic/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
+2 -2
View File
@@ -104,7 +104,7 @@ lib_deps =
${networking_extra.lib_deps}
${environmental_base.lib_deps}
${radiolib_base.lib_deps}
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
rweather/Crypto@0.4.0
# renovate: datasource=git-refs depName=meshtastic/Crypto packageName=https://github.com/meshtastic/Crypto gitBranch=master
https://github.com/meshtastic/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
@@ -5,4 +5,6 @@ void initVariant()
{
pinMode(LED_PAIRING, OUTPUT);
digitalWrite(LED_PAIRING, !LED_STATE_ON); // Turn off the LED to start
pinMode(LED_LORA, OUTPUT);
digitalWrite(LED_LORA, !LED_STATE_ON); // Turn off the LED to start
}
+2 -2
View File
@@ -25,8 +25,8 @@ lib_deps =
${networking_extra.lib_deps}
${radiolib_base.lib_deps}
${environmental_base.lib_deps}
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
rweather/Crypto@0.4.0
# renovate: datasource=git-refs depName=meshtastic/Crypto packageName=https://github.com/meshtastic/Crypto
https://github.com/meshtastic/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.21
; # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main
+4 -1
View File
@@ -124,7 +124,7 @@ test_testing_command =
; USB-SPI bridge. No BlueZ, libgpiod, or Linux I2C — those require Linux.
;
; Prerequisites (Homebrew):
; brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config
; brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config jsoncpp
; # Optional: enable the HTTP API (PiWebServer) on macOS:
; brew install ulfius
;
@@ -176,6 +176,9 @@ extends = native_base
; the macOS dev loop anyway, and Apple ld's equivalent (`-Wl,-map,<file>`)
; uses different argument shape.
build_unflags = -Wl,-Map,"${platformio.build_dir}"/output.map
; libi2c is Linux-only but build_flags_common carries -li2c (also duplicated in the Linux-only
; build_flags); macOS has no libi2c, so strip it here to let the link succeed.
-li2c
build_flags = ${portduino_base.build_flags_common}
-I variants/native/portduino
-I/opt/homebrew/include
@@ -56,8 +56,8 @@ void initVariant()
digitalWrite(DHT_POWER, HIGH);
pinMode(Battery_POWER, OUTPUT);
digitalWrite(Battery_POWER, HIGH);
pinMode(GPS_POWER, OUTPUT);
digitalWrite(GPS_POWER, HIGH);
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, HIGH);
}
// called from main-nrf52.cpp during the cpuDeepSleep() function
@@ -74,12 +74,11 @@ void variant_shutdown()
digitalWrite(DHT_POWER, LOW);
digitalWrite(ACC_POWER, LOW);
digitalWrite(Battery_POWER, LOW);
digitalWrite(GPS_POWER, LOW);
// This sets the pin to OUTPUT and LOW for the pins *not* in the if block.
for (int pin = 0; pin < 48; pin++) {
if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER ||
pin == ACC_POWER || pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN ||
pin == ACC_POWER || pin == Battery_POWER || pin == PIN_GPS_EN || pin == LR1110_SPI_MISO_PIN ||
pin == LR1110_SPI_MOSI_PIN || pin == LR1110_SPI_SCK_PIN || pin == LR1110_SPI_NSS_PIN || pin == LR1110_BUSY_PIN ||
pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || pin == GPS_RX_PIN || pin == LED_GREEN ||
pin == LED_RED || pin == LED_BLUE) {
@@ -101,4 +100,4 @@ void variant_shutdown()
nrf_gpio_cfg_input(PIN_POWER_USB, NRF_GPIO_PIN_PULLDOWN); // Configure the pin to be woken up as an input
nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_HIGH;
nrf_gpio_cfg_sense_set(PIN_POWER_USB, sense2);
}
}
@@ -37,7 +37,8 @@ extern "C" {
// Power Pin
#define NRF_APM
#define GPS_POWER 14
#define PIN_GPS_EN 14
#define GPS_EN_ACTIVE HIGH
#define PIN_POWER_USB 31
#define EXT_PWR_DETECT PIN_POWER_USB
#define PIN_POWER_DONE 24
@@ -25,8 +25,7 @@
const uint32_t g_ADigitalPinMap[] = {
// P0 - pins 0 and 1 are hardwired for xtal and should never be enabled
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31,
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
// P1
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
+41 -53
View File
@@ -18,21 +18,20 @@
#ifndef _VARIANT_HELTEC_MESH_NODE_T1_
#define _VARIANT_HELTEC_MESH_NODE_T1_
/** Master clock frequency */
#define VARIANT_MCK (64000000ul)
#define USE_LFXO // Board uses 32khz crystal for LF
/*----------------------------------------------------------------------------
* Headers
*----------------------------------------------------------------------------*/
#include "WVariant.h"
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#define HELTEC_MESH_NODE_T1
// Display (ST7735, 80x160 TFT via SPI1)
#define ST7735_CS (0 + 12)
#define ST7735_RS (0 + 22) // DC
#define ST7735_SDA (0 + 24)
@@ -41,7 +40,7 @@ extern "C" {
#define ST7735_MISO -1
#define ST7735_BUSY -1
#define ST7735_BL (0 + 15)
#define VTFT_CTRL (0 + 13) // Active HIGH, powers the ST7735 display
#define VTFT_CTRL (0 + 13) // Active HIGH, powers the ST7735 display
#define SPI_FREQUENCY 80000000
#define SPI_READ_FREQUENCY 16000000
#define SCREEN_ROTATE
@@ -50,51 +49,47 @@ extern "C" {
#define TFT_OFFSET_X 24
#define TFT_OFFSET_Y 0
#define TFT_INVERT false
#define SCREEN_TRANSITION_FRAMERATE 3 // fps
#define DISPLAY_FORCE_SMALL_FONTS
#define FORCE_LOW_RES 1 // 80px-wide panel causes artifacts with full-res UI elements
// Pins
// Number of pins defined in PinDescription array
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (1)
#define NUM_ANALOG_OUTPUTS (0)
// LEDs
#define PIN_LED1 (0 + 16)
#define LED_BLUE PIN_LED1 // fake for bluefruit library
#define LED_GREEN PIN_LED1
#define LED_STATE_ON 0 // State when LED is lit
/*
* Buttons
*/
// Buttons
#define PIN_BUTTON1 (32 + 10)
#define PIN_BUTTON2 (0 + 14)
/*
No longer populated on PCB
*/
// Serial (unused, not populated on PCB)
#define PIN_SERIAL2_RX (-1)
#define PIN_SERIAL2_TX (-1)
/*
* I2C
*/
// I2C (ICM42607P IMU and MMC5983MA compass)
#define WIRE_INTERFACES_COUNT 1
// I2C bus 1
#define PIN_WIRE_SDA (32 + 3)
#define PIN_WIRE_SCL (0 + 10)
#define PIN_SENSOR_EN (32 + 6) // Power control pin for sensors
#define PIN_SENSOR_EN_ACTIVE LOW // Power control active state
// #define ICM_42607P_INT_PIN (32 + 1) // ICM42607P INT1, Arduino pin 33 / nRF P1.01
// #define ICM_42607P_INT2_PIN (32 + 7) // ICM42607P INT2, Arduino pin 39 / nRF P1.07
#define PIN_SENSOR_EN (32 + 6) // Active LOW — controls IMU and compass VDD
#define PIN_SENSOR_EN_ACTIVE LOW
/*
* Lora radio
*/
// ICM42607P interrupt pins — populated on PCB, not yet used in firmware
// #define ICM_42607P_INT_PIN (32 + 1) // INT1 — P1.01
// #define ICM_42607P_INT2_PIN (32 + 7) // INT2 — P1.07
// LoRa (SX1262)
#define USE_SX1262
#define SX126X_CS (32 + 11) // FIXME - we really should define LORA_CS instead
@@ -105,46 +100,48 @@ No longer populated on PCB
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
/*
* SPI Interfaces
*/
// SPI
#define SPI_INTERFACES_COUNT 2
// For LORA, spi 0
// SPI0 — LoRa
#define PIN_SPI_MISO (0 + 3)
#define PIN_SPI_MOSI (32 + 14)
#define PIN_SPI_SCK (32 + 13)
#define PIN_SPI1_MISO ST7735_MISO
// SPI1 — Display (ST7735, write-only)
#define PIN_SPI1_MISO ST7735_MISO
#define PIN_SPI1_MOSI ST7735_SDA
#define PIN_SPI1_SCK ST7735_SCK
#define PIN_SPI1_SCK ST7735_SCK
// GPS (UC6580)
/*
* GPS pins
*/
#define GPS_UC6580
#define GPS_BAUDRATE 115200
#define PIN_GPS_RESET (0 + 26)
#define GPS_RESET_MODE LOW
#define PIN_GPS_EN (0 + 4)
#define GPS_EN_ACTIVE LOW
#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing
#define PERIPHERAL_WARMUP_MS 1000 // Allow I2C bus to stabilise after sensor power-on
#define PIN_GPS_PPS (32 + 9) // Pulse per second input from the GPS
#define GPS_TX_PIN (0 + 7)
#define GPS_RX_PIN (0 + 8)
#define GPS_THREAD_INTERVAL 50
#define PIN_SERIAL1_RX GPS_RX_PIN
#define PIN_SERIAL1_TX GPS_TX_PIN
// Buzzer
#define PIN_BUZZER (0 + 9)
#define PIN_BUZZER_VOLTAGE_MULTIPLIER_1 (32 + 2)
#define PIN_BUZZER_VOLTAGE_MULTIPLIER_2 (32 + 5)
// Battery / ADC
#define ADC_CTRL 11
#define ADC_CTRL_ENABLED HIGH
#define BATTERY_PIN 5
#define BATTERY_PIN 5 // nRF52840 AIN3
#define ADC_RESOLUTION 14
#define BATTERY_SENSE_RESOLUTION_BITS 12
@@ -154,24 +151,15 @@ No longer populated on PCB
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
#define ADC_MULTIPLIER (4.916F)
// nrf52840 AIN3 = Pin 5
// commented out due to power leakage of 2.9mA in shutdown state see reported issue #8801
#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3
// #define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3 // UNSAFE: causes 2.9 mA deep-sleep leakage (issue #8801)
// We have AIN3 with a VBAT divider so AIN3 = VBAT * (100/490)
// We have the device going deep sleep under 3.1V, which is AIN3 = 0.63V
// So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN3 = 0.67V
// Ratio 0.67/3.3 = 0.20, so we can pick a bit higher, 2/8 VDD, which means
// VBAT=4.04V
#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8
// Power / USB
#define NRF_APM // USB VBUS detection via nrfx_power_usbstatus_get() — no dedicated charging IC on this board
#define HAS_RTC 0 // No external RTC fitted
#define HAS_RTC 0
#ifdef __cplusplus
}
#endif
/*----------------------------------------------------------------------------
* Arduino objects - C++ only
*----------------------------------------------------------------------------*/
#endif
+2 -2
View File
@@ -48,8 +48,8 @@ build_src_filter =
lib_deps=
${arduino_base.lib_deps}
${radiolib_base.lib_deps}
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
rweather/Crypto@0.4.0
# renovate: datasource=github-tags depName=meshtastic/Crypto packageName=meshtastic/Crypto
https://github.com/meshtastic/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
lib_ignore =
BluetoothOTA
-6
View File
@@ -1,6 +0,0 @@
[nrf52832_base]
extends = nrf52_base
build_flags =
${nrf52_base.build_flags}
-DSERIAL_BUFFER_SIZE=1024
@@ -45,13 +45,13 @@ void initVariant()
digitalWrite(BUZZER_EN_PIN, HIGH);
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, LOW);
digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE);
pinMode(GPS_VRTC_EN, OUTPUT);
digitalWrite(GPS_VRTC_EN, HIGH);
pinMode(PIN_GPS_RESET, OUTPUT);
digitalWrite(PIN_GPS_RESET, LOW);
digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE);
pinMode(GPS_SLEEP_INT, OUTPUT);
digitalWrite(GPS_SLEEP_INT, HIGH);
@@ -59,5 +59,5 @@ void initVariant()
pinMode(GPS_RTC_INT, OUTPUT);
digitalWrite(GPS_RTC_INT, LOW);
pinMode(GPS_RESETB_OUT, INPUT);
}
pinMode(GPS_RESETB_OUT, INPUT_PULLUP);
}
+2 -1
View File
@@ -53,7 +53,8 @@ lib_compat_mode = off
lib_deps =
${arduino_base.lib_deps}
${radiolib_base.lib_deps}
rweather/Crypto@0.4.0
# renovate: datasource=git-refs depName=meshtastic/Crypto packageName=https://github.com/meshtastic/Crypto gitBranch=master
https://github.com/meshtastic/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
; Cherry-picked sensor libs from environmental_base. The full
; environmental_base pulls Adafruit_SSD1306 / GFX which need Arduino
; pin macros (digitalPinToPort / portOutputRegister) that the Zephyr
+2 -2
View File
@@ -31,5 +31,5 @@ lib_deps =
${environmental_base.lib_deps}
${environmental_extra.lib_deps}
${radiolib_base.lib_deps}
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
rweather/Crypto@0.4.0
# renovate: datasource=github-tags depName=meshtastic/Crypto packageName=meshtastic/Crypto
https://github.com/meshtastic/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
+2 -2
View File
@@ -28,5 +28,5 @@ lib_deps =
${environmental_base.lib_deps}
${environmental_extra.lib_deps}
${radiolib_base.lib_deps}
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
rweather/Crypto@0.4.0
# renovate: datasource=github-tags depName=meshtastic/Crypto packageName=meshtastic/Crypto
https://github.com/meshtastic/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
+3 -2
View File
@@ -25,6 +25,7 @@ build_flags =
-DMESHTASTIC_EXCLUDE_BLUETOOTH=1
-DMESHTASTIC_EXCLUDE_WIFI=1
-DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space.
-DMESHTASTIC_EXCLUDE_XEDDSA=1 ; The Ed25519 signing code does not fit in the 256KB flash. Packets are sent unsigned, like pre-XEdDSA firmware.
-DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small.
-DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported.
;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; Enable this if enabling debugg logging. It is REQUIRED for at least traceroute debug prints - without it the length returned by printf ends up uninitialized.
@@ -53,8 +54,8 @@ debug_tool = stlink
lib_deps =
${env.lib_deps}
${radiolib_base.lib_deps}
# renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main
https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
# renovate: datasource=git-refs depName=meshtastic/Crypto packageName=https://github.com/meshtastic/Crypto gitBranch=main
https://github.com/meshtastic/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
lib_ignore =
OneButton