Restore Keymind features after LoRa OTA merge
This commit is contained in:
1 parent
4c0dbe707d
commit
4f8f36354e
45 files changed
+1320
-235
No files matched your search
@@ -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
|
||||
|
||||
@@ -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]`
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(×tamp, 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();
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
|
||||
#include "MeshCore.h"
|
||||
#include "Identity.h"
|
||||
|
||||
@@ -85,3 +87,5 @@ private:
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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,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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user