Restore Keymind features after LoRa OTA merge

This commit is contained in:
mikecarper committed 2026-07-11 12:45:02 -07:00
1 parent 4c0dbe707d
commit 4f8f36354e
45 files changed
+1320 -235

No files matched your search

+224 -32
View File
@@ -3,12 +3,15 @@
ALL_PIO_ENVS=()
SUPPORTED_PIO_ENVS=()
declare -A PIO_ENV_PLATFORM_BY_NAME=()
declare -A PIO_ENV_MQTT_BY_NAME=()
PIO_CONFIG_JSON=""
MENU_CHOICE=""
SELECTED_TARGET=""
SELECTED_COMMAND_ARGS=()
MESHDEBUG_OVERRIDE=""
PACKET_LOGGING_OVERRIDE=""
MQTT_BRIDGE_OVERRIDE=""
MQTT_DEBUG_OVERRIDE=""
FIRMWARE_FILENAME_INFIX=""
RADIO_SETTINGS_API_URL="https://api.meshcore.nz/api/v1/config"
RADIO_SETTING_TITLE=""
@@ -35,6 +38,7 @@ DEFAULT_VARIANT_LABEL="default"
TAG_PREFIX_ROOM_SERVER="room-server"
TAG_PREFIX_COMPANION="companion"
TAG_PREFIX_REPEATER="repeater"
TAG_PREFIX_SENSOR="sensor"
SUPPORTED_PLATFORM_PATTERN='ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM'
OUTPUT_DIR="out"
FALLBACK_VERSION_PREFIX="dev"
@@ -54,11 +58,12 @@ Commands:
list|-l: List firmwares available to build.
build-firmware <target>: Build the firmware for the given build target.
build-firmwares: Build all firmwares for all targets.
build-firmwares-logging-matrix: Build all firmwares twice, first with MESH_DEBUG/MESH_PACKET_LOGGING off and then with both on for non-Bluetooth targets.
build-firmwares-logging-matrix: Build all firmwares in three profiles: logging off, logging on for non-Bluetooth targets, and MQTT bridge on with logging off.
build-matching-firmwares <build-match-spec>: Build all firmwares for build targets containing the string given for <build-match-spec>.
build-companion-firmwares: Build all companion firmwares for all build targets.
build-repeater-firmwares: Build all repeater firmwares for all build targets.
build-room-server-firmwares: Build all chat room server firmwares for all build targets.
build-sensor-firmwares: Build all sensor firmwares for all build targets.
Examples:
Build firmware for the "RAK_4631_repeater" device target
@@ -70,7 +75,7 @@ $ bash build.sh
Build all firmwares for device targets containing the string "RAK_4631"
$ bash build.sh build-matching-firmwares <build-match-spec>
Build all firmwares twice, with logging-off artifacts named "name-version" and logging-on artifacts named "name-logging-version":
Build all firmwares in three profiles, adding MQTT observer firmware with logging off after the logging-off and logging-on passes:
$ bash build.sh build-firmwares-logging-matrix
Build all companion firmwares
@@ -82,6 +87,9 @@ $ bash build.sh build-repeater-firmwares
Build all chat room server firmwares
$ bash build.sh build-room-server-firmwares
Build all sensor firmwares
$ bash build.sh build-sensor-firmwares
Environment Variables:
FIRMWARE_VERSION=vX.Y.Z: Firmware version to embed in the build output.
If not set, build.sh derives a default from the latest matching git tag and appends "-dev".
@@ -117,12 +125,13 @@ init_project_context() {
fi
if [ ${#SUPPORTED_PIO_ENVS[@]} -eq 0 ]; then
while IFS=$'\t' read -r env_name env_platform; do
while IFS=$'\t' read -r env_name env_platform env_mqtt; do
if [ -z "$env_name" ] || [ -z "$env_platform" ]; then
continue
fi
SUPPORTED_PIO_ENVS+=("$env_name")
PIO_ENV_PLATFORM_BY_NAME["$env_name"]=$env_platform
PIO_ENV_MQTT_BY_NAME["$env_name"]=$env_mqtt
done < <(
python3 -c '
import json
@@ -135,18 +144,20 @@ for section, options in data:
if not section.startswith("env:"):
continue
env_name = section[4:]
mqtt_enabled = False
platform = None
for key, value in options:
if key != "build_flags":
continue
values = value if isinstance(value, list) else str(value).split()
for flag in values:
if "WITH_MQTT_BRIDGE" in str(flag):
mqtt_enabled = True
match = pattern.search(str(flag))
if match:
print(f"{env_name}\t{match.group(0)}")
break
else:
continue
break
if match and platform is None:
platform = match.group(0)
if platform:
print(f"{env_name}\t{platform}\t{1 if mqtt_enabled else 0}")
' "$SUPPORTED_PLATFORM_PATTERN" <<<"$PIO_CONFIG_JSON"
)
fi
@@ -265,10 +276,11 @@ prompt_for_build_mode() {
local options=(
"Build one firmware target"
"Build all firmwares"
"Build all firmwares twice (logging off, then logging on for non-Bluetooth targets)"
"Build all firmwares in 3 profiles (logging off, logging on, MQTT bridge with logging off)"
"Build all repeater firmwares"
"Build all companion firmwares"
"Build all chat room server firmwares"
"Build all sensor firmwares"
)
echo "No command provided. Select a build action:"
@@ -306,6 +318,10 @@ prompt_for_build_mode() {
SELECTED_COMMAND_ARGS=(build-room-server-firmwares)
return 0
;;
7)
SELECTED_COMMAND_ARGS=(build-sensor-firmwares)
return 0
;;
esac
done
}
@@ -321,6 +337,13 @@ prompt_for_debug_build_settings() {
echo "Using debug options: meshdebug=${MESHDEBUG_OVERRIDE}, packet_logging=${PACKET_LOGGING_OVERRIDE}"
}
prompt_for_mqtt_bridge_build_setting() {
echo "MQTT bridge sends mesh radio traffic directly to MQTT over WiFi."
prompt_on_off_choice "MQTT bridge (radio WiFi to MQTT direct)" "off"
MQTT_BRIDGE_OVERRIDE="$MENU_CHOICE"
echo "Using MQTT bridge: ${MQTT_BRIDGE_OVERRIDE}"
}
is_logging_matrix_command() {
[ "$1" == "build-firmwares-logging-matrix" ]
}
@@ -630,6 +653,9 @@ get_env_metadata() {
repeater*)
tag_prefix="$TAG_PREFIX_REPEATER"
;;
sensor)
tag_prefix="$TAG_PREFIX_SENSOR"
;;
*)
tag_prefix=""
;;
@@ -943,6 +969,9 @@ get_pio_envs_for_variant_role() {
room_server:room_server)
echo "$env"
;;
sensor:sensor)
echo "$env"
;;
esac
done
}
@@ -1054,7 +1083,7 @@ normalize_resume_build_output() {
}
prompt_for_logging_matrix_output_policy() {
local choice
local choice file_count file_label
normalize_resume_build_output
@@ -1063,15 +1092,23 @@ prompt_for_logging_matrix_output_policy() {
return 0
fi
file_count=$(find "$OUTPUT_DIR" -type f -printf '.' | wc -c)
file_count=$((file_count))
if [ "$file_count" -eq 1 ]; then
file_label="file"
else
file_label="files"
fi
if ! [ -t 0 ]; then
if [ "$RESUME_BUILD_OUTPUT" == "1" ]; then
echo "Resuming previous logging matrix output in ${OUTPUT_DIR}."
echo "Resuming previous logging matrix output in ${OUTPUT_DIR} (${file_count} ${file_label})."
fi
return 0
fi
while true; do
read -r -p "Output directory '${OUTPUT_DIR}' exists. Resume previous option 3 progress or clean it? [resume/clean] (default: clean): " choice
read -r -p "Output directory '${OUTPUT_DIR}' exists with ${file_count} ${file_label}. Resume previous option 3 progress or clean it? [resume/clean] (default: clean): " choice
choice=${choice,,}
if [ -z "$choice" ]; then
choice="clean"
@@ -1144,12 +1181,97 @@ is_supported_build_env() {
[ -n "${PIO_ENV_PLATFORM_BY_NAME[$env_name]+x}" ]
}
is_mqtt_bridge_target() {
[ "${PIO_ENV_MQTT_BY_NAME[$1]:-0}" == "1" ]
}
normalize_resolved_targets_for_mqtt() {
local command=$1
local target
local candidate
local skipped_count=0
local -a candidates=("${RESOLVED_BUILD_TARGETS[@]}")
local -a normalized_targets=()
local -A seen_targets=()
if [ "${MQTT_BRIDGE_OVERRIDE,,}" == "on" ]; then
case "$command" in
build-repeater-firmwares)
for target in "${SUPPORTED_PIO_ENVS[@]}"; do
if is_mqtt_bridge_target "$target" && [[ "$target" == *_repeater_observer_mqtt ]]; then
candidates+=("$target")
fi
done
;;
build-room-server-firmwares)
for target in "${SUPPORTED_PIO_ENVS[@]}"; do
if is_mqtt_bridge_target "$target" && [[ "$target" == *_room_server_observer_mqtt ]]; then
candidates+=("$target")
fi
done
;;
esac
fi
for target in "${candidates[@]}"; do
candidate=""
if [ "${MQTT_BRIDGE_OVERRIDE,,}" == "on" ]; then
if is_mqtt_bridge_target "$target"; then
candidate=$target
elif is_mqtt_bridge_target "${target}_observer_mqtt"; then
candidate="${target}_observer_mqtt"
fi
else
if is_mqtt_bridge_target "$target"; then
candidate=${target%_observer_mqtt}
if ! is_supported_build_env "$candidate" || is_mqtt_bridge_target "$candidate"; then
candidate=""
fi
else
candidate=$target
fi
fi
if [ -z "$candidate" ]; then
skipped_count=$((skipped_count + 1))
continue
fi
if [ -z "${seen_targets[$candidate]+x}" ]; then
normalized_targets+=("$candidate")
seen_targets["$candidate"]=1
fi
done
RESOLVED_BUILD_TARGETS=("${normalized_targets[@]}")
if [ ${#RESOLVED_BUILD_TARGETS[@]} -eq 0 ]; then
echo "No targets support the selected MQTT bridge setting. MQTT bridge is for direct radio-to-MQTT forwarding over WiFi."
return 1
fi
if [ "${MQTT_BRIDGE_OVERRIDE,,}" == "on" ]; then
echo "MQTT bridge enabled for ${#RESOLVED_BUILD_TARGETS[@]} target(s); ${skipped_count} target(s) without a WiFi MQTT environment were skipped."
else
echo "MQTT bridge disabled; using ${#RESOLVED_BUILD_TARGETS[@]} standard target(s)."
fi
}
disable_debug_flags() {
if [ "$DISABLE_DEBUG" == "1" ]; then
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -UMESH_PACKET_LOGGING -UBLE_DEBUG_LOGGING -UWIFI_DEBUG_LOGGING -UBRIDGE_DEBUG -UGPS_NMEA_DEBUG -UCORE_DEBUG_LEVEL -UESPNOW_DEBUG_LOGGING -UDEBUG_RP2040_WIRE -UDEBUG_RP2040_SPI -UDEBUG_RP2040_CORE -UDEBUG_RP2040_PORT -URADIOLIB_DEBUG_SPI -DCFG_DEBUG=0 -URADIOLIB_DEBUG_BASIC -URADIOLIB_DEBUG_PROTOCOL"
fi
}
apply_mqtt_bridge_override() {
case "${MQTT_BRIDGE_OVERRIDE,,}" in
on)
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DWITH_MQTT_BRIDGE=1"
;;
off)
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UWITH_MQTT_BRIDGE"
;;
esac
}
apply_debug_overrides() {
case "${MESHDEBUG_OVERRIDE,,}" in
on)
@@ -1168,21 +1290,40 @@ apply_debug_overrides() {
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_PACKET_LOGGING"
;;
esac
case "${MQTT_DEBUG_OVERRIDE,,}" in
off)
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMQTT_DEBUG -UMQTT_MEMORY_DEBUG"
;;
esac
}
is_lora_ota_build() {
local env_name=$1
if [[ "$env_name" == *mqtt* ]] \
|| is_mqtt_bridge_target "$env_name" \
|| [ "${MQTT_BRIDGE_OVERRIDE,,}" == "on" ] \
|| [ "${MESHDEBUG_OVERRIDE,,}" == "on" ] \
|| [ "${PACKET_LOGGING_OVERRIDE,,}" == "on" ] \
|| [ "$FIRMWARE_FILENAME_INFIX" == "logging" ]; then
return 1
fi
case "$env_name" in
*repeater*|*repeatr*|*room_server*|*room_svr*|*sensor*) return 0 ;;
*) return 1 ;;
esac
}
apply_lora_ota_override() {
local env_name=$1
if [[ "$env_name" == *mqtt* ]] \
|| [ "${MQTT_BRIDGE_OVERRIDE,,}" == "on" ] \
|| [ "${MESHDEBUG_OVERRIDE,,}" == "on" ] \
|| [ "${PACKET_LOGGING_OVERRIDE,,}" == "on" ] \
|| [ "$FIRMWARE_FILENAME_INFIX" == "logging" ]; then
if is_lora_ota_build "$env_name"; then
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DENABLE_OTA=1"
else
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UENABLE_OTA"
return
fi
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DENABLE_OTA=1"
}
apply_radio_overrides() {
@@ -1417,10 +1558,12 @@ build_firmware() {
firmware_filename=$(get_firmware_filename "$env_name" "$firmware_version_string")
# OTA target id = sha2-256:4(env_name) as a little-endian uint32 (matches tools/mota target_id_for_env
# and the device's MainBoard::getOtaTargetId()). Harmless when OTA is disabled.
mota_target_id=$(python3 -c "import hashlib,sys;print('0x%08x'%int.from_bytes(hashlib.sha256(sys.argv[1].encode()).digest()[:4],'little'))" "$env_name" 2>/dev/null || echo "")
if [ -n "$mota_target_id" ]; then
mota_target_flag=" -DMOTA_TARGET_ID=${mota_target_id}"
# and the device's MainBoard::getOtaTargetId()). Only OTA-enabled profiles receive this identifier.
if is_lora_ota_build "$env_name"; then
mota_target_id=$(python3 -c "import hashlib,sys;print('0x%08x'%int.from_bytes(hashlib.sha256(sys.argv[1].encode()).digest()[:4],'little'))" "$env_name" 2>/dev/null || echo "")
if [ -n "$mota_target_id" ]; then
mota_target_flag=" -DMOTA_TARGET_ID=${mota_target_id}"
fi
fi
# Fork CI hooks (consumed by .github/workflows/build-observer*-firmwares.yml).
@@ -1456,6 +1599,7 @@ build_firmware() {
export PLATFORMIO_BUILD_FLAGS="${original_platformio_build_flags} -DFIRMWARE_BUILD_DATE='\"${firmware_build_date}\"' -DFIRMWARE_VERSION='\"${embedded_version_string}\"' -DOTA_VARIANT='\"${env_name}\"'${mota_target_flag}"
disable_debug_flags
apply_debug_overrides
apply_mqtt_bridge_override
apply_lora_ota_override "$env_name"
apply_radio_overrides
apply_firmware_profile_overrides
@@ -1497,6 +1641,10 @@ resolve_room_server_firmwares() {
get_pio_envs_for_variant_role room_server
}
resolve_sensor_firmwares() {
get_pio_envs_for_variant_role sensor
}
# Keep bulk build command names mapped to their target resolvers in one place.
get_bulk_build_resolver_name() {
case "$1" in
@@ -1515,6 +1663,9 @@ get_bulk_build_resolver_name() {
build-room-server-firmwares)
echo "resolve_room_server_firmwares"
;;
build-sensor-firmwares)
echo "resolve_sensor_firmwares"
;;
*)
return 1
;;
@@ -1640,9 +1791,14 @@ run_resolved_build_targets() {
run_logging_matrix_build_targets() {
local targets=("$@")
local target
local standard_targets=()
local logging_targets=()
local mqtt_targets=()
local original_meshdebug_override=$MESHDEBUG_OVERRIDE
local original_packet_logging_override=$PACKET_LOGGING_OVERRIDE
local original_mqtt_bridge_override=$MQTT_BRIDGE_OVERRIDE
local original_mqtt_debug_override=$MQTT_DEBUG_OVERRIDE
local original_firmware_filename_infix=$FIRMWARE_FILENAME_INFIX
local bluetooth_skip_count=0
local build_status=0
@@ -1652,26 +1808,38 @@ run_logging_matrix_build_targets() {
return 1
fi
echo "Building ${#targets[@]} target(s) with MESH_DEBUG=off and MESH_PACKET_LOGGING=off."
for target in "${targets[@]}"; do
if is_mqtt_bridge_target "$target"; then
mqtt_targets+=("$target")
else
standard_targets+=("$target")
fi
done
echo "Profile 1/3: building ${#standard_targets[@]} standard target(s) with logging off and MQTT bridge off."
MESHDEBUG_OVERRIDE="off"
PACKET_LOGGING_OVERRIDE="off"
MQTT_BRIDGE_OVERRIDE="off"
FIRMWARE_FILENAME_INFIX=""
run_resolved_build_targets "${targets[@]}"
build_status=$?
if [ ${#standard_targets[@]} -gt 0 ]; then
run_resolved_build_targets "${standard_targets[@]}"
build_status=$?
fi
if [ "$build_status" -eq 0 ]; then
mapfile -t logging_targets < <(filter_out_bluetooth_targets "${targets[@]}")
bluetooth_skip_count=$((${#targets[@]} - ${#logging_targets[@]}))
mapfile -t logging_targets < <(filter_out_bluetooth_targets "${standard_targets[@]}")
bluetooth_skip_count=$((${#standard_targets[@]} - ${#logging_targets[@]}))
if [ "$bluetooth_skip_count" -gt 0 ]; then
echo "Skipping ${bluetooth_skip_count} Bluetooth target(s) for logging-on pass."
fi
if [ ${#logging_targets[@]} -gt 0 ]; then
echo "Building ${#logging_targets[@]} target(s) with MESH_DEBUG=on and MESH_PACKET_LOGGING=on."
echo "Profile 2/3: building ${#logging_targets[@]} standard target(s) with logging on and MQTT bridge off."
echo "Logging-on artifacts use filename form: name-logging-version"
MESHDEBUG_OVERRIDE="on"
PACKET_LOGGING_OVERRIDE="on"
MQTT_BRIDGE_OVERRIDE="off"
FIRMWARE_FILENAME_INFIX="logging"
run_resolved_build_targets "${logging_targets[@]}"
build_status=$?
@@ -1680,8 +1848,25 @@ run_logging_matrix_build_targets() {
fi
fi
if [ "$build_status" -eq 0 ]; then
if [ ${#mqtt_targets[@]} -gt 0 ]; then
echo "Profile 3/3: building ${#mqtt_targets[@]} MQTT bridge target(s) for direct radio-to-MQTT forwarding over WiFi, with logging off."
MESHDEBUG_OVERRIDE="off"
PACKET_LOGGING_OVERRIDE="off"
MQTT_BRIDGE_OVERRIDE="on"
MQTT_DEBUG_OVERRIDE="off"
FIRMWARE_FILENAME_INFIX=""
run_resolved_build_targets "${mqtt_targets[@]}"
build_status=$?
else
echo "No MQTT bridge targets are configured; skipping profile 3/3."
fi
fi
MESHDEBUG_OVERRIDE=$original_meshdebug_override
PACKET_LOGGING_OVERRIDE=$original_packet_logging_override
MQTT_BRIDGE_OVERRIDE=$original_mqtt_bridge_override
MQTT_DEBUG_OVERRIDE=$original_mqtt_debug_override
FIRMWARE_FILENAME_INFIX=$original_firmware_filename_infix
return "$build_status"
@@ -1756,9 +1941,10 @@ main() {
prompt_for_build_mode
if is_logging_matrix_command "${SELECTED_COMMAND_ARGS[0]}"; then
echo "Skipping debug option prompts; this action builds logging off and logging on passes."
echo "Skipping debug and MQTT prompts; this action builds all three profiles automatically."
else
prompt_for_debug_build_settings
prompt_for_mqtt_bridge_build_setting
fi
prompt_for_radio_build_settings
prompt_for_firmware_profile_settings
@@ -1770,6 +1956,12 @@ main() {
exit 1
fi
if ! is_logging_matrix_command "$1" && [ -n "$MQTT_BRIDGE_OVERRIDE" ]; then
if ! normalize_resolved_targets_for_mqtt "$1"; then
exit 1
fi
fi
prompt_for_resolved_firmware_version
if is_logging_matrix_command "$1"; then
prompt_for_logging_matrix_output_policy
+16
View File
@@ -1663,6 +1663,22 @@ clear recent.repeater
### Sensors (When sensor support is compiled in)
#### View or change telemetry access mode
**Usage:**
- `get telemetry.access`
- `set telemetry.access <mode>`
**Parameters:**
- `mode`: `all`|`acl`
- `all`: allow telemetry requests using the requester-provided telemetry mask
- `acl`: require ACL read-only or higher for telemetry, including GPS
**Default:** `all`
**Note:** `all` matches the previous sensor telemetry behavior.
---
#### View the list of sensors on this node
**Usage:** `sensor list [start]`
+143
View File
@@ -0,0 +1,143 @@
# GPS Tracking
This document describes how GPS telemetry works for companion/client nodes and
sensor nodes.
## Scope
GPS tracking uses the existing CayenneLPP GPS telemetry field. It does not add a
new phone app protocol field.
The GPS telemetry value contains latitude, longitude, and altitude. Speed,
heading, and freshness are not sent as separate telemetry fields.
If the firmware does not have a valid fresh GPS cache, it omits the GPS field
from telemetry. This is intentional: stale or missing fixes are not reported as
zero coordinates.
## Freshness
GPS telemetry is cached separately from advert location.
The cache behavior is:
- Refresh the cached GPS value every 2 hours only when at least one contact or
ACL client can receive location telemetry.
- Try for up to 15 minutes during a refresh.
- If the position stays within about 100 meters for 30 seconds, cache a weighted
average of the stable fixes, with newer fixes weighted more heavily.
- If the position moves outside that 100 meter circle during acquisition, cache
the latest valid fix.
- Omit GPS from telemetry if there is no fix or if the cached fix is more than
12 hours old.
When a telemetry request asks for location, GPS is kept on for 2 hours after the
latest location request. During that hold window, later location telemetry
requests can use fresh GPS data as soon as valid fixes are available.
If GPS is manually enabled, it stays on and valid fixes continue to update the
cache.
If no contact or ACL client can receive location telemetry, the scheduled
2-hour refresh does not run. A real location telemetry request still turns GPS
on for the 2-hour hold window, and manual GPS-on still keeps the cache updated.
## Companion/Client Nodes
Companion/client telemetry uses the existing companion telemetry permission
system:
- base telemetry
- location telemetry
- environment telemetry
Location telemetry is sent only when the requester's effective telemetry
permissions include location. Those permissions are derived from the companion
telemetry mode settings and contact flags.
The scheduled GPS cache refresh runs only when at least one stored contact has
effective location telemetry access:
- `location: allow all` with at least one stored contact
- `location: allow flags` with at least one stored contact whose flags include
location
No new phone app behavior is required. Existing clients see the existing GPS
telemetry field when it is present.
## Sensor Nodes
Sensor telemetry access is controlled by:
```text
get telemetry.access
set telemetry.access all
set telemetry.access acl
```
`all` is the default and matches the previous sensor telemetry behavior. A
request that reaches the normal sensor telemetry request path can receive the
requested telemetry fields, including GPS, subject to the request mask and GPS
freshness rules.
`acl` gates telemetry through the sensor ACL. A requester with read-only or
higher permissions receives the existing telemetry set, including GPS. Guest or
unknown requesters receive no telemetry fields.
The scheduled GPS cache refresh follows the same access setting:
- `all`: runs only when at least one ACL client exists
- `acl`: runs only when at least one ACL client is read-only or higher
Use the existing ACL command to grant access:
```text
setperm <pubkey> 1
```
Permission values:
- `1`: read-only, suitable for telemetry access
- `2`: read-write
- `3`: admin
## Advert Location
Advert location is separate from telemetry GPS. The advert policy is controlled
with:
```text
gps advert
gps advert none
gps advert share
gps advert prefs
```
Policies:
- `none`: do not include location in adverts
- `share`: use the live/shared sensor manager location
- `prefs`: use the stored node latitude and longitude preferences
Telemetry GPS can be fresh while advert location is fixed or disabled, depending
on this policy.
## Recommended Setup
For private sensor tracking:
```text
set telemetry.access acl
setperm <owner_pubkey> 3
setperm <trusted_pubkey> 1
```
For compatibility with the older sensor behavior:
```text
set telemetry.access all
```
For GPS telemetry behavior, leave the phone app unchanged. The firmware omits
GPS when it is stale or missing and sends the existing GPS telemetry field when
fresh data is available.
+1
View File
@@ -6,6 +6,7 @@ Below are a few quick start guides.
- [Frequently Asked Questions](./faq.md)
- [CLI Commands](./cli_commands.md)
- [GPS Tracking](./gps_tracking.md)
- [Companion Protocol](./companion_protocol.md)
- [Packet Format](./packet_format.md)
- [QR Codes](./qr_codes.md)
+2 -1
View File
@@ -4,7 +4,8 @@ This guide is for **node operators**: how to update your MeshCore device's firmw
plain language. No cables, no programmer — your node can download a new firmware from a neighbour and
install it. (For the technical wire format, see [the OTA protocol spec](ota_protocol.md).)
LoRa OTA is present only in standard, non-logging Keymind firmware. MQTT bridge and logging builds exclude it.
LoRa OTA is present only in standard, non-logging Keymind repeater, room-server, and sensor firmware. MQTT
bridge, logging, and roles without `tempradio` support exclude it.
OTA radio traffic is accepted, generated, and relayed only while `tempradio` is actually running on that node.
Every source, receiver, and intermediate repeater must therefore have an overlapping temporary-radio window.
+99 -5
View File
@@ -121,6 +121,13 @@
#define DEFAULT_AUTOADD_CONFIG 0
#endif
#ifndef EMERGENCY_CLIENT_REPEAT_HOLD_MS
#define EMERGENCY_CLIENT_REPEAT_HOLD_MS 120000UL
#endif
#ifndef EMERGENCY_CLIENT_REPEAT_JITTER_MS
#define EMERGENCY_CLIENT_REPEAT_JITTER_MS 15000UL
#endif
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
// these are _pushed_ to client app at any time
@@ -138,6 +145,19 @@
#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B
#define PUSH_CODE_BINARY_RESPONSE 0x8C
#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
static const uint8_t EMERGENCY_CHANNEL_SECRET[CIPHER_KEY_SIZE] = {
0xe1, 0xad, 0x57, 0x8d, 0x25, 0x10, 0x8e, 0x34,
0x48, 0x08, 0xf3, 0x0d, 0xfd, 0xaa, 0xf9, 0x26
};
#define EMERGENCY_CLIENT_REPEAT_TABLE_SIZE 63
static uint16_t emergency_client_repeats[EMERGENCY_CLIENT_REPEAT_TABLE_SIZE];
static uint8_t emergency_client_repeat_next;
static unsigned long emergency_client_repeat_send_at;
static uint16_t emergency_client_repeat_key;
static mesh::Packet* emergency_client_repeat_packet;
#define PUSH_CODE_CONTROL_DATA 0x8E // v8+
#define PUSH_CODE_CONTACT_DELETED 0x8F // used to notify client app of deleted contact when overwriting oldest
#define PUSH_CODE_CONTACTS_FULL 0x90 // used to notify client app that contacts storage is full
@@ -280,9 +300,6 @@ bool MyMesh::getCADEnabled() const {
int MyMesh::getInterferenceThreshold() const {
return 0; // disabled for now, until currentRSSI() problem is resolved
}
bool MyMesh::getCADEnabled() const {
return true; // hardware CAD before TX (no CLI toggle on companion; enabled by default)
}
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
@@ -302,6 +319,26 @@ uint8_t MyMesh::getExtraAckTransmitCount() const {
return _prefs.multi_acks;
}
bool MyMesh::hasLocationTelemetryRecipient() {
if (_prefs.telemetry_mode_loc == TELEM_MODE_DENY) return false;
ContactsIterator iter = startContactsIterator();
ContactInfo contact;
while (iter.hasNext(this, contact)) {
if (contact.type == ADV_TYPE_NONE) continue;
if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_ALL) return true;
if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_FLAGS &&
((contact.flags >> 1) & TELEM_PERM_LOCATION)) {
return true;
}
}
return false;
}
void MyMesh::updateGpsTelemetryPolicy() {
sensors.setTelemetryLocationAccessAvailable(hasLocationTelemetryRecipient());
}
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
if (_serial->isConnected() && len + 3 <= MAX_FRAME_SIZE) {
int i = 0;
@@ -354,7 +391,7 @@ uint8_t MyMesh::getAutoAddMaxHops() const {
}
void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage
if (_serial->isConnected()) {
out_frame[0] = PUSH_CODE_CONTACT_DELETED;
memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE);
@@ -406,6 +443,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
updateGpsTelemetryPolicy();
}
static int sort_by_recent(const void *a, const void *b) {
@@ -495,7 +533,27 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
#endif
}
static uint16_t emergencyClientRepeatKey(const mesh::Packet* packet) {
const uint8_t* p = packet->payload;
return ((uint16_t)p[1]) | ((uint16_t)p[2] << 8);
}
static bool __attribute__((noinline)) hasEmergencyClientRepeat(uint16_t key) {
for (uint8_t i = 0; i < EMERGENCY_CLIENT_REPEAT_TABLE_SIZE; i++) {
if (emergency_client_repeats[i] == key) {
return true;
}
}
return false;
}
bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
if (emergency_client_repeat_packet != NULL && packet->getPathHashCount() > 0) {
if (emergencyClientRepeatKey(packet) == emergency_client_repeat_key) {
releasePacket(emergency_client_repeat_packet);
emergency_client_repeat_packet = NULL;
}
}
// REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses
// if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender
return false;
@@ -606,6 +664,29 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
}
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
#endif
if (pkt->isRouteFlood() && memcmp(channel.secret, EMERGENCY_CHANNEL_SECRET, sizeof(EMERGENCY_CHANNEL_SECRET)) == 0) {
bool zero_path = pkt->getPathHashCount() == 0;
uint16_t key = emergencyClientRepeatKey(pkt);
if (hasEmergencyClientRepeat(key)) {
pkt->markDoNotRetransmit();
} else {
emergency_client_repeats[emergency_client_repeat_next] = key;
emergency_client_repeat_next++;
if (emergency_client_repeat_next >= EMERGENCY_CLIENT_REPEAT_TABLE_SIZE) emergency_client_repeat_next = 0;
if (zero_path && emergency_client_repeat_packet == NULL) {
emergency_client_repeat_packet = obtainNewPacket();
if (emergency_client_repeat_packet != NULL) {
*emergency_client_repeat_packet = *pkt;
emergency_client_repeat_key = key;
emergency_client_repeat_send_at = futureMillis(
(int)(EMERGENCY_CLIENT_REPEAT_HOLD_MS + getRNG()->nextInt(0, EMERGENCY_CLIENT_REPEAT_JITTER_MS + 1)));
pkt->markDoNotRetransmit();
}
}
}
}
}
void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
@@ -989,6 +1070,7 @@ void MyMesh::begin(bool has_display) {
resetContacts();
_store->loadContacts(this);
updateGpsTelemetryPolicy();
bootstrapRTCfromContacts();
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
_store->loadChannels(this);
@@ -1307,6 +1389,7 @@ void MyMesh::handleCmdFrame(size_t len) {
updateContactFromFrame(*recipient, last_mod, cmd_frame, len);
recipient->lastmod = last_mod;
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
updateGpsTelemetryPolicy();
writeOKFrame();
} else {
ContactInfo contact;
@@ -1315,6 +1398,7 @@ void MyMesh::handleCmdFrame(size_t len) {
contact.sync_since = 0;
if (addContact(contact)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
updateGpsTelemetryPolicy();
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
@@ -1326,6 +1410,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (recipient && removeContact(*recipient)) {
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE);
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
updateGpsTelemetryPolicy();
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // not found, or unable to remove
@@ -1382,6 +1467,7 @@ void MyMesh::handleCmdFrame(size_t len) {
}
} else if (cmd_frame[0] == CMD_IMPORT_CONTACT && len > 2 + 32 + 64) {
if (importContact(&cmd_frame[1], len - 1)) {
updateGpsTelemetryPolicy();
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
@@ -1475,6 +1561,7 @@ void MyMesh::handleCmdFrame(size_t len) {
}
}
}
updateGpsTelemetryPolicy();
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && cmd_frame[1] == 0 && len >= 3) {
@@ -1523,6 +1610,7 @@ void MyMesh::handleCmdFrame(size_t len) {
// re-load contacts, to invalidate ecdh shared_secrets
resetContacts();
_store->loadContacts(this);
updateGpsTelemetryPolicy();
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
@@ -2268,6 +2356,11 @@ void MyMesh::checkSerialInterface() {
void MyMesh::loop() {
BaseChatMesh::loop();
if (emergency_client_repeat_packet != NULL && millisHasNowPassed(emergency_client_repeat_send_at)) {
mesh::Packet* pkt = emergency_client_repeat_packet;
emergency_client_repeat_packet = NULL;
sendPacket(pkt, 1, 0);
}
if (_cli_rescue) {
checkCLIRescueCmd();
@@ -2303,5 +2396,6 @@ bool MyMesh::advert() {
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0;
return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0
|| emergency_client_repeat_packet != NULL;
}
+2
View File
@@ -200,6 +200,8 @@ private:
void checkCLIRescueCmd();
void checkSerialInterface();
bool isValidClientRepeatFreq(uint32_t f) const;
bool hasLocationTelemetryRecipient();
void updateGpsTelemetryPolicy();
// helpers, short-cuts
void saveChannels() { _store->saveChannels(this); }
+29 -25
View File
@@ -814,10 +814,10 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
}
#endif
#ifdef WITH_BRIDGE
#ifdef WITH_MQTT_BRIDGE
if (_prefs.bridge_enabled) {
// Store raw radio data for MQTT messages
if (bridge) bridge->storeRawRadioData(raw, len, snr, rssi);
if (mqtt_bridge) mqtt_bridge->storeRawRadioData(raw, len, snr, rssi);
}
#endif
}
@@ -825,11 +825,11 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
#ifdef WITH_MQTT_BRIDGE
// MQTT bridge: always feed RX packets — bridge decides based on mqtt.rx setting
if (bridge) bridge->onPacketReceived(pkt);
if (mqtt_bridge) mqtt_bridge->onPacketReceived(pkt);
#elif defined(WITH_BRIDGE)
// Non-MQTT bridge (ESP-NOW): use bridge.source setting
// Non-MQTT bridge: use bridge.source setting
if (_prefs.bridge_pkt_src == 1) {
if (bridge) bridge->onPacketReceived(pkt);
activeBridge()->sendPacket(pkt);
}
#endif
@@ -855,11 +855,11 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
void MyMesh::logTx(mesh::Packet *pkt, int len) {
#ifdef WITH_MQTT_BRIDGE
// MQTT bridge: always feed TX packets — bridge decides based on mqtt.tx setting
if (bridge) bridge->sendPacket(pkt);
if (mqtt_bridge) mqtt_bridge->sendPacket(pkt);
#elif defined(WITH_BRIDGE)
// Non-MQTT bridge (ESP-NOW): use bridge.source setting
// Non-MQTT bridge: use bridge.source setting
if (_prefs.bridge_pkt_src == 0) {
if (bridge) bridge->sendPacket(pkt);
activeBridge()->sendPacket(pkt);
}
#endif
@@ -2210,12 +2210,12 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
telemetry(MAX_PACKET_PAYLOAD - 4),
discover_limiter(4, 120), // max 4 every 2 minutes
anon_limiter(4, 180) // max 4 every 3 minutes
#if defined(WITH_RS232_BRIDGE)
#if defined(WITH_MQTT_BRIDGE)
, mqtt_bridge(nullptr)
#elif defined(WITH_RS232_BRIDGE)
, bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
#elif defined(WITH_ESPNOW_BRIDGE)
, bridge(&_prefs, _mgr, &rtc)
#elif defined(WITH_MQTT_BRIDGE)
, bridge(nullptr)
#endif
{
last_millis = 0;
@@ -2365,38 +2365,39 @@ void MyMesh::begin(FILESYSTEM *fs) {
if (_prefs.bridge_enabled) {
#ifdef WITH_MQTT_BRIDGE
// Defer construction to avoid static init crashes on ESP32 classic
bridge = new MQTTBridge(&_prefs, _cli.getObserverPrefs(), _mgr, getRTCClock(), &self_id);
mqtt_bridge = new MQTTBridge(&_prefs, _cli.getObserverPrefs(), _mgr, getRTCClock(), &self_id);
#endif
if (bridge) {
BridgeBase* active_bridge = activeBridge();
if (active_bridge) {
#ifdef WITH_MQTT_BRIDGE
// Set device public key for MQTT topics
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
MESH_DEBUG_PRINTLN("Setting device ID: %s", device_id);
bridge->setDeviceID(device_id);
mqtt_bridge->setDeviceID(device_id);
// Set firmware version
bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
// Set board model
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
// Set build date
bridge->setBuildDate(getBuildDate());
mqtt_bridge->setBuildDate(getBuildDate());
#ifdef WITH_MQTT_BRIDGE
// Set stats sources for automatic stats collection
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#ifdef WITH_SNMP
if (_cli.getObserverPrefs()->snmp_enabled) {
_snmp_agent.setNodeName(_prefs.node_name);
_snmp_agent.setFirmwareVersion(getFirmwareVer());
bridge->setSNMPAgent(&_snmp_agent);
mqtt_bridge->setSNMPAgent(&_snmp_agent);
}
#endif
#endif
bridge->begin();
active_bridge->begin();
}
}
#endif
@@ -2407,7 +2408,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
// floods ride the same scope as adverts/channel messages.
#ifdef WITH_MQTT_BRIDGE
_alerter.begin(&_prefs, _cli.getObserverPrefs(), this, this);
_alerter.setBridge(bridge);
_alerter.setBridge(mqtt_bridge);
#endif
applySavedRadioParams();
@@ -3878,8 +3879,10 @@ void MyMesh::loop() {
mesh::Mesh::loop();
checkBatteryAlert();
#ifdef WITH_BRIDGE
// bridge.loop() is now handled by FreeRTOS task on Core 0 - no need to call it here
#if defined(WITH_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
// MQTT runs its own task; serial and ESP-NOW bridges remain cooperative.
BridgeBase* active_bridge = activeBridge();
if (active_bridge && active_bridge->isRunning()) active_bridge->loop();
#endif
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
@@ -3953,7 +3956,8 @@ void MyMesh::loop() {
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge && bridge->isRunning()) return true; // bridge needs WiFi radio, can't sleep
const BridgeBase* active_bridge = activeBridge();
if (active_bridge && active_bridge->isRunning()) return true;
#endif
if (_mgr->getOutboundTotal() > 0) return true;
if (isMillisTimerDue(next_flood_advert) || isMillisTimerDue(next_local_advert)) return true;
+71 -34
View File
@@ -167,12 +167,28 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
uint8_t active_cr; // live CR, including temporary radio overrides
ScheduledRadioSetting scheduled_radio_settings[MAX_SCHEDULED_RADIO_SETTINGS];
int matching_peer_indexes[MAX_CLIENTS];
#if defined(WITH_RS232_BRIDGE)
#if defined(WITH_MQTT_BRIDGE)
MQTTBridge* mqtt_bridge;
#elif defined(WITH_RS232_BRIDGE)
RS232Bridge bridge;
#elif defined(WITH_ESPNOW_BRIDGE)
ESPNowBridge bridge;
#elif defined(WITH_MQTT_BRIDGE)
MQTTBridge* bridge;
#endif
#ifdef WITH_BRIDGE
BridgeBase* activeBridge() {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge;
#else
return &bridge;
#endif
}
const BridgeBase* activeBridge() const {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge;
#else
return &bridge;
#endif
}
#endif
#ifdef WITH_SNMP
MeshSNMPAgent _snmp_agent;
@@ -402,34 +418,35 @@ public:
#if defined(WITH_BRIDGE)
void setBridgeState(bool enable) override {
if (!bridge) {
#ifdef WITH_MQTT_BRIDGE
bridge = new MQTTBridge(&_prefs, _cli.getObserverPrefs(), _mgr, getRTCClock(), &self_id);
#endif
if (!bridge) return;
if (!mqtt_bridge) {
mqtt_bridge = new MQTTBridge(&_prefs, _cli.getObserverPrefs(), _mgr, getRTCClock(), &self_id);
if (!mqtt_bridge) return;
}
if (enable == bridge->isRunning()) return;
#endif
BridgeBase* active_bridge = activeBridge();
if (!active_bridge || enable == active_bridge->isRunning()) return;
if (enable)
{
#ifdef WITH_MQTT_BRIDGE
// Set device metadata before starting bridge (same as in begin())
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
bridge->setDeviceID(device_id);
bridge->setFirmwareVersion(getFirmwareVer());
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
bridge->setBuildDate(getBuildDate());
#ifdef WITH_MQTT_BRIDGE
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
mqtt_bridge->setDeviceID(device_id);
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#endif
bridge->begin();
active_bridge->begin();
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(bridge);
_alerter.setBridge(mqtt_bridge);
#endif
}
else
{
bridge->end();
active_bridge->end();
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(nullptr);
#endif
@@ -437,26 +454,27 @@ public:
}
void restartBridge() override {
if (!bridge || !bridge->isRunning()) return;
bridge->end();
BridgeBase* active_bridge = activeBridge();
if (!active_bridge || !active_bridge->isRunning()) return;
active_bridge->end();
#ifdef WITH_MQTT_BRIDGE
// Set device metadata before restarting bridge (same as in begin())
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
bridge->setDeviceID(device_id);
bridge->setFirmwareVersion(getFirmwareVer());
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
bridge->setBuildDate(getBuildDate());
#ifdef WITH_MQTT_BRIDGE
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
mqtt_bridge->setDeviceID(device_id);
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#endif
bridge->begin();
active_bridge->begin();
}
void restartBridgeSlot(int slot) override {
#ifdef WITH_MQTT_BRIDGE
if (!bridge || !bridge->isRunning()) return;
bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return;
mqtt_bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
#else
(void)slot;
#endif
@@ -472,22 +490,41 @@ public:
}
int getQueueSize() override {
return bridge ? bridge->getQueueSize() : 0;
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge ? mqtt_bridge->getQueueSize() : 0;
#else
return 0;
#endif
}
bool isMqttBridgeRunning() override {
return bridge && bridge->isRunning();
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge && mqtt_bridge->isRunning();
#else
return false;
#endif
}
bool syncMqttNtp() override {
if (!bridge || !bridge->isRunning()) return false;
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return false;
// Marshal onto the MQTT task (Core 0); this runs on the CLI thread (Core 1).
return bridge->requestForcedNtpSync();
return mqtt_bridge->requestForcedNtpSync();
#else
return false;
#endif
}
bool runMqttNtpDiag(char* reply, size_t reply_size, bool verbose) override {
if (!bridge || !bridge->isRunning()) return false;
return bridge->ntpDiag(reply, reply_size, verbose);
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return false;
return mqtt_bridge->ntpDiag(reply, reply_size, verbose);
#else
(void)reply;
(void)reply_size;
(void)verbose;
return false;
#endif
}
#endif
+42 -8
View File
@@ -147,6 +147,36 @@ static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier,
return sign * ((float) value / multiplier);
}
static uint8_t getTelemetryPermissions(uint8_t acl_perms, uint8_t telemetry_access) {
if (telemetry_access == TELEMETRY_ACCESS_ALL) {
return TELEM_PERM_BASE | TELEM_PERM_LOCATION | TELEM_PERM_ENVIRONMENT;
}
uint8_t role = acl_perms & PERM_ACL_ROLE_MASK;
if (role >= PERM_ACL_READ_ONLY) {
return TELEM_PERM_BASE | TELEM_PERM_LOCATION | TELEM_PERM_ENVIRONMENT;
}
return 0;
}
bool SensorMesh::hasLocationTelemetryClient() {
if (_prefs.telemetry_access == TELEMETRY_ACCESS_ALL) {
return acl.getNumClients() > 0;
}
for (int i = 0; i < acl.getNumClients(); i++) {
ClientInfo* c = acl.getClientByIdx(i);
if ((c->permissions & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
return true;
}
}
return false;
}
void SensorMesh::updateGpsTelemetryPolicy() {
sensors.setTelemetryLocationAccessAvailable(hasLocationTelemetryClient());
}
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
// check sign
bool sign = value < 0;
@@ -173,13 +203,16 @@ static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t mult
uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) {
uint8_t perm_mask = ~(payload[0]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
uint8_t telemetry_permissions = getTelemetryPermissions(perms, _prefs.telemetry_access) & perm_mask;
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
if (telemetry_permissions & TELEM_PERM_BASE) {
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
}
// query other sensors -- target specific
sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
sensors.querySensors(telemetry_permissions, telemetry);
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
uint8_t tlen = telemetry.getSize();
@@ -327,9 +360,6 @@ uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
int SensorMesh::getInterferenceThreshold() const {
return _prefs.interference_threshold;
}
bool SensorMesh::getCADEnabled() const {
return _prefs.cad_enabled;
}
int SensorMesh::getAGCResetInterval() const {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
@@ -365,6 +395,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
updateGpsTelemetryPolicy();
}
if (is_flood) {
@@ -412,6 +443,7 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
uint8_t perms = atoi(sp);
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
updateGpsTelemetryPolicy();
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - invalid params");
@@ -452,6 +484,7 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
sprintf(reply, "%x", board.getGpio());
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
updateGpsTelemetryPolicy();
}
}
@@ -535,7 +568,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
memcpy(&timestamp, data, 4);
if (timestamp > from->last_timestamp) { // prevent replay attacks
uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5);
uint8_t reply_len = handleRequest(from->permissions, timestamp, data[4], &data[5], len - 5);
if (reply_len == 0) return; // invalid command
from->last_timestamp = timestamp;
@@ -754,6 +787,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
acl.load(_fs, self_id);
region_map.load(_fs);
updateGpsTelemetryPolicy();
// establish default-scope
{
@@ -935,7 +969,7 @@ void SensorMesh::loop() {
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions
sensors.querySensors(TELEM_PERM_BASE | TELEM_PERM_ENVIRONMENT, telemetry);
onSensorDataRead();
+2
View File
@@ -161,6 +161,8 @@ private:
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
bool hasLocationTelemetryClient();
void updateGpsTelemetryPolicy();
mesh::Packet* createSelfAdvert();
void sendAlert(const ClientInfo* c, Trigger* t);
@@ -1,3 +1,5 @@
#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
#include "PsychicMqttClient.h"
#include <string.h>
@@ -813,3 +815,5 @@ bool PsychicMqttClient::_isTopicMatch(const char *topic, const char *subscriptio
s = s_end + 1;
}
}
#endif
+44 -11
View File
@@ -774,6 +774,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->ota_advert_interval = 0;
_prefs->ota_max_hops = 3;
#endif
_prefs->telemetry_access = TELEMETRY_ACCESS_ALL;
// A remainder larger than the smallest legacy MQTT gap (864) means an old fork
// file with the zero-filled gap; detect and recover it below. Anything smaller
// (upstream/flex 5-byte tails, or the ~384-byte keymind retry tail) takes the
@@ -957,9 +958,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
if (file.available() >= (int)sizeof(_prefs->flood_channel_data_max_hops)) {
file.read((uint8_t *)&_prefs->flood_channel_data_max_hops, sizeof(_prefs->flood_channel_data_max_hops));
}
if (file.available() >= (int)sizeof(_prefs->telemetry_access)) {
file.read((uint8_t *)&_prefs->telemetry_access, sizeof(_prefs->telemetry_access));
}
}
#if defined(ENABLE_OTA)
// OTA config starts at 674. Guard every appended field so older files keep defaults.
// OTA config starts at 675, after telemetry_access. Guard every appended field so older files keep defaults.
if (file.available() >= (int)sizeof(_prefs->ota_autofetch)) {
file.read((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch));
}
@@ -981,7 +985,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
if (file.available() >= (int)sizeof(_prefs->ota_max_hops)) {
file.read((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops));
}
// next: 810
// next: 811
#endif
}
@@ -1044,6 +1048,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->battery_alert_enabled = constrain(_prefs->battery_alert_enabled, 0, 1);
_prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1);
_prefs->flood_channel_data_enabled = constrain(_prefs->flood_channel_data_enabled, 0, 1);
_prefs->telemetry_access = constrain(_prefs->telemetry_access, 0, 1);
if (_prefs->flood_channel_block_max_hops != FLOOD_CHANNEL_BLOCK_HOPS_ALL
&& (_prefs->flood_channel_block_max_hops < 1 || _prefs->flood_channel_block_max_hops > 7)) {
_prefs->flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL;
@@ -1163,15 +1168,16 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled));
file.write((uint8_t *)&_prefs->flood_channel_block_max_hops, sizeof(_prefs->flood_channel_block_max_hops));
file.write((uint8_t *)&_prefs->flood_channel_data_max_hops, sizeof(_prefs->flood_channel_data_max_hops));
file.write((uint8_t *)&_prefs->telemetry_access, sizeof(_prefs->telemetry_access)); // 674
#if defined(ENABLE_OTA)
file.write((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch)); // 674
file.write((uint8_t *)&_prefs->ota_autoinstall, sizeof(_prefs->ota_autoinstall)); // 675
file.write((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count)); // 676
file.write((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 677
file.write((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 805
file.write((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 807
file.write((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops)); // 809
// next: 810
file.write((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch)); // 675
file.write((uint8_t *)&_prefs->ota_autoinstall, sizeof(_prefs->ota_autoinstall)); // 676
file.write((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count)); // 677
file.write((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 678
file.write((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 806
file.write((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 808
file.write((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops)); // 810
// next: 811
#endif
file.close();
@@ -1528,6 +1534,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
} else {
strcpy(reply, "ERR: clock cannot go backwards");
}
#ifdef ESP_PLATFORM
} else if (memcmp(command, "memory", 6) == 0) {
sprintf(reply, "Free: %d, Min: %d, Max: %d, Queue: %d, IntFree: %d, IntMax: %d, PSRAM: %d/%d",
ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap(),
@@ -1536,15 +1543,27 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
(int)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL),
(int)heap_caps_get_free_size(MALLOC_CAP_SPIRAM),
(int)heap_caps_get_total_size(MALLOC_CAP_SPIRAM));
#endif
} else if (memcmp(command, "start ota", 9) == 0 && (command[9] == 0 || command[9] == ' ')) {
// Manual OTA: bring up the ElegantOTA server for a hand-uploaded binary.
// Manual OTA: bring up the board's browser server for a hand-uploaded binary.
if (!_board->startOTAUpdate(_prefs->node_name, reply)) {
strcpy(reply, "Error");
}
#if defined(WITH_MQTT_BRIDGE) && defined(LIGHTWEIGHT_WIFI_OTA)
else {
// Keep WiFi up, but release MQTT/TLS heap while the browser uploader runs.
_callbacks->setBridgeState(false);
}
#endif
} else if (memcmp(command, "stop ota", 8) == 0 && (command[8] == 0 || command[8] == ' ')) {
if (!_board->stopOTAUpdate(reply)) {
strcpy(reply, "Error");
}
#if defined(WITH_MQTT_BRIDGE) && defined(LIGHTWEIGHT_WIFI_OTA)
else if (_prefs->bridge_enabled) {
_callbacks->setBridgeState(true);
}
#endif
} else if (memcmp(command, "clock", 5) == 0) {
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
@@ -1881,6 +1900,18 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "telemetry.access ", 17) == 0) {
if (strcmp(&config[17], "all") == 0) {
_prefs->telemetry_access = TELEMETRY_ACCESS_ALL;
savePrefs();
strcpy(reply, "OK");
} else if (strcmp(&config[17], "acl") == 0) {
_prefs->telemetry_access = TELEMETRY_ACCESS_ACL;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "ERROR: telemetry.access must be all or acl");
}
} else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
int hours = _atoi(&config[22]);
if ((hours > 0 && hours < 3) || (hours > 168)) {
@@ -2509,6 +2540,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
} else if (memcmp(config, "allow.read.only", 15) == 0) {
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
} else if (memcmp(config, "telemetry.access", 16) == 0) {
sprintf(reply, "> %s", _prefs->telemetry_access == TELEMETRY_ACCESS_ACL ? "acl" : "all");
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
} else if (memcmp(config, "advert.interval", 15) == 0) {
+5 -1
View File
@@ -99,6 +99,9 @@
#define DIRECT_RETRY_PREFS_MAGIC_0 0xD1
#define DIRECT_RETRY_PREFS_MAGIC_1 0x52
#define TELEMETRY_ACCESS_ALL 0
#define TELEMETRY_ACCESS_ACL 1
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
@@ -176,10 +179,11 @@ struct NodePrefs { // persisted to file
uint8_t flood_channel_data_enabled;
uint8_t flood_channel_block_max_hops;
uint8_t flood_channel_data_max_hops;
uint8_t telemetry_access;
// NOTE: observer settings (MQTT/WiFi/timezone/SNMP/alert) were moved out of
// NodePrefs into MQTTPrefs (persisted to /mqtt_prefs) so this struct stays
// aligned with upstream (plus the keymind retry/flood tail above). See
// aligned with upstream (plus the keymind retry/flood/telemetry tail above). See
// struct MQTTPrefs below.
#if defined(ENABLE_OTA)
+260 -1
View File
@@ -3,7 +3,266 @@
#include "ESP32Board.h"
#include <target.h>
#if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only
#if defined(ADMIN_PASSWORD) && defined(LIGHTWEIGHT_WIFI_OTA)
#include <WiFi.h>
#include <Update.h>
#include <SPIFFS.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <strings.h>
static bool lightweight_ota_started_ap;
static const char LIGHTWEIGHT_OTA_PAGE[] = R"HTML(<!doctype html>
<html><head><meta name="viewport" content="width=device-width"><title>MeshCore OTA</title>
<style>body{font-family:sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}button,input{font-size:1rem;margin:.5rem 0}pre{white-space:pre-wrap}</style></head>
<body><h2>MeshCore firmware update</h2><input id="file" type="file" accept=".bin,application/octet-stream"><br>
<button id="upload">Upload and reboot</button><pre id="status"></pre><script>
const f=document.getElementById('file'),s=document.getElementById('status'),b=document.getElementById('upload');
b.onclick=()=>{if(!f.files.length){s.textContent='Select a firmware .bin file.';return}b.disabled=true;
const x=new XMLHttpRequest();x.open('POST','/update');x.setRequestHeader('Content-Type','application/octet-stream');
x.upload.onprogress=e=>{if(e.lengthComputable)s.textContent='Uploading '+Math.round(e.loaded*100/e.total)+'%'};
x.onload=()=>{s.textContent=x.responseText;b.disabled=false};x.onerror=()=>{s.textContent='Upload failed';b.disabled=false};x.send(f.files[0])};
</script></body></html>)HTML";
class LightweightOTAServer {
WiFiServer server{80};
TaskHandle_t task = nullptr;
volatile bool running = false;
ESP32Board* board = nullptr;
static void sendResponse(WiFiClient& client, int code, const char* reason,
const char* content_type, const char* body, size_t length) {
client.printf("HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %u\r\n"
"Connection: close\r\nCache-Control: no-store\r\n\r\n",
code, reason, content_type, static_cast<unsigned>(length));
if (length) client.write(reinterpret_cast<const uint8_t*>(body), length);
}
bool readLine(WiFiClient& client, char* line, size_t capacity) {
size_t length = 0;
uint32_t deadline = millis() + 5000;
while (running && client.connected()) {
while (client.available()) {
int c = client.read();
if (c < 0) break;
if (c == '\n') {
if (length && line[length - 1] == '\r') length--;
line[length] = 0;
return true;
}
if (length + 1 < capacity) line[length++] = static_cast<char>(c);
}
if (static_cast<int32_t>(millis() - deadline) >= 0) break;
delay(1);
}
return false;
}
void sendPage(WiFiClient& client) {
sendResponse(client, 200, "OK", "text/html", LIGHTWEIGHT_OTA_PAGE,
strlen(LIGHTWEIGHT_OTA_PAGE));
}
void sendLog(WiFiClient& client) {
File log = SPIFFS.open("/packet_log", FILE_READ);
if (!log) {
static const char missing[] = "packet log not found";
sendResponse(client, 404, "Not Found", "text/plain", missing, sizeof(missing) - 1);
return;
}
client.printf("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %u\r\n"
"Connection: close\r\nCache-Control: no-store\r\n\r\n",
static_cast<unsigned>(log.size()));
uint8_t buf[1024];
while (running && log.available() && client.connected()) {
size_t count = log.read(buf, sizeof(buf));
if (!count || client.write(buf, count) != count) break;
}
log.close();
}
void sendUpdateError(WiFiClient& client, int code, const char* reason) {
char error[96];
snprintf(error, sizeof(error), "OTA error: %s", Update.errorString());
sendResponse(client, code, reason, "text/plain", error, strlen(error));
}
void receiveUpdate(WiFiClient& client, size_t total) {
if (total == 0) {
static const char empty[] = "empty firmware image";
sendResponse(client, 400, "Bad Request", "text/plain", empty, sizeof(empty) - 1);
return;
}
board->setInhibitSleep(true);
if (!Update.begin(total, U_FLASH)) {
sendUpdateError(client, 400, "Bad Request");
return;
}
uint8_t buf[1024];
size_t received_total = 0;
uint32_t deadline = millis() + 15000;
while (running && received_total < total && client.connected()) {
int available = client.available();
if (available <= 0) {
if (static_cast<int32_t>(millis() - deadline) >= 0) break;
delay(1);
continue;
}
size_t wanted = total - received_total;
if (wanted > sizeof(buf)) wanted = sizeof(buf);
if (wanted > static_cast<size_t>(available)) wanted = static_cast<size_t>(available);
int received = client.read(buf, wanted);
if (received <= 0) break;
deadline = millis() + 15000;
if (Update.write(buf, static_cast<size_t>(received)) != static_cast<size_t>(received)) {
sendUpdateError(client, 500, "Internal Server Error");
Update.abort();
return;
}
received_total += static_cast<size_t>(received);
}
if (received_total != total) {
Update.abort();
static const char incomplete[] = "firmware upload incomplete";
sendResponse(client, 408, "Request Timeout", "text/plain", incomplete, sizeof(incomplete) - 1);
return;
}
if (!Update.end()) {
sendUpdateError(client, 500, "Internal Server Error");
return;
}
static const char success[] = "OK - firmware installed; rebooting";
sendResponse(client, 200, "OK", "text/plain", success, sizeof(success) - 1);
delay(500);
client.stop();
esp_restart();
}
void handleClient(WiFiClient& client) {
char line[256];
if (!readLine(client, line, sizeof(line))) return;
bool get_page = strncmp(line, "GET / ", 6) == 0 || strncmp(line, "GET /update ", 12) == 0;
bool get_log = strncmp(line, "GET /log ", 9) == 0;
bool post_update = strncmp(line, "POST /update ", 13) == 0;
size_t content_length = 0;
do {
if (!readLine(client, line, sizeof(line))) return;
if (strncasecmp(line, "Content-Length:", 15) == 0) {
content_length = static_cast<size_t>(strtoul(line + 15, nullptr, 10));
}
} while (line[0]);
if (get_page) sendPage(client);
else if (get_log) sendLog(client);
else if (post_update) receiveUpdate(client, content_length);
else {
static const char missing[] = "not found";
sendResponse(client, 404, "Not Found", "text/plain", missing, sizeof(missing) - 1);
}
}
static void taskEntry(void* arg) {
LightweightOTAServer* self = static_cast<LightweightOTAServer*>(arg);
while (self->running) {
WiFiClient client = self->server.available();
if (client) {
client.setTimeout(15000);
self->handleClient(client);
client.stop();
} else {
delay(10);
}
}
self->task = nullptr;
vTaskDelete(nullptr);
}
public:
bool begin(ESP32Board* owner) {
if (running) return true;
if (task != nullptr) return false;
board = owner;
server.begin();
server.setNoDelay(true);
running = true;
if (xTaskCreate(taskEntry, "ota-http", 6144, this, 4, &task) != pdPASS) {
running = false;
server.stop();
task = nullptr;
return false;
}
return true;
}
void end() {
running = false;
server.stop();
for (unsigned i = 0; task != nullptr && i < 100; i++) delay(10);
board = nullptr;
}
};
static LightweightOTAServer lightweight_ota_server;
bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
(void)id;
inhibit_sleep = true;
IPAddress ip;
if (WiFi.status() == WL_CONNECTED) {
ip = WiFi.localIP();
} else {
if (!lightweight_ota_started_ap) {
lightweight_ota_started_ap = WiFi.softAP("MeshCore-OTA", nullptr);
}
if (!lightweight_ota_started_ap) {
inhibit_sleep = false;
strcpy(reply, "ERR: OTA WiFi failed");
return false;
}
ip = WiFi.softAPIP();
}
if (ota_server == nullptr) {
if (!lightweight_ota_server.begin(this)) {
if (lightweight_ota_started_ap) WiFi.softAPdisconnect(true);
lightweight_ota_started_ap = false;
inhibit_sleep = false;
strcpy(reply, "ERR: OTA server failed");
return false;
}
ota_server = &lightweight_ota_server;
}
snprintf(reply, 160, "Started: http://%s/update", ip.toString().c_str());
MESH_DEBUG_PRINTLN("startOTAUpdate: %s", reply);
return true;
}
bool ESP32Board::stopOTAUpdate(char reply[]) {
if (ota_server == nullptr) {
strcpy(reply, "OK - OTA not running");
return true;
}
lightweight_ota_server.end();
ota_server = nullptr;
if (lightweight_ota_started_ap) WiFi.softAPdisconnect(true);
lightweight_ota_started_ap = false;
inhibit_sleep = false;
strcpy(reply, "OK - OTA stopped");
MESH_DEBUG_PRINTLN("stopOTAUpdate: %s", reply);
return true;
}
#elif defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
+6
View File
@@ -16,13 +16,19 @@
#include "esp_system.h"
#include <driver/rtc_io.h>
#if !defined(LIGHTWEIGHT_WIFI_OTA)
class AsyncWebServer;
#endif
class ESP32Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
bool inhibit_sleep = false;
#if defined(LIGHTWEIGHT_WIFI_OTA)
void* ota_server = nullptr;
#else
AsyncWebServer* ota_server = nullptr;
#endif
static inline portMUX_TYPE sleepMux = portMUX_INITIALIZER_UNLOCKED;
public:
+4
View File
@@ -1,4 +1,7 @@
#include "JWTHelper.h"
#ifdef WITH_MQTT_BRIDGE
#include <ArduinoJson.h>
#include <SHA256.h>
#include <string.h>
@@ -196,3 +199,4 @@ size_t JWTHelper::createPayload(
return base64UrlEncode((uint8_t*)jsonBuffer, len, output, outputSize);
}
#endif
+4
View File
@@ -1,5 +1,7 @@
#pragma once
#ifdef WITH_MQTT_BRIDGE
#include "MeshCore.h"
#include "Identity.h"
@@ -85,3 +87,5 @@ private:
);
};
#endif
+6 -1
View File
@@ -1,4 +1,7 @@
#include "MQTTMessageBuilder.h"
#ifdef WITH_MQTT_BRIDGE
#include <ArduinoJson.h>
#include <cstring>
#include <math.h>
@@ -435,4 +438,6 @@ void MQTTMessageBuilder::packetToHex(mesh::Packet* packet, char* hex, size_t hex
// Convert serialized packet to hex
bytesToHex(raw_buf, raw_len, hex, hex_size);
}
}
#endif
+4
View File
@@ -1,5 +1,7 @@
#pragma once
#ifdef WITH_MQTT_BRIDGE
#include "MeshCore.h"
#include <ArduinoJson.h>
#include <Mesh.h>
@@ -226,3 +228,5 @@ private:
*/
static void packetToHex(mesh::Packet* packet, char* hex, size_t hex_size);
};
#endif
+175
View File
@@ -0,0 +1,175 @@
#include "SensorManager.h"
#if ENV_INCLUDE_GPS
#ifndef GPS_TELEMETRY_CACHE_INTERVAL_SEC
#define GPS_TELEMETRY_CACHE_INTERVAL_SEC (2UL * 60UL * 60UL)
#endif
#ifndef GPS_TELEMETRY_MAX_ACQUIRE_SEC
#define GPS_TELEMETRY_MAX_ACQUIRE_SEC (15UL * 60UL)
#endif
#ifndef GPS_TELEMETRY_STABLE_SEC
#define GPS_TELEMETRY_STABLE_SEC 30UL
#endif
#ifndef GPS_TELEMETRY_HOLD_SEC
#define GPS_TELEMETRY_HOLD_SEC (2UL * 60UL * 60UL)
#endif
#ifndef GPS_TELEMETRY_MAX_STALE_SEC
#define GPS_TELEMETRY_MAX_STALE_SEC (12UL * 60UL * 60UL)
#endif
#ifndef GPS_TELEMETRY_STABLE_RADIUS_M
#define GPS_TELEMETRY_STABLE_RADIUS_M 100.0f
#endif
static bool millisDue(unsigned long now, unsigned long deadline) {
return (long)(now - deadline) >= 0;
}
static float gpsDistanceSquaredMeters(float lat1, float lon1, float lat2, float lon2) {
static const float METERS_PER_DEGREE = 111320.0f;
float x = (lon2 - lon1) * METERS_PER_DEGREE;
float y = (lat2 - lat1) * METERS_PER_DEGREE;
return x * x + y * y;
}
bool SensorManager::gpsTelemetryHoldActive(unsigned long now) const {
return gps_hold_until != 0 && !millisDue(now, gps_hold_until);
}
bool SensorManager::gpsTelemetryCacheFresh(unsigned long now) const {
return gps_cache_valid &&
(unsigned long)(now - gps_cache_updated_at) <= GPS_TELEMETRY_MAX_STALE_SEC * 1000UL;
}
void SensorManager::updateGpsTelemetryCache(float lat, float lon, float altitude, unsigned long now) {
gps_cache_lat = lat;
gps_cache_lon = lon;
gps_cache_altitude = altitude;
gps_cache_updated_at = now;
gps_cache_valid = true;
}
void SensorManager::maybeStopGpsForTelemetry(unsigned long now) {
#if defined(PERSISTANT_GPS) || defined(FORCE_GPS_ALIVE)
(void)now;
#else
if (telemetryGpsActive() && !gps_user_enabled && !gps_acquiring && !gpsTelemetryHoldActive(now)) {
telemetryGpsStop();
gps_next_cache_update_at = now + GPS_TELEMETRY_CACHE_INTERVAL_SEC * 1000UL;
}
#endif
}
void SensorManager::beginGpsTelemetryAcquisition(unsigned long now) {
if (!telemetryGpsDetected() || gps_acquiring) return;
gps_acquiring = true;
gps_acquire_has_fix = false;
gps_acquire_started_at = now;
gps_stable_started_at = now;
gps_weighted_lat = 0;
gps_weighted_lon = 0;
gps_weighted_altitude = 0;
gps_weight_sum = 0;
gps_weight_count = 0;
if (!telemetryGpsActive()) telemetryGpsStart();
}
void SensorManager::finishGpsTelemetryAcquisition(unsigned long now, bool use_weighted_average) {
if (use_weighted_average && gps_weight_sum > 0) {
updateGpsTelemetryCache(gps_weighted_lat / gps_weight_sum,
gps_weighted_lon / gps_weight_sum,
gps_weighted_altitude / gps_weight_sum,
now);
}
gps_acquiring = false;
gps_next_cache_update_at = now + GPS_TELEMETRY_CACHE_INTERVAL_SEC * 1000UL;
maybeStopGpsForTelemetry(now);
}
bool SensorManager::queryGpsTelemetry(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (!(requester_permissions & TELEM_PERM_LOCATION) || !telemetryGpsDetected()) return false;
unsigned long now = millis();
gps_hold_until = now + GPS_TELEMETRY_HOLD_SEC * 1000UL;
if (!telemetryGpsActive()) telemetryGpsStart();
if (!gpsTelemetryCacheFresh(now) && !gps_acquiring) beginGpsTelemetryAcquisition(now);
if (!gpsTelemetryCacheFresh(now)) return false;
telemetry.addGPS(TELEM_CHANNEL_SELF, gps_cache_lat, gps_cache_lon, gps_cache_altitude);
return true;
}
void SensorManager::processGpsTelemetryFix(float lat, float lon, float altitude, unsigned long now) {
if (!gps_acquiring) {
if (gps_user_enabled || gpsTelemetryHoldActive(now)) {
updateGpsTelemetryCache(lat, lon, altitude, now);
}
return;
}
if (!gps_acquire_has_fix) {
gps_acquire_has_fix = true;
gps_stable_started_at = now;
gps_stable_origin_lat = lat;
gps_stable_origin_lon = lon;
}
static const float STABLE_RADIUS_M2 =
GPS_TELEMETRY_STABLE_RADIUS_M * GPS_TELEMETRY_STABLE_RADIUS_M;
if (gpsDistanceSquaredMeters(gps_stable_origin_lat, gps_stable_origin_lon, lat, lon) > STABLE_RADIUS_M2) {
updateGpsTelemetryCache(lat, lon, altitude, now);
finishGpsTelemetryAcquisition(now, false);
return;
}
float weight = (float)++gps_weight_count;
gps_weighted_lat += lat * weight;
gps_weighted_lon += lon * weight;
gps_weighted_altitude += altitude * weight;
gps_weight_sum += weight;
if (millisDue(now, gps_stable_started_at + GPS_TELEMETRY_STABLE_SEC * 1000UL)) {
finishGpsTelemetryAcquisition(now, true);
}
}
void SensorManager::loopGpsTelemetry(unsigned long now) {
if (!gps_user_enabled && !gpsTelemetryHoldActive(now) && !gps_acquiring) {
maybeStopGpsForTelemetry(now);
}
if (gps_location_access_available && telemetryGpsDetected() && !gps_user_enabled &&
!gpsTelemetryHoldActive(now) && !gps_acquiring &&
(gps_next_cache_update_at == 0 || millisDue(now, gps_next_cache_update_at))) {
beginGpsTelemetryAcquisition(now);
}
if (gps_acquiring && millisDue(now, gps_acquire_started_at + GPS_TELEMETRY_MAX_ACQUIRE_SEC * 1000UL)) {
finishGpsTelemetryAcquisition(now, gps_acquire_has_fix && gps_weight_sum > 0);
}
}
void SensorManager::setGpsTelemetryUserEnabled(bool enabled) {
gps_user_enabled = enabled;
unsigned long now = millis();
if (enabled) {
if (telemetryGpsDetected() && !telemetryGpsActive()) telemetryGpsStart();
} else {
maybeStopGpsForTelemetry(now);
}
}
#endif
void SensorManager::setTelemetryLocationAccessAvailable(bool available) {
#if ENV_INCLUDE_GPS
unsigned long now = millis();
if (gps_location_access_available == available) return;
gps_location_access_available = available;
if (available) {
gps_next_cache_update_at = 0;
} else if (gps_acquiring && !gps_user_enabled && !gpsTelemetryHoldActive(now)) {
gps_acquiring = false;
maybeStopGpsForTelemetry(now);
}
#else
(void)available;
#endif
}
+42
View File
@@ -11,6 +11,47 @@
#define TELEM_CHANNEL_SELF 1 // LPP data channel for 'self' device
class SensorManager {
#if ENV_INCLUDE_GPS
bool gps_cache_valid = false;
bool gps_location_access_available = false;
bool gps_user_enabled = false;
bool gps_acquiring = false;
bool gps_acquire_has_fix = false;
float gps_cache_lat = 0;
float gps_cache_lon = 0;
float gps_cache_altitude = 0;
float gps_stable_origin_lat = 0;
float gps_stable_origin_lon = 0;
float gps_weighted_lat = 0;
float gps_weighted_lon = 0;
float gps_weighted_altitude = 0;
float gps_weight_sum = 0;
uint16_t gps_weight_count = 0;
unsigned long gps_cache_updated_at = 0;
unsigned long gps_next_cache_update_at = 0;
unsigned long gps_hold_until = 0;
unsigned long gps_acquire_started_at = 0;
unsigned long gps_stable_started_at = 0;
bool gpsTelemetryHoldActive(unsigned long now) const;
bool gpsTelemetryCacheFresh(unsigned long now) const;
void beginGpsTelemetryAcquisition(unsigned long now);
void finishGpsTelemetryAcquisition(unsigned long now, bool use_weighted_average);
void updateGpsTelemetryCache(float lat, float lon, float altitude, unsigned long now);
void maybeStopGpsForTelemetry(unsigned long now);
protected:
virtual bool telemetryGpsDetected() const { return false; }
virtual bool telemetryGpsActive() const { return false; }
virtual void telemetryGpsStart() { }
virtual void telemetryGpsStop() { }
bool queryGpsTelemetry(uint8_t requester_permissions, CayenneLPP& telemetry);
void processGpsTelemetryFix(float lat, float lon, float altitude, unsigned long now);
void loopGpsTelemetry(unsigned long now);
void setGpsTelemetryUserEnabled(bool enabled);
bool isGpsTelemetryUserEnabled() const { return gps_user_enabled; }
#endif
public:
double node_lat, node_lon; // modify these, if you want to affect Advert location
double node_altitude; // altitude in meters
@@ -20,6 +61,7 @@ public:
virtual bool begin() { return false; }
virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; }
virtual void loop() { }
virtual void setTelemetryLocationAccessAvailable(bool available);
virtual int getNumSettings() const { return 0; }
virtual const char* getSettingName(int i) const { return NULL; }
virtual const char* getSettingValue(int i) const { return NULL; }
@@ -689,9 +689,9 @@ bool EnvironmentSensorManager::begin() {
bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
next_available_channel = TELEM_CHANNEL_SELF + 1;
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
#if ENV_INCLUDE_GPS
queryGpsTelemetry(requester_permissions, telemetry);
#endif
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
for (int i = 0; i < _active_sensor_count; i++) {
@@ -726,7 +726,7 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const {
int settings = 0;
#if ENV_INCLUDE_GPS
if (gps_detected && i == settings++) {
return gps_active ? "1" : "0";
return isGpsTelemetryUserEnabled() ? "1" : "0";
}
#endif
return NULL;
@@ -735,11 +735,7 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const {
bool EnvironmentSensorManager::setSettingValue(const char* name, const char* value) {
#if ENV_INCLUDE_GPS
if (gps_detected && strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
return true;
}
if (strcmp(name, "gps_interval") == 0) {
@@ -784,6 +780,7 @@ void EnvironmentSensorManager::initBasicGPS() {
MESH_DEBUG_PRINTLN("GPS detected");
#ifdef PERSISTANT_GPS
gps_active = true;
setGpsTelemetryUserEnabled(true);
return;
#endif
} else {
@@ -876,6 +873,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
#endif
void EnvironmentSensorManager::start_gps() {
if (gps_active) return;
gps_active = true;
#ifdef RAK_WISBLOCK_GPS
pinMode(gpsResetPin, OUTPUT);
@@ -911,32 +909,43 @@ void EnvironmentSensorManager::stop_gps() {
void EnvironmentSensorManager::loop() {
#if ENV_INCLUDE_GPS
static long next_gps_update = 0;
static unsigned long next_gps_update = 0;
unsigned long now = millis();
loopGpsTelemetry(now);
if (gps_active) {
_location->loop();
}
if (millis() > next_gps_update) {
if ((long)(now - next_gps_update) >= 0) {
if(gps_active){
#ifdef RAK_WISBLOCK_GPS
if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
float gps_lat = ((float)_location->getLatitude()) / 1000000.0f;
float gps_lon = ((float)_location->getLongitude()) / 1000000.0f;
float gps_altitude = ((float)_location->getAltitude()) / 1000.0f;
node_lat = gps_lat;
node_lon = gps_lon;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
node_altitude = ((double)_location->getAltitude()) / 1000.0;
node_altitude = gps_altitude;
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
processGpsTelemetryFix(gps_lat, gps_lon, gps_altitude, now);
}
#else
if (_location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
float gps_lat = ((float)_location->getLatitude()) / 1000000.0f;
float gps_lon = ((float)_location->getLongitude()) / 1000000.0f;
float gps_altitude = ((float)_location->getAltitude()) / 1000.0f;
node_lat = gps_lat;
node_lon = gps_lon;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
node_altitude = ((double)_location->getAltitude()) / 1000.0;
node_altitude = gps_altitude;
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
processGpsTelemetryFix(gps_lat, gps_lon, gps_altitude, now);
}
#endif
}
next_gps_update = millis() + (gps_update_interval_sec * 1000);
next_gps_update = now + (gps_update_interval_sec * 1000);
}
#endif
#if ENV_INCLUDE_BME680_BSEC
@@ -27,6 +27,10 @@ protected:
LocationProvider* _location;
void start_gps();
void stop_gps();
bool telemetryGpsDetected() const override { return gps_detected; }
bool telemetryGpsActive() const override { return gps_active; }
void telemetryGpsStart() override { start_gps(); }
void telemetryGpsStop() override { stop_gps(); }
void initBasicGPS();
#ifdef RAK_BOARD
void rakGPSInit();
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/heltec_mesh_solar
-D HELTEC_MESH_SOLAR
-D ENV_INCLUDE_GPS=1
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
+11 -14
View File
@@ -58,25 +58,26 @@ bool SolarSensorManager::begin() {
}
bool SolarSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
queryGpsTelemetry(requester_permissions, telemetry);
return true;
}
void SolarSensorManager::loop() {
static long next_gps_update = 0;
static unsigned long next_gps_update = 0;
unsigned long now = millis();
loopGpsTelemetry(now);
_location->loop();
if (gps_active) _location->loop();
if (millis() > next_gps_update) {
if (_location->isValid()) {
if ((long)(now - next_gps_update) >= 0) {
if (gps_active && _location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
node_altitude = ((double)_location->getAltitude()) / 1000.0;
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
}
next_gps_update = millis() + 1000;
next_gps_update = now + 1000;
}
}
@@ -90,18 +91,14 @@ const char* SolarSensorManager::getSettingName(int i) const {
const char* SolarSensorManager::getSettingValue(int i) const {
if (gps_detected && i == 0) {
return gps_active ? "1" : "0";
return isGpsTelemetryUserEnabled() ? "1" : "0";
}
return NULL;
}
bool SolarSensorManager::setSettingValue(const char* name, const char* value) {
if (gps_detected && strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
return true;
}
return false; // not supported
+4
View File
@@ -19,6 +19,10 @@ class SolarSensorManager : public SensorManager {
void start_gps();
void stop_gps();
bool telemetryGpsDetected() const override { return gps_detected; }
bool telemetryGpsActive() const override { return gps_active; }
void telemetryGpsStart() override { start_gps(); }
void telemetryGpsStop() override { stop_gps(); }
public:
SolarSensorManager(LocationProvider &location): _location(&location) { }
bool begin() override;
+10 -13
View File
@@ -64,25 +64,26 @@ bool HWTSensorManager::begin() {
}
bool HWTSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
queryGpsTelemetry(requester_permissions, telemetry);
return true;
}
void HWTSensorManager::loop() {
static long next_gps_update = 0;
static unsigned long next_gps_update = 0;
unsigned long now = millis();
loopGpsTelemetry(now);
_location->loop();
if (gps_active) _location->loop();
if (millis() > next_gps_update) {
if ((long)(now - next_gps_update) >= 0) {
if (gps_active && _location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
node_altitude = ((double)_location->getAltitude()) / 1000.0;
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
}
next_gps_update = millis() + 1000;
next_gps_update = now + 1000;
}
}
@@ -93,17 +94,13 @@ const char* HWTSensorManager::getSettingName(int i) const {
}
const char* HWTSensorManager::getSettingValue(int i) const {
if (i == 0) {
return gps_active ? "1" : "0";
return isGpsTelemetryUserEnabled() ? "1" : "0";
}
return NULL;
}
bool HWTSensorManager::setSettingValue(const char* name, const char* value) {
if (strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
return true;
}
return false; // not supported
+4
View File
@@ -19,6 +19,10 @@ class HWTSensorManager : public SensorManager {
void start_gps();
void stop_gps();
bool telemetryGpsDetected() const override { return true; }
bool telemetryGpsActive() const override { return gps_active; }
void telemetryGpsStart() override { start_gps(); }
void telemetryGpsStop() override { stop_gps(); }
public:
HWTSensorManager(LocationProvider &location): _location(&location) { }
bool begin() override;
+3 -5
View File
@@ -165,6 +165,7 @@ build_flags =
-D MQTT_MAX_PACKET_SIZE=1024
-D MQTT_TASK_STACK_SIZE=16384
-D ESP32_CPU_FREQ=240
-D LIGHTWEIGHT_WIFI_OTA=1 ; manual browser upload without the ElegantOTA dependency cost
; -D MQTT_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
@@ -183,7 +184,6 @@ build_src_filter = ${LilyGo_TLora_V2_1_1_6_core.build_src_filter}
+<../examples/simple_repeater>
lib_deps =
${LilyGo_TLora_V2_1_1_6_core.lib_deps}
${esp32_ota.lib_deps}
elims/PsychicMqttClient@^0.2.4
bblanchon/ArduinoJson
arduino-libraries/NTPClient
@@ -211,9 +211,8 @@ build_flags =
-D MQTT_MAX_PACKET_SIZE=1024
-D MQTT_TASK_STACK_SIZE=16384
-D ESP32_CPU_FREQ=240
; MQTT_DEBUG must stay off here: with it on, this min_spiffs build overflows the
; app partition by ~357 bytes and produces no binary (the repeater observer env
; keeps it commented for the same reason).
-D LIGHTWEIGHT_WIFI_OTA=1 ; manual browser upload without the ElegantOTA dependency cost
; Keep MQTT debug logging off to preserve flash and runtime heap headroom.
; -D MQTT_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
@@ -226,7 +225,6 @@ build_src_filter = ${LilyGo_TLora_V2_1_1_6_core.build_src_filter}
+<../examples/simple_room_server>
lib_deps =
${LilyGo_TLora_V2_1_1_6_core.lib_deps}
${esp32_ota.lib_deps}
elims/PsychicMqttClient@^0.2.4
bblanchon/ArduinoJson
arduino-libraries/NTPClient
+1
View File
@@ -6,6 +6,7 @@ build_flags =
${esp32_base.build_flags}
-I variants/meshadventurer
-D MESHADVENTURER
-D ENV_INCLUDE_GPS=1
-D P_LORA_TX_LED=2
-D PIN_VBAT_READ=35
-D PIN_USER_BTN=39
+10 -13
View File
@@ -58,23 +58,24 @@ bool MASensorManager::begin() {
}
bool MASensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if(requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
queryGpsTelemetry(requester_permissions, telemetry);
return true;
}
void MASensorManager::loop() {
static long next_gps_update = 0;
_location->loop();
if(millis() > next_gps_update && gps_active) {
static unsigned long next_gps_update = 0;
unsigned long now = millis();
loopGpsTelemetry(now);
if (gps_active) _location->loop();
if ((long)(now - next_gps_update) >= 0 && gps_active) {
if(_location->isValid()) {
node_lat = ((double)_location->getLatitude()) / 1000000.;
node_lon = ((double)_location->getLongitude()) / 1000000.;
node_altitude = ((double)_location->getAltitude()) / 1000.0;
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
}
next_gps_update = millis() + 1000;
next_gps_update = now + 1000;
}
}
@@ -85,17 +86,13 @@ const char* MASensorManager::getSettingName(int i) const {
}
const char* MASensorManager::getSettingValue(int i) const {
if(i == 0) {
return gps_active ? "1" : "0";
return isGpsTelemetryUserEnabled() ? "1" : "0";
}
return NULL;
}
bool MASensorManager::setSettingValue(const char* name, const char* value) {
if(strcmp(name, "gps") == 0) {
if(strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
return true;
}
return false; // not supported
+4 -1
View File
@@ -20,6 +20,10 @@ class MASensorManager : public SensorManager {
void start_gps();
void stop_gps();
bool telemetryGpsDetected() const override { return true; }
bool telemetryGpsActive() const override { return gps_active; }
void telemetryGpsStart() override { start_gps(); }
void telemetryGpsStop() override { stop_gps(); }
public:
MASensorManager(LocationProvider &location): _location(&location) { }
bool begin() override;
@@ -44,4 +48,3 @@ extern MASensorManager sensors;
bool radio_init();
mesh::LocalIdentity radio_new_identity();
+1
View File
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/nano_g2_ultra
-D NANO_G2_ULTRA
-D ENV_INCLUDE_GPS=1
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
+10 -13
View File
@@ -66,14 +66,14 @@ bool NanoG2UltraSensorManager::begin() {
}
bool NanoG2UltraSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP &telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
queryGpsTelemetry(requester_permissions, telemetry);
return true;
}
void NanoG2UltraSensorManager::loop() {
static long next_gps_update = 0;
static unsigned long next_gps_update = 0;
unsigned long now = millis();
loopGpsTelemetry(now);
if (!gps_active) {
return; // GPS is not active, skip further processing
@@ -81,17 +81,18 @@ void NanoG2UltraSensorManager::loop() {
_location->loop();
if (millis() > next_gps_update) {
if ((long)(now - next_gps_update) >= 0) {
if (_location->isValid()) {
node_lat = ((double)_location->getLatitude()) / 1000000.;
node_lon = ((double)_location->getLongitude()) / 1000000.;
node_altitude = ((double)_location->getAltitude()) / 1000.0;
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
MESH_DEBUG_PRINTLN("VALID location: lat %f lon %f", node_lat, node_lon);
} else {
MESH_DEBUG_PRINTLN("INVALID location, waiting for fix");
}
MESH_DEBUG_PRINTLN("GPS satellites: %d", _location->satellitesCount());
next_gps_update = millis() + 1000;
next_gps_update = now + 1000;
}
}
@@ -105,18 +106,14 @@ const char *NanoG2UltraSensorManager::getSettingName(int i) const {
const char *NanoG2UltraSensorManager::getSettingValue(int i) const {
if (i == 0) {
return gps_active ? "1" : "0";
return isGpsTelemetryUserEnabled() ? "1" : "0";
}
return NULL;
}
bool NanoG2UltraSensorManager::setSettingValue(const char *name, const char *value) {
if (strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
return true;
}
return false; // not supported
@@ -125,4 +122,4 @@ bool NanoG2UltraSensorManager::setSettingValue(const char *name, const char *val
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
}
+4 -1
View File
@@ -20,6 +20,10 @@ class NanoG2UltraSensorManager : public SensorManager {
void start_gps();
void stop_gps();
bool telemetryGpsDetected() const override { return true; }
bool telemetryGpsActive() const override { return gps_active; }
void telemetryGpsStart() override { start_gps(); }
void telemetryGpsStop() override { stop_gps(); }
public:
NanoG2UltraSensorManager(LocationProvider &location) : _location(&location) {}
@@ -44,4 +48,3 @@ extern MomentaryButton user_btn;
bool radio_init();
mesh::LocalIdentity radio_new_identity();
+2 -2
View File
@@ -58,8 +58,8 @@ void initVariant() {
pinMode(PIN_QSPI_CS, OUTPUT);
digitalWrite(PIN_QSPI_CS, HIGH);
pinMode(LED_GREEN, OUTPUT);
digitalWrite(LED_GREEN, LOW);
pinMode(LED_WHITE, OUTPUT);
digitalWrite(LED_WHITE, LOW);
pinMode(LED_BLUE, OUTPUT);
digitalWrite(LED_BLUE, LOW);
+2 -2
View File
@@ -24,8 +24,8 @@
#define LED_BUILTIN (PIN_LED)
#define LED_RED (PINS_COUNT)
#define LED_GREEN (12)
#define LED_BLUE (11)
#define LED_WHITE (11)
#define LED_BLUE (12) // LoRa TX indicator
#define LED_STATE_ON (1) // State when LED is litted
+1
View File
@@ -1,6 +1,7 @@
[Station_G2]
extends = esp32_base
board = station-g2
board_build.partitions = default_16MB.csv
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
+1
View File
@@ -1,6 +1,7 @@
[Station_G3_ESP32]
extends = esp32_base
board = station-g3-esp32
board_build.partitions = default_16MB.csv
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
+11 -13
View File
@@ -80,6 +80,7 @@ mesh::LocalIdentity radio_new_identity() {
}
void T1000SensorManager::start_gps() {
if (gps_active) return;
gps_active = true;
//_nmea->begin();
// this init sequence should be better
@@ -135,9 +136,7 @@ bool T1000SensorManager::begin() {
}
bool T1000SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
queryGpsTelemetry(requester_permissions, telemetry);
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
// Firmware reports light as a 0-100 % scale, but expose it via Luminosity so app labels it "Luminosity".
telemetry.addLuminosity(TELEM_CHANNEL_SELF, t1000e_get_light());
@@ -147,18 +146,21 @@ bool T1000SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP&
}
void T1000SensorManager::loop() {
static long next_gps_update = 0;
static unsigned long next_gps_update = 0;
unsigned long now = millis();
loopGpsTelemetry(now);
_nmea->loop();
if (gps_active) _nmea->loop();
if (millis() > next_gps_update) {
if ((long)(now - next_gps_update) >= 0) {
if (gps_active && _nmea->isValid()) {
node_lat = ((double)_nmea->getLatitude())/1000000.;
node_lon = ((double)_nmea->getLongitude())/1000000.;
node_altitude = ((double)_nmea->getAltitude()) / 1000.0;
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
//Serial.printf("lat %f lon %f\r\n", _lat, _lon);
}
next_gps_update = millis() + 1000;
next_gps_update = now + 1000;
}
}
@@ -169,17 +171,13 @@ const char* T1000SensorManager::getSettingName(int i) const {
}
const char* T1000SensorManager::getSettingValue(int i) const {
if (i == 0) {
return gps_active ? "1" : "0";
return isGpsTelemetryUserEnabled() ? "1" : "0";
}
return NULL;
}
bool T1000SensorManager::setSettingValue(const char* name, const char* value) {
if (strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
sleep_gps(); // sleep for faster fix !
} else {
start_gps();
}
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
return true;
}
return false; // not supported
+4
View File
@@ -19,6 +19,10 @@ class T1000SensorManager: public SensorManager {
void start_gps();
void sleep_gps();
void stop_gps();
bool telemetryGpsDetected() const override { return true; }
bool telemetryGpsActive() const override { return gps_active; }
void telemetryGpsStart() override { start_gps(); }
void telemetryGpsStop() override { sleep_gps(); }
public:
T1000SensorManager(LocationProvider &nmea): _nmea(&nmea) { }
bool begin() override;
+1 -1
View File
@@ -8,6 +8,7 @@ build_flags = ${rak4631_hw.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/thinknode_m1
-D THINKNODE_M1=1
-D ENV_INCLUDE_GPS=1
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D P_LORA_DIO_1=20
@@ -83,7 +84,6 @@ build_flags =
-D PIN_BUZZER=6
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
-D QSPIFLASH=1
-D ENV_INCLUDE_GPS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ThinkNode_M1.build_src_filter}
+15 -19
View File
@@ -55,25 +55,25 @@ bool ThinkNodeM1SensorManager::begin() {
// Check initial switch state to determine if GPS should be active
if (last_gps_switch_state == HIGH) { // Switch is HIGH when ON
start_gps();
setGpsTelemetryUserEnabled(true);
}
return true;
}
bool ThinkNodeM1SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
queryGpsTelemetry(requester_permissions, telemetry);
return true;
}
void ThinkNodeM1SensorManager::loop() {
static long next_gps_update = 0;
static long last_switch_check = 0;
static unsigned long next_gps_update = 0;
static unsigned long last_switch_check = 0;
unsigned long now = millis();
loopGpsTelemetry(now);
// Check GPS switch state every second
if (millis() - last_switch_check > 1000) {
if (now - last_switch_check > 1000) {
bool current_switch_state = digitalRead(PIN_GPS_SWITCH);
// Detect switch state change
@@ -82,14 +82,14 @@ void ThinkNodeM1SensorManager::loop() {
if (current_switch_state == HIGH) { // Switch is ON
MESH_DEBUG_PRINTLN("GPS switch ON");
start_gps();
setGpsTelemetryUserEnabled(true);
} else { // Switch is OFF
MESH_DEBUG_PRINTLN("GPS switch OFF");
stop_gps();
setGpsTelemetryUserEnabled(false);
}
}
last_switch_check = millis();
last_switch_check = now;
}
if (!gps_active) {
@@ -98,14 +98,15 @@ void ThinkNodeM1SensorManager::loop() {
_location->loop();
if (millis() > next_gps_update) {
if ((long)(now - next_gps_update) >= 0) {
if (_location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
node_altitude = ((double)_location->getAltitude()) / 1000.0;
processGpsTelemetryFix(node_lat, node_lon, node_altitude, now);
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
}
next_gps_update = millis() + 1000;
next_gps_update = now + 1000;
}
}
@@ -119,20 +120,15 @@ const char* ThinkNodeM1SensorManager::getSettingName(int i) const {
const char* ThinkNodeM1SensorManager::getSettingValue(int i) const {
if (i == 0) {
return gps_active ? "1" : "0";
return isGpsTelemetryUserEnabled() ? "1" : "0";
}
return NULL;
}
bool ThinkNodeM1SensorManager::setSettingValue(const char* name, const char* value) {
if (strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
setGpsTelemetryUserEnabled(strcmp(value, "0") != 0);
return true;
}
return false; // not supported
}
+4 -1
View File
@@ -20,6 +20,10 @@ class ThinkNodeM1SensorManager : public SensorManager {
void start_gps();
void stop_gps();
bool telemetryGpsDetected() const override { return true; }
bool telemetryGpsActive() const override { return gps_active; }
void telemetryGpsStart() override { start_gps(); }
void telemetryGpsStop() override { stop_gps(); }
public:
ThinkNodeM1SensorManager(LocationProvider &location): _location(&location) { }
LocationProvider* getLocationProvider() override { return _location; }
@@ -44,4 +48,3 @@ extern ThinkNodeM1SensorManager sensors;
bool radio_init();
mesh::LocalIdentity radio_new_identity();