Compare commits

...
Author SHA1 Message Date
HarukiToreda f896c32fb7 trunk 2026-06-15 10:23:27 -04:00
HarukiToreda 2101785b3d Wipe messages 2026-06-15 10:14:38 -04: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
70 changed files with 1514 additions and 329 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:
+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()
+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
+2 -2
View File
@@ -37,8 +37,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
{
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P};
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P};
return firstOfOrNONE(11, types);
}
+1 -1
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
+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;
@@ -129,6 +129,7 @@ enum MenuAction {
// Administration
RESET_NODEDB_ALL,
RESET_NODEDB_KEEP_FAVORITES,
WIPE_MESSAGES_ALL,
};
} // namespace NicheGraphics::InkHUD
@@ -6,6 +6,7 @@
#include "GPS.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "RTC.h"
#include "Router.h"
#include "airtime.h"
@@ -1013,6 +1014,13 @@ void InkHUD::MenuApplet::execute(MenuItem item)
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
break;
case WIPE_MESSAGES_ALL:
LOG_INFO("Wiping all messages from menu");
messageStore.clearAllMessages();
inkhud->persistence->loadLatestMessage();
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true);
break;
default:
LOG_WARN("Action not implemented");
}
@@ -1130,6 +1138,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
// Administration Section
items.push_back(MenuItem::Header("Administration"));
items.push_back(MenuItem("Reset NodeDB", MenuPage::NODE_CONFIG_ADMIN_RESET));
items.push_back(MenuItem("Wipe Messages", MenuPage::NODE_CONFIG_ADMIN_MESSAGES));
// Exit
items.push_back(MenuItem("Exit", MenuPage::EXIT));
@@ -1534,6 +1543,13 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case NODE_CONFIG_ADMIN_MESSAGES:
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Wipe All Messages", MenuAction::WIPE_MESSAGES_ALL, MenuPage::EXIT));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
// Exit
case EXIT:
sendToBackground(); // Menu applet dismissed, allow normal behavior to resume
@@ -36,6 +36,7 @@ enum MenuPage : uint8_t {
NODE_CONFIG_BLUETOOTH,
NODE_CONFIG_POSITION,
NODE_CONFIG_ADMIN_RESET,
NODE_CONFIG_ADMIN_MESSAGES,
TIMEZONE,
APPLETS,
AUTOSHOW,
+3 -1
View File
@@ -22,6 +22,8 @@ void InkHUD::Persistence::loadSettings()
// are immediately available to applets (DMApplet, AllMessageApplet, NotificationApplet).
void InkHUD::Persistence::loadLatestMessage()
{
latestMessage = LatestMessage();
int lastBroadcastPos = -1, lastDMPos = -1, pos = 0;
for (const StoredMessage &m : messageStore.getLiveMessages()) {
if (m.type == MessageType::BROADCAST) {
@@ -75,4 +77,4 @@ void InkHUD::Persistence::printSettings(Settings *settings)
}
*/
#endif
#endif
+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);
+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 = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ);
ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ);
}
heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma) + 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
+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