Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce23bcf91f | ||
|
|
d3d606c1d5 | ||
|
|
c7f17a80b2 | ||
|
|
41a558cc5e | ||
|
|
be42d00728 | ||
|
|
6cd9a3a42b | ||
|
|
f56536417b | ||
|
|
b5940fc9f0 | ||
|
|
9b11352d42 | ||
|
|
1daeb2edce | ||
|
|
f6956160bb | ||
|
|
63d0781e29 | ||
|
|
266c143359 | ||
|
|
cd56674429 | ||
|
|
9e672db74c | ||
|
|
367cb9208a | ||
|
|
fe08803321 | ||
|
|
51f56ccb4e | ||
|
|
7ddecd1dd7 | ||
|
|
747fc01669 | ||
|
|
c8bc76bc60 | ||
|
|
c355215122 | ||
|
|
4d799cf66a | ||
|
|
ec80eb7d66 | ||
|
|
da1ec940dd | ||
|
|
b6caf1e6e7 | ||
|
|
cb867cc6c8 | ||
|
|
8e4ea08b98 | ||
|
|
f6c9b9aab0 | ||
|
|
7a3b4395f2 | ||
|
|
0abd202b82 | ||
|
|
8049d77522 | ||
|
|
0ee360295b | ||
|
|
1c0182f329 | ||
|
|
472b14c4e4 | ||
|
|
3851fbfec3 | ||
|
|
8b22448285 | ||
|
|
73e79797b2 | ||
|
|
a96f83fd01 | ||
|
|
0f761d930b | ||
|
|
4d4906772f | ||
|
|
711abb56f3 | ||
|
|
bfc536f96c | ||
|
|
7d7bfbcf24 | ||
|
|
8fcaf5ff30 | ||
|
|
3773683df6 | ||
|
|
2c782223a5 | ||
|
|
dcabb85000 | ||
|
|
9c3ceaf6e9 | ||
|
|
b2c07708bd | ||
|
|
b5737d089b | ||
|
|
3ccec98b58 | ||
|
|
30a3f39ed0 | ||
|
|
55da30ab58 | ||
|
|
078cd68191 | ||
|
|
e932c9786f | ||
|
|
8d1659a993 | ||
|
|
d04509bbf0 | ||
|
|
8d1674c3e5 | ||
|
|
af87c55530 | ||
|
|
1898aec281 | ||
|
|
dd2a4a3121 | ||
|
|
a77836279a | ||
|
|
ebc41e571c |
@@ -333,6 +333,7 @@ jobs:
|
||||
prerelease: true
|
||||
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
|
||||
tag_name: v${{ needs.version.outputs.long }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
body: ${{ steps.release_notes.outputs.notes }}
|
||||
|
||||
- name: Download source deb
|
||||
|
||||
@@ -17,13 +17,13 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Trunk Check
|
||||
uses: trunk-io/trunk-action@v1
|
||||
uses: trunk-io/trunk-action@v1.3.1
|
||||
with:
|
||||
trunk-token: ${{ secrets.TRUNK_TOKEN }}
|
||||
|
||||
trunk_upgrade:
|
||||
if: github.repository == 'meshtastic/firmware'
|
||||
# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades
|
||||
# See: https://github.com/trunk-io/trunk-action/blob/main/readme.md#automatic-upgrades
|
||||
name: Trunk Upgrade (PR)
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
@@ -34,6 +34,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Trunk Upgrade
|
||||
uses: trunk-io/trunk-action/upgrade@v1
|
||||
uses: trunk-io/trunk-action/upgrade@v1.3.1
|
||||
with:
|
||||
base: master
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Stale PR+Issues
|
||||
uses: actions/stale@v10.2.0
|
||||
uses: actions/stale@v10.3.0
|
||||
with:
|
||||
days-before-stale: 45
|
||||
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Annotate PR with trunk issues
|
||||
# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#getting-inline-annotations-for-fork-prs
|
||||
# See: https://github.com/trunk-io/trunk-action/blob/main/readme.md#getting-inline-annotations-for-fork-prs
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
@@ -21,6 +21,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Trunk Check
|
||||
uses: trunk-io/trunk-action@v1
|
||||
uses: trunk-io/trunk-action@v1.3.1
|
||||
with:
|
||||
post-annotations: true
|
||||
|
||||
@@ -19,6 +19,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Trunk Check
|
||||
uses: trunk-io/trunk-action@v1
|
||||
uses: trunk-io/trunk-action@v1.3.1
|
||||
with:
|
||||
save-annotations: true
|
||||
|
||||
+5
-5
@@ -8,15 +8,15 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.529
|
||||
- renovate@43.150.0
|
||||
- checkov@3.2.532
|
||||
- renovate@43.209.1
|
||||
- prettier@3.8.3
|
||||
- trufflehog@3.95.3
|
||||
- trufflehog@3.95.5
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.4
|
||||
- trivy@0.70.0
|
||||
- trivy@0.71.0
|
||||
- taplo@0.10.0
|
||||
- ruff@0.15.13
|
||||
- ruff@0.15.15
|
||||
- isort@8.0.1
|
||||
- markdownlint@0.48.0
|
||||
- oxipng@10.1.1
|
||||
|
||||
@@ -5,7 +5,9 @@ Meta:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
### RAK13300 in Slot 2 pins
|
||||
|
||||
### RAK13300 in Slot 2
|
||||
Module: sx1262
|
||||
IRQ: 18 #IO6
|
||||
Reset: 24 # IO4
|
||||
Busy: 19 # IO5
|
||||
@@ -13,5 +15,7 @@ Lora:
|
||||
Enable_Pins:
|
||||
- 26
|
||||
- 23
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
@@ -5,14 +5,18 @@ Meta:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
### RAK13302 in Slot 2 pins
|
||||
|
||||
### RAK13302 in Slot 2
|
||||
Module: sx1262
|
||||
IRQ: 18 #IO6
|
||||
Reset: 24 # IO4
|
||||
Busy: 19 # IO5
|
||||
# Ant_sw: 23 # IO3
|
||||
# Ant_sw: 23 # IO3
|
||||
Enable_Pins:
|
||||
- 26
|
||||
- 23
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
|
||||
@@ -87,6 +87,9 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.25" date="2026-05-23">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.25</url>
|
||||
</release>
|
||||
<release version="2.7.24" date="2026-05-08">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.24</url>
|
||||
</release>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x4405"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"],
|
||||
["0x2886", "0x1667"]
|
||||
],
|
||||
"usb_product": "HT-n5262",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "heltec_mesh_node_t1",
|
||||
"variants_dir": "variants",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"onboard_tools": ["jlink"],
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Heltec Mesh Node T1",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://heltec.org",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_NRF52840_T_IMPULSE_PLUS -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [["0x239A", "0x8029"]],
|
||||
"usb_product": "T-Impulse-Plus-nRF52840",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "t-impulse-plus",
|
||||
"variants_dir": "variants",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"onboard_tools": ["jlink"],
|
||||
"svd_path": "nrf52840.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Lilygo T-Impulse-Plus-nRF52840",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"require_upload_port": true,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"nrfjprog",
|
||||
"nrfutil",
|
||||
"stlink",
|
||||
"cmsis-dap",
|
||||
"blackmagic"
|
||||
]
|
||||
},
|
||||
"url": "https://www.lilygo.cc/",
|
||||
"vendor": "Lilygo"
|
||||
}
|
||||
Vendored
+6
@@ -1,3 +1,9 @@
|
||||
meshtasticd (2.7.25.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.25
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Sat, 23 May 2026 01:16:20 +0000
|
||||
|
||||
meshtasticd (2.7.24.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.24
|
||||
|
||||
Vendored
+1
-1
@@ -33,5 +33,5 @@ if [[ -n $GPG_KEY_ID ]]; then
|
||||
debuild -S -nc -k"$GPG_KEY_ID"
|
||||
else
|
||||
# Build the source deb without signing (forks)
|
||||
debuild -S -nc
|
||||
debuild -S -nc -us -uc
|
||||
fi
|
||||
|
||||
+7
-3
@@ -68,7 +68,7 @@ monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/6bfd1f135e1ebe37afd6050bb4b9964cea3fcfda.zip
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/2e26010040e028baee72e2093402fa7b3c59e430.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||
@@ -126,7 +126,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/4bf593a82100b911ff816dddf7158ffdee2114cd.zip
|
||||
https://github.com/meshtastic/device-ui/archive/502ba30dbccc6a323f1d4f7ca961131c27f60672.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
@@ -140,7 +140,7 @@ lib_deps =
|
||||
# renovate: datasource=github-tags depName=NeoPixel packageName=adafruit/Adafruit_NeoPixel
|
||||
https://github.com/adafruit/Adafruit_NeoPixel/archive/1.15.5.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SSD1306 packageName=adafruit/Adafruit_SSD1306
|
||||
https://github.com/adafruit/Adafruit_SSD1306/archive/refs/tags/2.5.16.zip
|
||||
https://github.com/adafruit/Adafruit_SSD1306/archive/2.5.17.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit BMP280 packageName=adafruit/Adafruit_BMP280_Library
|
||||
https://github.com/adafruit/Adafruit_BMP280_Library/archive/refs/tags/3.0.0.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit BMP085 packageName=adafruit/Adafruit-BMP085-Library
|
||||
@@ -185,12 +185,16 @@ lib_deps =
|
||||
https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/archive/refs/tags/v1.1.2.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/SparkFun_ICM-20948_ArduinoLibrary
|
||||
https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary/archive/refs/tags/v1.3.2.zip
|
||||
# renovate: datasource=github-tags depName=TDK InvenSense ICM42670P packageName=tdk-invn-oss/motion.arduino.ICM42670P
|
||||
https://github.com/tdk-invn-oss/motion.arduino.ICM42670P/archive/refs/tags/1.0.8.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390
|
||||
https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075
|
||||
https://github.com/adafruit/Adafruit_PCT2075/archive/refs/tags/1.0.6.zip
|
||||
# renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150
|
||||
https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun MMC5983MA Magnetometer packageName=sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library
|
||||
https://github.com/sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library/archive/v1.1.5.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit_TSL2561 packageName=adafruit/Adafruit_TSL2561
|
||||
https://github.com/adafruit/Adafruit_TSL2561/archive/refs/tags/1.1.3.zip
|
||||
# renovate: datasource=github-tags depName=BH1750_WE packageName=wollewald/BH1750_WE
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "power/PowerHAL.h"
|
||||
#include "power/SGM41562.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
@@ -453,6 +454,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
/// source
|
||||
virtual bool isVbusIn() override
|
||||
{
|
||||
#ifdef HAS_SGM41562
|
||||
if (sgm41562 && sgm41562->refresh())
|
||||
return sgm41562->isInputPowerGood();
|
||||
#endif
|
||||
#ifdef EXT_PWR_DETECT
|
||||
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
|
||||
// if external powered that pin will be pulled down
|
||||
@@ -483,6 +488,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
/// we can't be smart enough to say 'full'?
|
||||
virtual bool isCharging() override
|
||||
{
|
||||
#ifdef HAS_SGM41562
|
||||
if (sgm41562 && sgm41562->refresh())
|
||||
return sgm41562->isCharging();
|
||||
#endif
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU)
|
||||
if (hasRAK()) {
|
||||
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
|
||||
@@ -697,6 +706,12 @@ bool Power::analogInit()
|
||||
*/
|
||||
bool Power::setup()
|
||||
{
|
||||
#ifdef HAS_SGM41562
|
||||
// Initialize the charger early so AnalogBatteryLevel can read charging
|
||||
// state from it. The charger does not provide battery voltage / percent —
|
||||
// those still come from the platform ADC via analogInit() below.
|
||||
initSGM41562(SGM41562_WIRE);
|
||||
#endif
|
||||
bool found = false;
|
||||
if (axpChipInit()) {
|
||||
found = true;
|
||||
|
||||
@@ -31,6 +31,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
#if __has_include("SensorRtcHelper.hpp")
|
||||
#include "SensorRtcHelper.hpp"
|
||||
// SensorLib defines isBitSet as a macro; undefine it here to avoid conflicts
|
||||
// with the SparkFun MMC5983MA library, which has a class method of the same name.
|
||||
#ifdef isBitSet
|
||||
#undef isBitSet
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Offer chance for variant-specific defines */
|
||||
@@ -238,6 +243,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define QMI8658_ADDR 0x6B
|
||||
#define QMC5883L_ADDR 0x0D
|
||||
#define HMC5883L_ADDR 0x1E
|
||||
#define MMC5983MA_ADDR 0x30
|
||||
#define SHTC3_ADDR 0x70
|
||||
#define LPS22HB_ADDR 0x5C
|
||||
#define LPS22HB_ADDR_ALT 0x5D
|
||||
@@ -287,6 +293,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define DA217_ADDR 0x26
|
||||
#define BMI270_ADDR 0x68
|
||||
#define BMI270_ADDR_ALT 0x69
|
||||
#define ICM42607P_ADDR 0x68
|
||||
#define ICM42607P_ADDR_ALT 0x69
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// LED
|
||||
|
||||
@@ -37,8 +37,15 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270};
|
||||
return firstOfOrNONE(10, types);
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
|
||||
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P};
|
||||
return firstOfOrNONE(11, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {MMC5983MA};
|
||||
return firstOfOrNONE(1, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||
|
||||
@@ -41,6 +41,7 @@ class ScanI2C
|
||||
QMI8658,
|
||||
QMC5883L,
|
||||
HMC5883L,
|
||||
MMC5983MA,
|
||||
PMSA003I,
|
||||
QMA6100P,
|
||||
MPU6050,
|
||||
@@ -65,6 +66,7 @@ class ScanI2C
|
||||
FT6336U,
|
||||
STK8BAXX,
|
||||
ICM20948,
|
||||
ICM42607P,
|
||||
SCD4X,
|
||||
MAX30102,
|
||||
TPS65233,
|
||||
@@ -149,6 +151,8 @@ class ScanI2C
|
||||
|
||||
FoundDevice firstAccelerometer() const;
|
||||
|
||||
FoundDevice firstMagnetometer() const;
|
||||
|
||||
FoundDevice firstAQI() const;
|
||||
|
||||
FoundDevice firstRGBLED() const;
|
||||
|
||||
@@ -85,8 +85,9 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const
|
||||
|
||||
return o_probe;
|
||||
}
|
||||
|
||||
uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation ®isterLocation,
|
||||
ScanI2CTwoWire::ResponseWidth responseWidth, bool zeropad = false) const
|
||||
ScanI2CTwoWire::ResponseWidth responseWidth, bool zeropad) const
|
||||
{
|
||||
uint16_t value = 0x00;
|
||||
TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress);
|
||||
@@ -175,6 +176,62 @@ String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
|
||||
return String(productName);
|
||||
}
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
static uint8_t crcSHT2X(const uint8_t *data, uint8_t len)
|
||||
{
|
||||
uint8_t crc = 0;
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t bit = 0; bit < 8; bit++) {
|
||||
crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : crc << 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
|
||||
{
|
||||
uint8_t serialA[8] = {0};
|
||||
uint8_t serialB[6] = {0};
|
||||
|
||||
i2cBus->beginTransmission(address);
|
||||
i2cBus->write(0xFA);
|
||||
i2cBus->write(0x0F);
|
||||
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return false;
|
||||
|
||||
if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialA)) != sizeof(serialA))
|
||||
return false;
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(serialA); i++) {
|
||||
if (!i2cBus->available())
|
||||
return false;
|
||||
serialA[i] = i2cBus->read();
|
||||
}
|
||||
|
||||
i2cBus->beginTransmission(address);
|
||||
i2cBus->write(0xFC);
|
||||
i2cBus->write(0xC9);
|
||||
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return false;
|
||||
|
||||
if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialB)) != sizeof(serialB))
|
||||
return false;
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(serialB); i++) {
|
||||
if (!i2cBus->available())
|
||||
return false;
|
||||
serialB[i] = i2cBus->read();
|
||||
}
|
||||
|
||||
return crcSHT2X(&serialA[0], 1) == serialA[1] && crcSHT2X(&serialA[2], 1) == serialA[3] &&
|
||||
crcSHT2X(&serialA[4], 1) == serialA[5] && crcSHT2X(&serialA[6], 1) == serialA[7] &&
|
||||
crcSHT2X(&serialB[0], 2) == serialB[2] && crcSHT2X(&serialB[3], 2) == serialB[5];
|
||||
}
|
||||
#endif
|
||||
|
||||
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||
case ADDR: \
|
||||
logFoundDevice(__VA_ARGS__); \
|
||||
@@ -301,9 +358,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address);
|
||||
#ifdef HAS_NCP5623
|
||||
SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address);
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address);
|
||||
#endif
|
||||
case XPOWERS_AXP192_AXP2101_ADDRESS:
|
||||
// Do we have the axp2101/192 or the TCA8418
|
||||
@@ -524,6 +578,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
#else
|
||||
SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address)
|
||||
#endif
|
||||
case MMC5983MA_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x2F), 1);
|
||||
if (registerValue == 0x30) {
|
||||
type = MMC5983MA;
|
||||
logFoundDevice("MMC5983MA", (uint8_t)addr.address);
|
||||
#ifdef HAS_LP5562
|
||||
} else {
|
||||
type = LP5562;
|
||||
logFoundDevice("LP5562", (uint8_t)addr.address);
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);
|
||||
if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
|
||||
@@ -678,8 +744,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
}
|
||||
break;
|
||||
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR
|
||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, ICM42607P_ADDR_ALT, and SEN5X_ADDR
|
||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR, and ICM42607P_ADDR
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
||||
#ifdef HAS_ICM20948
|
||||
type = ICM20948;
|
||||
@@ -699,6 +765,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
break;
|
||||
} else {
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x75), 1); // WHO_AM_I
|
||||
if (registerValue == 0x60) {
|
||||
type = ICM42607P;
|
||||
logFoundDevice("ICM-42607-P", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
String prod = "";
|
||||
prod = readSEN5xProductName(i2cBus, addr.address);
|
||||
if (prod.startsWith("SEN55")) {
|
||||
|
||||
@@ -53,7 +53,7 @@ class ScanI2CTwoWire : public ScanI2C
|
||||
|
||||
concurrency::Lock lock;
|
||||
|
||||
uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const;
|
||||
uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool = false) const;
|
||||
|
||||
bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const;
|
||||
|
||||
|
||||
+87
-12
@@ -31,13 +31,63 @@ static uint32_t
|
||||
timeStartMsec; // Once we have a GPS lock, this is where we hold the initial msec clock that corresponds to that time
|
||||
static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only updated once on initial lock
|
||||
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
// Test seam: unit tests can inject a fake system clock (e.g. the uptime seconds that
|
||||
// gettimeofday() returns on boards without a real RTC, like RP2040) and force readFromRTC()
|
||||
// down the no-hardware-RTC fallback even when a hardware-RTC branch is compiled in.
|
||||
static bool hasMockSystemTime = false;
|
||||
static bool forceSystemTimeFallback = false;
|
||||
static struct timeval mockSystemTime = {};
|
||||
#endif
|
||||
|
||||
// Reads the platform system clock (or the injected mock during unit tests). Used only by the
|
||||
// no-hardware-RTC fallback below, so it may be unused on builds with a hardware RTC.
|
||||
[[maybe_unused]] static bool readSystemTime(struct timeval *tv)
|
||||
{
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
if (hasMockSystemTime) {
|
||||
*tv = mockSystemTime;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return gettimeofday(tv, NULL) == 0;
|
||||
}
|
||||
|
||||
// Seeds the clock from the system time on boards without a hardware RTC. gettimeofday() can
|
||||
// return uptime rather than wall-clock time there (e.g. RP2040), so only adopt it when we have
|
||||
// nothing better yet -- never clobber a higher-quality GPS/NTP/phone source (issue #9828).
|
||||
[[maybe_unused]] static RTCSetResult readFromSystemTimeFallback()
|
||||
{
|
||||
struct timeval tv;
|
||||
if (readSystemTime(&tv)) {
|
||||
uint32_t now = millis();
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
LOG_DEBUG("Seed time from system clock: %lu", (unsigned long)printableEpoch);
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
} else {
|
||||
LOG_DEBUG("Ignore system clock fallback (%lu); current RTC quality is %s", (unsigned long)printableEpoch,
|
||||
RtcName(currentQuality));
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
return RTCSetResultNotSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current date and time from the RTC module and updates the system time.
|
||||
* @return True if the RTC was successfully read and the system time was updated, false otherwise.
|
||||
* Reads date/time from the RTC module (or system-time fallback) and seeds internal timekeeping.
|
||||
* @return RTCSetResultSuccess if a time source was read successfully (even if an existing higher-quality time is retained).
|
||||
*/
|
||||
RTCSetResult readFromRTC()
|
||||
{
|
||||
struct timeval tv; /* btw settimeofday() is helpful here too*/
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
if (forceSystemTimeFallback) {
|
||||
return readFromSystemTimeFallback();
|
||||
}
|
||||
#endif
|
||||
|
||||
[[maybe_unused]] struct timeval tv; /* btw settimeofday() is helpful here too*/
|
||||
#ifdef RV3028_RTC
|
||||
if (rtc_found.address == RV3028_RTC) {
|
||||
uint32_t now = millis();
|
||||
@@ -162,14 +212,7 @@ RTCSetResult readFromRTC()
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (!gettimeofday(&tv, NULL)) {
|
||||
uint32_t now = millis();
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
LOG_DEBUG("Read RTC time as %ld", printableEpoch);
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
return readFromSystemTimeFallback();
|
||||
#endif
|
||||
return RTCSetResultNotSet;
|
||||
}
|
||||
@@ -292,7 +335,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
LOG_WARN("Failed to set time for RX8130CE");
|
||||
}
|
||||
}
|
||||
#elif defined(ARCH_ESP32)
|
||||
#elif defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
settimeofday(tv, NULL);
|
||||
#endif
|
||||
|
||||
@@ -423,6 +466,38 @@ void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot)
|
||||
lastSetFromPhoneNtpOrGps = 0;
|
||||
lastTimeValidationWarning = 0;
|
||||
}
|
||||
|
||||
void clearRTCSystemTimeForTests()
|
||||
{
|
||||
hasMockSystemTime = false;
|
||||
mockSystemTime = {};
|
||||
}
|
||||
|
||||
void setRTCSystemTimeForTests(const struct timeval *tv)
|
||||
{
|
||||
if (tv == NULL) {
|
||||
clearRTCSystemTimeForTests();
|
||||
return;
|
||||
}
|
||||
mockSystemTime = *tv;
|
||||
hasMockSystemTime = true;
|
||||
}
|
||||
|
||||
void setReadFromRTCUseSystemTimeForTests(bool enabled)
|
||||
{
|
||||
forceSystemTimeFallback = enabled;
|
||||
}
|
||||
|
||||
void resetRTCStateForTests()
|
||||
{
|
||||
currentQuality = RTCQualityNone;
|
||||
timeStartMsec = 0;
|
||||
zeroOffsetSecs = 0;
|
||||
lastSetFromPhoneNtpOrGps = 0;
|
||||
lastTimeValidationWarning = 0;
|
||||
setReadFromRTCUseSystemTimeForTests(false);
|
||||
clearRTCSystemTimeForTests();
|
||||
}
|
||||
#endif
|
||||
|
||||
time_t gm_mktime(const struct tm *tm)
|
||||
|
||||
@@ -56,6 +56,10 @@ RTCSetResult readFromRTC();
|
||||
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot);
|
||||
void resetRTCStateForTests();
|
||||
void setRTCSystemTimeForTests(const struct timeval *tv);
|
||||
void clearRTCSystemTimeForTests();
|
||||
void setReadFromRTCUseSystemTimeForTests(bool enabled);
|
||||
#endif
|
||||
|
||||
time_t gm_mktime(const struct tm *tm);
|
||||
|
||||
+97
-5
@@ -60,6 +60,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/Channels.h"
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
@@ -98,6 +99,7 @@ namespace graphics
|
||||
|
||||
// This means the *visible* area (sh1106 can address 132, but shows 128 for example)
|
||||
#define IDLE_FRAMERATE 1 // in fps
|
||||
#define COMPASS_ACTIVE_FRAMERATE 20
|
||||
|
||||
// DEBUG
|
||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||
@@ -135,6 +137,60 @@ static bool heartbeat = false;
|
||||
|
||||
extern bool hasUnreadMessage;
|
||||
|
||||
static inline float wrapHeading360(float heading)
|
||||
{
|
||||
if (heading < 0.0f) {
|
||||
heading += 360.0f;
|
||||
} else if (heading >= 360.0f) {
|
||||
heading -= 360.0f;
|
||||
}
|
||||
return heading;
|
||||
}
|
||||
|
||||
void Screen::setHeading(float heading)
|
||||
{
|
||||
const float wrappedHeading = wrapHeading360(heading);
|
||||
|
||||
if (!hasCompass) {
|
||||
hasCompass = true;
|
||||
compassHeading = wrappedHeading;
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
compassHeading = wrapHeading360(compassHeading + step);
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// Overlay Alert Banner Renderer
|
||||
// ==============================
|
||||
@@ -272,10 +328,25 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
float Screen::estimatedHeading(double lat, double lon)
|
||||
{
|
||||
static double oldLat, oldLon;
|
||||
static float b;
|
||||
static float b = -1.0f;
|
||||
static uint32_t lastHeadingAtMs = 0;
|
||||
const uint32_t now = millis();
|
||||
const uint32_t gpsUpdateIntervalSecs =
|
||||
Default::getConfiguredOrDefault(config.position.gps_update_interval, default_gps_update_interval);
|
||||
uint32_t effectiveUpdateIntervalSecs = gpsUpdateIntervalSecs;
|
||||
if (config.position.position_broadcast_smart_enabled) {
|
||||
const uint32_t smartMinIntervalSecs = Default::getConfiguredOrDefault(
|
||||
config.position.broadcast_smart_minimum_interval_secs, default_broadcast_smart_minimum_interval_secs);
|
||||
if (smartMinIntervalSecs > effectiveUpdateIntervalSecs) {
|
||||
effectiveUpdateIntervalSecs = smartMinIntervalSecs;
|
||||
}
|
||||
}
|
||||
// Two expected update windows; keep arithmetic 32-bit to avoid pulling in larger 64-bit helpers.
|
||||
const uint32_t headingStaleMs =
|
||||
(effectiveUpdateIntervalSecs > (UINT32_MAX / 2000U)) ? UINT32_MAX : (effectiveUpdateIntervalSecs * 2000U);
|
||||
|
||||
if (oldLat == 0) {
|
||||
// just prepare for next time
|
||||
// Need at least two position points before we can infer heading.
|
||||
oldLat = lat;
|
||||
oldLon = lon;
|
||||
|
||||
@@ -283,12 +354,20 @@ float Screen::estimatedHeading(double lat, double lon)
|
||||
}
|
||||
|
||||
float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon);
|
||||
if (d < 10) // haven't moved enough, just keep current bearing
|
||||
if (d < 10) { // haven't moved enough, keep previous heading (invalid until first real movement)
|
||||
if (lastHeadingAtMs != 0 && (now - lastHeadingAtMs) >= headingStaleMs) {
|
||||
// Heading is stale after prolonged no-movement; force reacquire.
|
||||
b = -1.0f;
|
||||
oldLat = lat;
|
||||
oldLon = lon;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG;
|
||||
oldLat = lat;
|
||||
oldLon = lon;
|
||||
lastHeadingAtMs = now;
|
||||
|
||||
return b;
|
||||
}
|
||||
@@ -928,9 +1007,22 @@ int32_t Screen::runOnce()
|
||||
// but we should only call setTargetFPS when framestate changes, because
|
||||
// otherwise that breaks animations.
|
||||
|
||||
if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) {
|
||||
uint32_t desiredFramerate = IDLE_FRAMERATE;
|
||||
#if HAS_GPS && !defined(USE_EINK)
|
||||
if (showingNormalScreen && hasCompass) {
|
||||
const uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||||
if ((framesetInfo.positions.gps != 255 && currentFrame == framesetInfo.positions.gps) ||
|
||||
(framesetInfo.positions.waypoint != 255 && currentFrame == framesetInfo.positions.waypoint) ||
|
||||
(framesetInfo.positions.firstFavorite != 255 && currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||
currentFrame <= framesetInfo.positions.lastFavorite)) {
|
||||
desiredFramerate = COMPASS_ACTIVE_FRAMERATE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (targetFramerate != desiredFramerate && ui->getUiState()->frameState == FIXED) {
|
||||
// oldFrameState = ui->getUiState()->frameState;
|
||||
targetFramerate = IDLE_FRAMERATE;
|
||||
targetFramerate = desiredFramerate;
|
||||
|
||||
ui->setTargetFPS(targetFramerate);
|
||||
forceDisplay();
|
||||
|
||||
@@ -330,15 +330,11 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
// Function to allow the AccelerometerThread to set the heading if a sensor provides it
|
||||
// Mutex needed?
|
||||
void setHeading(long _heading)
|
||||
{
|
||||
hasCompass = true;
|
||||
compassHeading = fmod(_heading, 360);
|
||||
}
|
||||
void setHeading(float heading);
|
||||
|
||||
bool hasHeading() { return hasCompass; }
|
||||
|
||||
long getHeading() { return compassHeading; }
|
||||
float getHeading() { return compassHeading; }
|
||||
|
||||
void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; }
|
||||
uint32_t getEndCalibration() { return endCalibrationAt; }
|
||||
@@ -792,4 +788,4 @@ extern std::vector<std::string> functionSymbol;
|
||||
extern std::string functionSymbolString;
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1359,7 +1359,8 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
tft->displayOn();
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
|
||||
!defined(HELTEC_MESH_NODE_T1)
|
||||
tft->wakeup();
|
||||
tft->powerSaveOff();
|
||||
#endif
|
||||
@@ -1370,7 +1371,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
#ifdef UNPHONE
|
||||
unphone.backlight(true); // using unPhone library
|
||||
#endif
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
#elif !defined(M5STACK) && !defined(ST7789_CS) && \
|
||||
!defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
|
||||
tft->setBrightness(172);
|
||||
@@ -1386,7 +1387,8 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
tft->displayOff();
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
|
||||
!defined(HELTEC_MESH_NODE_T1)
|
||||
tft->sleep();
|
||||
tft->powerSaveOn();
|
||||
#endif
|
||||
@@ -1397,7 +1399,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
#ifdef UNPHONE
|
||||
unphone.backlight(false); // using unPhone library
|
||||
#endif
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
|
||||
tft->setBrightness(0);
|
||||
#endif
|
||||
@@ -1412,7 +1414,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
|
||||
void TFTDisplay::setDisplayBrightness(uint8_t _brightness)
|
||||
{
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
// todo
|
||||
#elif !defined(HACKADAY_COMMUNICATOR)
|
||||
tft->setBrightness(_brightness);
|
||||
@@ -1432,7 +1434,7 @@ bool TFTDisplay::hasTouch(void)
|
||||
{
|
||||
#ifdef RAK14014
|
||||
return true;
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1)
|
||||
return tft->touch() != nullptr;
|
||||
#else
|
||||
return false;
|
||||
@@ -1451,7 +1453,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1)
|
||||
return tft->getTouch(x, y);
|
||||
#else
|
||||
return false;
|
||||
@@ -1468,7 +1470,7 @@ bool TFTDisplay::connect()
|
||||
{
|
||||
concurrency::LockGuard g(spiLock);
|
||||
LOG_INFO("Do TFT init");
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
tft = new TFT_eSPI;
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||
|
||||
@@ -142,8 +142,9 @@ void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offset
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
} else {
|
||||
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
|
||||
cellH = KEY_HEIGHT;
|
||||
// Default (non-wide, non-64px) e.g. SH1107 128x128:
|
||||
// cellH = FONT_HEIGHT_SMALL - 2 so rows are tighter while still hosting the font
|
||||
cellH = std::max((int)KEY_HEIGHT, FONT_HEIGHT_SMALL - 2);
|
||||
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||
keyboardStartY = screenH - keyboardHeight;
|
||||
if (keyboardStartY < 0)
|
||||
@@ -446,11 +447,8 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
|
||||
if (textX < x)
|
||||
textX = x; // guard
|
||||
} else {
|
||||
if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) {
|
||||
textX = x + (width - textWidth + 1) / 2;
|
||||
} else {
|
||||
textX = x + (width - textWidth) / 2;
|
||||
}
|
||||
// Use ceil rounding for all screens (consistent with 128x64 behavior for numbers)
|
||||
textX = x + (width - textWidth + 1) / 2;
|
||||
}
|
||||
int contentTop = y;
|
||||
int contentH = height;
|
||||
@@ -746,4 +744,4 @@ bool VirtualKeyboard::isTimedOut() const
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -183,9 +183,13 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
static float segmentHeight = SEGMENT_HEIGHT * 0.75f;
|
||||
|
||||
if (!scaleInitialized) {
|
||||
#ifdef DISPLAY_FORCE_SMALL_FONTS
|
||||
float screenwidth_target_ratio = 0.70f; // Target 70% of display width (adjustable)
|
||||
#else
|
||||
float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable)
|
||||
float max_scale = 3.5f; // Safety limit to avoid runaway scaling
|
||||
float step = 0.05f; // Step increment per iteration
|
||||
#endif
|
||||
float max_scale = 3.5f; // Safety limit to avoid runaway scaling
|
||||
float step = 0.05f; // Step increment per iteration
|
||||
|
||||
float target_width = display->getWidth() * screenwidth_target_ratio;
|
||||
float target_height =
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include <cmath>
|
||||
@@ -21,8 +17,8 @@ struct Point {
|
||||
|
||||
void rotate(float angle)
|
||||
{
|
||||
float cos_a = cos(angle);
|
||||
float sin_a = sin(angle);
|
||||
float cos_a = cosf(angle);
|
||||
float sin_a = sinf(angle);
|
||||
float new_x = x * cos_a - y * sin_a;
|
||||
float new_y = x * sin_a + y * cos_a;
|
||||
x = new_x;
|
||||
@@ -51,21 +47,30 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
radius += 4;
|
||||
}
|
||||
Point north(0, -radius);
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
north.rotate(-myHeading);
|
||||
north.translate(compassX, compassY);
|
||||
float northX = 0.0f;
|
||||
float northY = -radius;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
|
||||
const float c = cosf(-myHeading);
|
||||
const float s = sinf(-myHeading);
|
||||
const float rx = northX * c - northY * s;
|
||||
const float ry = northX * s + northY * c;
|
||||
northX = rx;
|
||||
northY = ry;
|
||||
}
|
||||
northX += compassX;
|
||||
northY += compassY;
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setColor(BLACK);
|
||||
const int16_t nLabelWidth = display->getStringWidth("N");
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||
display->fillRect(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
|
||||
} else {
|
||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||
display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
display->drawString(north.x, north.y - 3, "N");
|
||||
display->drawString(northX, northY - 3, "N");
|
||||
}
|
||||
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||
@@ -113,11 +118,46 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f
|
||||
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
||||
}
|
||||
|
||||
float estimatedHeading(double lat, double lon)
|
||||
bool getHeadingRadians(double lat, double lon, float &headingRadian)
|
||||
{
|
||||
// Simple magnetic declination estimation
|
||||
// This is a very basic implementation - the original might be more sophisticated
|
||||
return 0.0f; // Return 0 for now, indicating no heading available
|
||||
headingRadian = 0.0f;
|
||||
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING)
|
||||
return true;
|
||||
|
||||
if (!screen)
|
||||
return false;
|
||||
|
||||
if (screen->hasHeading()) {
|
||||
headingRadian = screen->getHeading() * DEG_TO_RAD;
|
||||
return true;
|
||||
}
|
||||
|
||||
const float estimatedHeadingDeg = screen->estimatedHeading(lat, lon);
|
||||
if (!(estimatedHeadingDeg >= 0.0f))
|
||||
return false;
|
||||
|
||||
headingRadian = estimatedHeadingDeg * DEG_TO_RAD;
|
||||
return true;
|
||||
}
|
||||
|
||||
float adjustBearingForCompassMode(float bearingRadian, float headingRadian)
|
||||
{
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
return bearingRadian - headingRadian;
|
||||
|
||||
return bearingRadian;
|
||||
}
|
||||
|
||||
float radiansToDegrees360(float angleRadian)
|
||||
{
|
||||
constexpr float fullTurnDeg = 360.0f;
|
||||
float degrees = angleRadian * RAD_TO_DEG;
|
||||
if (degrees < 0.0f)
|
||||
degrees += fullTurnDeg;
|
||||
else if (degrees >= fullTurnDeg)
|
||||
degrees -= fullTurnDeg;
|
||||
return degrees;
|
||||
}
|
||||
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||
@@ -137,4 +177,4 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||
|
||||
} // namespace CompassRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "graphics/Screen.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
@@ -25,7 +24,9 @@ void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, u
|
||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing);
|
||||
|
||||
// Navigation and location functions
|
||||
float estimatedHeading(double lat, double lon);
|
||||
bool getHeadingRadians(double lat, double lon, float &headingRadian);
|
||||
float adjustBearingForCompassMode(float bearingRadian, float headingRadian);
|
||||
float radiansToDegrees360(float angleRadian);
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
||||
|
||||
} // namespace CompassRenderer
|
||||
|
||||
@@ -1256,6 +1256,11 @@ void menuHandler::positionBaseMenu()
|
||||
if (accelerometerThread) {
|
||||
accelerometerThread->calibrate(30);
|
||||
}
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
if (magnetometerThread) {
|
||||
magnetometerThread->calibrate(30);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case PositionAction::GPSSmartPosition:
|
||||
|
||||
@@ -373,14 +373,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen(distStr) > 0) {
|
||||
int offset = (currentResolution == ScreenResolution::High)
|
||||
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||
int rightEdge = x + columnWidth - offset;
|
||||
int textWidth = display->getStringWidth(distStr);
|
||||
display->drawString(rightEdge - textWidth, y, distStr);
|
||||
}
|
||||
const char *distanceLabel = (strlen(distStr) > 0) ? distStr : "?";
|
||||
int offset = (currentResolution == ScreenResolution::High)
|
||||
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||
int rightEdge = x + columnWidth - offset;
|
||||
int textWidth = display->getStringWidth(distanceLabel);
|
||||
display->drawString(rightEdge - textWidth, y, distanceLabel);
|
||||
}
|
||||
|
||||
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
@@ -431,8 +430,8 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
}
|
||||
}
|
||||
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
double userLat, double userLon)
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth,
|
||||
float myHeadingRadian, double userLat, double userLon)
|
||||
{
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
return;
|
||||
@@ -446,11 +445,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
double nodeLat = node->position.latitude_i * 1e-7;
|
||||
double nodeLon = node->position.longitude_i * 1e-7;
|
||||
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
|
||||
float bearingToNode = RAD_TO_DEG * bearing;
|
||||
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
|
||||
float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
|
||||
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
|
||||
// Shrink size by 2px
|
||||
int size = FONT_HEIGHT_SMALL - 5;
|
||||
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing);
|
||||
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearingDeg);
|
||||
/*
|
||||
float angle = relativeBearing * DEG_TO_RAD;
|
||||
float halfSize = size / 2.0;
|
||||
@@ -480,12 +479,27 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
*/
|
||||
}
|
||||
|
||||
void drawCompassUnknown(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float, double,
|
||||
double)
|
||||
{
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
return;
|
||||
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
||||
int centerX = x + columnWidth - arrowXOffset;
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(centerX, y, "?");
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Main Screen Functions
|
||||
// =============================
|
||||
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon)
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras, float headingRadian, double lat, double lon)
|
||||
{
|
||||
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
|
||||
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
||||
@@ -570,7 +584,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
renderer(display, node, xPos, yPos, columnWidth);
|
||||
|
||||
if (extras)
|
||||
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||
extras(display, node, xPos, yPos, columnWidth, headingRadian, lat, lon);
|
||||
|
||||
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
yOffset += rowYOffset;
|
||||
@@ -765,9 +779,13 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
#endif
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
float heading = 0;
|
||||
bool validHeading = false;
|
||||
float headingRadian = 0.0f;
|
||||
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (!ourNode || !nodeDB->hasValidPosition(ourNode)) {
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
double lat = DegD(ourNode->position.latitude_i);
|
||||
double lon = DegD(ourNode->position.longitude_i);
|
||||
|
||||
@@ -779,21 +797,12 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
lastSwitchTime = now;
|
||||
}
|
||||
#endif
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
#if HAS_GPS
|
||||
if (screen->hasHeading()) {
|
||||
heading = screen->getHeading(); // degrees
|
||||
validHeading = true;
|
||||
} else {
|
||||
heading = screen->estimatedHeading(lat, lon);
|
||||
validHeading = !isnan(heading);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!validHeading)
|
||||
return;
|
||||
if (!CompassRenderer::getHeadingRadians(lat, lon, headingRadian)) {
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, lat, lon);
|
||||
return;
|
||||
}
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon);
|
||||
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, headingRadian, lat, lon);
|
||||
}
|
||||
|
||||
/// Draw a series of fields in a column, wrapping to multiple columns if needed
|
||||
|
||||
@@ -32,7 +32,7 @@ enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATIO
|
||||
|
||||
// Main node list screen function
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float headingRadian = 0, double lat = 0,
|
||||
double lon = 0);
|
||||
|
||||
// Entry renderers
|
||||
@@ -43,8 +43,8 @@ void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
|
||||
// Extras renderers
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
double userLat, double userLon);
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth,
|
||||
float myHeadingRadian, double userLat, double userLon);
|
||||
|
||||
// Screen frame functions
|
||||
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
+129
-102
@@ -38,6 +38,15 @@ static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
}
|
||||
}
|
||||
|
||||
static void drawCompassStatusText(OLEDDisplay *display, int16_t compassX, int16_t compassY, const char *statusLine1,
|
||||
const char *statusLine2)
|
||||
{
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1);
|
||||
display->drawString(compassX, compassY, statusLine2);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
}
|
||||
|
||||
void graphics::UIRenderer::rebuildFavoritedNodes()
|
||||
{
|
||||
favoritedNodes.clear();
|
||||
@@ -638,51 +647,54 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
display->drawString(x, getTextPositions(display)[line++], batLine);
|
||||
}
|
||||
|
||||
bool showCompass = false;
|
||||
float myHeading = 0.0f;
|
||||
float bearing = 0.0f;
|
||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||
const bool hasNodePositionFix = nodeDB->hasValidPosition(node);
|
||||
const char *statusLine1 = nullptr;
|
||||
const char *statusLine2 = nullptr;
|
||||
if (hasOwnPositionFix && hasNodePositionFix) {
|
||||
const auto &op = ourNode->position;
|
||||
showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
|
||||
if (showCompass) {
|
||||
const auto &p = node->position;
|
||||
bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||
bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading);
|
||||
} else {
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Heading";
|
||||
}
|
||||
} else if (!hasOwnPositionFix || !hasNodePositionFix) {
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Fix";
|
||||
}
|
||||
|
||||
// --- Compass Rendering: landscape (wide) screens use the original side-aligned logic ---
|
||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||
bool showCompass = false;
|
||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) {
|
||||
showCompass = true;
|
||||
}
|
||||
if (showCompass) {
|
||||
if (showCompass || statusLine1) {
|
||||
const int16_t topY = getTextPositions(display)[1];
|
||||
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1);
|
||||
const int16_t usableHeight = bottomY - topY - 5;
|
||||
int16_t compassRadius = usableHeight / 2;
|
||||
if (compassRadius < 8)
|
||||
compassRadius = 8;
|
||||
const int16_t compassDiam = compassRadius * 2;
|
||||
const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8;
|
||||
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
||||
|
||||
const auto &op = ourNode->position;
|
||||
float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
|
||||
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
|
||||
const auto &p = node->position;
|
||||
/* unused
|
||||
float d =
|
||||
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
*/
|
||||
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
myHeading = 0;
|
||||
} else {
|
||||
bearing -= myHeading;
|
||||
}
|
||||
const int16_t compassDiam = compassRadius * 2;
|
||||
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing);
|
||||
if (showCompass) {
|
||||
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing);
|
||||
} else {
|
||||
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||
}
|
||||
}
|
||||
// else show nothing
|
||||
} else {
|
||||
// Portrait or square: put compass at the bottom and centered, scaled to fit available space
|
||||
bool showCompass = false;
|
||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) {
|
||||
showCompass = true;
|
||||
}
|
||||
if (showCompass) {
|
||||
if (showCompass || statusLine1) {
|
||||
int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2)
|
||||
: getTextPositions(display)[1];
|
||||
const int margin = 4;
|
||||
@@ -693,8 +705,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
#else
|
||||
const int navBarHeight = 0;
|
||||
#endif
|
||||
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
||||
// --------- END PATCH FOR EINK NAV BAR -----------
|
||||
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
||||
|
||||
if (availableHeight < FONT_HEIGHT_SMALL * 2)
|
||||
return;
|
||||
@@ -708,25 +720,13 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
int compassX = x + SCREEN_WIDTH / 2;
|
||||
int compassY = yBelowContent + availableHeight / 2;
|
||||
|
||||
const auto &op = ourNode->position;
|
||||
float myHeading = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
|
||||
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
}
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
|
||||
const auto &p = node->position;
|
||||
/* unused
|
||||
float d =
|
||||
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
*/
|
||||
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
|
||||
bearing -= myHeading;
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
|
||||
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
if (showCompass) {
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
|
||||
} else {
|
||||
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||
}
|
||||
}
|
||||
// else show nothing
|
||||
}
|
||||
@@ -1162,6 +1162,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
const int *textPos = getTextPositions(display);
|
||||
|
||||
// === First Row: My Location ===
|
||||
#if HAS_GPS
|
||||
@@ -1176,12 +1177,12 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
} else {
|
||||
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
||||
}
|
||||
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
|
||||
drawSatelliteIcon(display, x, textPos[line]);
|
||||
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0;
|
||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||
display->drawString(x + 11 + xOffset, textPos[line++], displayLine);
|
||||
} else {
|
||||
// Onboard GPS
|
||||
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
|
||||
UIRenderer::drawGps(display, 0, textPos[line++], gpsStatus);
|
||||
}
|
||||
|
||||
config.display.heading_bold = origBold;
|
||||
@@ -1190,18 +1191,36 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()),
|
||||
int32_t(gpsStatus->getAltitude()));
|
||||
|
||||
// === Determine Compass Heading ===
|
||||
float heading = 0;
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||
const bool hasLiveGpsFix =
|
||||
(gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0));
|
||||
const bool hasSensorHeading = screen->hasHeading();
|
||||
float heading = 0.0f;
|
||||
bool validHeading = false;
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
validHeading = true;
|
||||
} else {
|
||||
if (screen->hasHeading()) {
|
||||
heading = radians(screen->getHeading());
|
||||
validHeading = true;
|
||||
const char *statusLine1 = nullptr;
|
||||
const char *statusLine2 = nullptr;
|
||||
if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) {
|
||||
double headingLat = 0.0;
|
||||
double headingLon = 0.0;
|
||||
if (hasLiveGpsFix) {
|
||||
headingLat = DegD(gpsStatus->getLatitude());
|
||||
headingLon = DegD(gpsStatus->getLongitude());
|
||||
} else if (hasOwnPositionFix) {
|
||||
const auto &op = ourNode->position;
|
||||
headingLat = DegD(op.latitude_i);
|
||||
headingLon = DegD(op.longitude_i);
|
||||
}
|
||||
validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading);
|
||||
}
|
||||
|
||||
if (!validHeading) {
|
||||
if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) {
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Heading";
|
||||
} else {
|
||||
heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7);
|
||||
validHeading = !isnan(heading);
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Fix";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1219,18 +1238,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true);
|
||||
#endif
|
||||
|
||||
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
|
||||
display->drawString(0, textPos[line++], uptimeStr);
|
||||
} else {
|
||||
display->drawString(0, getTextPositions(display)[line++], "Last: ?");
|
||||
display->drawString(0, textPos[line++], "Last: ?");
|
||||
}
|
||||
|
||||
// === Third Row: Line 1 GPS Info ===
|
||||
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1");
|
||||
UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line1");
|
||||
|
||||
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC &&
|
||||
uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) {
|
||||
// === Fourth Row: Line 2 GPS Info ===
|
||||
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
|
||||
UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line2");
|
||||
}
|
||||
|
||||
// === Final Row: Altitude ===
|
||||
@@ -1241,14 +1260,14 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
} else {
|
||||
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
|
||||
}
|
||||
display->drawString(x, getTextPositions(display)[line++], altitudeLine);
|
||||
display->drawString(x, textPos[line++], altitudeLine);
|
||||
}
|
||||
#if !defined(OLED_TINY)
|
||||
// === Draw Compass if heading is valid ===
|
||||
if (validHeading) {
|
||||
// === Draw Compass ===
|
||||
if (validHeading || statusLine1) {
|
||||
// --- Compass Rendering: landscape (wide) screens use original side-aligned logic ---
|
||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||
const int16_t topY = getTextPositions(display)[1];
|
||||
const int16_t topY = textPos[1];
|
||||
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height
|
||||
const int16_t usableHeight = bottomY - topY - 5;
|
||||
|
||||
@@ -1261,29 +1280,33 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
// Center vertically and nudge down slightly to keep "N" clear of header
|
||||
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
||||
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading);
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
if (validHeading) {
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading);
|
||||
|
||||
// "N" label
|
||||
float northAngle = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
northAngle = -heading;
|
||||
float radius = compassRadius;
|
||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||
// "N" label
|
||||
float northAngle = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
northAngle = -heading;
|
||||
float radius = compassRadius;
|
||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||
} else {
|
||||
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||
}
|
||||
} else {
|
||||
// Portrait or square: put compass at the bottom and centered, scaled to fit available space
|
||||
// For E-Ink screens, account for navigation bar at the bottom!
|
||||
int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2;
|
||||
int yBelowContent = textPos[5] + FONT_HEIGHT_SMALL + 2;
|
||||
const int margin = 4;
|
||||
int availableHeight =
|
||||
#if defined(USE_EINK)
|
||||
@@ -1304,25 +1327,29 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
int compassX = x + SCREEN_WIDTH / 2;
|
||||
int compassY = yBelowContent + availableHeight / 2;
|
||||
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading);
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
if (validHeading) {
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading);
|
||||
|
||||
// "N" label
|
||||
float northAngle = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
northAngle = -heading;
|
||||
float radius = compassRadius;
|
||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||
// "N" label
|
||||
float northAngle = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
northAngle = -heading;
|
||||
float radius = compassRadius;
|
||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||
} else {
|
||||
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "HapticFeedback.h"
|
||||
|
||||
#ifdef HAPTIC_FEEDBACK_PIN
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef HAPTIC_FEEDBACK_ACTIVE_LOW
|
||||
#define HAPTIC_FEEDBACK_ON_STATE LOW
|
||||
#define HAPTIC_FEEDBACK_OFF_STATE HIGH
|
||||
#else
|
||||
#define HAPTIC_FEEDBACK_ON_STATE HIGH
|
||||
#define HAPTIC_FEEDBACK_OFF_STATE LOW
|
||||
#endif
|
||||
|
||||
HapticFeedback *hapticFeedback = nullptr;
|
||||
|
||||
void initHapticFeedback()
|
||||
{
|
||||
if (!hapticFeedback)
|
||||
hapticFeedback = new HapticFeedback();
|
||||
}
|
||||
|
||||
HapticFeedback::HapticFeedback() : concurrency::OSThread("Haptic")
|
||||
{
|
||||
pinMode(HAPTIC_FEEDBACK_PIN, OUTPUT);
|
||||
digitalWrite(HAPTIC_FEEDBACK_PIN, HAPTIC_FEEDBACK_OFF_STATE);
|
||||
}
|
||||
|
||||
void HapticFeedback::motorWrite(bool on)
|
||||
{
|
||||
digitalWrite(HAPTIC_FEEDBACK_PIN, on ? HAPTIC_FEEDBACK_ON_STATE : HAPTIC_FEEDBACK_OFF_STATE);
|
||||
}
|
||||
|
||||
void HapticFeedback::pulse(uint16_t durationMs)
|
||||
{
|
||||
motorWrite(true);
|
||||
pulseOffAt = millis() + durationMs;
|
||||
if (pulseOffAt == 0) // 0 is the "no pulse" sentinel
|
||||
pulseOffAt = 1;
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
void HapticFeedback::armDelayedPulse(uint16_t delayMs, uint16_t durationMs)
|
||||
{
|
||||
delayedPulseAt = millis() + delayMs;
|
||||
if (delayedPulseAt == 0)
|
||||
delayedPulseAt = 1;
|
||||
delayedPulseDuration = durationMs;
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
void HapticFeedback::cancelDelayedPulse()
|
||||
{
|
||||
delayedPulseAt = 0;
|
||||
}
|
||||
|
||||
void HapticFeedback::scheduleNext()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
uint32_t next = 0;
|
||||
if (pulseOffAt != 0)
|
||||
next = pulseOffAt;
|
||||
if (delayedPulseAt != 0 && (next == 0 || (int32_t)(delayedPulseAt - next) < 0))
|
||||
next = delayedPulseAt;
|
||||
if (next == 0)
|
||||
return;
|
||||
int32_t delay = (int32_t)(next - now);
|
||||
setIntervalFromNow(delay > 0 ? (unsigned long)delay : 0);
|
||||
}
|
||||
|
||||
int32_t HapticFeedback::runOnce()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
|
||||
if (pulseOffAt != 0 && (int32_t)(now - pulseOffAt) >= 0) {
|
||||
motorWrite(false);
|
||||
pulseOffAt = 0;
|
||||
}
|
||||
|
||||
if (delayedPulseAt != 0 && (int32_t)(now - delayedPulseAt) >= 0) {
|
||||
uint16_t dur = delayedPulseDuration;
|
||||
delayedPulseAt = 0;
|
||||
pulse(dur);
|
||||
}
|
||||
|
||||
uint32_t next = 0;
|
||||
if (pulseOffAt != 0)
|
||||
next = pulseOffAt;
|
||||
if (delayedPulseAt != 0 && (next == 0 || (int32_t)(delayedPulseAt - next) < 0))
|
||||
next = delayedPulseAt;
|
||||
if (next == 0)
|
||||
return 60 * 1000;
|
||||
int32_t delay = (int32_t)(next - now);
|
||||
return delay > 0 ? delay : 0;
|
||||
}
|
||||
|
||||
#endif // HAPTIC_FEEDBACK_PIN
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef HAPTIC_FEEDBACK_PIN
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <stdint.h>
|
||||
|
||||
// Non-blocking pulses on a GPIO vibration motor. HAPTIC_FEEDBACK_ACTIVE_LOW inverts polarity.
|
||||
class HapticFeedback : public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
HapticFeedback();
|
||||
void pulse(uint16_t durationMs = 30);
|
||||
void armDelayedPulse(uint16_t delayMs, uint16_t durationMs = 30);
|
||||
void cancelDelayedPulse();
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
uint32_t pulseOffAt = 0;
|
||||
uint32_t delayedPulseAt = 0;
|
||||
uint16_t delayedPulseDuration = 0;
|
||||
|
||||
void motorWrite(bool on);
|
||||
// Reschedule to the soonest pending event so later arms don't clobber earlier wakes.
|
||||
void scheduleNext();
|
||||
};
|
||||
|
||||
extern HapticFeedback *hapticFeedback;
|
||||
void initHapticFeedback();
|
||||
|
||||
#endif // HAPTIC_FEEDBACK_PIN
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "PowerFSM.h" // needed for event trigger
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "input/HapticFeedback.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
@@ -237,6 +238,16 @@ void InputBroker::Init()
|
||||
}
|
||||
touchBacklightActive = false;
|
||||
};
|
||||
#endif
|
||||
#if defined(HAPTIC_FEEDBACK_PIN)
|
||||
// Blip on touch, second blip when long-press fires (500 ms = touchConfig.longPressTime default).
|
||||
touchConfig.suppressLeadUpSound = true;
|
||||
initHapticFeedback();
|
||||
touchConfig.onPress = []() {
|
||||
hapticFeedback->pulse(80);
|
||||
hapticFeedback->armDelayedPulse(500, 80);
|
||||
};
|
||||
touchConfig.onRelease = []() { hapticFeedback->cancelDelayedPulse(); };
|
||||
#endif
|
||||
TouchButtonThread->initButton(touchConfig);
|
||||
#endif
|
||||
@@ -372,8 +383,10 @@ void InputBroker::Init()
|
||||
rotaryEncoderInterruptImpl1 = nullptr;
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
cardKbI2cImpl = new CardKbI2cImpl();
|
||||
cardKbI2cImpl->init();
|
||||
#endif
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
i2cButton = new i2cButtonThread("i2cButtonThread");
|
||||
#endif
|
||||
|
||||
+50
-5
@@ -127,6 +127,10 @@ void printPartitionTable()
|
||||
#include "motion/AccelerometerThread.h"
|
||||
AccelerometerThread *accelerometerThread = nullptr;
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
#include "motion/MagnetometerThread.h"
|
||||
MagnetometerThread *magnetometerThread = nullptr;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_I2S
|
||||
#include "AudioThread.h"
|
||||
@@ -197,6 +201,8 @@ bool osk_found = false;
|
||||
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
|
||||
// The I2C address of the Accelerometer (if found)
|
||||
ScanI2C::DeviceAddress accelerometer_found = ScanI2C::ADDRESS_NONE;
|
||||
// The I2C address of the Magnetometer (if found)
|
||||
ScanI2C::DeviceAddress magnetometer_found = ScanI2C::ADDRESS_NONE;
|
||||
// The I2C address of the RGB LED (if found)
|
||||
ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ScanI2C::ADDRESS_NONE);
|
||||
/// The I2C address of our Air Quality Indicator (if found)
|
||||
@@ -215,6 +221,8 @@ bool pauseBluetoothLogging = false;
|
||||
|
||||
bool pmu_found;
|
||||
|
||||
uint8_t pa_fan_percentage = 50;
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
// Array map of sensor types with i2c address and wire as we'll find in the i2c scan
|
||||
std::pair<uint8_t, TwoWire *> nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1] = {};
|
||||
@@ -422,6 +430,11 @@ void setup()
|
||||
digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power
|
||||
#endif
|
||||
|
||||
#if defined(PIN_SENSOR_EN)
|
||||
pinMode(PIN_SENSOR_EN, OUTPUT);
|
||||
digitalWrite(PIN_SENSOR_EN, PIN_SENSOR_EN_ACTIVE); // turn on sensor power
|
||||
#endif
|
||||
|
||||
#if defined(BIAS_T_ENABLE)
|
||||
pinMode(BIAS_T_ENABLE, OUTPUT);
|
||||
digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna
|
||||
@@ -662,6 +675,11 @@ void setup()
|
||||
accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found;
|
||||
LOG_DEBUG("acc_info = %i", acc_info.type);
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
auto mag_info = i2cScanner->firstMagnetometer();
|
||||
magnetometer_found = mag_info.type != ScanI2C::DeviceType::NONE ? mag_info.address : magnetometer_found;
|
||||
LOG_DEBUG("mag_info = %i", mag_info.type);
|
||||
#endif
|
||||
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226);
|
||||
@@ -674,6 +692,8 @@ void setup()
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MMC5983MA, meshtastic_TelemetrySensorType_MMC5983MA);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM42607P, meshtastic_TelemetrySensorType_ICM42607P);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
|
||||
@@ -754,6 +774,11 @@ void setup()
|
||||
accelerometerThread = new AccelerometerThread(acc_info.type);
|
||||
}
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
if (mag_info.type != ScanI2C::DeviceType::NONE) {
|
||||
magnetometerThread = new MagnetometerThread(mag_info.type);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(HAS_NEOPIXEL) || defined(UNPHONE) || defined(RGBLED_RED)
|
||||
ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE);
|
||||
@@ -971,16 +996,36 @@ void setup()
|
||||
mqttInit();
|
||||
#endif
|
||||
|
||||
#ifdef RF95_FAN_EN
|
||||
// Ability to disable FAN if PIN has been set with RF95_FAN_EN.
|
||||
#ifdef RADIO_FAN_EN
|
||||
// Ability to disable FAN if PIN has been set with RADIO_FAN_EN.
|
||||
// Make sure LoRa has been started before disabling FAN.
|
||||
if (config.lora.pa_fan_disabled)
|
||||
digitalWrite(RF95_FAN_EN, LOW ^ 0);
|
||||
#ifdef RADIO_FAN_PWM
|
||||
#if defined(ARCH_ESP32)
|
||||
// Set up PWM at Channel 1 at 25KHz, using 8-bit resolution
|
||||
// Turn ON/OFF fan to the specified value if enabled by config.
|
||||
// code by https://github.com/gjelsoe/
|
||||
if (ledcSetup(1, 25000, 8)) {
|
||||
ledcAttachPin(RADIO_FAN_EN, 1);
|
||||
LOG_INFO("PWM init C1 P%d\n", RADIO_FAN_EN);
|
||||
// Set PWM duty cycle based on fan disabled state
|
||||
ledcWrite(1, config.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55));
|
||||
} else {
|
||||
LOG_WARN("PWM init fail P%d\n", RADIO_FAN_EN);
|
||||
}
|
||||
#elif defined(ARCH_NRF52)
|
||||
pinMode(RADIO_FAN_EN, OUTPUT);
|
||||
analogWrite(RADIO_FAN_EN, config.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55));
|
||||
#endif
|
||||
#else
|
||||
// Set up as ON/OFF switch of fan; default on unless disabled by config.
|
||||
pinMode(RADIO_FAN_EN, OUTPUT);
|
||||
digitalWrite(RADIO_FAN_EN, config.lora.pa_fan_disabled ? (LOW ^ 0) : (HIGH ^ 0));
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef ARCH_PORTDUINO
|
||||
|
||||
// Initialize Wifi
|
||||
// Initialize Wifi
|
||||
#if HAS_WIFI
|
||||
initWifi();
|
||||
#endif
|
||||
|
||||
@@ -35,6 +35,7 @@ extern bool kb_found;
|
||||
extern bool osk_found;
|
||||
extern ScanI2C::DeviceAddress rtc_found;
|
||||
extern ScanI2C::DeviceAddress accelerometer_found;
|
||||
extern ScanI2C::DeviceAddress magnetometer_found;
|
||||
extern ScanI2C::FoundDevice rgb_found;
|
||||
extern ScanI2C::DeviceAddress aqi_found;
|
||||
|
||||
@@ -69,6 +70,10 @@ extern graphics::Screen *screen;
|
||||
#include "motion/AccelerometerThread.h"
|
||||
extern AccelerometerThread *accelerometerThread;
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
#include "motion/MagnetometerThread.h"
|
||||
extern MagnetometerThread *magnetometerThread;
|
||||
#endif
|
||||
|
||||
extern bool isVibrating;
|
||||
|
||||
@@ -85,6 +90,8 @@ extern bool suppressRebootBanner;
|
||||
|
||||
extern uint32_t serialSinceMsec;
|
||||
|
||||
extern uint8_t pa_fan_percentage;
|
||||
|
||||
// If a thread does something that might need for it to be rescheduled ASAP it can set this flag
|
||||
// This will suppress the current delay and instead try to run ASAP.
|
||||
extern bool runASAP;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#if RADIOLIB_EXCLUDE_LR11X0 != 1
|
||||
#include "LR11x0Interface.h"
|
||||
#include "RadioExternalPa.h"
|
||||
#include "Throttle.h"
|
||||
#include "configuration.h"
|
||||
#include "error.h"
|
||||
#include "main.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
#ifdef LR11X0_DIO_AS_RF_SWITCH
|
||||
#include "rfswitch.h"
|
||||
@@ -54,6 +56,8 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
digitalWrite(LR11X0_POWER_EN, HIGH);
|
||||
#endif
|
||||
|
||||
enableFan();
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
|
||||
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE
|
||||
@@ -184,8 +188,10 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
if (power > LR1110_MAX_POWER) // This chip has lower power limits than some
|
||||
power = LR1110_MAX_POWER;
|
||||
// Re-apply regulatory limits and any external-PA power mapping. applyModemConfig()
|
||||
// (run by the base reconfigure()) reset `power` to the requested total, so without
|
||||
// this a runtime tx_power change would skip region clamping and external-PA mapping.
|
||||
limitPower(LR1110_MAX_POWER);
|
||||
if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) // 2.4G power limit
|
||||
power = LR1120_MAX_POWER;
|
||||
|
||||
@@ -236,6 +242,7 @@ template <typename T> void LR11x0Interface<T>::addReceiveMetadata(meshtastic_Mes
|
||||
*/
|
||||
template <typename T> void LR11x0Interface<T>::configHardwareForSend()
|
||||
{
|
||||
radioExternalPaTxEnable(); // bias an external PA (if any) before we transmit
|
||||
RadioLibInterface::configHardwareForSend();
|
||||
}
|
||||
|
||||
@@ -250,6 +257,8 @@ template <typename T> void LR11x0Interface<T>::startReceive()
|
||||
|
||||
setStandby();
|
||||
|
||||
radioExternalPaRxIdle(); // drop external PA bias while receiving/idle
|
||||
|
||||
lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed.
|
||||
|
||||
// We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly.
|
||||
@@ -337,6 +346,8 @@ template <typename T> bool LR11x0Interface<T>::sleep()
|
||||
LOG_DEBUG("LR11x0 entering sleep mode");
|
||||
setStandby(); // Stop any pending operations
|
||||
|
||||
radioExternalPaSleep(); // power down an external PA (if any)
|
||||
|
||||
// turn off TCXO if it was powered
|
||||
lora.setTCXO(0);
|
||||
|
||||
@@ -350,4 +361,10 @@ template <typename T> bool LR11x0Interface<T>::sleep()
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> int16_t LR11x0Interface<T>::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora.getRSSI();
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -37,6 +37,8 @@ template <class T> class LR11x0Interface : public RadioLibInterface
|
||||
*/
|
||||
T lora;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
|
||||
#include "LR20x0Interface.h"
|
||||
#include "error.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
|
||||
// Keep LR20x0 naming while RadioLib exposes LR2021 symbols.
|
||||
#ifndef LR20x0
|
||||
#define LR20x0 LR2021
|
||||
#endif
|
||||
|
||||
#ifdef LR2021_DIO_AS_RF_SWITCH
|
||||
#include "rfswitch.h"
|
||||
#elif ARCH_PORTDUINO
|
||||
#include "PortduinoGlue.h"
|
||||
#define lr20x0_rfswitch_dio_pins portduino_config.rfswitch_dio_pins
|
||||
#define lr20x0_rfswitch_table portduino_config.rfswitch_table
|
||||
#else
|
||||
static const uint32_t lr20x0_rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
|
||||
static const Module::RfSwitchMode_t lr20x0_rfswitch_table[] = {
|
||||
{LR20x0::MODE_STBY, {}}, {LR20x0::MODE_RX, {}}, {LR20x0::MODE_TX, {}},
|
||||
{LR20x0::MODE_RX_HF, {}}, {LR20x0::MODE_TX_HF, {}}, END_OF_MODE_TABLE,
|
||||
};
|
||||
#endif
|
||||
|
||||
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
|
||||
// specified (may be dangerous if using external PA and LR20x0 power config forgotten)
|
||||
#if ARCH_PORTDUINO
|
||||
#define LR2021_MAX_POWER portduino_config.lr2021_max_power
|
||||
#endif
|
||||
#ifndef LR2021_MAX_POWER
|
||||
#define LR2021_MAX_POWER 22
|
||||
#endif
|
||||
|
||||
// the 2.4G part maxes at 12dBm
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
#define LR2021_MAX_POWER_HF portduino_config.lr2021_max_power_hf
|
||||
#endif
|
||||
#ifndef LR2021_MAX_POWER_HF
|
||||
#define LR2021_MAX_POWER_HF 12
|
||||
#endif
|
||||
|
||||
template <typename T>
|
||||
LR20x0Interface<T>::LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||||
RADIOLIB_PIN_TYPE busy)
|
||||
: RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module)
|
||||
{
|
||||
LOG_WARN("LR20x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy);
|
||||
}
|
||||
|
||||
/// Initialise the Driver transport hardware and software.
|
||||
/// Make sure the Driver is properly configured before calling init().
|
||||
/// \return true if initialisation succeeded.
|
||||
template <typename T> bool LR20x0Interface<T>::init()
|
||||
{
|
||||
#ifdef LR2021_POWER_EN
|
||||
pinMode(LR2021_POWER_EN, OUTPUT);
|
||||
digitalWrite(LR2021_POWER_EN, HIGH);
|
||||
#endif
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
|
||||
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR20x0_DIO3_TCXO_VOLTAGE
|
||||
#elif defined(LR2021_DIO3_TCXO_VOLTAGE)
|
||||
float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE;
|
||||
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR2021_DIO3_TCXO_VOLTAGE);
|
||||
// (DIO3 is not free to be used as an IRQ)
|
||||
#elif defined(TCXO_OPTIONAL)
|
||||
float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure
|
||||
LOG_DEBUG("TCXO_OPTIONAL: no LR2021_DIO3_TCXO_VOLTAGE defined, trying default TCXO Vref 1.6 V first");
|
||||
#else
|
||||
float tcxoVoltage =
|
||||
0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
|
||||
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
|
||||
// (DIO3 is free to be used as an IRQ)
|
||||
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
|
||||
#endif
|
||||
|
||||
RadioLibInterface::init();
|
||||
|
||||
#ifdef LR2021_IRQ_DIO_NUM
|
||||
lora.irqDioNum = LR2021_IRQ_DIO_NUM;
|
||||
LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum);
|
||||
#elif defined(IRQ_DIO_NUM)
|
||||
lora.irqDioNum = IRQ_DIO_NUM;
|
||||
LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum);
|
||||
#else
|
||||
LOG_DEBUG("Use default irqDioNum %d", lora.irqDioNum);
|
||||
#endif
|
||||
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
||||
limitPower(LR2021_MAX_POWER_HF);
|
||||
} else {
|
||||
limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range
|
||||
}
|
||||
|
||||
#ifdef LR2021_RF_SWITCH_SUBGHZ
|
||||
pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT);
|
||||
digitalWrite(LR2021_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW);
|
||||
LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
|
||||
#endif
|
||||
|
||||
#ifdef LR2021_RF_SWITCH_2_4GHZ
|
||||
pinMode(LR2021_RF_SWITCH_2_4GHZ, OUTPUT);
|
||||
digitalWrite(LR2021_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH);
|
||||
LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
|
||||
#endif
|
||||
|
||||
// Allow extra time for TCXO to stabilize after power-on
|
||||
delay(10);
|
||||
|
||||
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
|
||||
// Retry if we get SPI command failed - some units need extra TCXO stabilization time
|
||||
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
|
||||
LOG_WARN("LR20x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res);
|
||||
delay(100);
|
||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
}
|
||||
|
||||
#if defined(TCXO_OPTIONAL)
|
||||
// If init failed for any reason other than chip not found, retry without TCXO (XTAL mode)
|
||||
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
|
||||
LOG_WARN("LR20x0 init failed with TCXO Vref %f V (err %d), retrying without TCXO", tcxoVoltage, res);
|
||||
tcxoVoltage = 0;
|
||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
LOG_INFO("LR20x0 init success without TCXO (XTAL mode)");
|
||||
}
|
||||
#endif
|
||||
|
||||
// \todo Display actual typename of the adapter, not just `LR20x0`
|
||||
LOG_INFO("LR20x0 init result %d", res);
|
||||
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
||||
return false;
|
||||
|
||||
LOG_INFO("Frequency set to %f", getFreq());
|
||||
LOG_INFO("Bandwidth set to %f", bw);
|
||||
LOG_INFO("Power output set to %d", power);
|
||||
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
res = lora.setCRC(2);
|
||||
|
||||
#ifdef LR2021_DIO_AS_RF_SWITCH
|
||||
bool dioAsRfSwitch = true;
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
bool dioAsRfSwitch = portduino_config.has_rfswitch_table;
|
||||
#else
|
||||
bool dioAsRfSwitch = false;
|
||||
#endif
|
||||
|
||||
if (dioAsRfSwitch) {
|
||||
lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table);
|
||||
LOG_DEBUG("Set DIO RF switch");
|
||||
}
|
||||
|
||||
if (res == RADIOLIB_ERR_NONE) {
|
||||
if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate
|
||||
res = lora.setRxBoostedGainMode(true);
|
||||
LOG_INFO("Set RX gain to boosted mode; result: %d", res);
|
||||
} else {
|
||||
res = lora.setRxBoostedGainMode(false);
|
||||
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res);
|
||||
}
|
||||
}
|
||||
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
startReceive(); // start receiving
|
||||
|
||||
return res == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
template <typename T> bool LR20x0Interface<T>::reconfigure()
|
||||
{
|
||||
RadioLibInterface::reconfigure();
|
||||
|
||||
// set mode to standby
|
||||
setStandby();
|
||||
|
||||
// configure publicly accessible settings
|
||||
int err = lora.setSpreadingFactor(sf);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
err = lora.setBandwidth(bw); // different form than LR11xx
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
err = lora.setSyncWord(syncWord);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
||||
limitPower(LR2021_MAX_POWER_HF);
|
||||
} else {
|
||||
limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range
|
||||
}
|
||||
|
||||
err = lora.setPreambleLength(preambleLength);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
err = lora.setFrequency(getFreq());
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
err = lora.setOutputPower(power);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
// Apply RX gain mode — valid in STDBY, matches resetAGC() pattern
|
||||
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err);
|
||||
|
||||
startReceive(); // restart receiving
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> void LR20x0Interface<T>::disableInterrupt()
|
||||
{
|
||||
lora.clearIrqAction();
|
||||
}
|
||||
|
||||
template <typename T> void LR20x0Interface<T>::setStandby()
|
||||
{
|
||||
checkNotification(); // handle any pending interrupts before we force standby
|
||||
|
||||
int err = lora.standby();
|
||||
|
||||
if (err != RADIOLIB_ERR_NONE) {
|
||||
LOG_DEBUG("LR20x0 standby failed with error %d", err);
|
||||
}
|
||||
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
isReceiving = false; // If we were receiving, not any more
|
||||
activeReceiveStart = 0;
|
||||
disableInterrupt();
|
||||
completeSending(); // If we were sending, not anymore
|
||||
RadioLibInterface::setStandby();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add SNR data to received messages
|
||||
*/
|
||||
template <typename T> void LR20x0Interface<T>::addReceiveMetadata(meshtastic_MeshPacket *mp)
|
||||
{
|
||||
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
||||
mp->rx_snr = lora.getSNR();
|
||||
mp->rx_rssi = lround(lora.getRSSI());
|
||||
// LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); // not implemented for LR20x0, but noop for LR11x0
|
||||
// too(!)
|
||||
}
|
||||
|
||||
/** We override to turn on transmitter power as needed.
|
||||
*/
|
||||
template <typename T> void LR20x0Interface<T>::configHardwareForSend()
|
||||
{
|
||||
RadioLibInterface::configHardwareForSend();
|
||||
}
|
||||
|
||||
// For power draw measurements, helpful to force radio to stay sleeping
|
||||
// #define SLEEP_ONLY
|
||||
|
||||
template <typename T> void LR20x0Interface<T>::startReceive()
|
||||
{
|
||||
#ifdef SLEEP_ONLY
|
||||
sleep();
|
||||
#else
|
||||
|
||||
setStandby();
|
||||
|
||||
lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed.
|
||||
|
||||
// We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly.
|
||||
int err =
|
||||
lora.startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
|
||||
if (err)
|
||||
LOG_ERROR("StartReceive error: %d", err);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
RadioLibInterface::startReceive();
|
||||
|
||||
// Must be done AFTER starting receive, because startReceive clears (possibly stale) interrupt pending register bits
|
||||
enableInterrupt(isrRxLevel0);
|
||||
checkRxDoneIrqFlag();
|
||||
#endif
|
||||
}
|
||||
|
||||
/** Is the channel currently active? */
|
||||
template <typename T> bool LR20x0Interface<T>::isChannelActive()
|
||||
{
|
||||
// check if we can detect a LoRa preamble on the current channel
|
||||
ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD,
|
||||
.detPeak = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
|
||||
.detMin = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
|
||||
.exitMode = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
|
||||
.timeout = 0,
|
||||
.irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS,
|
||||
.irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}};
|
||||
int16_t result;
|
||||
|
||||
setStandby();
|
||||
result = lora.scanChannel(cfg);
|
||||
if (result == RADIOLIB_LORA_DETECTED)
|
||||
return true;
|
||||
|
||||
assert(result != RADIOLIB_ERR_WRONG_MODEM);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Could we send right now (i.e. either not actively receiving or transmitting)? */
|
||||
template <typename T> bool LR20x0Interface<T>::isActivelyReceiving()
|
||||
{
|
||||
// The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet
|
||||
// received and handled the interrupt for reading the packet/handling errors.
|
||||
return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID, RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED);
|
||||
}
|
||||
|
||||
#ifdef LR20X0_AGC_RESET
|
||||
template <typename T> void LR20x0Interface<T>::resetAGC()
|
||||
{
|
||||
// Safety: don't reset mid-packet
|
||||
if (sendingPacket != NULL || (isReceiving && isActivelyReceiving()))
|
||||
return;
|
||||
|
||||
LOG_DEBUG("LR20x0 AGC reset: warm sleep + Calibrate(0x3F)");
|
||||
|
||||
// 1. Warm sleep — powers down the analog frontend, resetting AGC state
|
||||
lora.sleep(true, 0);
|
||||
|
||||
// 2. Wake to RC standby for stable calibration
|
||||
lora.standby(RADIOLIB_LR20X0_STANDBY_RC, true);
|
||||
|
||||
// 3. Calibrate all blocks (PLL, ADC, image, RC oscillators)
|
||||
// calibrate() is protected on LR20x0, so use raw SPI (same as internal implementation)
|
||||
uint8_t calData = RADIOLIB_LR20X0_CALIBRATE_ALL;
|
||||
module.SPIwriteStream(RADIOLIB_LR20X0_CMD_CALIBRATE, &calData, 1, true, true);
|
||||
|
||||
// 4. Re-calibrate image rejection for actual operating frequency
|
||||
// Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions.
|
||||
lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f);
|
||||
|
||||
// 5. Re-apply RX boosted gain mode
|
||||
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
|
||||
// 6. Resume receiving
|
||||
startReceive();
|
||||
}
|
||||
#endif
|
||||
|
||||
template <typename T> bool LR20x0Interface<T>::sleep()
|
||||
{
|
||||
// \todo Display actual typename of the adapter, not just `LR20x0`
|
||||
LOG_DEBUG("LR20x0 entering sleep mode");
|
||||
setStandby(); // Stop any pending operations
|
||||
|
||||
// turn off TCXO if it was powered
|
||||
lora.setTCXO(0);
|
||||
|
||||
// put chipset into sleep mode (we've already disabled interrupts by now)
|
||||
bool keepConfig = false;
|
||||
lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed
|
||||
|
||||
#ifdef LR2021_POWER_EN
|
||||
digitalWrite(LR2021_POWER_EN, LOW);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> int16_t LR20x0Interface<T>::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora.getRSSI();
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
#if RADIOLIB_EXCLUDE_LR2021 != 1
|
||||
#include "RadioLibInterface.h"
|
||||
|
||||
/**
|
||||
* \brief Adapter for LR20x0 radio family. Implements common logic for child classes.
|
||||
* \tparam T RadioLib module type for LR20x0, e.g. LR2021.
|
||||
*/
|
||||
template <class T> class LR20x0Interface : public RadioLibInterface
|
||||
{
|
||||
public:
|
||||
LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||||
RADIOLIB_PIN_TYPE busy);
|
||||
|
||||
/// Initialise the Driver transport hardware and software.
|
||||
/// Make sure the Driver is properly configured before calling init().
|
||||
/// \return true if initialisation succeeded.
|
||||
virtual bool init() override;
|
||||
|
||||
/// Apply any radio provisioning changes
|
||||
/// Make sure the Driver is properly configured before calling init().
|
||||
/// \return true if initialisation succeeded.
|
||||
virtual bool reconfigure() override;
|
||||
|
||||
/// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep.
|
||||
virtual bool sleep() override;
|
||||
|
||||
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
|
||||
|
||||
#ifdef LR20X0_AGC_RESET
|
||||
void resetAGC() override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Specific module instance
|
||||
*/
|
||||
T lora;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
virtual void disableInterrupt() override;
|
||||
|
||||
/**
|
||||
* Enable a particular ISR callback glue function
|
||||
*/
|
||||
virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); }
|
||||
|
||||
/** can we detect a LoRa preamble on the current channel? */
|
||||
virtual bool isChannelActive() override;
|
||||
|
||||
/** are we actively receiving a packet (only called during receiving state) */
|
||||
virtual bool isActivelyReceiving() override;
|
||||
|
||||
/**
|
||||
* Start waiting to receive a message
|
||||
*/
|
||||
virtual void startReceive() override;
|
||||
|
||||
/**
|
||||
* We override to turn on transmitter power as needed.
|
||||
*/
|
||||
virtual void configHardwareForSend() override;
|
||||
|
||||
/**
|
||||
* Add SNR data to received messages
|
||||
*/
|
||||
virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override;
|
||||
|
||||
virtual void setStandby() override;
|
||||
|
||||
uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); }
|
||||
};
|
||||
#endif
|
||||
@@ -4,17 +4,19 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
|
||||
uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel)
|
||||
{
|
||||
const meshtastic_Channel &channel = channels.getByIndex(channelIndex);
|
||||
|
||||
if (channel.settings.has_module_settings) {
|
||||
return channel.settings.module_settings.position_precision;
|
||||
} else if (channel.role == meshtastic_Channel_Role_PRIMARY) {
|
||||
return 32;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
// No module settings: fail closed. A PRIMARY channel used to default to 32
|
||||
// here, leaking an exact position on a sharing-disabled channel. See #10509.
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
|
||||
{
|
||||
return getPositionPrecisionForChannel(channels.getByIndex(channelIndex));
|
||||
}
|
||||
|
||||
static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "meshtastic/channel.pb.h"
|
||||
#include "meshtastic/mesh.pb.h"
|
||||
#include <stdint.h>
|
||||
|
||||
uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel);
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex);
|
||||
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision);
|
||||
bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision);
|
||||
|
||||
@@ -154,10 +154,7 @@ bool RF95Interface::init()
|
||||
digitalWrite(RF95_TXEN, 0);
|
||||
#endif
|
||||
|
||||
#ifdef RF95_FAN_EN
|
||||
pinMode(RF95_FAN_EN, OUTPUT);
|
||||
digitalWrite(RF95_FAN_EN, 1);
|
||||
#endif
|
||||
enableFan();
|
||||
|
||||
#ifdef RF95_RXEN
|
||||
pinMode(RF95_RXEN, OUTPUT);
|
||||
@@ -335,11 +332,14 @@ bool RF95Interface::sleep()
|
||||
// put chipset into sleep mode
|
||||
setStandby(); // First cancel any active receiving/sending
|
||||
lora->sleep();
|
||||
|
||||
#ifdef RF95_FAN_EN
|
||||
digitalWrite(RF95_FAN_EN, 0);
|
||||
#endif
|
||||
disableFan();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int16_t RF95Interface::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora->getRSSI(false);
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
#endif
|
||||
@@ -37,6 +37,8 @@ class RF95Interface : public RadioLibInterface
|
||||
*/
|
||||
virtual void disableInterrupt() override;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Enable a particular ISR callback glue function
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#include "RadioExternalPa.h"
|
||||
|
||||
// Weak default implementations: no external PA. A board with an external PA
|
||||
// provides strong overrides (see e.g.
|
||||
// src/platform/extra_variants/radiomaster_nomad_gemini/variant.cpp).
|
||||
|
||||
int8_t __attribute__((weak)) radioExternalPaMapPower(int8_t requestedTotalDbm, float freqHz)
|
||||
{
|
||||
(void)freqHz;
|
||||
(void)requestedTotalDbm;
|
||||
return RADIO_EXTERNAL_PA_NO_MAP;
|
||||
}
|
||||
|
||||
void __attribute__((weak)) radioExternalPaTxEnable() {}
|
||||
|
||||
void __attribute__((weak)) radioExternalPaRxIdle() {}
|
||||
|
||||
void __attribute__((weak)) radioExternalPaSleep() {}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* Generic hooks for boards with an external power amplifier whose gain/bias is
|
||||
* controlled outside the LoRa transceiver (e.g. an analog PA biased through a DAC
|
||||
* pin, as on the RadioMaster Nomad Gemini).
|
||||
*
|
||||
* All four functions have weak no-op / pass-through defaults (see RadioExternalPa.cpp),
|
||||
* so boards without an external PA are completely unaffected. A board provides an
|
||||
* external PA by giving STRONG overrides (typically in its
|
||||
* src/platform/extra_variants/<board>/variant.cpp).
|
||||
*
|
||||
* Relationship to the existing TX_GAIN_LORA / LoRaFEMInterface mechanisms:
|
||||
* - TX_GAIN_LORA assumes the chip output index is a non-negative dBm value.
|
||||
* - LoRaFEMInterface models *digital* front-end modules (discrete enable pins).
|
||||
* An analog PA that needs the transceiver driven at negative dBm fits neither, so it
|
||||
* is handled here instead.
|
||||
*/
|
||||
|
||||
/// Sentinel returned by radioExternalPaMapPower() when the board has no external PA.
|
||||
#define RADIO_EXTERNAL_PA_NO_MAP INT8_MIN
|
||||
|
||||
/**
|
||||
* Map a desired *total* radiated output power to the transceiver chip output power.
|
||||
*
|
||||
* Called from RadioInterface::limitPower() AFTER regional/regulatory clamping, so
|
||||
* @p requestedTotalDbm is already the legal total output we want out of the antenna.
|
||||
* The override returns the chip output power (dBm, may be negative) that, combined
|
||||
* with the external PA gain, yields that total, and configures/stashes the PA bias.
|
||||
*
|
||||
* @param requestedTotalDbm desired total output power in dBm (already region-limited)
|
||||
* @param freqHz operating frequency in Hz (for band-dependent PAs)
|
||||
* @return chip output power in dBm, or RADIO_EXTERNAL_PA_NO_MAP if no external PA
|
||||
*/
|
||||
int8_t radioExternalPaMapPower(int8_t requestedTotalDbm, float freqHz);
|
||||
|
||||
/// Engage the external PA bias for transmit (called just before a transmission).
|
||||
void radioExternalPaTxEnable();
|
||||
|
||||
/// Drop the external PA bias for receive/idle (called when entering RX/standby).
|
||||
void radioExternalPaRxIdle();
|
||||
|
||||
/// Power the external PA fully down (called when the radio goes to sleep).
|
||||
void radioExternalPaSleep();
|
||||
+35
-22
@@ -9,6 +9,7 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RF95Interface.h"
|
||||
#include "RadioExternalPa.h"
|
||||
#include "Router.h"
|
||||
#include "SX1262Interface.h"
|
||||
#include "SX1268Interface.h"
|
||||
@@ -917,38 +918,50 @@ void RadioInterface::limitPower(int8_t loraMaxPower)
|
||||
power = maxPower;
|
||||
}
|
||||
|
||||
// Boards with an external PA controlled outside the transceiver (e.g. an analog PA
|
||||
// biased through a DAC pin) map the desired total output to a possibly-negative chip
|
||||
// output power here. This is applied even for licensed operators, because it reflects
|
||||
// a hardware gain stage, not a regulatory limit (the regulatory clamp above already
|
||||
// honors the license). Pass-through by default; see RadioExternalPa.h.
|
||||
int8_t externalPaChip = radioExternalPaMapPower(power, getFreq());
|
||||
if (externalPaChip != RADIO_EXTERNAL_PA_NO_MAP) {
|
||||
LOG_INFO("External PA: %d dBm total -> chip %d dBm", power, externalPaChip);
|
||||
power = externalPaChip;
|
||||
} else {
|
||||
#if HAS_LORA_FEM
|
||||
if (!devicestate.owner.is_licensed) {
|
||||
power = loraFEMInterface.powerConversion(power);
|
||||
}
|
||||
if (!devicestate.owner.is_licensed) {
|
||||
power = loraFEMInterface.powerConversion(power);
|
||||
}
|
||||
#else
|
||||
// todo:All entries containing "lora fem" are grouped together above.
|
||||
#ifdef ARCH_PORTDUINO
|
||||
size_t num_pa_points = portduino_config.num_pa_points;
|
||||
const uint16_t *tx_gain = portduino_config.tx_gain_lora;
|
||||
size_t num_pa_points = portduino_config.num_pa_points;
|
||||
const uint16_t *tx_gain = portduino_config.tx_gain_lora;
|
||||
#else
|
||||
size_t num_pa_points = NUM_PA_POINTS;
|
||||
const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA};
|
||||
size_t num_pa_points = NUM_PA_POINTS;
|
||||
const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA};
|
||||
#endif
|
||||
|
||||
if (num_pa_points == 1) {
|
||||
if (tx_gain[0] > 0 && !devicestate.owner.is_licensed) {
|
||||
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[0]);
|
||||
power -= tx_gain[0];
|
||||
}
|
||||
} else if (!devicestate.owner.is_licensed) {
|
||||
// we have an array of PA gain values. Find the highest power setting that works.
|
||||
for (int radio_dbm = 0; radio_dbm < (int)num_pa_points; radio_dbm++) {
|
||||
if (((radio_dbm + tx_gain[radio_dbm]) > power) ||
|
||||
((radio_dbm == (int)(num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) {
|
||||
// we've exceeded the power limit, or hit the max we can do
|
||||
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]);
|
||||
power -= tx_gain[radio_dbm];
|
||||
break;
|
||||
if (num_pa_points == 1) {
|
||||
if (tx_gain[0] > 0 && !devicestate.owner.is_licensed) {
|
||||
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[0]);
|
||||
power -= tx_gain[0];
|
||||
}
|
||||
} else if (!devicestate.owner.is_licensed) {
|
||||
// we have an array of PA gain values. Find the highest power setting that works.
|
||||
for (int radio_dbm = 0; radio_dbm < (int)num_pa_points; radio_dbm++) {
|
||||
if (((radio_dbm + tx_gain[radio_dbm]) > power) ||
|
||||
((radio_dbm == (int)(num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) {
|
||||
// we've exceeded the power limit, or hit the max we can do
|
||||
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]);
|
||||
power -= tx_gain[radio_dbm];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (power > loraMaxPower) // Clamp power to maximum defined level
|
||||
power = loraMaxPower;
|
||||
|
||||
|
||||
@@ -264,6 +264,12 @@ class RadioInterface
|
||||
*/
|
||||
virtual void saveChannelNum(uint32_t savedChannelNum);
|
||||
|
||||
/**
|
||||
* Get current RSSI reading from the radio.
|
||||
* Returns 0 if not available.
|
||||
*/
|
||||
virtual int16_t getCurrentRSSI() { return 0; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* Convert our modemConfig enum into wf, sf, etc...
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "PortduinoGlue.h"
|
||||
#include "meshUtils.h"
|
||||
#endif
|
||||
|
||||
void LockingArduinoHal::spiBeginTransaction()
|
||||
{
|
||||
spiLock->lock();
|
||||
@@ -28,6 +29,7 @@ void LockingArduinoHal::spiEndTransaction()
|
||||
|
||||
spiLock->unlock();
|
||||
}
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in)
|
||||
{
|
||||
@@ -40,6 +42,12 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
|
||||
: NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface)
|
||||
{
|
||||
instance = this;
|
||||
|
||||
// Initialize unused sample slots to a sane default; sample count controls averaging.
|
||||
for (uint8_t i = 0; i < NOISE_FLOOR_SAMPLES; i++) {
|
||||
noiseFloorSamples[i] = NOISE_FLOOR_DEFAULT;
|
||||
}
|
||||
|
||||
#if defined(ARCH_STM32WL) && defined(USE_SX1262)
|
||||
module.setCb_digitalWrite(stm32wl_emulate_digitalWrite);
|
||||
module.setCb_digitalRead(stm32wl_emulate_digitalRead);
|
||||
@@ -246,6 +254,87 @@ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id)
|
||||
return txQueue.find(from, id);
|
||||
}
|
||||
|
||||
void RadioLibInterface::updateNoiseFloor()
|
||||
{
|
||||
// Only sample from idle receive mode. TX/RX-critical paths must return to radio work quickly.
|
||||
if (!isReceiving || sendingPacket != NULL || isActivelyReceiving() || isIRQPending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now - lastNoiseFloorUpdate < NOISE_FLOOR_UPDATE_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
lastNoiseFloorUpdate = now;
|
||||
|
||||
int16_t rssi = getCurrentRSSI();
|
||||
if (rssi == NOISE_FLOOR_INVALID || rssi >= 0 || rssi < NOISE_FLOOR_VALID_MIN) {
|
||||
LOG_DEBUG("Skipping invalid RSSI reading: %d", rssi);
|
||||
return;
|
||||
}
|
||||
|
||||
noiseFloorSamples[currentSampleIndex] = (int32_t)rssi;
|
||||
currentSampleIndex++;
|
||||
|
||||
if (currentSampleIndex >= NOISE_FLOOR_SAMPLES) {
|
||||
currentSampleIndex = 0;
|
||||
isNoiseFloorBufferFull = true;
|
||||
}
|
||||
|
||||
currentNoiseFloor = getAverageNoiseFloorInternal();
|
||||
|
||||
LOG_DEBUG("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCountInternal(), rssi);
|
||||
}
|
||||
|
||||
uint8_t RadioLibInterface::getNoiseFloorSampleCountInternal() const
|
||||
{
|
||||
return isNoiseFloorBufferFull ? NOISE_FLOOR_SAMPLES : currentSampleIndex;
|
||||
}
|
||||
|
||||
int32_t RadioLibInterface::getAverageNoiseFloorInternal() const
|
||||
{
|
||||
uint8_t sampleCount = getNoiseFloorSampleCountInternal();
|
||||
|
||||
if (sampleCount == 0) {
|
||||
return NOISE_FLOOR_DEFAULT;
|
||||
}
|
||||
|
||||
int32_t sum = 0;
|
||||
for (uint8_t i = 0; i < sampleCount; i++) {
|
||||
sum += noiseFloorSamples[i];
|
||||
}
|
||||
|
||||
return sum / sampleCount;
|
||||
}
|
||||
|
||||
int32_t RadioLibInterface::getAverageNoiseFloor()
|
||||
{
|
||||
return getAverageNoiseFloorInternal();
|
||||
}
|
||||
|
||||
int32_t RadioLibInterface::getNoiseFloor()
|
||||
{
|
||||
return currentNoiseFloor;
|
||||
}
|
||||
|
||||
bool RadioLibInterface::hasNoiseFloorSamples()
|
||||
{
|
||||
return getNoiseFloorSampleCountInternal() > 0;
|
||||
}
|
||||
|
||||
uint8_t RadioLibInterface::getNoiseFloorSampleCount()
|
||||
{
|
||||
return getNoiseFloorSampleCountInternal();
|
||||
}
|
||||
|
||||
void RadioLibInterface::resetNoiseFloor()
|
||||
{
|
||||
currentSampleIndex = 0;
|
||||
isNoiseFloorBufferFull = false;
|
||||
currentNoiseFloor = NOISE_FLOOR_DEFAULT;
|
||||
LOG_INFO("Noise floor reset - rolling window collection will restart");
|
||||
}
|
||||
|
||||
bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length)
|
||||
{
|
||||
if (!buffer || length == 0 || !iface) {
|
||||
@@ -273,6 +362,7 @@ currently active.
|
||||
*/
|
||||
void RadioLibInterface::onNotify(uint32_t notification)
|
||||
{
|
||||
|
||||
switch (notification) {
|
||||
case ISR_TX:
|
||||
handleTransmitInterrupt();
|
||||
@@ -404,11 +494,6 @@ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a packet that is eligible for replacement from the TX queue
|
||||
*/
|
||||
// void RadioLibInterface::removePending
|
||||
|
||||
void RadioLibInterface::handleTransmitInterrupt()
|
||||
{
|
||||
// This can be null if we forced the device to enter standby mode. In that case
|
||||
@@ -606,3 +691,36 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
|
||||
return res == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibInterface::enableFan()
|
||||
{
|
||||
#ifdef RADIO_FAN_EN
|
||||
|
||||
#ifdef RADIO_FAN_PWM
|
||||
#if defined(ARCH_ESP32)
|
||||
ledcWrite(1, config.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55));
|
||||
#elif defined(ARCH_NRF52)
|
||||
analogWrite(RADIO_FAN_EN, config.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55));
|
||||
#endif
|
||||
#else
|
||||
pinMode(RADIO_FAN_EN, OUTPUT);
|
||||
digitalWrite(RADIO_FAN_EN, config.lora.pa_fan_disabled ? 0 : 1);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void RadioLibInterface::disableFan()
|
||||
{
|
||||
#ifdef RADIO_FAN_EN
|
||||
#ifdef RADIO_FAN_PWM
|
||||
#if defined(ARCH_ESP32)
|
||||
ledcWrite(1, 0);
|
||||
#elif defined(ARCH_NRF52)
|
||||
analogWrite(RADIO_FAN_EN, 0);
|
||||
#endif
|
||||
#else
|
||||
pinMode(RADIO_FAN_EN, OUTPUT);
|
||||
digitalWrite(RADIO_FAN_EN, 0);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -99,11 +99,42 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
/// are _trying_ to receive a packet currently (note - we might just be waiting for one)
|
||||
bool isReceiving = false;
|
||||
|
||||
protected:
|
||||
// Noise floor tracking - rolling window of samples.
|
||||
static const uint8_t NOISE_FLOOR_SAMPLES = 20;
|
||||
static const int32_t NOISE_FLOOR_DEFAULT = -120;
|
||||
static const int32_t NOISE_FLOOR_VALID_MIN = -127;
|
||||
static const int32_t NOISE_FLOOR_INVALID = -128;
|
||||
int32_t noiseFloorSamples[NOISE_FLOOR_SAMPLES];
|
||||
uint8_t currentSampleIndex = 0;
|
||||
bool isNoiseFloorBufferFull = false;
|
||||
uint32_t lastNoiseFloorUpdate = 0;
|
||||
static const uint32_t NOISE_FLOOR_UPDATE_INTERVAL_MS = 5000;
|
||||
int32_t currentNoiseFloor = NOISE_FLOOR_DEFAULT;
|
||||
|
||||
/**
|
||||
* Pure virtual hook for derived radio interfaces to provide instantaneous RSSI.
|
||||
* Implementations should return dBm, or an invalid value that updateNoiseFloor()
|
||||
* can reject.
|
||||
*/
|
||||
virtual int16_t getCurrentRSSI() = 0;
|
||||
|
||||
public:
|
||||
/** Our ISR code currently needs this to find our active instance
|
||||
*/
|
||||
static RadioLibInterface *instance;
|
||||
|
||||
/**
|
||||
* Get the current calculated noise floor in dBm
|
||||
* Returns -120 dBm if not yet calibrated
|
||||
*/
|
||||
int32_t getNoiseFloor();
|
||||
|
||||
/**
|
||||
* Calculate the average noise floor from collected samples
|
||||
*/
|
||||
int32_t getAverageNoiseFloor();
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
@@ -172,6 +203,31 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
|
||||
virtual bool findInTxQueue(NodeNum from, PacketId id) override;
|
||||
|
||||
void enableFan();
|
||||
void disableFan();
|
||||
|
||||
/**
|
||||
* Update the noise floor measurement by sampling RSSI from a slow path.
|
||||
* This should not be called from radio interrupt or TX/RX critical paths.
|
||||
*/
|
||||
void updateNoiseFloor();
|
||||
|
||||
/**
|
||||
* Check if we have collected any noise floor samples
|
||||
*/
|
||||
bool hasNoiseFloorSamples();
|
||||
|
||||
/**
|
||||
* Get the number of samples in the rolling window
|
||||
*/
|
||||
uint8_t getNoiseFloorSampleCount();
|
||||
|
||||
/**
|
||||
* Reset the noise floor calibration
|
||||
* Will automatically restart collection
|
||||
*/
|
||||
void resetNoiseFloor();
|
||||
|
||||
/**
|
||||
* Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface.
|
||||
* @return true if len bytes were produced, false otherwise.
|
||||
@@ -179,6 +235,9 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
bool randomBytes(uint8_t *buffer, size_t length);
|
||||
|
||||
private:
|
||||
uint8_t getNoiseFloorSampleCountInternal() const;
|
||||
int32_t getAverageNoiseFloorInternal() const;
|
||||
|
||||
/** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually
|
||||
* doing the transmit */
|
||||
void setTransmitDelay();
|
||||
|
||||
+8
-4
@@ -351,10 +351,14 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
}
|
||||
|
||||
fixPriority(p); // Before encryption, fix the priority if it's unset
|
||||
if (!applyPositionPrecisionForChannel(*p, p->channel)) {
|
||||
LOG_ERROR("Dropping malformed position packet before send");
|
||||
packetPool.release(p);
|
||||
return meshtastic_Routing_Error_BAD_REQUEST;
|
||||
// Position precision is an originator-only privacy policy. Relays keep
|
||||
// p->from as the original sender, so do not rewrite their POSITION_APP payload.
|
||||
if (isFromUs(p)) {
|
||||
if (!applyPositionPrecisionForChannel(*p, p->channel)) {
|
||||
LOG_ERROR("Dropping malformed position packet before send");
|
||||
packetPool.release(p);
|
||||
return meshtastic_Routing_Error_BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
// If the packet is not yet encrypted, do so now
|
||||
|
||||
@@ -52,8 +52,8 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
|
||||
#ifdef SX126X_POWER_EN // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly
|
||||
// used and not part of the 'default' set of pin definitions.
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
pinMode(SX126X_POWER_EN, OUTPUT);
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
#endif
|
||||
|
||||
#if HAS_LORA_FEM
|
||||
@@ -64,10 +64,7 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef RF95_FAN_EN
|
||||
digitalWrite(RF95_FAN_EN, HIGH);
|
||||
pinMode(RF95_FAN_EN, OUTPUT);
|
||||
#endif
|
||||
enableFan();
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
|
||||
@@ -258,6 +255,12 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
|
||||
return RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
template <typename T> int16_t SX126xInterface<T>::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora.getRSSI(false);
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
|
||||
template <typename T> void SX126xInterface<T>::disableInterrupt()
|
||||
{
|
||||
lora.clearDio1Action();
|
||||
|
||||
@@ -41,6 +41,8 @@ template <class T> class SX126xInterface : public RadioLibInterface
|
||||
*/
|
||||
T lora;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
|
||||
@@ -35,10 +35,7 @@ template <typename T> bool SX128xInterface<T>::init()
|
||||
digitalWrite(SX128X_POWER_EN, HIGH);
|
||||
#endif
|
||||
|
||||
#ifdef RF95_FAN_EN
|
||||
pinMode(RF95_FAN_EN, OUTPUT);
|
||||
digitalWrite(RF95_FAN_EN, 1);
|
||||
#endif
|
||||
enableFan();
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) {
|
||||
@@ -326,4 +323,10 @@ template <typename T> bool SX128xInterface<T>::sleep()
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> int16_t SX128xInterface<T>::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora.getRSSI(false);
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
#endif
|
||||
@@ -35,6 +35,8 @@ template <class T> class SX128xInterface : public RadioLibInterface
|
||||
*/
|
||||
T lora;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
|
||||
@@ -793,13 +793,21 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef RF95_FAN_EN
|
||||
#ifdef RADIO_FAN_EN
|
||||
#ifdef RADIO_FAN_PWM
|
||||
#if defined(ARCH_ESP32)
|
||||
ledcWrite(1, c.payload_variant.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55));
|
||||
#elif defined(ARCH_NRF52)
|
||||
analogWrite(RADIO_FAN_EN, c.payload_variant.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55));
|
||||
#endif
|
||||
#else
|
||||
// Turn PA off if disabled by config
|
||||
if (c.payload_variant.lora.pa_fan_disabled) {
|
||||
digitalWrite(RF95_FAN_EN, LOW ^ 0);
|
||||
digitalWrite(RADIO_FAN_EN, LOW ^ 0);
|
||||
} else {
|
||||
digitalWrite(RF95_FAN_EN, HIGH ^ 0);
|
||||
digitalWrite(RADIO_FAN_EN, HIGH ^ 0);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
config.lora = validatedLora;
|
||||
|
||||
@@ -987,11 +995,11 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
case meshtastic_ModuleConfig_neighbor_info_tag:
|
||||
LOG_INFO("Set module config: Neighbor Info");
|
||||
moduleConfig.has_neighbor_info = true;
|
||||
moduleConfig.neighbor_info = c.payload_variant.neighbor_info;
|
||||
if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) {
|
||||
LOG_DEBUG("Tried to set update_interval too low, setting to %d", default_neighbor_info_broadcast_secs);
|
||||
moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs;
|
||||
}
|
||||
moduleConfig.neighbor_info = c.payload_variant.neighbor_info;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_detection_sensor_tag:
|
||||
LOG_INFO("Set module config: Detection Sensor");
|
||||
|
||||
@@ -20,6 +20,7 @@ static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001;
|
||||
|
||||
int32_t DeviceTelemetryModule::runOnce()
|
||||
{
|
||||
|
||||
refreshUptime();
|
||||
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0;
|
||||
bool isImpoliteRole = isSensorOrRouterRole();
|
||||
@@ -125,6 +126,8 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry()
|
||||
telemetry.variant.local_stats.num_online_nodes = numOnlineNodes;
|
||||
telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes();
|
||||
if (RadioLibInterface::instance) {
|
||||
RadioLibInterface::instance->updateNoiseFloor();
|
||||
telemetry.variant.local_stats.noise_floor = RadioLibInterface::instance->getAverageNoiseFloor();
|
||||
telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood;
|
||||
telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad;
|
||||
telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad;
|
||||
@@ -133,6 +136,8 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry()
|
||||
}
|
||||
#ifdef ARCH_PORTDUINO
|
||||
if (SimRadio::instance) {
|
||||
if (!RadioLibInterface::instance)
|
||||
telemetry.variant.local_stats.noise_floor = SimRadio::instance->getCurrentRSSI();
|
||||
telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood;
|
||||
telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad;
|
||||
telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad;
|
||||
@@ -148,10 +153,11 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry()
|
||||
telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled;
|
||||
}
|
||||
|
||||
LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i",
|
||||
LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i, "
|
||||
"noise_floor=%d",
|
||||
telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization,
|
||||
telemetry.variant.local_stats.air_util_tx, telemetry.variant.local_stats.num_online_nodes,
|
||||
telemetry.variant.local_stats.num_total_nodes);
|
||||
telemetry.variant.local_stats.num_total_nodes, telemetry.variant.local_stats.noise_floor);
|
||||
|
||||
LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx,
|
||||
telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad);
|
||||
@@ -194,4 +200,4 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,15 +15,6 @@
|
||||
|
||||
WaypointModule *waypointModule;
|
||||
|
||||
static inline float degToRad(float deg)
|
||||
{
|
||||
return deg * PI / 180.0f;
|
||||
}
|
||||
static inline float radToDeg(float rad)
|
||||
{
|
||||
return rad * 180.0f / PI;
|
||||
}
|
||||
|
||||
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
@@ -91,9 +82,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
|
||||
const int w = display->getWidth();
|
||||
const int h = display->getHeight();
|
||||
const int *textPos = graphics::getTextPositions(display);
|
||||
|
||||
// Decode the waypoint
|
||||
const meshtastic_MeshPacket &mp = devicestate.rx_waypoint;
|
||||
@@ -108,71 +97,118 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
|
||||
|
||||
// Will contain distance information, passed as a field to drawColumns
|
||||
char distStr[20];
|
||||
char distStr[20] = "";
|
||||
|
||||
// Get our node, to use our own position
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
// Dimensions / co-ordinates for the compass/circle
|
||||
const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h);
|
||||
const int16_t compassX = x + w - (compassDiam / 2) - 5;
|
||||
const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT)
|
||||
? y + h / 2
|
||||
: y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2;
|
||||
// Match compass sizing/placement to favorite node screen logic.
|
||||
const int w = display->getWidth();
|
||||
int16_t compassRadius = 8;
|
||||
int16_t compassX = x + w - compassRadius - 8;
|
||||
int16_t compassY = y + display->getHeight() / 2;
|
||||
|
||||
// If our node has a position:
|
||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
|
||||
const meshtastic_PositionLite &op = ourNode->position;
|
||||
float myHeading;
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
myHeading = 0;
|
||||
} else {
|
||||
if (screen->hasHeading())
|
||||
myHeading = degToRad(screen->getHeading());
|
||||
else
|
||||
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||
const int16_t topY = textPos[1];
|
||||
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1);
|
||||
const int16_t usableHeight = bottomY - topY - 5;
|
||||
compassRadius = usableHeight / 2;
|
||||
if (compassRadius < 8)
|
||||
compassRadius = 8;
|
||||
compassX = x + SCREEN_WIDTH - compassRadius - 8;
|
||||
compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
||||
} else {
|
||||
// Waypoint content uses rows 1..4, so place the compass below that block.
|
||||
const int yBelowContent = textPos[4] + FONT_HEIGHT_SMALL + 2;
|
||||
const int margin = 4;
|
||||
#if defined(USE_EINK)
|
||||
const int iconSize = (graphics::currentResolution == graphics::ScreenResolution::High) ? 16 : 8;
|
||||
const int navBarHeight = iconSize + 6;
|
||||
#else
|
||||
const int navBarHeight = 0;
|
||||
#endif
|
||||
const int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
||||
if (availableHeight > 0) {
|
||||
compassRadius = availableHeight / 2;
|
||||
if (compassRadius < 8)
|
||||
compassRadius = 8;
|
||||
if (compassRadius * 2 > SCREEN_WIDTH - 16)
|
||||
compassRadius = (SCREEN_WIDTH - 16) / 2;
|
||||
if (compassRadius < 8)
|
||||
compassRadius = 8;
|
||||
compassX = x + SCREEN_WIDTH / 2;
|
||||
compassY = yBelowContent + availableHeight / 2;
|
||||
}
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2));
|
||||
}
|
||||
const uint16_t compassDiam = compassRadius * 2;
|
||||
|
||||
// Compass bearing to waypoint
|
||||
float bearingToOther =
|
||||
GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i));
|
||||
// If the top of the compass is a static north then bearingToOther can be drawn on the compass directly
|
||||
// If the top of the compass is not a static north we need adjust bearingToOther based on heading
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
|
||||
bearingToOther -= myHeading;
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
|
||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||
const char *statusLine1 = nullptr;
|
||||
const char *statusLine2 = nullptr;
|
||||
|
||||
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
|
||||
bearingToOtherDegrees = radToDeg(bearingToOtherDegrees);
|
||||
// Distance only needs our own position fix; compass/bearing additionally needs heading.
|
||||
if (hasOwnPositionFix) {
|
||||
const meshtastic_PositionLite &op = ourNode->position;
|
||||
const float d =
|
||||
GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
|
||||
// Distance to Waypoint
|
||||
float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
// Always show distance once we have an own-position fix, even without heading.
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
float feet = d * METERS_TO_FEET;
|
||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
|
||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
|
||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft" : "%.1fmi",
|
||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET);
|
||||
} else {
|
||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
|
||||
bearingToOtherDegrees);
|
||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm" : "%.1fkm", d < 2000 ? d : d / 1000);
|
||||
}
|
||||
|
||||
float myHeading = 0.0f;
|
||||
const bool hasHeading =
|
||||
graphics::CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
|
||||
if (hasHeading) {
|
||||
// Draw compass circle
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
|
||||
// Compass bearing to waypoint
|
||||
float bearingToOther =
|
||||
GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i));
|
||||
bearingToOther = graphics::CompassRenderer::adjustBearingForCompassMode(bearingToOther, myHeading);
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
|
||||
|
||||
const float bearingToOtherDegrees = graphics::CompassRenderer::radiansToDegrees360(bearingToOther);
|
||||
|
||||
// Distance to waypoint with relative bearing when heading is available.
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
float feet = d * METERS_TO_FEET;
|
||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
|
||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
|
||||
} else {
|
||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
|
||||
bearingToOtherDegrees);
|
||||
}
|
||||
|
||||
} else {
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Heading";
|
||||
}
|
||||
} else {
|
||||
// No own fix yet, so compass/bearing data would be misleading.
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Fix";
|
||||
}
|
||||
|
||||
else {
|
||||
display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?");
|
||||
|
||||
// ? in the distance field
|
||||
snprintf(distStr, sizeof(distStr), "? %s ?°",
|
||||
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km");
|
||||
if (statusLine1) {
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1);
|
||||
display->drawString(compassX, compassY, statusLine2);
|
||||
}
|
||||
|
||||
// Draw compass circle
|
||||
display->drawCircle(compassX, compassY, compassDiam / 2);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here!
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], lastStr);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], wp.name);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], wp.description);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], distStr);
|
||||
display->drawString(0, textPos[line++], lastStr);
|
||||
display->drawString(0, textPos[line++], wp.name);
|
||||
display->drawString(0, textPos[line++], wp.description);
|
||||
if (distStr[0])
|
||||
display->drawString(0, textPos[line++], distStr);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "BMM150Sensor.h"
|
||||
#include "BMX160Sensor.h"
|
||||
#include "ICM20948Sensor.h"
|
||||
#include "ICM42607PSensor.h"
|
||||
#include "LIS3DHSensor.h"
|
||||
#include "LSM6DS3Sensor.h"
|
||||
#include "MPU6050Sensor.h"
|
||||
@@ -111,6 +112,9 @@ class AccelerometerThread : public concurrency::OSThread
|
||||
case ScanI2C::DeviceType::ICM20948:
|
||||
sensor = new ICM20948Sensor(device);
|
||||
break;
|
||||
case ScanI2C::DeviceType::ICM42607P:
|
||||
sensor = new ICM42607PSensor(device);
|
||||
break;
|
||||
case ScanI2C::DeviceType::BMM150:
|
||||
sensor = new BMM150Sensor(device);
|
||||
break;
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
extern graphics::Screen *screen;
|
||||
#endif
|
||||
|
||||
// Flag when an interrupt has been detected
|
||||
volatile static bool BMM150_IRQ = false;
|
||||
|
||||
BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
|
||||
|
||||
bool BMM150Sensor::init()
|
||||
@@ -23,24 +20,7 @@ int32_t BMM150Sensor::runOnce()
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
float heading = sensor->getCompassDegree();
|
||||
|
||||
switch (config.display.compass_orientation) {
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0:
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||
heading += 90;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||
heading += 180;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||
heading += 270;
|
||||
break;
|
||||
}
|
||||
heading = applyCompassOrientation(heading);
|
||||
if (screen)
|
||||
screen->setHeading(heading);
|
||||
#endif
|
||||
@@ -90,4 +70,4 @@ bool BMM150Singleton::init(ScanI2C::FoundDevice device)
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -16,6 +16,7 @@ bool BMX160Sensor::init()
|
||||
if (sensor.begin()) {
|
||||
// set output data rate
|
||||
sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ);
|
||||
loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
LOG_DEBUG("BMX160 init ok");
|
||||
return true;
|
||||
}
|
||||
@@ -33,42 +34,12 @@ int32_t BMX160Sensor::runOnce()
|
||||
sensor.getAllData(&magAccel, NULL, &gAccel);
|
||||
|
||||
if (doCalibration) {
|
||||
|
||||
if (!showingScreen) {
|
||||
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
|
||||
showingScreen = true;
|
||||
if (screen)
|
||||
screen->startAlert((FrameCallback)drawFrameCalibration);
|
||||
}
|
||||
|
||||
if (magAccel.x > highestX)
|
||||
highestX = magAccel.x;
|
||||
if (magAccel.x < lowestX)
|
||||
lowestX = magAccel.x;
|
||||
if (magAccel.y > highestY)
|
||||
highestY = magAccel.y;
|
||||
if (magAccel.y < lowestY)
|
||||
lowestY = magAccel.y;
|
||||
if (magAccel.z > highestZ)
|
||||
highestZ = magAccel.z;
|
||||
if (magAccel.z < lowestZ)
|
||||
lowestZ = magAccel.z;
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now > endCalibrationAt) {
|
||||
doCalibration = false;
|
||||
endCalibrationAt = 0;
|
||||
showingScreen = false;
|
||||
if (screen)
|
||||
screen->endAlert();
|
||||
}
|
||||
|
||||
// LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX,
|
||||
// lowestY, highestY, lowestZ, highestZ);
|
||||
beginCalibrationDisplay(showingScreen);
|
||||
updateCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
|
||||
lowestZ);
|
||||
}
|
||||
|
||||
int highestRealX = highestX - (highestX + lowestX) / 2;
|
||||
|
||||
magAccel.x -= (highestX + lowestX) / 2;
|
||||
magAccel.y -= (highestY + lowestY) / 2;
|
||||
magAccel.z -= (highestZ + lowestZ) / 2;
|
||||
@@ -88,23 +59,7 @@ int32_t BMX160Sensor::runOnce()
|
||||
|
||||
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
|
||||
|
||||
switch (config.display.compass_orientation) {
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0:
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||
heading += 90;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||
heading += 180;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||
heading += 270;
|
||||
break;
|
||||
}
|
||||
heading = applyCompassOrientation(heading);
|
||||
if (screen)
|
||||
screen->setHeading(heading);
|
||||
#endif
|
||||
@@ -119,15 +74,8 @@ void BMX160Sensor::calibrate(uint16_t forSeconds)
|
||||
sBmx160SensorData_t gAccel;
|
||||
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
|
||||
sensor.getAllData(&magAccel, NULL, &gAccel);
|
||||
highestX = magAccel.x, lowestX = magAccel.x;
|
||||
highestY = magAccel.y, lowestY = magAccel.y;
|
||||
highestZ = magAccel.z, lowestZ = magAccel.z;
|
||||
|
||||
doCalibration = true;
|
||||
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
|
||||
endCalibrationAt = millis() + calibrateFor;
|
||||
if (screen)
|
||||
screen->setEndCalibration(endCalibrationAt);
|
||||
seedCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
startCalibrationWindow(forSeconds);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class BMX160Sensor : public MotionSensor
|
||||
private:
|
||||
RAK_BMX160 sensor;
|
||||
bool showingScreen = false;
|
||||
static constexpr const char *compassCalibrationFileName = "/prefs/compass_bmx160.dat";
|
||||
float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
||||
|
||||
public:
|
||||
@@ -39,4 +40,4 @@ class BMX160Sensor : public MotionSensor
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -26,7 +26,11 @@ bool ICM20948Sensor::init()
|
||||
return false;
|
||||
|
||||
// Enable simple Wake on Motion
|
||||
return sensor->setWakeOnMotion();
|
||||
bool wakeOnMotionOk = sensor->setWakeOnMotion();
|
||||
if (wakeOnMotionOk) {
|
||||
loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
}
|
||||
return wakeOnMotionOk;
|
||||
}
|
||||
|
||||
#ifdef ICM_20948_INT_PIN
|
||||
@@ -47,7 +51,8 @@ int32_t ICM20948Sensor::runOnce()
|
||||
int32_t ICM20948Sensor::runOnce()
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) {
|
||||
if (screen && !doCalibration && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion &&
|
||||
!config.device.double_tap_as_button_press) {
|
||||
if (!isAsleep) {
|
||||
LOG_DEBUG("sleeping IMU");
|
||||
sensor->sleep(true);
|
||||
@@ -69,38 +74,10 @@ int32_t ICM20948Sensor::runOnce()
|
||||
}
|
||||
|
||||
if (doCalibration) {
|
||||
|
||||
if (!showingScreen) {
|
||||
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
|
||||
showingScreen = true;
|
||||
if (screen)
|
||||
screen->startAlert((FrameCallback)drawFrameCalibration);
|
||||
}
|
||||
|
||||
if (magX > highestX)
|
||||
highestX = magX;
|
||||
if (magX < lowestX)
|
||||
lowestX = magX;
|
||||
if (magY > highestY)
|
||||
highestY = magY;
|
||||
if (magY < lowestY)
|
||||
lowestY = magY;
|
||||
if (magZ > highestZ)
|
||||
highestZ = magZ;
|
||||
if (magZ < lowestZ)
|
||||
lowestZ = magZ;
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now > endCalibrationAt) {
|
||||
doCalibration = false;
|
||||
endCalibrationAt = 0;
|
||||
showingScreen = false;
|
||||
if (screen)
|
||||
screen->endAlert();
|
||||
}
|
||||
|
||||
// LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX,
|
||||
// lowestY, highestY, lowestZ, highestZ);
|
||||
beginCalibrationDisplay(showingScreen);
|
||||
updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
|
||||
lowestZ);
|
||||
}
|
||||
|
||||
magX -= (highestX + lowestX) / 2;
|
||||
@@ -122,23 +99,7 @@ int32_t ICM20948Sensor::runOnce()
|
||||
|
||||
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
|
||||
|
||||
switch (config.display.compass_orientation) {
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0:
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||
heading += 90;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||
heading += 180;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||
heading += 270;
|
||||
break;
|
||||
}
|
||||
heading = applyCompassOrientation(heading);
|
||||
if (screen)
|
||||
screen->setHeading(heading);
|
||||
#endif
|
||||
@@ -169,26 +130,16 @@ int32_t ICM20948Sensor::runOnce()
|
||||
void ICM20948Sensor::calibrate(uint16_t forSeconds)
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f",
|
||||
highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
|
||||
LOG_DEBUG("ICM20948 cal start %is", forSeconds);
|
||||
if (sensor->dataReady()) {
|
||||
sensor->getAGMT();
|
||||
highestX = sensor->agmt.mag.axes.x;
|
||||
lowestX = sensor->agmt.mag.axes.x;
|
||||
highestY = sensor->agmt.mag.axes.y;
|
||||
lowestY = sensor->agmt.mag.axes.y;
|
||||
highestZ = sensor->agmt.mag.axes.z;
|
||||
lowestZ = sensor->agmt.mag.axes.z;
|
||||
seedCalibrationExtrema(sensor->agmt.mag.axes.x, sensor->agmt.mag.axes.y, sensor->agmt.mag.axes.z, highestX, lowestX,
|
||||
highestY, lowestY, highestZ, lowestZ);
|
||||
} else {
|
||||
highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
||||
seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
}
|
||||
|
||||
doCalibration = true;
|
||||
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
|
||||
endCalibrationAt = millis() + calibrateFor;
|
||||
if (screen)
|
||||
screen->setEndCalibration(endCalibrationAt);
|
||||
startCalibrationWindow(forSeconds);
|
||||
#endif
|
||||
}
|
||||
// ----------------------------------------------------------------------
|
||||
@@ -314,11 +265,6 @@ bool ICM20948Singleton::setWakeOnMotion()
|
||||
status = intEnableWOM(true);
|
||||
LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString());
|
||||
return status == ICM_20948_Stat_Ok;
|
||||
|
||||
// Clear any current interrupts
|
||||
ICM20948_IRQ = false;
|
||||
clearInterrupts();
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -83,6 +83,7 @@ class ICM20948Sensor : public MotionSensor
|
||||
ICM20948Singleton *sensor = nullptr;
|
||||
bool showingScreen = false;
|
||||
bool isAsleep = false;
|
||||
static constexpr const char *compassCalibrationFileName = "/prefs/compass_icm20948.dat";
|
||||
#ifdef MUZI_BASE
|
||||
float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000,
|
||||
lowestZ = 98.000000;
|
||||
@@ -103,4 +104,4 @@ class ICM20948Sensor : public MotionSensor
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "ICM42607PSensor.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<ICM42670P.h>)
|
||||
|
||||
#include "detect/ScanI2CTwoWire.h"
|
||||
#include <ICM42670P.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;
|
||||
|
||||
#ifdef ICM_42607P_INT_PIN
|
||||
volatile static bool ICM42607P_IRQ = false;
|
||||
|
||||
void ICM42607PSetInterrupt()
|
||||
{
|
||||
ICM42607P_IRQ = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
ICM42607PSensor::ICM42607PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice)
|
||||
{
|
||||
wire = ScanI2CTwoWire::fetchI2CBus(foundDevice.address);
|
||||
}
|
||||
|
||||
ICM42607PSensor::~ICM42607PSensor() = default;
|
||||
|
||||
bool ICM42607PSensor::init()
|
||||
{
|
||||
bool addressLsb = deviceAddress() == ICM42607P_ADDR_ALT;
|
||||
|
||||
LOG_DEBUG("ICM-42607-P begin on addr 0x%02X (port=%d)", deviceAddress(), devicePort());
|
||||
sensor.reset();
|
||||
auto newSensor = std::make_unique<ICM42670>(*wire, addressLsb);
|
||||
|
||||
int status = newSensor->begin();
|
||||
// ICM42670P library returns -3 for ICM42607P because WHO_AM_I differs; the register map is compatible.
|
||||
if (status != 0 && status != -3) {
|
||||
LOG_DEBUG("ICM-42607-P init error %d", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
status = newSensor->startAccel(ICM42607P_ACCEL_ODR_HZ, ICM42607P_ACCEL_FSR_G);
|
||||
if (status != 0) {
|
||||
LOG_DEBUG("ICM-42607-P accel start error %d", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef ICM_42607P_INT_PIN
|
||||
ICM42607P_IRQ = false;
|
||||
status = newSensor->startWakeOnMotion(ICM_42607P_INT_PIN, ICM42607PSetInterrupt);
|
||||
if (status != 0) {
|
||||
LOG_DEBUG("ICM-42607-P wake-on-motion start error %d", status);
|
||||
return false;
|
||||
}
|
||||
LOG_DEBUG("ICM-42607-P wake-on-motion interrupt ok pin=%d", ICM_42607P_INT_PIN);
|
||||
#endif
|
||||
|
||||
sensor = std::move(newSensor);
|
||||
LOG_DEBUG("ICM-42607-P init ok");
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t ICM42607PSensor::runOnce()
|
||||
{
|
||||
#ifdef ICM_42607P_INT_PIN
|
||||
if (ICM42607P_IRQ) {
|
||||
ICM42607P_IRQ = false;
|
||||
LOG_DEBUG("ICM-42607-P motion interrupt");
|
||||
wakeScreen();
|
||||
}
|
||||
return MOTION_SENSOR_CHECK_INTERVAL_MS;
|
||||
#else
|
||||
int16_t x = 0;
|
||||
int16_t y = 0;
|
||||
int16_t z = 0;
|
||||
inv_imu_sensor_event_t event = {};
|
||||
|
||||
if (sensor == nullptr || sensor->getDataFromRegisters(event) != 0) {
|
||||
return MOTION_SENSOR_CHECK_INTERVAL_MS;
|
||||
}
|
||||
|
||||
// getDataFromRegisters() fills accel[] but does not set sensor_mask in this library version.
|
||||
if (event.accel[0] == 0 && event.accel[1] == 0 && event.accel[2] == 0) {
|
||||
return MOTION_SENSOR_CHECK_INTERVAL_MS;
|
||||
}
|
||||
|
||||
x = event.accel[0];
|
||||
y = event.accel[1];
|
||||
z = event.accel[2];
|
||||
// LOG_DEBUG("ICM-42607-P accel read x=%.3fg y=%.3fg z=%.3fg", (float)x / ICM42607P_COUNTS_PER_G,
|
||||
// (float)y / ICM42607P_COUNTS_PER_G, (float)z / ICM42607P_COUNTS_PER_G);
|
||||
|
||||
return MOTION_SENSOR_CHECK_INTERVAL_MS;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#ifndef _ICM42607P_SENSOR_H_
|
||||
#define _ICM42607P_SENSOR_H_
|
||||
|
||||
#include "MotionSensor.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<ICM42670P.h>)
|
||||
|
||||
#include <memory>
|
||||
|
||||
class ICM42670;
|
||||
|
||||
class ICM42607PSensor : public MotionSensor
|
||||
{
|
||||
private:
|
||||
std::unique_ptr<ICM42670> sensor;
|
||||
TwoWire *wire = nullptr;
|
||||
|
||||
public:
|
||||
explicit ICM42607PSensor(ScanI2C::FoundDevice foundDevice);
|
||||
~ICM42607PSensor() override;
|
||||
virtual bool init() override;
|
||||
virtual int32_t runOnce() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "MMC5983MASensor.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<SparkFun_MMC5983MA_Arduino_Library.h>)
|
||||
|
||||
#include "detect/ScanI2CTwoWire.h"
|
||||
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
|
||||
extern graphics::Screen *screen;
|
||||
#endif
|
||||
|
||||
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 float MMC5983MA_HEADING_OFFSET_DEG = 180.0f;
|
||||
|
||||
MMC5983MASensor::MMC5983MASensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
|
||||
|
||||
bool MMC5983MASensor::init()
|
||||
{
|
||||
LOG_DEBUG("MMC5983MA begin on addr 0x%02X (port=%d)", device.address.address, device.address.port);
|
||||
TwoWire *wire = ScanI2CTwoWire::fetchI2CBus(device.address);
|
||||
|
||||
if (!sensor.begin(*wire)) {
|
||||
LOG_DEBUG("MMC5983MA init error");
|
||||
return false;
|
||||
}
|
||||
|
||||
sensor.softReset();
|
||||
sensor.setFilterBandwidth(100);
|
||||
sensor.performSetOperation();
|
||||
sensor.enableAutomaticSetReset();
|
||||
continuousMode = sensor.setContinuousModeFrequency(MMC5983MA_CONTINUOUS_FREQUENCY_HZ);
|
||||
continuousMode &= sensor.enableContinuousMode();
|
||||
|
||||
if (!continuousMode) {
|
||||
LOG_DEBUG("MMC5983MA continuous mode failed, using single-shot reads");
|
||||
sensor.disableContinuousMode();
|
||||
}
|
||||
|
||||
loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MMC5983MASensor::readMagnetometer(float &xGauss, float &yGauss, float &zGauss)
|
||||
{
|
||||
uint32_t rawX = 0;
|
||||
uint32_t rawY = 0;
|
||||
uint32_t rawZ = 0;
|
||||
|
||||
if (!(continuousMode ? sensor.readFieldsXYZ(&rawX, &rawY, &rawZ) : sensor.getMeasurementXYZ(&rawX, &rawY, &rawZ))) {
|
||||
LOG_DEBUG("MMC5983MA read failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
xGauss = ((float)rawX - MMC5983MA_ZERO_FIELD) / MMC5983MA_COUNTS_PER_GAUSS;
|
||||
yGauss = ((float)rawY - MMC5983MA_ZERO_FIELD) / MMC5983MA_COUNTS_PER_GAUSS;
|
||||
zGauss = ((float)rawZ - MMC5983MA_ZERO_FIELD) / MMC5983MA_COUNTS_PER_GAUSS;
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t MMC5983MASensor::runOnce()
|
||||
{
|
||||
float magX = 0, magY = 0, magZ = 0;
|
||||
if (!readMagnetometer(magX, magY, magZ)) {
|
||||
return MOTION_SENSOR_CHECK_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
|
||||
|
||||
magX -= (highestX + lowestX) / 2;
|
||||
magY -= (highestY + lowestY) / 2;
|
||||
magZ -= (highestZ + lowestZ) / 2;
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
void MMC5983MASensor::calibrate(uint16_t forSeconds)
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
|
||||
float xGauss = 0.0f;
|
||||
float yGauss = 0.0f;
|
||||
float zGauss = 0.0f;
|
||||
|
||||
LOG_DEBUG("MMC5983MA calibration started for %is", forSeconds);
|
||||
if (readMagnetometer(xGauss, yGauss, zGauss)) {
|
||||
seedCalibrationExtrema(xGauss, yGauss, zGauss, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
} else {
|
||||
seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
}
|
||||
startCalibrationWindow(forSeconds);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#ifndef _MMC5983MA_SENSOR_H_
|
||||
#define _MMC5983MA_SENSOR_H_
|
||||
|
||||
#include "MotionSensor.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<SparkFun_MMC5983MA_Arduino_Library.h>)
|
||||
|
||||
#include <SparkFun_MMC5983MA_Arduino_Library.h>
|
||||
|
||||
class MMC5983MASensor : public MotionSensor
|
||||
{
|
||||
private:
|
||||
SFE_MMC5983MA sensor;
|
||||
bool continuousMode = false;
|
||||
bool showingScreen = false;
|
||||
static constexpr const char *compassCalibrationFileName = "/prefs/compass_mmc5983ma.dat";
|
||||
float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
||||
|
||||
bool readMagnetometer(float &xGauss, float &yGauss, float &zGauss);
|
||||
|
||||
public:
|
||||
explicit MMC5983MASensor(ScanI2C::FoundDevice foundDevice);
|
||||
virtual bool init() override;
|
||||
virtual int32_t runOnce() override;
|
||||
virtual void calibrate(uint16_t forSeconds) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
#ifndef _MAGNETOMETER_THREAD_H_
|
||||
#define _MAGNETOMETER_THREAD_H_
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
|
||||
#include "../concurrency/OSThread.h"
|
||||
#include "MMC5983MASensor.h"
|
||||
#include "MotionSensor.h"
|
||||
|
||||
extern ScanI2C::DeviceAddress magnetometer_found;
|
||||
|
||||
class MagnetometerThread : public concurrency::OSThread
|
||||
{
|
||||
private:
|
||||
MotionSensor *sensor = nullptr;
|
||||
ScanI2C::FoundDevice device;
|
||||
bool isInitialised = false;
|
||||
|
||||
public:
|
||||
explicit MagnetometerThread(ScanI2C::FoundDevice foundDevice) : OSThread("Magnetometer")
|
||||
{
|
||||
device = foundDevice;
|
||||
init();
|
||||
}
|
||||
|
||||
explicit MagnetometerThread(ScanI2C::DeviceType type) : MagnetometerThread(ScanI2C::FoundDevice{type, magnetometer_found}) {}
|
||||
|
||||
void start()
|
||||
{
|
||||
init();
|
||||
setIntervalFromNow(0);
|
||||
};
|
||||
|
||||
void calibrate(uint16_t forSeconds)
|
||||
{
|
||||
if (sensor) {
|
||||
sensor->calibrate(forSeconds);
|
||||
setIntervalFromNow(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override
|
||||
{
|
||||
canSleep = true;
|
||||
|
||||
if (isInitialised) {
|
||||
return sensor->runOnce();
|
||||
}
|
||||
|
||||
return MOTION_SENSOR_CHECK_INTERVAL_MS;
|
||||
}
|
||||
|
||||
private:
|
||||
void init()
|
||||
{
|
||||
if (isInitialised) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.address.port == ScanI2C::I2CPort::NO_I2C || device.address.address == 0 || device.type == ScanI2C::NONE) {
|
||||
LOG_DEBUG("MagnetometerThread Disable due to no sensors found");
|
||||
disable();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (device.type) {
|
||||
case ScanI2C::DeviceType::MMC5983MA:
|
||||
sensor = new MMC5983MASensor(device);
|
||||
break;
|
||||
default:
|
||||
disable();
|
||||
return;
|
||||
}
|
||||
|
||||
isInitialised = sensor->init();
|
||||
if (!isInitialised) {
|
||||
clean();
|
||||
}
|
||||
LOG_DEBUG("MagnetometerThread::init %s", isInitialised ? "ok" : "failed");
|
||||
}
|
||||
|
||||
MagnetometerThread(const MagnetometerThread &other) : OSThread::OSThread("Magnetometer") { this->copy(other); }
|
||||
|
||||
virtual ~MagnetometerThread() { clean(); }
|
||||
|
||||
MagnetometerThread &operator=(const MagnetometerThread &other)
|
||||
{
|
||||
this->copy(other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
void copy(const MagnetometerThread &other)
|
||||
{
|
||||
if (this != &other) {
|
||||
clean();
|
||||
this->device = ScanI2C::FoundDevice(other.device.type,
|
||||
ScanI2C::DeviceAddress(other.device.address.port, other.device.address.address));
|
||||
}
|
||||
}
|
||||
|
||||
void clean()
|
||||
{
|
||||
isInitialised = false;
|
||||
delete sensor;
|
||||
sensor = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+245
-14
@@ -1,10 +1,37 @@
|
||||
#include "MotionSensor.h"
|
||||
#include "FSCommon.h"
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "graphics/draw/CompassRenderer.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
|
||||
|
||||
char timeRemainingBuffer[12];
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t COMPASS_CALIBRATION_MAGIC = 0x4D43414CL; // "MCAL"
|
||||
constexpr uint16_t COMPASS_CALIBRATION_VERSION = 1;
|
||||
|
||||
struct CompassCalibrationRecord {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint16_t reserved;
|
||||
float highestX;
|
||||
float lowestX;
|
||||
float highestY;
|
||||
float lowestY;
|
||||
float highestZ;
|
||||
float lowestZ;
|
||||
};
|
||||
|
||||
bool isRangeValid(float highest, float lowest)
|
||||
{
|
||||
// NaN/Inf guard without pulling in extra math helpers.
|
||||
return (highest == highest) && (lowest == lowest) && (highest > lowest);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// screen is defined in main.cpp
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
@@ -32,33 +59,237 @@ ScanI2C::I2CPort MotionSensor::devicePort()
|
||||
return device.address.port;
|
||||
}
|
||||
|
||||
bool MotionSensor::saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY,
|
||||
float highestZ, float lowestZ)
|
||||
{
|
||||
#ifdef FSCom
|
||||
if (!isRangeValid(highestX, lowestX) || !isRangeValid(highestY, lowestY) || !isRangeValid(highestZ, lowestZ)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FSCom.mkdir("/prefs");
|
||||
CompassCalibrationRecord record = {
|
||||
COMPASS_CALIBRATION_MAGIC, COMPASS_CALIBRATION_VERSION, 0, highestX, lowestX, highestY, lowestY, highestZ, lowestZ};
|
||||
|
||||
auto file = SafeFile(filePath, true);
|
||||
const size_t written = file.write(reinterpret_cast<const uint8_t *>(&record), sizeof(record));
|
||||
return (written == sizeof(record)) && file.close();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MotionSensor::loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY,
|
||||
float &lowestY, float &highestZ, float &lowestZ)
|
||||
{
|
||||
#ifdef FSCom
|
||||
CompassCalibrationRecord record = {};
|
||||
size_t bytesRead = 0;
|
||||
|
||||
spiLock->lock();
|
||||
auto file = FSCom.open(filePath, FILE_O_READ);
|
||||
if (!file) {
|
||||
spiLock->unlock();
|
||||
return false;
|
||||
}
|
||||
bytesRead = file.read(reinterpret_cast<uint8_t *>(&record), sizeof(record));
|
||||
file.close();
|
||||
spiLock->unlock();
|
||||
|
||||
const bool headerValid = (bytesRead == sizeof(record)) && (record.magic == COMPASS_CALIBRATION_MAGIC) &&
|
||||
(record.version == COMPASS_CALIBRATION_VERSION) && (record.reserved == 0U);
|
||||
const bool rangeValid = isRangeValid(record.highestX, record.lowestX) && isRangeValid(record.highestY, record.lowestY) &&
|
||||
isRangeValid(record.highestZ, record.lowestZ);
|
||||
if (!headerValid || !rangeValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
highestX = record.highestX;
|
||||
lowestX = record.lowestX;
|
||||
highestY = record.highestY;
|
||||
lowestY = record.lowestY;
|
||||
highestZ = record.highestZ;
|
||||
lowestZ = record.lowestZ;
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MotionSensor::beginCalibrationDisplay(bool &showingScreen)
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
if (!showingScreen) {
|
||||
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
|
||||
showingScreen = true;
|
||||
if (screen)
|
||||
screen->startAlert((FrameCallback)drawFrameCalibration);
|
||||
}
|
||||
#else
|
||||
(void)showingScreen;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MotionSensor::finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX,
|
||||
float highestY, float lowestY, float highestZ, float lowestZ)
|
||||
{
|
||||
const uint32_t now = millis();
|
||||
if ((int32_t)(now - endCalibrationAt) < 0)
|
||||
return;
|
||||
|
||||
doCalibration = false;
|
||||
endCalibrationAt = 0;
|
||||
showingScreen = false;
|
||||
saveMagnetometerCalibration(filePath, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
if (screen) {
|
||||
screen->setEndCalibration(0);
|
||||
screen->endAlert();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MotionSensor::startCalibrationWindow(uint16_t forSeconds)
|
||||
{
|
||||
doCalibration = true;
|
||||
const uint32_t calibrateFor = static_cast<uint32_t>(forSeconds) * 1000U;
|
||||
endCalibrationAt = millis() + calibrateFor;
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
if (screen)
|
||||
screen->setEndCalibration(endCalibrationAt);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MotionSensor::seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
||||
float &lowestY, float &highestZ, float &lowestZ)
|
||||
{
|
||||
highestX = lowestX = x;
|
||||
highestY = lowestY = y;
|
||||
highestZ = lowestZ = z;
|
||||
}
|
||||
|
||||
void MotionSensor::updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
||||
float &lowestY, float &highestZ, float &lowestZ)
|
||||
{
|
||||
if (x > highestX)
|
||||
highestX = x;
|
||||
if (x < lowestX)
|
||||
lowestX = x;
|
||||
if (y > highestY)
|
||||
highestY = y;
|
||||
if (y < lowestY)
|
||||
lowestY = y;
|
||||
if (z > highestZ)
|
||||
highestZ = z;
|
||||
if (z < lowestZ)
|
||||
lowestZ = z;
|
||||
}
|
||||
|
||||
float MotionSensor::applyCompassOrientation(float heading)
|
||||
{
|
||||
switch (config.display.compass_orientation) {
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||
return heading + 90;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||
return heading + 180;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||
return heading + 270;
|
||||
default:
|
||||
return heading;
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (screen == nullptr)
|
||||
return;
|
||||
// int x_offset = display->width() / 2;
|
||||
// int y_offset = display->height() <= 80 ? 0 : 32;
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x, y, "Calibrating\nCompass");
|
||||
|
||||
uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000;
|
||||
sprintf(timeRemainingBuffer, "( %02d )", timeRemaining);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawString(x, y + 40, timeRemainingBuffer);
|
||||
const int16_t width = display->getWidth();
|
||||
const int16_t height = display->getHeight();
|
||||
const bool compactLayout = (height <= 80);
|
||||
const int16_t margin = 4;
|
||||
|
||||
const uint32_t now = millis();
|
||||
const uint32_t endCalibrationAt = screen->getEndCalibration();
|
||||
uint32_t timeRemaining = 0;
|
||||
if (endCalibrationAt > now) {
|
||||
timeRemaining = (endCalibrationAt - now + 999) / 1000;
|
||||
}
|
||||
|
||||
int16_t compassX = 0, compassY = 0;
|
||||
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
|
||||
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(width, height);
|
||||
const int16_t compassRadius = compassDiam / 2;
|
||||
|
||||
// coordinates for the center of the compass/circle
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||
compassY = y + display->getHeight() / 2;
|
||||
compassX = x + width - compassRadius - margin;
|
||||
compassY = y + height / 2;
|
||||
} else {
|
||||
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||
compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2;
|
||||
compassX = x + width - compassRadius - margin;
|
||||
compassY = y + FONT_HEIGHT_SMALL + (height - FONT_HEIGHT_SMALL) / 2;
|
||||
}
|
||||
|
||||
const int16_t textLeft = x + 1;
|
||||
const int16_t textRight = compassX - compassRadius - margin;
|
||||
const int16_t textWidth = textRight - textLeft;
|
||||
int16_t lineY = y;
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
if (textWidth > 12) {
|
||||
const char *title = "Cal";
|
||||
const char *line1 = "Figure-8";
|
||||
const char *line2 = "Rotate axes";
|
||||
const char *line3 = "Away from metal";
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
if (!compactLayout && display->getStringWidth("Compass Calibration") <= textWidth) {
|
||||
display->setFont(FONT_MEDIUM);
|
||||
title = "Compass Calibration";
|
||||
line1 = "Move in figure-8";
|
||||
line2 = "Rotate all axes";
|
||||
line3 = "Keep from metal";
|
||||
display->drawString(textLeft, lineY, title);
|
||||
lineY += FONT_HEIGHT_MEDIUM;
|
||||
display->setFont(FONT_SMALL);
|
||||
} else if (display->getStringWidth("Compass Cal") <= textWidth) {
|
||||
title = "Compass Cal";
|
||||
if (textWidth >= display->getStringWidth("Move in figure-8")) {
|
||||
line1 = "Move in figure-8";
|
||||
line2 = "Rotate all axes";
|
||||
line3 = "Keep from metal";
|
||||
}
|
||||
display->drawString(textLeft, lineY, title);
|
||||
lineY += FONT_HEIGHT_SMALL;
|
||||
} else {
|
||||
display->drawString(textLeft, lineY, title);
|
||||
lineY += FONT_HEIGHT_SMALL;
|
||||
}
|
||||
|
||||
display->drawString(textLeft, lineY, line1);
|
||||
lineY += FONT_HEIGHT_SMALL;
|
||||
display->drawString(textLeft, lineY, line2);
|
||||
lineY += FONT_HEIGHT_SMALL;
|
||||
if (!compactLayout || textWidth >= display->getStringWidth(line3)) {
|
||||
display->drawString(textLeft, lineY, line3);
|
||||
}
|
||||
}
|
||||
|
||||
if (textWidth >= display->getStringWidth("000s left")) {
|
||||
snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus left", (unsigned long)timeRemaining);
|
||||
} else {
|
||||
snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus", (unsigned long)timeRemaining);
|
||||
}
|
||||
display->setFont(FONT_SMALL);
|
||||
if (textWidth > 12) {
|
||||
display->drawString(textLeft, y + height - FONT_HEIGHT_SMALL - 1, timeRemainingBuffer);
|
||||
}
|
||||
|
||||
display->drawCircle(compassX, compassY, compassDiam / 2);
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#ifndef _MOTION_SENSOR_H_
|
||||
#define _MOTION_SENSOR_H_
|
||||
|
||||
#define MOTION_SENSOR_CHECK_INTERVAL_MS 100
|
||||
#define MOTION_SENSOR_CHECK_INTERVAL_MS 50
|
||||
#define MOTION_SENSOR_CLICK_THRESHOLD 40
|
||||
|
||||
#include "../configuration.h"
|
||||
@@ -54,6 +54,20 @@ class MotionSensor
|
||||
static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
#endif
|
||||
|
||||
bool saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY,
|
||||
float highestZ, float lowestZ);
|
||||
bool loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY, float &lowestY,
|
||||
float &highestZ, float &lowestZ);
|
||||
void beginCalibrationDisplay(bool &showingScreen);
|
||||
void finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX, float highestY,
|
||||
float lowestY, float highestZ, float lowestZ);
|
||||
void startCalibrationWindow(uint16_t forSeconds);
|
||||
static void seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
||||
float &lowestY, float &highestZ, float &lowestZ);
|
||||
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);
|
||||
|
||||
ScanI2C::FoundDevice device;
|
||||
|
||||
// Do calibration if true
|
||||
@@ -63,4 +77,4 @@ class MotionSensor
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef RADIOMASTER_NOMAD_GEMINI
|
||||
|
||||
#include "RadioExternalPa.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
/**
|
||||
* RadioMaster Nomad Gemini external analog PA driver.
|
||||
*
|
||||
* The Nomad's PA is biased by an analog control voltage on the APC2 pin
|
||||
* (RADIO_PA_APC2_PIN = GPIO26 = ESP32 DAC channel 2), driven with dacWrite()
|
||||
* (0-255 -> 0-3.3V). The DAC is essentially a fixed bias; the actual output power
|
||||
* is set by driving the LR1121 chip at a (negative) output power and letting the PA
|
||||
* add ~25-31 dB of gain.
|
||||
*
|
||||
* The calibration below is derived from the ExpressLRS hardware target
|
||||
* "TX/Radiomaster Nomad.json" (power_values = APC2 DAC codes, power_values2 = chip
|
||||
* output dBm). These values reproduce ELRS behaviour but have NOT been validated on
|
||||
* a bench against a power meter for Meshtastic — verify before trusting at 1 W, both
|
||||
* for PA safety and for regulatory compliance.
|
||||
*
|
||||
* TODO: dual-band (Gemini) operation uses ELRS power_values_dual and a second radio;
|
||||
* once JANUS_RADIO is implemented, branch on freqHz / active radio here.
|
||||
*/
|
||||
|
||||
// total dBm corresponds to the ELRS power presets 10/25/50/100/250/500/1000 mW.
|
||||
static const struct {
|
||||
int8_t totalDbm; // desired total radiated power
|
||||
int8_t chipDbm; // LR1121 output power register (ELRS power_values2)
|
||||
uint8_t apc2; // APC2 DAC code (ELRS power_values)
|
||||
} NOMAD_PA_CAL[] = {
|
||||
{10, -17, 120}, // 10 mW
|
||||
{14, -16, 120}, // 25 mW
|
||||
{17, -14, 120}, // 50 mW
|
||||
{20, -11, 120}, // 100 mW
|
||||
{24, -7, 120}, // 250 mW
|
||||
{27, -3, 120}, // 500 mW
|
||||
{30, 5, 95}, // 1000 mW (1 W)
|
||||
};
|
||||
static const size_t NOMAD_PA_CAL_N = sizeof(NOMAD_PA_CAL) / sizeof(NOMAD_PA_CAL[0]);
|
||||
|
||||
// APC2 bias chosen for the currently-configured power level; applied on TX, dropped
|
||||
// to 0 for RX/idle/sleep so the PA only draws current while transmitting.
|
||||
static uint8_t nomadApc2Level = 0;
|
||||
|
||||
// Runs at the start of setup(), before the LoRa radio is initialized.
|
||||
void earlyInitVariant()
|
||||
{
|
||||
dacWrite(RADIO_PA_APC2_PIN, 0); // bias the PA off until we actually transmit
|
||||
nomadApc2Level = 0;
|
||||
}
|
||||
|
||||
int8_t radioExternalPaMapPower(int8_t requestedTotalDbm, float freqHz)
|
||||
{
|
||||
(void)freqHz; // single-band for now (see dual-band TODO above)
|
||||
|
||||
if (requestedTotalDbm <= NOMAD_PA_CAL[0].totalDbm) {
|
||||
nomadApc2Level = NOMAD_PA_CAL[0].apc2;
|
||||
return NOMAD_PA_CAL[0].chipDbm;
|
||||
}
|
||||
const size_t last = NOMAD_PA_CAL_N - 1;
|
||||
if (requestedTotalDbm >= NOMAD_PA_CAL[last].totalDbm) {
|
||||
nomadApc2Level = NOMAD_PA_CAL[last].apc2;
|
||||
return NOMAD_PA_CAL[last].chipDbm;
|
||||
}
|
||||
|
||||
// Piecewise-linear interpolation of chip dBm between the bracketing points.
|
||||
for (size_t i = 1; i < NOMAD_PA_CAL_N; i++) {
|
||||
if (requestedTotalDbm <= NOMAD_PA_CAL[i].totalDbm) {
|
||||
const int t0 = NOMAD_PA_CAL[i - 1].totalDbm, t1 = NOMAD_PA_CAL[i].totalDbm;
|
||||
const int c0 = NOMAD_PA_CAL[i - 1].chipDbm, c1 = NOMAD_PA_CAL[i].chipDbm;
|
||||
const int chip = c0 + ((c1 - c0) * (requestedTotalDbm - t0) + (t1 - t0) / 2) / (t1 - t0); // round-to-nearest
|
||||
nomadApc2Level = NOMAD_PA_CAL[i - 1].apc2; // only the top point lowers the bias
|
||||
return (int8_t)chip;
|
||||
}
|
||||
}
|
||||
nomadApc2Level = NOMAD_PA_CAL[last].apc2; // unreachable
|
||||
return NOMAD_PA_CAL[last].chipDbm;
|
||||
}
|
||||
|
||||
void radioExternalPaTxEnable()
|
||||
{
|
||||
dacWrite(RADIO_PA_APC2_PIN, nomadApc2Level);
|
||||
}
|
||||
|
||||
void radioExternalPaRxIdle()
|
||||
{
|
||||
dacWrite(RADIO_PA_APC2_PIN, 0);
|
||||
}
|
||||
|
||||
void radioExternalPaSleep()
|
||||
{
|
||||
dacWrite(RADIO_PA_APC2_PIN, 0);
|
||||
}
|
||||
|
||||
#endif // RADIOMASTER_NOMAD_GEMINI
|
||||
@@ -85,8 +85,12 @@
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_ECHO
|
||||
#elif defined(T_ECHO_LITE)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_LITE
|
||||
#elif defined(T_ECHO_CARD)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_CARD
|
||||
#elif defined(TTGO_T_ECHO_PLUS)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_PLUS
|
||||
#elif defined(T_IMPULSE_PLUS)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_IMPULSE_PLUS
|
||||
#elif defined(ELECROW_ThinkNode_M1)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M1
|
||||
#elif defined(ELECROW_ThinkNode_M3)
|
||||
@@ -117,6 +121,8 @@
|
||||
#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW
|
||||
#elif defined(HELTEC_T114)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_NODE_T114
|
||||
#elif defined(HELTEC_MESH_NODE_T1)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_NODE_T1
|
||||
#elif defined(MESHLINK)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_MESHLINK
|
||||
#elif defined(SEEED_XIAO_NRF52840_KIT)
|
||||
|
||||
@@ -362,4 +362,10 @@ uint32_t SimRadio::getPacketTime(uint32_t pl, bool received)
|
||||
|
||||
uint32_t msecs = tPacket * 1000;
|
||||
return msecs;
|
||||
}
|
||||
|
||||
int16_t SimRadio::getCurrentRSSI()
|
||||
{
|
||||
// Simulated radio - return a reasonable default noise floor
|
||||
return -120;
|
||||
}
|
||||
@@ -48,6 +48,8 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr
|
||||
// Convert Compressed_msg to normal msg and receive it
|
||||
void unpackAndReceive(meshtastic_MeshPacket &p);
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Debugging counts
|
||||
*/
|
||||
@@ -93,4 +95,4 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr
|
||||
virtual uint32_t getPacketTime(uint32_t pl, bool received = false) override;
|
||||
};
|
||||
|
||||
extern SimRadio *simRadio;
|
||||
extern SimRadio *simRadio;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "SGM41562.h"
|
||||
|
||||
#ifdef HAS_SGM41562
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
SGM41562 *sgm41562 = nullptr;
|
||||
|
||||
bool initSGM41562(TwoWire &wire)
|
||||
{
|
||||
if (sgm41562)
|
||||
return true;
|
||||
sgm41562 = new SGM41562();
|
||||
if (!sgm41562->begin(wire)) {
|
||||
delete sgm41562;
|
||||
sgm41562 = nullptr;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SGM41562::readReg(uint8_t reg, uint8_t &value)
|
||||
{
|
||||
wire_->beginTransmission(address_);
|
||||
wire_->write(reg);
|
||||
if (wire_->endTransmission(false) != 0)
|
||||
return false;
|
||||
if (wire_->requestFrom((int)address_, 1) != 1)
|
||||
return false;
|
||||
value = wire_->read();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SGM41562::writeReg(uint8_t reg, uint8_t value)
|
||||
{
|
||||
wire_->beginTransmission(address_);
|
||||
wire_->write(reg);
|
||||
wire_->write(value);
|
||||
return wire_->endTransmission() == 0;
|
||||
}
|
||||
|
||||
bool SGM41562::updateReg(uint8_t reg, uint8_t mask, uint8_t value)
|
||||
{
|
||||
uint8_t cur;
|
||||
if (!readReg(reg, cur))
|
||||
return false;
|
||||
cur = (cur & ~mask) | (value & mask);
|
||||
return writeReg(reg, cur);
|
||||
}
|
||||
|
||||
bool SGM41562::begin(TwoWire &wire, uint8_t address)
|
||||
{
|
||||
wire_ = &wire;
|
||||
address_ = address;
|
||||
|
||||
uint8_t id;
|
||||
if (!readReg(REG_DEVICE_ID, id)) {
|
||||
LOG_WARN("SGM41562: I2C read failed at 0x%02X", address_);
|
||||
return false;
|
||||
}
|
||||
if (id != DEVICE_ID_EXPECTED) {
|
||||
LOG_WARN("SGM41562: unexpected device ID 0x%02X (expected 0x%02X)", id, DEVICE_ID_EXPECTED);
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("SGM41562: detected at 0x%02X (id 0x%02X)", address_, id);
|
||||
|
||||
// Mirror the vendor reference init sequence: PCB OTP off, NTC off,
|
||||
// watchdog off, charger enabled. These match LilyGo's stock firmware
|
||||
// for the T-Impulse Plus.
|
||||
delay(120);
|
||||
writeReg(REG_SYS_VOLTAGE_REG, 0xB7);
|
||||
writeReg(REG_MISC_OP_CONTROL, 0x40);
|
||||
writeReg(REG_CHARGE_TERM_TIMER, 0x1A);
|
||||
writeReg(REG_POWER_ON_CFG, 0xA4);
|
||||
|
||||
return refresh();
|
||||
}
|
||||
|
||||
bool SGM41562::refresh()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
if (lastRefreshMs_ != 0 && (now - lastRefreshMs_) < 250)
|
||||
return true; // cached
|
||||
lastRefreshMs_ = now == 0 ? 1 : now;
|
||||
|
||||
uint8_t status, fault;
|
||||
if (!readReg(REG_SYSTEM_STATUS, status))
|
||||
return false;
|
||||
if (!readReg(REG_FAULT, fault))
|
||||
return false;
|
||||
|
||||
chargeStatus_ = static_cast<ChargeStatus>((status >> SYS_STATUS_CHRG_SHIFT) & SYS_STATUS_CHRG_MASK);
|
||||
inputPowerGood_ = (status & SYS_STATUS_PG) != 0;
|
||||
thermalReg_ = (status & SYS_STATUS_THERM_REG) != 0;
|
||||
faultMask_ = fault & 0x3F; // bits [7:6] are enter_ship_time config, not faults
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SGM41562::setChargeEnable(bool enable)
|
||||
{
|
||||
return updateReg(REG_POWER_ON_CFG, POWER_ON_CFG_CHG_DISABLE, enable ? 0x00 : POWER_ON_CFG_CHG_DISABLE);
|
||||
}
|
||||
|
||||
bool SGM41562::setShippingModeEnable(bool enable)
|
||||
{
|
||||
return updateReg(REG_MISC_OP_CONTROL, MISC_OP_SHIPPING_MODE, enable ? MISC_OP_SHIPPING_MODE : 0x00);
|
||||
}
|
||||
|
||||
#endif // HAS_SGM41562
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef HAS_SGM41562
|
||||
|
||||
#include <Wire.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// SG Micro SGM41562 — single-cell Li-ion buck charger, I²C-controlled, no
|
||||
// fuel gauge. This driver exposes status (charging / input good / fault),
|
||||
// charge enable, and shipping-mode control. Battery voltage/percent still
|
||||
// come from the platform ADC path; the charger is plumbed in as a
|
||||
// side-channel for isCharging()/isVbusIn() in AnalogBatteryLevel.
|
||||
//
|
||||
// Reference: SGM41562 datasheet (Cmd map + bit fields cross-verified against
|
||||
// LilyGo's `Cpp_Bus_Driver::Sgm41562xx` driver, which is what their vendor
|
||||
// example for this board uses).
|
||||
|
||||
#ifndef SGM41562_ADDR
|
||||
#define SGM41562_ADDR 0x03 // Per datasheet — unusual but correct
|
||||
#endif
|
||||
|
||||
#ifndef SGM41562_WIRE
|
||||
#define SGM41562_WIRE Wire1 // Most boards put the PMU on the secondary bus
|
||||
#endif
|
||||
|
||||
class SGM41562
|
||||
{
|
||||
public:
|
||||
enum class ChargeStatus : uint8_t {
|
||||
NotCharging = 0b00,
|
||||
Precharge = 0b01,
|
||||
FastCharge = 0b10,
|
||||
ChargeDone = 0b11,
|
||||
};
|
||||
|
||||
bool begin(TwoWire &wire, uint8_t address = SGM41562_ADDR);
|
||||
|
||||
// Re-read the system status + fault registers. Throttled internally to
|
||||
// at most one I²C transaction per 250 ms — call as often as you like.
|
||||
bool refresh();
|
||||
|
||||
// Status — cached from the most recent refresh().
|
||||
ChargeStatus chargeStatus() const { return chargeStatus_; }
|
||||
bool isCharging() const { return chargeStatus_ == ChargeStatus::Precharge || chargeStatus_ == ChargeStatus::FastCharge; }
|
||||
bool isChargeDone() const { return chargeStatus_ == ChargeStatus::ChargeDone; }
|
||||
bool isInputPowerGood() const { return inputPowerGood_; }
|
||||
bool isThermalRegulation() const { return thermalReg_; }
|
||||
uint8_t faultMask() const { return faultMask_; }
|
||||
|
||||
// Control.
|
||||
bool setChargeEnable(bool enable);
|
||||
bool setShippingModeEnable(bool enable);
|
||||
|
||||
private:
|
||||
TwoWire *wire_ = nullptr;
|
||||
uint8_t address_ = SGM41562_ADDR;
|
||||
uint32_t lastRefreshMs_ = 0;
|
||||
|
||||
ChargeStatus chargeStatus_ = ChargeStatus::NotCharging;
|
||||
bool inputPowerGood_ = false;
|
||||
bool thermalReg_ = false;
|
||||
uint8_t faultMask_ = 0;
|
||||
|
||||
bool readReg(uint8_t reg, uint8_t &value);
|
||||
bool writeReg(uint8_t reg, uint8_t value);
|
||||
bool updateReg(uint8_t reg, uint8_t mask, uint8_t value);
|
||||
|
||||
// SGM41562 register addresses
|
||||
static constexpr uint8_t REG_INPUT_SOURCE = 0x00;
|
||||
static constexpr uint8_t REG_POWER_ON_CFG = 0x01;
|
||||
static constexpr uint8_t REG_CHARGE_CURRENT = 0x02;
|
||||
static constexpr uint8_t REG_DISCHARGE_TERM_CURRENT = 0x03;
|
||||
static constexpr uint8_t REG_CHARGE_VOLTAGE = 0x04;
|
||||
static constexpr uint8_t REG_CHARGE_TERM_TIMER = 0x05;
|
||||
static constexpr uint8_t REG_MISC_OP_CONTROL = 0x06;
|
||||
static constexpr uint8_t REG_SYS_VOLTAGE_REG = 0x07;
|
||||
static constexpr uint8_t REG_SYSTEM_STATUS = 0x08;
|
||||
static constexpr uint8_t REG_FAULT = 0x09;
|
||||
static constexpr uint8_t REG_I2C_ADDR_MISC = 0x0A;
|
||||
static constexpr uint8_t REG_DEVICE_ID = 0x0B;
|
||||
|
||||
// Bit positions in REG_POWER_ON_CFG.
|
||||
static constexpr uint8_t POWER_ON_CFG_CHG_DISABLE = 0x08; // bit 3: 1 = charging disabled
|
||||
// Bit positions in REG_MISC_OP_CONTROL.
|
||||
static constexpr uint8_t MISC_OP_SHIPPING_MODE = 0x20; // bit 5: 1 = enter shipping mode
|
||||
// Bit positions in REG_SYSTEM_STATUS.
|
||||
static constexpr uint8_t SYS_STATUS_CHRG_SHIFT = 3;
|
||||
static constexpr uint8_t SYS_STATUS_CHRG_MASK = 0x03;
|
||||
static constexpr uint8_t SYS_STATUS_PG = 0x02; // bit 1: input power good
|
||||
static constexpr uint8_t SYS_STATUS_THERM_REG = 0x01; // bit 0: thermal regulation
|
||||
|
||||
static constexpr uint8_t DEVICE_ID_EXPECTED = 0x04;
|
||||
};
|
||||
|
||||
extern SGM41562 *sgm41562;
|
||||
|
||||
// Lazy-instantiate the global on the supplied wire. Returns true on success.
|
||||
bool initSGM41562(TwoWire &wire);
|
||||
|
||||
#endif // HAS_SGM41562
|
||||
@@ -19,6 +19,16 @@ static meshtastic_Position makePosition()
|
||||
return position;
|
||||
}
|
||||
|
||||
static meshtastic_Channel makeChannel(meshtastic_Channel_Role role, bool hasModuleSettings, uint32_t positionPrecision)
|
||||
{
|
||||
meshtastic_Channel channel = meshtastic_Channel_init_default;
|
||||
channel.has_settings = true;
|
||||
channel.role = role;
|
||||
channel.settings.has_module_settings = hasModuleSettings;
|
||||
channel.settings.module_settings.position_precision = positionPrecision;
|
||||
return channel;
|
||||
}
|
||||
|
||||
static void test_applyPositionPrecision_clampsLatLonAndSetsPrecisionBits()
|
||||
{
|
||||
meshtastic_Position position = makePosition();
|
||||
@@ -80,6 +90,35 @@ static void test_applyPositionPrecision_reencodesPositionPacket()
|
||||
TEST_ASSERT_EQUAL_UINT32(16, decoded.precision_bits);
|
||||
}
|
||||
|
||||
static void test_getPositionPrecisionForChannel_explicitPrecisionIsHonored()
|
||||
{
|
||||
meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_PRIMARY, true, 16);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(16, getPositionPrecisionForChannel(channel));
|
||||
}
|
||||
|
||||
static void test_getPositionPrecisionForChannel_explicitZeroDisablesPrimary()
|
||||
{
|
||||
meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_PRIMARY, true, 0);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(channel));
|
||||
}
|
||||
|
||||
static void test_getPositionPrecisionForChannel_primaryWithoutModuleSettingsFailsClosed()
|
||||
{
|
||||
// Regression guard for #10509: precision 32 below must be ignored (no module settings).
|
||||
meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_PRIMARY, false, 32);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(channel));
|
||||
}
|
||||
|
||||
static void test_getPositionPrecisionForChannel_secondaryWithoutModuleSettingsFailsClosed()
|
||||
{
|
||||
meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_SECONDARY, false, 32);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(channel));
|
||||
}
|
||||
|
||||
void setUp(void) {}
|
||||
|
||||
void tearDown(void) {}
|
||||
@@ -93,6 +132,10 @@ void setup()
|
||||
RUN_TEST(test_applyPositionPrecision_fullPrecisionKeepsLatLon);
|
||||
RUN_TEST(test_applyPositionPrecision_zeroScrubsLocationButKeepsTime);
|
||||
RUN_TEST(test_applyPositionPrecision_reencodesPositionPacket);
|
||||
RUN_TEST(test_getPositionPrecisionForChannel_explicitPrecisionIsHonored);
|
||||
RUN_TEST(test_getPositionPrecisionForChannel_explicitZeroDisablesPrimary);
|
||||
RUN_TEST(test_getPositionPrecisionForChannel_primaryWithoutModuleSettingsFailsClosed);
|
||||
RUN_TEST(test_getPositionPrecisionForChannel_secondaryWithoutModuleSettingsFailsClosed);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "TestUtil.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <unity.h>
|
||||
|
||||
// Regression coverage for issue #9828: on boards without a hardware RTC (e.g. RP2040),
|
||||
// gettimeofday() can return uptime seconds rather than wall-clock time. A later readFromRTC()
|
||||
// must not overwrite a higher-quality network/GPS time with that value, but it should still seed
|
||||
// the clock when nothing better exists yet.
|
||||
//
|
||||
// The native test build compiles the RV3028 hardware-RTC branch (variants/native/portduino
|
||||
// defines RV3028_RTC), so these tests use setReadFromRTCUseSystemTimeForTests() to force the
|
||||
// no-hardware-RTC fallback path and setRTCSystemTimeForTests() to inject a deterministic clock.
|
||||
|
||||
static const uint32_t kAllowedDriftSeconds = 2;
|
||||
static const time_t kUptimeSeconds = 21; // what gettimeofday() returns on RP2040 without a real clock
|
||||
|
||||
// A clearly-valid wall-clock epoch, safely inside any BUILD_EPOCH validity window.
|
||||
static time_t makeValidEpoch()
|
||||
{
|
||||
return time(NULL) + SEC_PER_DAY;
|
||||
}
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
resetRTCStateForTests();
|
||||
}
|
||||
|
||||
void tearDown(void)
|
||||
{
|
||||
resetRTCStateForTests();
|
||||
}
|
||||
|
||||
// A higher-quality network time must survive a later system-time read that only knows uptime.
|
||||
static void test_readFromRTC_preserves_better_network_time(void)
|
||||
{
|
||||
const time_t networkEpoch = makeValidEpoch();
|
||||
struct timeval networkTime;
|
||||
networkTime.tv_sec = networkEpoch;
|
||||
networkTime.tv_usec = 0;
|
||||
TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, perhapsSetRTC(RTCQualityFromNet, &networkTime));
|
||||
|
||||
// Simulate a later readFromRTC() falling back to a system clock that only knows uptime.
|
||||
struct timeval uptime;
|
||||
uptime.tv_sec = kUptimeSeconds;
|
||||
uptime.tv_usec = 0;
|
||||
setRTCSystemTimeForTests(&uptime);
|
||||
setReadFromRTCUseSystemTimeForTests(true);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, readFromRTC());
|
||||
TEST_ASSERT_EQUAL_INT(RTCQualityFromNet, getRTCQuality());
|
||||
TEST_ASSERT_UINT32_WITHIN(kAllowedDriftSeconds, (uint32_t)networkEpoch, getValidTime(RTCQualityFromNet));
|
||||
}
|
||||
|
||||
// Before any higher-quality source exists, the fallback should still seed the clock.
|
||||
static void test_readFromRTC_initializes_time_when_no_better_source(void)
|
||||
{
|
||||
const time_t systemEpoch = makeValidEpoch();
|
||||
struct timeval systemTime;
|
||||
systemTime.tv_sec = systemEpoch;
|
||||
systemTime.tv_usec = 0;
|
||||
setRTCSystemTimeForTests(&systemTime);
|
||||
setReadFromRTCUseSystemTimeForTests(true);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, readFromRTC());
|
||||
TEST_ASSERT_EQUAL_INT(RTCQualityNone, getRTCQuality());
|
||||
TEST_ASSERT_UINT32_WITHIN(kAllowedDriftSeconds, (uint32_t)systemEpoch, getTime());
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
delay(10);
|
||||
initializeTestEnvironment();
|
||||
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_readFromRTC_preserves_better_network_time);
|
||||
RUN_TEST(test_readFromRTC_initializes_time_when_no_better_source);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
@@ -12,7 +12,7 @@
|
||||
#define LORA_MISO 19
|
||||
#define LORA_MOSI 23
|
||||
#define LORA_CS 5
|
||||
#define RF95_FAN_EN 17
|
||||
#define RADIO_FAN_EN 17
|
||||
|
||||
// This is a LED_WS2812 not a standard LED
|
||||
#define HAS_NEOPIXEL // Enable the use of neopixels
|
||||
|
||||
@@ -72,7 +72,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
|
||||
h2zero/NimBLE-Arduino@1.4.3
|
||||
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
|
||||
https://github.com/dbinfrago/libpax/archive/df424747f9acb86ab07c5a206ded1e8e3650726a.zip
|
||||
https://github.com/dbinfrago/libpax/archive/17302340f100efbc4bee5022ecc72047d6d93ed4.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
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
FAN is active at 250mW on it's ExpressLRS Firmware.
|
||||
This FAN has TACHO signal on Pin 27 for use with PWM.
|
||||
*/
|
||||
#define RF95_FAN_EN 2
|
||||
#define RADIO_FAN_EN 2
|
||||
#define RADIO_FAN_PWM
|
||||
|
||||
/*
|
||||
LED PIN setup and it has a NeoPixel LED.
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
This unit has a FAN built-in.
|
||||
FAN is active at 250mW on it's ExpressLRS Firmware.
|
||||
*/
|
||||
#define RF95_FAN_EN 2
|
||||
#define RADIO_FAN_EN 2
|
||||
|
||||
/*
|
||||
LED PIN setup.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[env:radiomaster_nomad_gemini]
|
||||
extends = esp32_base
|
||||
board = esp32doit-devkit-v1
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
-DRADIOMASTER_NOMAD_GEMINI
|
||||
-DPRIVATE_HW
|
||||
-DVTABLES_IN_FLASH=1
|
||||
-DCONFIG_DISABLE_HAL_LOCKS=1
|
||||
-O2
|
||||
-Ivariants/esp32/radiomaster_nomad_gemini
|
||||
-DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
|
||||
-DMESHTASTIC_EXCLUDE_I2C=1
|
||||
-DRADIOLIB_EXCLUDE_SX128X=1
|
||||
-DRADIOLIB_EXCLUDE_SX127X=1
|
||||
-DRADIOLIB_EXCLUDE_SX126X=1
|
||||
board_build.f_cpu = 240000000L
|
||||
upload_protocol = esptool
|
||||
lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "RadioLib.h"
|
||||
|
||||
static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7,
|
||||
RADIOLIB_LR11X0_DIO8, RADIOLIB_NC};
|
||||
|
||||
static const Module::RfSwitchMode_t rfswitch_table[] = {
|
||||
// mode DIO5 DIO6 DIO7 DIO8
|
||||
{LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, LOW, HIGH, LOW}},
|
||||
{LR11x0::MODE_TX, {LOW, LOW, LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, LOW, LOW, HIGH}},
|
||||
{LR11x0::MODE_TX_HF, {LOW, HIGH, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, LOW, LOW}},
|
||||
{LR11x0::MODE_WIFI, {HIGH, LOW, LOW, LOW}}, END_OF_MODE_TABLE,
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
DIO5: RXEN 2.4GHz
|
||||
DIO6: TXEN 2.4GHz
|
||||
DIO7: RXEN 900MHz
|
||||
DIO8: TXEN 900MHz
|
||||
|
||||
|
||||
"radio_dcdc": true,
|
||||
"radio_rfo_hf": true,
|
||||
|
||||
"power_apc2": 26,
|
||||
"power_min": 0,
|
||||
"power_high": 6,
|
||||
"power_max": 6,
|
||||
"power_default": 3,
|
||||
"power_control": 3, POWER_OUTPUT_DACWRITE // use internal dacWrite function to set value on GPIO_PIN_RFamp_APC2
|
||||
[0, 1, 2, 3, 4, 5, 6 ] // 0-6
|
||||
"power_values": [120, 120, 120, 120, 120, 120, 95] // DAC Value
|
||||
"power_values2": [-17, -16, -14, -11, -7, -3, 5 ] // 900M
|
||||
"power_values_dual": [-18, -14, -8, -6, -2, 3, 5 ] // 2.4G
|
||||
|
||||
// default value 0 means direct!
|
||||
#define POWER_OUTPUT_DACWRITE (hardware_int(HARDWARE_power_control)==3)
|
||||
#define POWER_OUTPUT_VALUES hardware_i16_array(HARDWARE_power_values)
|
||||
#define POWER_OUTPUT_VALUES_COUNT hardware_int(HARDWARE_power_values_count)
|
||||
#define POWER_OUTPUT_VALUES2 hardware_i16_array(HARDWARE_power_values2)
|
||||
#define POWER_OUTPUT_VALUES_DUAL hardware_i16_array(HARDWARE_power_values_dual)
|
||||
#define POWER_OUTPUT_VALUES_DUAL_COUNT hardware_int(HARDWARE_power_values_dual_count)
|
||||
|
||||
#define GPIO_PIN_FAN_EN hardware_pin(HARDWARE_misc_fan_en)
|
||||
|
||||
case PWR_10mW: return 10;
|
||||
case PWR_25mW: return 14;
|
||||
case PWR_50mW: return 17;
|
||||
case PWR_100mW: return 20;
|
||||
case PWR_250mW: return 24;
|
||||
case PWR_500mW: return 27;
|
||||
case PWR_1000mW: return 30;
|
||||
|
||||
95 -> +25dBm
|
||||
120 -> +24dBm
|
||||
*/
|
||||
@@ -0,0 +1,68 @@
|
||||
#define HAS_SCREEN 0
|
||||
#define HAS_WIRE 0
|
||||
#define HAS_GPS 0
|
||||
#undef GPS_RX_PIN
|
||||
#undef GPS_TX_PIN
|
||||
|
||||
#define PIN_SPI_MISO 33
|
||||
#define PIN_SPI_MOSI 32
|
||||
#define PIN_SPI_SCK 25
|
||||
#define PIN_SPI_NSS 27
|
||||
|
||||
#define LORA_RESET 15
|
||||
#define LORA_DIO1 37
|
||||
#define LORA_DIO2 36
|
||||
#define LORA_SCK PIN_SPI_SCK
|
||||
#define LORA_MISO PIN_SPI_MISO
|
||||
#define LORA_MOSI PIN_SPI_MOSI
|
||||
#define LORA_CS PIN_SPI_NSS
|
||||
|
||||
// supported modules list
|
||||
#define USE_LR1121
|
||||
|
||||
#define LR1121_IRQ_PIN LORA_DIO1
|
||||
#define LR1121_NRESET_PIN LORA_RESET
|
||||
#define LR1121_BUSY_PIN LORA_DIO2
|
||||
#define LR1121_SPI_NSS_PIN LORA_CS
|
||||
#define LR1121_SPI_SCK_PIN LORA_SCK
|
||||
#define LR1121_SPI_MOSI_PIN LORA_MOSI
|
||||
#define LR1121_SPI_MISO_PIN LORA_MISO
|
||||
|
||||
// Caps for the LR1121 *chip* output. The external analog PA (driven via the APC2
|
||||
// DAC pin, see RADIO_PA_APC2_PIN below) provides the rest of the gain, so the chip
|
||||
// itself is driven low. See src/platform/extra_variants/radiomaster_nomad_gemini.
|
||||
#define LR1110_MAX_POWER 5
|
||||
// 2.4G Part
|
||||
#define LR1120_MAX_POWER 5
|
||||
|
||||
// not yet implemented
|
||||
#define JANUS_RADIO
|
||||
#define LR1121_IRQ2_PIN 34
|
||||
#define LR1121_NRESET2_PIN 21
|
||||
#define LR1121_BUSY2_PIN 39
|
||||
#define LR1121_SPI_NSS2_PIN 13
|
||||
|
||||
// TODO: check if this is correct
|
||||
// #define LR11X0_DIO3_TCXO_VOLTAGE 1.6
|
||||
#define LR11X0_DIO_AS_RF_SWITCH
|
||||
|
||||
#define HAS_NEOPIXEL // Enable the use of neopixels
|
||||
#define NEOPIXEL_COUNT 2 // How many neopixels are connected
|
||||
#define NEOPIXEL_DATA 22 // GPIO pin used to send data to the neopixels
|
||||
#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // Type of neopixels in use
|
||||
#define ENABLE_AMBIENTLIGHTING // Turn on Ambient Lighting
|
||||
|
||||
// Primary button is GPIO14 (ELRS "button"); GPIO12 is the second button ("button2").
|
||||
// NOTE: GPIO34 is the second radio's DIO1 (LR1121_IRQ2_PIN) and is input-only with no
|
||||
// internal pull resistor, so it must NOT be reused as a button.
|
||||
#define BUTTON_PIN 14
|
||||
#define BUTTON_NEED_PULLUP
|
||||
|
||||
#undef EXT_NOTIFY_OUT
|
||||
|
||||
#define RADIO_FAN_EN 2
|
||||
|
||||
// Analog PA bias control (ELRS APC2). GPIO26 = ESP32 DAC channel 2, driven with
|
||||
// dacWrite() (0-255 -> 0-3.3V). Defining this enables the external-PA driver in
|
||||
// src/platform/extra_variants/radiomaster_nomad_gemini/variant.cpp.
|
||||
#define RADIO_PA_APC2_PIN 26
|
||||
@@ -21,4 +21,4 @@ build_src_filter =
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=github-tags depName=ESP32-CH390 packageName=meshtastic/ESP32-CH390
|
||||
https://github.com/meshtastic/ESP32-CH390/archive/refs/tags/v1.0.1.zip
|
||||
https://github.com/meshtastic/ESP32-CH390/archive/v1.1.0.zip
|
||||
|
||||
@@ -20,4 +20,4 @@ build_src_filter =
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=github-tags depName=ESP32-CH390 packageName=meshtastic/ESP32-CH390
|
||||
https://github.com/meshtastic/ESP32-CH390/archive/refs/tags/v1.0.1.zip
|
||||
https://github.com/meshtastic/ESP32-CH390/archive/v1.1.0.zip
|
||||
|
||||
@@ -4,3 +4,9 @@ void variantDefaultConfig()
|
||||
{
|
||||
config.network.eth_enabled = true;
|
||||
}
|
||||
|
||||
void initVariant()
|
||||
{
|
||||
pinMode(LED_PAIRING, OUTPUT);
|
||||
digitalWrite(LED_PAIRING, !LED_STATE_ON); // Turn off the LED to start
|
||||
}
|
||||
@@ -6,11 +6,12 @@
|
||||
#define UART_TX 43
|
||||
#define UART_RX 44
|
||||
|
||||
#define WIFI_LED 3
|
||||
#define WIFI_STATE_ON 0
|
||||
#define LED_PAIRING 46
|
||||
#define LED_LORA 46
|
||||
|
||||
#define LED_PIN 46
|
||||
#define LED_PIN 3
|
||||
#define LED_STATE_ON 0
|
||||
#define LED_STATE_OFF 1
|
||||
#define BUTTON_PIN 4
|
||||
#define BUTTON_ACTIVE_LOW true
|
||||
#define BUTTON_ACTIVE_PULLUP true
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
// Fan control
|
||||
#define FAN_CTRL_PIN 41
|
||||
// Meshtastic standard fan control pin macro
|
||||
#define RF95_FAN_EN FAN_CTRL_PIN
|
||||
#define RADIO_FAN_EN FAN_CTRL_PIN
|
||||
|
||||
// PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us)
|
||||
// Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
; Heltec Mesh Node T1 nRF52840/SX1262 device
|
||||
[env:heltec-mesh-node-t1]
|
||||
custom_meshtastic_hw_model = 133
|
||||
custom_meshtastic_hw_model_slug = HELTEC_MESH_NODE_T1
|
||||
custom_meshtastic_architecture = nrf52840
|
||||
custom_meshtastic_actively_supported = true
|
||||
custom_meshtastic_support_level = 1
|
||||
custom_meshtastic_display_name = Heltec Mesh Node T1
|
||||
custom_meshtastic_images = heltec-mesh-node-t1.svg
|
||||
custom_meshtastic_tags = Heltec
|
||||
|
||||
extends = nrf52840_base
|
||||
board = heltec_mesh_node_t1
|
||||
board_level = pr
|
||||
debug_tool = jlink
|
||||
|
||||
build_flags = ${nrf52840_base.build_flags}
|
||||
-Ivariants/nrf52840/heltec_mesh_node_t1
|
||||
-DHELTEC_MESH_NODE_T1
|
||||
-DUSE_TFTDISPLAY
|
||||
-DUSER_SETUP_LOADED
|
||||
-DST7735_DRIVER
|
||||
-DST7735_REDTAB160x80
|
||||
-D TFT_SPI_PORT=SPI1
|
||||
-D TFT_CS=ST7735_CS
|
||||
-D TFT_DC=ST7735_RS
|
||||
-D TFT_RST=ST7735_RESET
|
||||
-D TFT_BL=ST7735_BL
|
||||
-D TFT_BACKLIGHT_ON=LOW
|
||||
|
||||
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t1>
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
lewisxhe/PCF8563_Library@^1.0.1
|
||||
bodmer/TFT_eSPI@2.5.43
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
|
||||
Copyright (c) 2016 Sandeep Mistry All right reserved.
|
||||
Copyright (c) 2018, Adafruit Industries (adafruit.com)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "variant.h"
|
||||
#include "nrf.h"
|
||||
#include "wiring_constants.h"
|
||||
#include "wiring_digital.h"
|
||||
|
||||
const uint32_t g_ADigitalPinMap[] = {
|
||||
// P0 - pins 0 and 1 are hardwired for xtal and should never be enabled
|
||||
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
|
||||
|
||||
// P1
|
||||
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
|
||||
|
||||
void initVariant()
|
||||
{
|
||||
pinMode(PIN_BUZZER_VOLTAGE_MULTIPLIER_1, OUTPUT);
|
||||
pinMode(PIN_BUZZER_VOLTAGE_MULTIPLIER_2, OUTPUT);
|
||||
digitalWrite(PIN_BUZZER_VOLTAGE_MULTIPLIER_1, HIGH);
|
||||
digitalWrite(PIN_BUZZER_VOLTAGE_MULTIPLIER_2, HIGH);
|
||||
}
|
||||
|
||||
void variant_shutdown()
|
||||
{
|
||||
nrf_gpio_cfg_default(ST7735_CS);
|
||||
nrf_gpio_cfg_default(ST7735_RS);
|
||||
nrf_gpio_cfg_default(ST7735_SDA);
|
||||
nrf_gpio_cfg_default(ST7735_SCK);
|
||||
nrf_gpio_cfg_default(ST7735_RESET);
|
||||
nrf_gpio_cfg_default(ST7735_BL);
|
||||
nrf_gpio_cfg_default(VTFT_CTRL);
|
||||
|
||||
nrf_gpio_cfg_default(PIN_WIRE_SDA);
|
||||
nrf_gpio_cfg_default(PIN_WIRE_SCL);
|
||||
|
||||
nrf_gpio_cfg_default(SX126X_CS);
|
||||
nrf_gpio_cfg_default(SX126X_DIO1);
|
||||
nrf_gpio_cfg_default(SX126X_BUSY);
|
||||
nrf_gpio_cfg_default(SX126X_RESET);
|
||||
|
||||
nrf_gpio_cfg_default(PIN_SPI_MISO);
|
||||
nrf_gpio_cfg_default(PIN_SPI_MOSI);
|
||||
nrf_gpio_cfg_default(PIN_SPI_SCK);
|
||||
|
||||
// nrf_gpio_cfg_default(PIN_SPI1_MISO);// ST7735 doesn't support MISO, so we don't configure it at all
|
||||
nrf_gpio_cfg_default(PIN_SPI1_MOSI);
|
||||
nrf_gpio_cfg_default(PIN_SPI1_SCK);
|
||||
|
||||
nrf_gpio_cfg_default(PIN_GPS_RESET);
|
||||
nrf_gpio_cfg_default(PIN_GPS_EN);
|
||||
nrf_gpio_cfg_default(PIN_GPS_PPS);
|
||||
nrf_gpio_cfg_default(GPS_TX_PIN);
|
||||
nrf_gpio_cfg_default(GPS_RX_PIN);
|
||||
|
||||
nrf_gpio_cfg_default(PIN_BUZZER_VOLTAGE_MULTIPLIER_1);
|
||||
nrf_gpio_cfg_default(PIN_BUZZER_VOLTAGE_MULTIPLIER_2);
|
||||
|
||||
pinMode(PIN_BUZZER, OUTPUT);
|
||||
digitalWrite(PIN_BUZZER, LOW);
|
||||
|
||||
pinMode(PIN_SENSOR_EN, OUTPUT);
|
||||
digitalWrite(PIN_SENSOR_EN, !PIN_SENSOR_EN_ACTIVE); // Turn off sensor power
|
||||
|
||||
pinMode(PIN_LED1, OUTPUT);
|
||||
digitalWrite(PIN_LED1, HIGH);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
|
||||
Copyright (c) 2016 Sandeep Mistry All right reserved.
|
||||
Copyright (c) 2018, Adafruit Industries (adafruit.com)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU Lesser General Public License for more details.
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#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 ST7735_CS (0 + 12)
|
||||
#define ST7735_RS (0 + 22) // DC
|
||||
#define ST7735_SDA (0 + 24)
|
||||
#define ST7735_SCK (32 + 0)
|
||||
#define ST7735_RESET (0 + 20)
|
||||
#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 SPI_FREQUENCY 80000000
|
||||
#define SPI_READ_FREQUENCY 16000000
|
||||
#define SCREEN_ROTATE
|
||||
#define TFT_HEIGHT 160
|
||||
#define TFT_WIDTH 80
|
||||
#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
|
||||
|
||||
// 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
|
||||
*/
|
||||
#define PIN_BUTTON1 (32 + 10)
|
||||
#define PIN_BUTTON2 (0 + 14)
|
||||
|
||||
/*
|
||||
No longer populated on PCB
|
||||
*/
|
||||
#define PIN_SERIAL2_RX (-1)
|
||||
#define PIN_SERIAL2_TX (-1)
|
||||
|
||||
/*
|
||||
* I2C
|
||||
*/
|
||||
|
||||
#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
|
||||
|
||||
/*
|
||||
* Lora radio
|
||||
*/
|
||||
|
||||
#define USE_SX1262
|
||||
#define SX126X_CS (32 + 11) // FIXME - we really should define LORA_CS instead
|
||||
#define LORA_CS SX126X_CS
|
||||
#define SX126X_DIO1 (0 + 31)
|
||||
#define SX126X_BUSY (0 + 29)
|
||||
#define SX126X_RESET (0 + 2)
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||
|
||||
/*
|
||||
* SPI Interfaces
|
||||
*/
|
||||
#define SPI_INTERFACES_COUNT 2
|
||||
|
||||
// For LORA, spi 0
|
||||
#define PIN_SPI_MISO (0 + 3)
|
||||
#define PIN_SPI_MOSI (32 + 14)
|
||||
#define PIN_SPI_SCK (32 + 13)
|
||||
|
||||
#define PIN_SPI1_MISO ST7735_MISO
|
||||
#define PIN_SPI1_MOSI ST7735_SDA
|
||||
#define PIN_SPI1_SCK ST7735_SCK
|
||||
|
||||
/*
|
||||
* 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 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
|
||||
|
||||
#define PIN_BUZZER (0 + 9)
|
||||
#define PIN_BUZZER_VOLTAGE_MULTIPLIER_1 (32 + 2)
|
||||
#define PIN_BUZZER_VOLTAGE_MULTIPLIER_2 (32 + 5)
|
||||
|
||||
#define ADC_CTRL 11
|
||||
#define ADC_CTRL_ENABLED HIGH
|
||||
#define BATTERY_PIN 5
|
||||
#define ADC_RESOLUTION 14
|
||||
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#define BATTERY_SENSE_RESOLUTION 4096.0
|
||||
#undef AREF_VOLTAGE
|
||||
#define AREF_VOLTAGE 3.0
|
||||
#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
|
||||
|
||||
// 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
|
||||
|
||||
#define HAS_RTC 0
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Arduino objects - C++ only
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
#endif
|
||||
@@ -6,7 +6,6 @@ debug_tool = jlink
|
||||
|
||||
build_flags = ${nrf52840_base.build_flags}
|
||||
-I variants/nrf52840/t-echo-card
|
||||
-D PRIVATE_HW
|
||||
-D T_ECHO_CARD
|
||||
|
||||
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo-card>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[env:t-impulse-plus]
|
||||
custom_meshtastic_hw_model = 135
|
||||
custom_meshtastic_hw_model_slug = T_IMPULSE_PLUS
|
||||
custom_meshtastic_architecture = nrf52840
|
||||
custom_meshtastic_actively_supported = true
|
||||
custom_meshtastic_support_level = 1
|
||||
custom_meshtastic_display_name = LILYGO T-Impulse Plus
|
||||
custom_meshtastic_tags = LilyGo
|
||||
custom_meshtastic_requires_dfu = true
|
||||
|
||||
extends = nrf52840_base
|
||||
board = t-impulse-plus
|
||||
board_level = pr
|
||||
|
||||
build_flags = ${nrf52840_base.build_flags}
|
||||
-I variants/nrf52840/t-impulse-plus
|
||||
-D T_IMPULSE_PLUS
|
||||
|
||||
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-impulse-plus>
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* variant.cpp - Digital pin mapping for LilyGo T-Impulse Plus
|
||||
*
|
||||
* Board: T-Impulse Plus V1.0 (nRF52840)
|
||||
* Hardware:
|
||||
* - SSD1315 OLED
|
||||
* - SX1262 (S62F)
|
||||
* - MIA-M10Q GPS
|
||||
* - ICM20948 IMU
|
||||
* - ZD25WQ32C Flash
|
||||
* - TTP223 Touch Button
|
||||
* - Vibration Motor
|
||||
*/
|
||||
|
||||
#include "variant.h"
|
||||
#include "nrf.h"
|
||||
#include "wiring_constants.h"
|
||||
#include "wiring_digital.h"
|
||||
|
||||
extern "C" {
|
||||
const uint32_t g_ADigitalPinMap[] = {
|
||||
// D0-D6: LoRa SX1262 (S62F module) SPI + control
|
||||
2, // D0 P0.02 SX1262_RST
|
||||
29, // D1 P0.29 SX1262_DIO1
|
||||
31, // D2 P0.31 SX1262_BUSY
|
||||
46, // D3 P1.14 SX1262_CS
|
||||
3, // D4 P0.03 SPI_SCK
|
||||
30, // D5 P0.30 SPI_MISO
|
||||
28, // D6 P0.28 SPI_MOSI
|
||||
|
||||
// D7-D8: RF switch control
|
||||
45, // D7 P1.13 SX1262_RF_VC1 (TXEN)
|
||||
39, // D8 P1.07 SX1262_RF_VC2 (RXEN)
|
||||
|
||||
// D9-D11: GPS (u-blox MIA-M10Q)
|
||||
44, // D9 P1.12 GPS_TX (MCU TX -> GPS RX)
|
||||
43, // D10 P1.11 GPS_RX (MCU RX <- GPS TX)
|
||||
42, // D11 P1.10 GPS_EN
|
||||
|
||||
// D12-D13: Display I2C (SSD1315)
|
||||
20, // D12 P0.20 SCREEN_SDA
|
||||
15, // D13 P0.15 SCREEN_SCL
|
||||
|
||||
// D14-D15: Sensor I2C (ICM20948, SGM41562)
|
||||
40, // D14 P1.08 IMU_SDA
|
||||
11, // D15 P0.11 IMU_SCL
|
||||
|
||||
// D16-D17: Battery management
|
||||
5, // D16 P0.05 BATTERY_ADC
|
||||
25, // D17 P0.25 BATTERY_MEASUREMENT_CONTROL
|
||||
|
||||
// D18: Touch button (TTP223)
|
||||
36, // D18 P1.04 TTP223_KEY
|
||||
|
||||
// D19: Vibration motor
|
||||
22, // D19 P0.22 VIBRATION_MOTOR
|
||||
|
||||
// D20: LDO enable
|
||||
14, // D20 P0.14 RT9080_EN
|
||||
|
||||
// D21-D26: Flash QSPI (ZD25WQ32C)
|
||||
12, // D21 P0.12 FLASH_CS
|
||||
4, // D22 P0.04 FLASH_SCLK
|
||||
6, // D23 P0.06 FLASH_IO0
|
||||
41, // D24 P1.09 FLASH_IO1
|
||||
8, // D25 P0.08 FLASH_IO2
|
||||
26, // D26 P0.26 FLASH_IO3
|
||||
|
||||
// D27-D28: Interrupt lines
|
||||
7, // D27 P0.07 ICM20948_INT
|
||||
16, // D28 P0.16 SGM41562_INT
|
||||
|
||||
// D29: Boot button
|
||||
24, // D29 P0.24 BOOT
|
||||
};
|
||||
}
|
||||
|
||||
void initVariant()
|
||||
{
|
||||
// Flash CS high (deselect)
|
||||
pinMode(PIN_QSPI_CS, OUTPUT);
|
||||
digitalWrite(PIN_QSPI_CS, HIGH);
|
||||
|
||||
// Enable battery voltage measurement
|
||||
pinMode(BAT_READ, OUTPUT);
|
||||
digitalWrite(BAT_READ, HIGH);
|
||||
|
||||
// Enable RT9080 LDO
|
||||
pinMode(D20, OUTPUT);
|
||||
digitalWrite(D20, HIGH);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
#ifndef _T_IMPULSE_PLUS_H_
|
||||
#define _T_IMPULSE_PLUS_H_
|
||||
#include "WVariant.h"
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Clock Configuration
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define VARIANT_MCK (64000000ul)
|
||||
#define USE_LFXO
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Pin Capacity Definitions
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define PINS_COUNT (30u)
|
||||
#define NUM_DIGITAL_PINS (30u)
|
||||
#define NUM_ANALOG_INPUTS (1u)
|
||||
#define NUM_ANALOG_OUTPUTS (0u)
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Digital Pin Mapping
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define D0 0 // P0.02 SX1262_RST
|
||||
#define D1 1 // P0.29 SX1262_DIO1
|
||||
#define D2 2 // P0.31 SX1262_BUSY
|
||||
#define D3 3 // P1.14 SX1262_CS
|
||||
#define D4 4 // P0.03 SPI_SCK
|
||||
#define D5 5 // P0.30 SPI_MISO
|
||||
#define D6 6 // P0.28 SPI_MOSI
|
||||
#define D7 7 // P1.13 RF_VC1 (TXEN)
|
||||
#define D8 8 // P1.07 RF_VC2 (RXEN)
|
||||
#define D9 9 // P1.12 GPS module TX → MCU RX
|
||||
#define D10 10 // P1.11 GPS module RX ← MCU TX
|
||||
#define D11 11 // P1.10 GPS_EN (active LOW)
|
||||
#define D12 12 // P0.20 SCREEN_SDA
|
||||
#define D13 13 // P0.15 SCREEN_SCL
|
||||
#define D14 14 // P1.08 IMU_SDA
|
||||
#define D15 15 // P0.11 IMU_SCL
|
||||
#define D16 16 // P0.05 BATTERY_ADC
|
||||
#define D17 17 // P0.25 BATTERY_CTL
|
||||
#define D18 18 // P1.04 TTP223_KEY
|
||||
#define D19 19 // P0.22 VIBRATION_MOTOR
|
||||
#define D20 20 // P0.14 RT9080_EN
|
||||
#define D21 21 // P0.12 FLASH_CS
|
||||
#define D22 22 // P0.04 FLASH_SCLK
|
||||
#define D23 23 // P0.06 FLASH_IO0
|
||||
#define D24 24 // P1.09 FLASH_IO1
|
||||
#define D25 25 // P0.08 FLASH_IO2
|
||||
#define D26 26 // P0.26 FLASH_IO3
|
||||
#define D27 27 // P0.07 ICM20948_INT
|
||||
#define D28 28 // P0.16 SGM41562_INT
|
||||
#define D29 29 // P0.24 BOOT
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// LED Configuration (no physical LED)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define LED_STATE_ON 1
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Button Configuration (TTP223 capacitive touch)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define PIN_BUTTON_TOUCH D18
|
||||
#define BUTTON_TOUCH_ACTIVE_LOW true
|
||||
#define BUTTON_TOUCH_ACTIVE_PULLUP false
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Analog Pin Definitions
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define PIN_VBAT D16 // P0.05 Battery voltage sense
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// I2C Configuration
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Primary I2C: Display (SSD1315)
|
||||
#define PIN_WIRE_SDA D12 // P0.20
|
||||
#define PIN_WIRE_SCL D13 // P0.15
|
||||
|
||||
// Secondary I2C: IMU (ICM20948) + PMU (SGM41562)
|
||||
#define WIRE_INTERFACES_COUNT 2
|
||||
#define PIN_WIRE1_SDA D14 // P1.08
|
||||
#define PIN_WIRE1_SCL D15 // P0.11
|
||||
#define I2C_NO_RESCAN
|
||||
|
||||
static const uint8_t SDA = PIN_WIRE_SDA;
|
||||
static const uint8_t SCL = PIN_WIRE_SCL;
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Display (SSD1315, compatible with SSD1306 driver)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define HAS_SCREEN 1
|
||||
#define USE_SSD1306 1
|
||||
#define OLED_TINY
|
||||
#define OLED_GEOMETRY_OVERRIDE GEOMETRY_64_32
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// SPI Configuration (SX1262 LoRa)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define SPI_INTERFACES_COUNT 1
|
||||
#define PIN_SPI_SCK D4 // P0.03
|
||||
#define PIN_SPI_MISO D5 // P0.30
|
||||
#define PIN_SPI_MOSI D6 // P0.28
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// SX1262 LoRa (S62F module)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define USE_SX1262
|
||||
#define SX126X_CS D3
|
||||
#define SX126X_DIO1 D1
|
||||
#define SX126X_BUSY D2
|
||||
#define SX126X_RESET D0
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||
#define SX126X_TXEN D7 // RF_VC1
|
||||
#define SX126X_RXEN D8 // RF_VC2
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Power Management
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define BAT_READ D17 // P0.25 Battery measurement control (HIGH = enable)
|
||||
#define BATTERY_PIN PIN_VBAT
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#define ADC_MULTIPLIER 2.0
|
||||
#define AREF_VOLTAGE 3.6
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// GPS (u-blox MIA-M10Q)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define GPS_UBLOX
|
||||
#define HAS_GPS 1
|
||||
#define GPS_RX_PIN D9 // P1.12 — MCU RX, wired to GPS module TX
|
||||
#define GPS_TX_PIN D10 // P1.11 — MCU TX, wired to GPS module RX
|
||||
#define PIN_GPS_EN D11 // P1.10
|
||||
#define GPS_EN_ACTIVE LOW
|
||||
#define GPS_BAUDRATE 38400
|
||||
#define GPS_THREAD_INTERVAL 50
|
||||
#define PIN_SERIAL1_TX GPS_TX_PIN
|
||||
#define PIN_SERIAL1_RX GPS_RX_PIN
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// On-board QSPI Flash (ZD25WQ32CEIGR)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define PIN_QSPI_SCK D22 // P0.04
|
||||
#define PIN_QSPI_CS D21 // P0.12
|
||||
#define PIN_QSPI_IO0 D23 // P0.06
|
||||
#define PIN_QSPI_IO1 D24 // P1.09
|
||||
#define PIN_QSPI_IO2 D25 // P0.08
|
||||
#define PIN_QSPI_IO3 D26 // P0.26
|
||||
|
||||
#define EXTERNAL_FLASH_DEVICES W25Q32JV_IQ
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Vibration Motor (GPIO active-high)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define LED_NOTIFICATION D19 // P0.22
|
||||
#define HAPTIC_FEEDBACK_PIN LED_NOTIFICATION
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// IMU (ICM20948 on Wire1)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define HAS_ICM20948
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Charger (SGM41562 on Wire1 @ 0x03)
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define HAS_SGM41562
|
||||
#define SGM41562_WIRE Wire1
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Compatibility Definitions
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PIN_SERIAL2_RX (-1)
|
||||
#define PIN_SERIAL2_TX (-1)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // _T_IMPULSE_PLUS_H_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user