7 results in the default
- */
- uint32 hop_limit = 8;
-
- /*
- * Disable TX from the LoRa radio. Useful for hot-swapping antennas and other tests.
- * Defaults to false
- */
- bool tx_enabled = 9;
-
- /*
- * If zero, then use default max legal continuous power (ie. something that won't
- * burn out the radio hardware)
- * In most cases you should use zero here.
- * Units are in dBm.
- */
- int32 tx_power = 10;
-
- /*
- * This controls the actual hardware frequency the radio transmits on.
- * Most users should never need to be exposed to this field/concept.
- * A channel number between 1 and NUM_CHANNELS (whatever the max is in the current region).
- * If ZERO then the rule is "use the old channel name hash based
- * algorithm to derive the channel number")
- * If using the hash algorithm the channel number will be: hash(channel_name) %
- * NUM_CHANNELS (Where num channels depends on the regulatory region).
- */
- uint32 channel_num = 11;
-
- /*
- * If true, duty cycle limits will be exceeded and thus you're possibly not following
- * the local regulations if you're not a HAM.
- * Has no effect if the duty cycle of the used region is 100%.
- */
- bool override_duty_cycle = 12;
-
- /*
- * If true, sets RX boosted gain mode on SX126X based radios
- */
- bool sx126x_rx_boosted_gain = 13;
-
- /*
- * This parameter is for advanced users and licensed HAM radio operators.
- * Ignore Channel Calculation and use this frequency instead. The frequency_offset
- * will still be applied. This will allow you to use out-of-band frequencies.
- * Please respect your local laws and regulations. If you are a HAM, make sure you
- * enable HAM mode and turn off encryption.
- */
- float override_frequency = 14;
-
- /*
- * For testing it is useful sometimes to force a node to never listen to
- * particular other nodes (simulating radio out of range). All nodenums listed
- * in ignore_incoming will have packets they send dropped on receive (by router.cpp)
- */
- repeated uint32 ignore_incoming = 103;
-
- /*
- * If true, the device will not process any packets received via LoRa that passed via MQTT anywhere on the path towards it.
- */
- bool ignore_mqtt = 104;
- }
-
- message BluetoothConfig {
- enum PairingMode {
- /*
- * Device generates a random PIN that will be shown on the screen of the device for pairing
- */
- RANDOM_PIN = 0;
-
- /*
- * Device requires a specified fixed PIN for pairing
- */
- FIXED_PIN = 1;
-
- /*
- * Device requires no PIN for pairing
- */
- NO_PIN = 2;
- }
-
- /*
- * Enable Bluetooth on the device
- */
- bool enabled = 1;
-
- /*
- * Determines the pairing strategy for the device
- */
- PairingMode mode = 2;
-
- /*
- * Specified PIN for PairingMode.FixedPin
- */
- uint32 fixed_pin = 3;
- }
-
- /*
- * Payload Variant
- */
- oneof payload_variant {
- DeviceConfig device = 1;
- PositionConfig position = 2;
- PowerConfig power = 3;
- NetworkConfig network = 4;
- DisplayConfig display = 5;
- LoRaConfig lora = 6;
- BluetoothConfig bluetooth = 7;
- }
-}
diff --git a/src/protos/meshtastic/connection_status.options b/src/protos/meshtastic/connection_status.options
deleted file mode 100644
index d4901dd..0000000
--- a/src/protos/meshtastic/connection_status.options
+++ /dev/null
@@ -1 +0,0 @@
-*WifiConnectionStatus.ssid max_size:33
diff --git a/src/protos/meshtastic/connection_status.proto b/src/protos/meshtastic/connection_status.proto
deleted file mode 100644
index 7551596..0000000
--- a/src/protos/meshtastic/connection_status.proto
+++ /dev/null
@@ -1,120 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "ConnStatusProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-message DeviceConnectionStatus {
- /*
- * WiFi Status
- */
- optional WifiConnectionStatus wifi = 1;
- /*
- * WiFi Status
- */
- optional EthernetConnectionStatus ethernet = 2;
-
- /*
- * Bluetooth Status
- */
- optional BluetoothConnectionStatus bluetooth = 3;
-
- /*
- * Serial Status
- */
- optional SerialConnectionStatus serial = 4;
-}
-
-/*
- * WiFi connection status
- */
-message WifiConnectionStatus {
- /*
- * Connection status
- */
- NetworkConnectionStatus status = 1;
-
- /*
- * WiFi access point SSID
- */
- string ssid = 2;
-
- /*
- * RSSI of wireless connection
- */
- int32 rssi = 3;
-}
-
-/*
- * Ethernet connection status
- */
-message EthernetConnectionStatus {
- /*
- * Connection status
- */
- NetworkConnectionStatus status = 1;
-}
-
-/*
- * Ethernet or WiFi connection status
- */
-message NetworkConnectionStatus {
- /*
- * IP address of device
- */
- fixed32 ip_address = 1;
-
- /*
- * Whether the device has an active connection or not
- */
- bool is_connected = 2;
-
- /*
- * Whether the device has an active connection to an MQTT broker or not
- */
- bool is_mqtt_connected = 3;
-
- /*
- * Whether the device is actively remote syslogging or not
- */
- bool is_syslog_connected = 4;
-}
-
-/*
- * Bluetooth connection status
- */
-message BluetoothConnectionStatus {
- /*
- * The pairing PIN for bluetooth
- */
- uint32 pin = 1;
-
- /*
- * RSSI of bluetooth connection
- */
- int32 rssi = 2;
-
- /*
- * Whether the device has an active connection or not
- */
- bool is_connected = 3;
-}
-
-/*
- * Serial connection status
- */
-message SerialConnectionStatus {
- /*
- * Serial baud rate
- */
- uint32 baud = 1;
-
- /*
- * Whether the device has an active connection or not
- */
- bool is_connected = 2;
-}
diff --git a/src/protos/meshtastic/deviceonly.options b/src/protos/meshtastic/deviceonly.options
deleted file mode 100644
index d870ace..0000000
--- a/src/protos/meshtastic/deviceonly.options
+++ /dev/null
@@ -1,19 +0,0 @@
-# options for nanopb
-# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
-
-# FIXME pick a higher number someday? or do dynamic alloc in nanopb?
-*DeviceState.node_db_lite max_count:100
-
-# FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in RAM
-*DeviceState.receive_queue max_count:1
-
-*ChannelFile.channels max_count:8
-
-*OEMStore.oem_text max_size:40
-*OEMStore.oem_icon_bits max_size:2048
-*OEMStore.oem_aes_key max_size:32
-
-*DeviceState.node_remote_hardware_pins max_count:12
-
-*NodeInfoLite.channel int_size:8
-*NodeInfoLite.hops_away int_size:8
\ No newline at end of file
diff --git a/src/protos/meshtastic/deviceonly.proto b/src/protos/meshtastic/deviceonly.proto
deleted file mode 100644
index 169df1c..0000000
--- a/src/protos/meshtastic/deviceonly.proto
+++ /dev/null
@@ -1,262 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-import "meshtastic/channel.proto";
-import "meshtastic/localonly.proto";
-import "meshtastic/mesh.proto";
-import "meshtastic/telemetry.proto";
-import "meshtastic/module_config.proto";
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "DeviceOnly";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * This message is never sent over the wire, but it is used for serializing DB
- * state to flash in the device code
- * FIXME, since we write this each time we enter deep sleep (and have infinite
- * flash) it would be better to use some sort of append only data structure for
- * the receive queue and use the preferences store for the other stuff
- */
-message DeviceState {
- /*
- * Read only settings/info about this node
- */
- MyNodeInfo my_node = 2;
-
- /*
- * My owner info
- */
- User owner = 3;
-
- /*
- * Received packets saved for delivery to the phone
- */
- repeated MeshPacket receive_queue = 5;
-
- /*
- * A version integer used to invalidate old save files when we make
- * incompatible changes This integer is set at build time and is private to
- * NodeDB.cpp in the device code.
- */
- uint32 version = 8;
-
- /*
- * We keep the last received text message (only) stored in the device flash,
- * so we can show it on the screen.
- * Might be null
- */
- MeshPacket rx_text_message = 7;
-
- /*
- * Used only during development.
- * Indicates developer is testing and changes should never be saved to flash.
- */
- bool no_save = 9;
-
- /*
- * Some GPS receivers seem to have bogus settings from the factory, so we always do one factory reset.
- */
- bool did_gps_reset = 11;
-
- /*
- * We keep the last received waypoint stored in the device flash,
- * so we can show it on the screen.
- * Might be null
- */
- MeshPacket rx_waypoint = 12;
-
- /*
- * The mesh's nodes with their available gpio pins for RemoteHardware module
- */
- repeated NodeRemoteHardwarePin node_remote_hardware_pins = 13;
-
- /*
- * New lite version of NodeDB to decrease memory footprint
- */
- repeated NodeInfoLite node_db_lite = 14;
-}
-
-message NodeInfoLite {
- /*
- * The node number
- */
- uint32 num = 1;
-
- /*
- * The user info for this node
- */
- User user = 2;
-
- /*
- * This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true.
- * Position.time now indicates the last time we received a POSITION from that node.
- */
- PositionLite position = 3;
-
- /*
- * Returns the Signal-to-noise ratio (SNR) of the last received message,
- * as measured by the receiver. Return SNR of the last received message in dB
- */
- float snr = 4;
-
- /*
- * Set to indicate the last time we received a packet from this node
- */
- fixed32 last_heard = 5;
- /*
- * The latest device metrics for the node.
- */
- DeviceMetrics device_metrics = 6;
-
- /*
- * local channel index we heard that node on. Only populated if its not the default channel.
- */
- uint32 channel = 7;
-
- /*
- * True if we witnessed the node over MQTT instead of LoRA transport
- */
- bool via_mqtt = 8;
-
- /*
- * Number of hops away from us this node is (0 if adjacent)
- */
- uint32 hops_away = 9;
-}
-
-/*
- * Position with static location information only for NodeDBLite
- */
-message PositionLite {
- /*
- * The new preferred location encoding, multiply by 1e-7 to get degrees
- * in floating point
- */
- sfixed32 latitude_i = 1;
-
- /*
- * TODO: REPLACE
- */
- sfixed32 longitude_i = 2;
-
- /*
- * In meters above MSL (but see issue #359)
- */
- int32 altitude = 3;
-
- /*
- * This is usually not sent over the mesh (to save space), but it is sent
- * from the phone so that the local device can set its RTC If it is sent over
- * the mesh (because there are devices on the mesh without GPS), it will only
- * be sent by devices which has a hardware GPS clock.
- * seconds since 1970
- */
- fixed32 time = 4;
-
- /*
- * TODO: REPLACE
- */
- Position.LocSource location_source = 5;
-}
-
-/*
- * The on-disk saved channels
- */
-message ChannelFile {
- /*
- * The channels our node knows about
- */
- repeated Channel channels = 1;
-
- /*
- * A version integer used to invalidate old save files when we make
- * incompatible changes This integer is set at build time and is private to
- * NodeDB.cpp in the device code.
- */
- uint32 version = 2;
-}
-
-/*
- * TODO: REPLACE
- */
-enum ScreenFonts {
- /*
- * TODO: REPLACE
- */
- FONT_SMALL = 0;
-
- /*
- * TODO: REPLACE
- */
- FONT_MEDIUM = 1;
-
- /*
- * TODO: REPLACE
- */
- FONT_LARGE = 2;
-}
-
-/*
- * This can be used for customizing the firmware distribution. If populated,
- * show a secondary bootup screen with custom logo and text for 2.5 seconds.
- */
-message OEMStore {
- /*
- * The Logo width in Px
- */
- uint32 oem_icon_width = 1;
-
- /*
- * The Logo height in Px
- */
- uint32 oem_icon_height = 2;
-
- /*
- * The Logo in XBM bytechar format
- */
- bytes oem_icon_bits = 3;
-
- /*
- * Use this font for the OEM text.
- */
- ScreenFonts oem_font = 4;
-
- /*
- * Use this font for the OEM text.
- */
- string oem_text = 5;
-
- /*
- * The default device encryption key, 16 or 32 byte
- */
- bytes oem_aes_key = 6;
-
- /*
- * A Preset LocalConfig to apply during factory reset
- */
- LocalConfig oem_local_config = 7;
-
- /*
- * A Preset LocalModuleConfig to apply during factory reset
- */
- LocalModuleConfig oem_local_module_config = 8;
-}
-
-/*
- * RemoteHardwarePins associated with a node
- */
-message NodeRemoteHardwarePin {
- /*
- * The node_num exposing the available gpio pin
- */
- uint32 node_num = 1;
-
- /*
- * The the available gpio pin for usage with RemoteHardware module
- */
- RemoteHardwarePin pin = 2;
-}
\ No newline at end of file
diff --git a/src/protos/meshtastic/localonly.proto b/src/protos/meshtastic/localonly.proto
deleted file mode 100644
index 9297dff..0000000
--- a/src/protos/meshtastic/localonly.proto
+++ /dev/null
@@ -1,135 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-import "meshtastic/config.proto";
-import "meshtastic/module_config.proto";
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "LocalOnlyProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * Protobuf structures common to apponly.proto and deviceonly.proto
- * This is never sent over the wire, only for local use
- */
-
-message LocalConfig {
- /*
- * The part of the config that is specific to the Device
- */
- Config.DeviceConfig device = 1;
-
- /*
- * The part of the config that is specific to the GPS Position
- */
- Config.PositionConfig position = 2;
-
- /*
- * The part of the config that is specific to the Power settings
- */
- Config.PowerConfig power = 3;
-
- /*
- * The part of the config that is specific to the Wifi Settings
- */
- Config.NetworkConfig network = 4;
-
- /*
- * The part of the config that is specific to the Display
- */
- Config.DisplayConfig display = 5;
-
- /*
- * The part of the config that is specific to the Lora Radio
- */
- Config.LoRaConfig lora = 6;
-
- /*
- * The part of the config that is specific to the Bluetooth settings
- */
- Config.BluetoothConfig bluetooth = 7;
-
- /*
- * A version integer used to invalidate old save files when we make
- * incompatible changes This integer is set at build time and is private to
- * NodeDB.cpp in the device code.
- */
- uint32 version = 8;
-}
-
-message LocalModuleConfig {
- /*
- * The part of the config that is specific to the MQTT module
- */
- ModuleConfig.MQTTConfig mqtt = 1;
-
- /*
- * The part of the config that is specific to the Serial module
- */
- ModuleConfig.SerialConfig serial = 2;
-
- /*
- * The part of the config that is specific to the ExternalNotification module
- */
- ModuleConfig.ExternalNotificationConfig external_notification = 3;
-
- /*
- * The part of the config that is specific to the Store & Forward module
- */
- ModuleConfig.StoreForwardConfig store_forward = 4;
-
- /*
- * The part of the config that is specific to the RangeTest module
- */
- ModuleConfig.RangeTestConfig range_test = 5;
-
- /*
- * The part of the config that is specific to the Telemetry module
- */
- ModuleConfig.TelemetryConfig telemetry = 6;
-
- /*
- * The part of the config that is specific to the Canned Message module
- */
- ModuleConfig.CannedMessageConfig canned_message = 7;
-
- /*
- * The part of the config that is specific to the Audio module
- */
- ModuleConfig.AudioConfig audio = 9;
-
- /*
- * The part of the config that is specific to the Remote Hardware module
- */
- ModuleConfig.RemoteHardwareConfig remote_hardware = 10;
-
- /*
- * The part of the config that is specific to the Neighbor Info module
- */
- ModuleConfig.NeighborInfoConfig neighbor_info = 11;
-
- /*
- * The part of the config that is specific to the Ambient Lighting module
- */
- ModuleConfig.AmbientLightingConfig ambient_lighting = 12;
-
- /*
- * The part of the config that is specific to the Detection Sensor module
- */
- ModuleConfig.DetectionSensorConfig detection_sensor = 13;
-
- /*
- * Paxcounter Config
- */
- ModuleConfig.PaxcounterConfig paxcounter = 14;
-
- /*
- * A version integer used to invalidate old save files when we make
- * incompatible changes This integer is set at build time and is private to
- * NodeDB.cpp in the device code.
- */
- uint32 version = 8;
-}
\ No newline at end of file
diff --git a/src/protos/meshtastic/mesh.options b/src/protos/meshtastic/mesh.options
deleted file mode 100644
index aedfe99..0000000
--- a/src/protos/meshtastic/mesh.options
+++ /dev/null
@@ -1,61 +0,0 @@
-# options for nanopb
-# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
-
-*macaddr max_size:6 fixed_length:true # macaddrs
-*id max_size:16 # node id strings
-
-*User.long_name max_size:40
-*User.short_name max_size:5
-
-*RouteDiscovery.route max_count:8
-
-# note: this payload length is ONLY the bytes that are sent inside of the Data protobuf (excluding protobuf overhead). The 16 byte header is
-# outside of this envelope
-*Data.payload max_size:237
-
-*NodeInfo.channel int_size:8
-*NodeInfo.hops_away int_size:8
-
-# Big enough for 1.2.28.568032c-d
-*MyNodeInfo.firmware_version max_size:18
-
-*MyNodeInfo.air_period_tx max_count:8
-*MyNodeInfo.air_period_rx max_count:8
-
-# Note: the actual limit (because of header bytes) on the size of encrypted payloads is 251 bytes, but I use 256
-# here because we might need to fill with zeros for padding to encryption block size (16 bytes per block)
-*MeshPacket.encrypted max_size:256
-*MeshPacket.payload_variant anonymous_oneof:true
-*MeshPacket.hop_limit int_size:8
-*MeshPacket.hop_start int_size:8
-*MeshPacket.channel int_size:8
-
-*QueueStatus.res int_size:8
-*QueueStatus.free int_size:8
-*QueueStatus.maxlen int_size:8
-
-*ToRadio.payload_variant anonymous_oneof:true
-
-*FromRadio.payload_variant anonymous_oneof:true
-
-*Routing.variant anonymous_oneof:true
-
-*LogRecord.message max_size:64
-*LogRecord.source max_size:8
-
-# MyMessage.name max_size:40
-# or fixed_length or fixed_count, or max_count
-
-#This value may want to be a few bytes smaller to compensate for the parent fields.
-*Compressed.data max_size:237
-
-*Waypoint.name max_size:30
-*Waypoint.description max_size:100
-
-*NeighborInfo.neighbors max_count:10
-
-*DeviceMetadata.firmware_version max_size:18
-
-*MqttClientProxyMessage.topic max_size:60
-*MqttClientProxyMessage.data max_size:435
-*MqttClientProxyMessage.text max_size:435
diff --git a/src/protos/meshtastic/mesh.proto b/src/protos/meshtastic/mesh.proto
deleted file mode 100644
index 7f8b7e5..0000000
--- a/src/protos/meshtastic/mesh.proto
+++ /dev/null
@@ -1,1943 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-import "meshtastic/channel.proto";
-import "meshtastic/config.proto";
-import "meshtastic/module_config.proto";
-import "meshtastic/portnums.proto";
-import "meshtastic/telemetry.proto";
-import "meshtastic/xmodem.proto";
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "MeshProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * a gps position
- */
-message Position {
- /*
- * The new preferred location encoding, multiply by 1e-7 to get degrees
- * in floating point
- */
- optional sfixed32 latitude_i = 1;
-
- /*
- * TODO: REPLACE
- */
- optional sfixed32 longitude_i = 2;
-
- /*
- * In meters above MSL (but see issue #359)
- */
- optional int32 altitude = 3;
-
- /*
- * This is usually not sent over the mesh (to save space), but it is sent
- * from the phone so that the local device can set its time if it is sent over
- * the mesh (because there are devices on the mesh without GPS or RTC).
- * seconds since 1970
- */
- fixed32 time = 4;
-
- /*
- * How the location was acquired: manual, onboard GPS, external (EUD) GPS
- */
- enum LocSource {
- /*
- * TODO: REPLACE
- */
- LOC_UNSET = 0;
-
- /*
- * TODO: REPLACE
- */
- LOC_MANUAL = 1;
-
- /*
- * TODO: REPLACE
- */
- LOC_INTERNAL = 2;
-
- /*
- * TODO: REPLACE
- */
- LOC_EXTERNAL = 3;
- }
-
- /*
- * TODO: REPLACE
- */
- LocSource location_source = 5;
-
- /*
- * How the altitude was acquired: manual, GPS int/ext, etc
- * Default: same as location_source if present
- */
- enum AltSource {
- /*
- * TODO: REPLACE
- */
- ALT_UNSET = 0;
-
- /*
- * TODO: REPLACE
- */
- ALT_MANUAL = 1;
-
- /*
- * TODO: REPLACE
- */
- ALT_INTERNAL = 2;
-
- /*
- * TODO: REPLACE
- */
- ALT_EXTERNAL = 3;
-
- /*
- * TODO: REPLACE
- */
- ALT_BAROMETRIC = 4;
- }
-
- /*
- * TODO: REPLACE
- */
- AltSource altitude_source = 6;
-
- /*
- * Positional timestamp (actual timestamp of GPS solution) in integer epoch seconds
- */
- fixed32 timestamp = 7;
-
- /*
- * Pos. timestamp milliseconds adjustment (rarely available or required)
- */
- int32 timestamp_millis_adjust = 8;
-
- /*
- * HAE altitude in meters - can be used instead of MSL altitude
- */
- optional sint32 altitude_hae = 9;
-
- /*
- * Geoidal separation in meters
- */
- optional sint32 altitude_geoidal_separation = 10;
-
- /*
- * Horizontal, Vertical and Position Dilution of Precision, in 1/100 units
- * - PDOP is sufficient for most cases
- * - for higher precision scenarios, HDOP and VDOP can be used instead,
- * in which case PDOP becomes redundant (PDOP=sqrt(HDOP^2 + VDOP^2))
- * TODO: REMOVE/INTEGRATE
- */
- uint32 PDOP = 11;
-
- /*
- * TODO: REPLACE
- */
- uint32 HDOP = 12;
-
- /*
- * TODO: REPLACE
- */
- uint32 VDOP = 13;
-
- /*
- * GPS accuracy (a hardware specific constant) in mm
- * multiplied with DOP to calculate positional accuracy
- * Default: "'bout three meters-ish" :)
- */
- uint32 gps_accuracy = 14;
-
- /*
- * Ground speed in m/s and True North TRACK in 1/100 degrees
- * Clarification of terms:
- * - "track" is the direction of motion (measured in horizontal plane)
- * - "heading" is where the fuselage points (measured in horizontal plane)
- * - "yaw" indicates a relative rotation about the vertical axis
- * TODO: REMOVE/INTEGRATE
- */
- optional uint32 ground_speed = 15;
-
- /*
- * TODO: REPLACE
- */
- optional uint32 ground_track = 16;
-
- /*
- * GPS fix quality (from NMEA GxGGA statement or similar)
- */
- uint32 fix_quality = 17;
-
- /*
- * GPS fix type 2D/3D (from NMEA GxGSA statement)
- */
- uint32 fix_type = 18;
-
- /*
- * GPS "Satellites in View" number
- */
- uint32 sats_in_view = 19;
-
- /*
- * Sensor ID - in case multiple positioning sensors are being used
- */
- uint32 sensor_id = 20;
-
- /*
- * Estimated/expected time (in seconds) until next update:
- * - if we update at fixed intervals of X seconds, use X
- * - if we update at dynamic intervals (based on relative movement etc),
- * but "AT LEAST every Y seconds", use Y
- */
- uint32 next_update = 21;
-
- /*
- * A sequence number, incremented with each Position message to help
- * detect lost updates if needed
- */
- uint32 seq_number = 22;
-
- /*
- * Indicates the bits of precision set by the sending node
- */
- uint32 precision_bits = 23;
-}
-
-/*
- * Note: these enum names must EXACTLY match the string used in the device
- * bin/build-all.sh script.
- * Because they will be used to find firmware filenames in the android app for OTA updates.
- * To match the old style filenames, _ is converted to -, p is converted to .
- */
-enum HardwareModel {
- /*
- * TODO: REPLACE
- */
- UNSET = 0;
-
- /*
- * TODO: REPLACE
- */
- TLORA_V2 = 1;
-
- /*
- * TODO: REPLACE
- */
- TLORA_V1 = 2;
-
- /*
- * TODO: REPLACE
- */
- TLORA_V2_1_1P6 = 3;
-
- /*
- * TODO: REPLACE
- */
- TBEAM = 4;
-
- /*
- * The original heltec WiFi_Lora_32_V2, which had battery voltage sensing hooked to GPIO 13
- * (see HELTEC_V2 for the new version).
- */
- HELTEC_V2_0 = 5;
-
- /*
- * TODO: REPLACE
- */
- TBEAM_V0P7 = 6;
-
- /*
- * TODO: REPLACE
- */
- T_ECHO = 7;
-
- /*
- * TODO: REPLACE
- */
- TLORA_V1_1P3 = 8;
-
- /*
- * TODO: REPLACE
- */
- RAK4631 = 9;
-
- /*
- * The new version of the heltec WiFi_Lora_32_V2 board that has battery sensing hooked to GPIO 37.
- * Sadly they did not update anything on the silkscreen to identify this board
- */
- HELTEC_V2_1 = 10;
-
- /*
- * Ancient heltec WiFi_Lora_32 board
- */
- HELTEC_V1 = 11;
-
- /*
- * New T-BEAM with ESP32-S3 CPU
- */
- LILYGO_TBEAM_S3_CORE = 12;
-
- /*
- * RAK WisBlock ESP32 core: https://docs.rakwireless.com/Product-Categories/WisBlock/RAK11200/Overview/
- */
- RAK11200 = 13;
-
- /*
- * B&Q Consulting Nano Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:nano
- */
- NANO_G1 = 14;
-
- /*
- * TODO: REPLACE
- */
- TLORA_V2_1_1P8 = 15;
-
- /*
- * TODO: REPLACE
- */
- TLORA_T3_S3 = 16;
-
- /*
- * B&Q Consulting Nano G1 Explorer: https://wiki.uniteng.com/en/meshtastic/nano-g1-explorer
- */
- NANO_G1_EXPLORER = 17;
-
- /*
- * B&Q Consulting Nano G2 Ultra: https://wiki.uniteng.com/en/meshtastic/nano-g2-ultra
- */
- NANO_G2_ULTRA = 18;
-
- /*
- * LoRAType device: https://loratype.org/
- */
- LORA_TYPE = 19;
-
- /*
- * wiphone https://www.wiphone.io/
- */
- WIPHONE = 20;
-
- /*
- * WIO Tracker WM1110 family from Seeed Studio. Includes wio-1110-tracker and wio-1110-sdk
- */
- WIO_WM1110 = 21;
-
- /*
- * RAK2560 Solar base station based on RAK4630
- */
- RAK2560 = 22;
-
- /*
- * Heltec HRU-3601: https://heltec.org/project/hru-3601/
- */
- HELTEC_HRU_3601 = 23;
-
- /*
- * Heltec Wireless Bridge
- */
- HELTEC_WIRELESS_BRIDGE = 24;
-
- /*
- * B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
- */
- STATION_G1 = 25;
-
- /*
- * RAK11310 (RP2040 + SX1262)
- */
- RAK11310 = 26;
-
- /*
- * Makerfabs SenseLoRA Receiver (RP2040 + RFM96)
- */
- SENSELORA_RP2040 = 27;
-
- /*
- * Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96)
- */
- SENSELORA_S3 = 28;
-
- /*
- * Canary Radio Company - CanaryOne: https://canaryradio.io/products/canaryone
- */
- CANARYONE = 29;
-
- /*
- * Waveshare RP2040 LoRa - https://www.waveshare.com/rp2040-lora.htm
- */
- RP2040_LORA = 30;
-
- /*
- * B&Q Consulting Station G2: https://wiki.uniteng.com/en/meshtastic/station-g2
- */
- STATION_G2 = 31;
-
- /*
- * ---------------------------------------------------------------------------
- * Less common/prototype boards listed here (needs one more byte over the air)
- * ---------------------------------------------------------------------------
- */
- LORA_RELAY_V1 = 32;
-
- /*
- * TODO: REPLACE
- */
- NRF52840DK = 33;
-
- /*
- * TODO: REPLACE
- */
- PPR = 34;
-
- /*
- * TODO: REPLACE
- */
- GENIEBLOCKS = 35;
-
- /*
- * TODO: REPLACE
- */
- NRF52_UNKNOWN = 36;
-
- /*
- * TODO: REPLACE
- */
- PORTDUINO = 37;
-
- /*
- * The simulator built into the android app
- */
- ANDROID_SIM = 38;
-
- /*
- * Custom DIY device based on @NanoVHF schematics: https://github.com/NanoVHF/Meshtastic-DIY/tree/main/Schematics
- */
- DIY_V1 = 39;
-
- /*
- * nRF52840 Dongle : https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dongle/
- */
- NRF52840_PCA10059 = 40;
-
- /*
- * Custom Disaster Radio esp32 v3 device https://github.com/sudomesh/disaster-radio/tree/master/hardware/board_esp32_v3
- */
- DR_DEV = 41;
-
- /*
- * M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/
- */
- M5STACK = 42;
-
- /*
- * New Heltec LoRA32 with ESP32-S3 CPU
- */
- HELTEC_V3 = 43;
-
- /*
- * New Heltec Wireless Stick Lite with ESP32-S3 CPU
- */
- HELTEC_WSL_V3 = 44;
-
- /*
- * New BETAFPV ELRS Micro TX Module 2.4G with ESP32 CPU
- */
- BETAFPV_2400_TX = 45;
-
- /*
- * BetaFPV ExpressLRS "Nano" TX Module 900MHz with ESP32 CPU
- */
- BETAFPV_900_NANO_TX = 46;
-
- /*
- * Raspberry Pi Pico (W) with Waveshare SX1262 LoRa Node Module
- */
- RPI_PICO = 47;
-
- /*
- * Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT
- * Newer V1.1, version is written on the PCB near the display.
- */
- HELTEC_WIRELESS_TRACKER = 48;
-
- /*
- * Heltec Wireless Paper with ESP32-S3 CPU and E-Ink display
- */
- HELTEC_WIRELESS_PAPER = 49;
-
- /*
- * LilyGo T-Deck with ESP32-S3 CPU, Keyboard and IPS display
- */
- T_DECK = 50;
-
- /*
- * LilyGo T-Watch S3 with ESP32-S3 CPU and IPS display
- */
- T_WATCH_S3 = 51;
-
- /*
- * Bobricius Picomputer with ESP32-S3 CPU, Keyboard and IPS display
- */
- PICOMPUTER_S3 = 52;
-
- /*
- * Heltec HT-CT62 with ESP32-C3 CPU and SX1262 LoRa
- */
- HELTEC_HT62 = 53;
-
- /*
- * EBYTE SPI LoRa module and ESP32-S3
- */
- EBYTE_ESP32_S3 = 54;
-
- /*
- * Waveshare ESP32-S3-PICO with PICO LoRa HAT and 2.9inch e-Ink
- */
- ESP32_S3_PICO = 55;
-
- /*
- * CircuitMess Chatter 2 LLCC68 Lora Module and ESP32 Wroom
- * Lora module can be swapped out for a Heltec RA-62 which is "almost" pin compatible
- * with one cut and one jumper Meshtastic works
- */
- CHATTER_2 = 56;
-
- /*
- * Heltec Wireless Paper, With ESP32-S3 CPU and E-Ink display
- * Older "V1.0" Variant, has no "version sticker"
- * E-Ink model is DEPG0213BNS800
- * Tab on the screen protector is RED
- * Flex connector marking is FPC-7528B
- */
- HELTEC_WIRELESS_PAPER_V1_0 = 57;
-
- /*
- * Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT
- * Older "V1.0" Variant
- */
- HELTEC_WIRELESS_TRACKER_V1_0 = 58;
-
- /*
- * unPhone with ESP32-S3, TFT touchscreen, LSM6DS3TR-C accelerometer and gyroscope
- */
- UNPHONE = 59;
-
- /*
- * Teledatics TD-LORAC NRF52840 based M.2 LoRA module
- * Compatible with the TD-WRLS development board
- */
- TD_LORAC = 60;
-
- /*
- * CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3
- */
- CDEBYTE_EORA_S3 = 61;
-
- /*
- * TWC_MESH_V4
- * Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS
- */
- TWC_MESH_V4 = 62;
-
- /*
- * NRF52_PROMICRO_DIY
- * Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS
- */
- NRF52_PROMICRO_DIY = 63;
-
- /*
- * RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module
- * ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS
- */
- RADIOMASTER_900_BANDIT_NANO = 64;
-
- /*
- * Heltec Capsule Sensor V3 with ESP32-S3 CPU, Portable LoRa device that can replace GNSS modules or sensors
- */
- HELTEC_CAPSULE_SENSOR_V3 = 65;
-
- /*
- * Heltec Vision Master T190 with ESP32-S3 CPU, and a 1.90 inch TFT display
- */
- HELTEC_VISION_MASTER_T190 = 66;
-
- /*
- * Heltec Vision Master E213 with ESP32-S3 CPU, and a 2.13 inch E-Ink display
- */
- HELTEC_VISION_MASTER_E213 = 67;
-
- /*
- * Heltec Vision Master E290 with ESP32-S3 CPU, and a 2.9 inch E-Ink display
- */
- HELTEC_VISION_MASTER_E290 = 68;
-
- /*
- * Heltec Mesh Node T114 board with nRF52840 CPU, and a 1.14 inch TFT display, Ultimate low-power design,
- * specifically adapted for the Meshtatic project
- */
- HELTEC_MESH_NODE_T114 = 69;
-
- /*
- * Sensecap Indicator from Seeed Studio. ESP32-S3 device with TFT and RP2040 coprocessor
- */
- SENSECAP_INDICATOR = 70;
-
- /*
- * Seeed studio T1000-E tracker card. NRF52840 w/ LR1110 radio, GPS, button, buzzer, and sensors.
- */
- TRACKER_T1000_E = 71;
-
- /*
- * RAK3172 STM32WLE5 Module (https://store.rakwireless.com/products/wisduo-lpwan-module-rak3172)
- */
- RAK3172 = 72;
-
- /*
- * Seeed Studio Wio-E5 (either mini or Dev kit) using STM32WL chip.
- */
- WIO_E5 = 73;
-
- /*
- * RadioMaster 900 Bandit, https://www.radiomasterrc.com/products/bandit-expresslrs-rf-module
- * SSD1306 OLED and No GPS
- */
- RADIOMASTER_900_BANDIT = 74;
-
- /*
- * Minewsemi ME25LS01 (ME25LE01_V1.0). NRF52840 w/ LR1110 radio, buttons and leds and pins.
- */
- ME25LS01_4Y10TD = 75;
-
- /*
- * RP2040_FEATHER_RFM95
- * Adafruit Feather RP2040 with RFM95 LoRa Radio RFM95 with SX1272, SSD1306 OLED
- * https://www.adafruit.com/product/5714
- * https://www.adafruit.com/product/326
- * https://www.adafruit.com/product/938
- * ^^^ short A0 to switch to I2C address 0x3C
- *
- */
- RP2040_FEATHER_RFM95 = 76;
-
- /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ */
- M5STACK_COREBASIC = 77;
- M5STACK_CORE2 = 78;
-
- /* Pico2 with Waveshare Hat, same as Pico */
- RPI_PICO2 = 79;
-
- /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ */
- M5STACK_CORES3 = 80;
-
- /* Seeed XIAO S3 DK*/
- SEEED_XIAO_S3 = 81;
-
- /*
- * Nordic nRF52840+Semtech SX1262 LoRa BLE Combo Module. nRF52840+SX1262 MS24SF1
- */
- MS24SF1 = 82;
-
- /*
- * Lilygo TLora-C6 with the new ESP32-C6 MCU
- */
- TLORA_C6 = 83;
-
- /*
- * WisMesh Tap
- * RAK-4631 w/ TFT in injection modled case
- */
- WISMESH_TAP = 84;
-
- /*
- * Similar to PORTDUINO but used by Routastic devices, this is not any
- * particular device and does not run Meshtastic's code but supports
- * the same frame format.
- * Runs on linux, see https://github.com/Jorropo/routastic
- */
- ROUTASTIC = 85;
-
- /*
- * Mesh-Tab, esp32 based
- * https://github.com/valzzu/Mesh-Tab
- */
- MESH_TAB = 86;
-
- /*
- * ------------------------------------------------------------------------------------------------------------------------------------------
- * Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
- * ------------------------------------------------------------------------------------------------------------------------------------------
- */
- PRIVATE_HW = 255;
-}
-
-/*
- * Broadcast when a newly powered mesh node wants to find a node num it can use
- * Sent from the phone over bluetooth to set the user id for the owner of this node.
- * Also sent from nodes to each other when a new node signs on (so all clients can have this info)
- * The algorithm is as follows:
- * when a node starts up, it broadcasts their user and the normal flow is for all
- * other nodes to reply with their User as well (so the new node can build its nodedb)
- * If a node ever receives a User (not just the first broadcast) message where
- * the sender node number equals our node number, that indicates a collision has
- * occurred and the following steps should happen:
- * If the receiving node (that was already in the mesh)'s macaddr is LOWER than the
- * new User who just tried to sign in: it gets to keep its nodenum.
- * We send a broadcast message of OUR User (we use a broadcast so that the other node can
- * receive our message, considering we have the same id - it also serves to let
- * observers correct their nodedb) - this case is rare so it should be okay.
- * If any node receives a User where the macaddr is GTE than their local macaddr,
- * they have been vetoed and should pick a new random nodenum (filtering against
- * whatever it knows about the nodedb) and rebroadcast their User.
- * A few nodenums are reserved and will never be requested:
- * 0xff - broadcast
- * 0 through 3 - for future use
- */
-message User {
- /*
- * A globally unique ID string for this user.
- * In the case of Signal that would mean +16504442323, for the default macaddr derived id it would be !<8 hexidecimal bytes>.
- * Note: app developers are encouraged to also use the following standard
- * node IDs "^all" (for broadcast), "^local" (for the locally connected node)
- */
- string id = 1;
-
- /*
- * A full name for this user, i.e. "Kevin Hester"
- */
- string long_name = 2;
-
- /*
- * A VERY short name, ideally two characters.
- * Suitable for a tiny OLED screen
- */
- string short_name = 3;
-
- /*
- * Deprecated in Meshtastic 2.1.x
- * This is the addr of the radio.
- * Not populated by the phone, but added by the esp32 when broadcasting
- */
- bytes macaddr = 4 [deprecated = true];
-
- /*
- * TBEAM, HELTEC, etc...
- * Starting in 1.2.11 moved to hw_model enum in the NodeInfo object.
- * Apps will still need the string here for older builds
- * (so OTA update can find the right image), but if the enum is available it will be used instead.
- */
- HardwareModel hw_model = 5;
-
- /*
- * In some regions Ham radio operators have different bandwidth limitations than others.
- * If this user is a licensed operator, set this flag.
- * Also, "long_name" should be their licence number.
- */
- bool is_licensed = 6;
-
- /*
- * Indicates that the user's role in the mesh
- */
- Config.DeviceConfig.Role role = 7;
-
- /*
- * The public key of the user's device.
- * This is sent out to other nodes on the mesh to allow them to compute a shared secret key.
- */
- bytes public_key = 8;
-}
-
-/*
- * A message used in a traceroute
- */
-message RouteDiscovery {
- /*
- * The list of nodenums this packet has visited so far to the destination.
- */
- repeated fixed32 route = 1;
-
- /*
- * The list of SNRs (in dB, scaled by 4) in the route towards the destination.
- */
- repeated int32 snr_towards = 2;
-
- /*
- * The list of nodenums the packet has visited on the way back from the destination.
- */
- repeated fixed32 route_back = 3;
-
- /*
- * The list of SNRs (in dB, scaled by 4) in the route back from the destination.
- */
- repeated int32 snr_back = 4;
-}
-
-/*
- * A Routing control Data packet handled by the routing module
- */
-message Routing {
- /*
- * A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide
- * details on the type of failure).
- */
- enum Error {
- /*
- * This message is not a failure
- */
- NONE = 0;
-
- /*
- * Our node doesn't have a route to the requested destination anymore.
- */
- NO_ROUTE = 1;
-
- /*
- * We received a nak while trying to forward on your behalf
- */
- GOT_NAK = 2;
-
- /*
- * TODO: REPLACE
- */
- TIMEOUT = 3;
-
- /*
- * No suitable interface could be found for delivering this packet
- */
- NO_INTERFACE = 4;
-
- /*
- * We reached the max retransmission count (typically for naive flood routing)
- */
- MAX_RETRANSMIT = 5;
-
- /*
- * No suitable channel was found for sending this packet (i.e. was requested channel index disabled?)
- */
- NO_CHANNEL = 6;
-
- /*
- * The packet was too big for sending (exceeds interface MTU after encoding)
- */
- TOO_LARGE = 7;
-
- /*
- * The request had want_response set, the request reached the destination node, but no service on that node wants to send a response
- * (possibly due to bad channel permissions)
- */
- NO_RESPONSE = 8;
-
- /*
- * Cannot send currently because duty cycle regulations will be violated.
- */
- DUTY_CYCLE_LIMIT = 9;
-
- /*
- * The application layer service on the remote node received your request, but considered your request somehow invalid
- */
- BAD_REQUEST = 32;
-
- /*
- * The application layer service on the remote node received your request, but considered your request not authorized
- * (i.e you did not send the request on the required bound channel)
- */
- NOT_AUTHORIZED = 33;
-
- /*
- * The client specified a PKI transport, but the node was unable to send the packet using PKI (and did not send the message at all)
- */
- PKI_FAILED = 34;
-
- /*
- * The receiving node does not have a Public Key to decode with
- */
- PKI_UNKNOWN_PUBKEY = 35;
- }
-
- oneof variant {
- /*
- * A route request going from the requester
- */
- RouteDiscovery route_request = 1;
-
- /*
- * A route reply
- */
- RouteDiscovery route_reply = 2;
-
- /*
- * A failure in delivering a message (usually used for routing control messages, but might be provided
- * in addition to ack.fail_id to provide details on the type of failure).
- */
- Error error_reason = 3;
- }
-}
-
-/*
- * (Formerly called SubPacket)
- * The payload portion fo a packet, this is the actual bytes that are sent
- * inside a radio packet (because from/to are broken out by the comms library)
- */
-message Data {
- /*
- * Formerly named typ and of type Type
- */
- PortNum portnum = 1;
-
- /*
- * TODO: REPLACE
- */
- bytes payload = 2;
-
- /*
- * Not normally used, but for testing a sender can request that recipient
- * responds in kind (i.e. if it received a position, it should unicast back it's position).
- * Note: that if you set this on a broadcast you will receive many replies.
- */
- bool want_response = 3;
-
- /*
- * The address of the destination node.
- * This field is is filled in by the mesh radio device software, application
- * layer software should never need it.
- * RouteDiscovery messages _must_ populate this.
- * Other message types might need to if they are doing multihop routing.
- */
- fixed32 dest = 4;
-
- /*
- * The address of the original sender for this message.
- * This field should _only_ be populated for reliable multihop packets (to keep
- * packets small).
- */
- fixed32 source = 5;
-
- /*
- * Only used in routing or response messages.
- * Indicates the original message ID that this message is reporting failure on. (formerly called original_id)
- */
- fixed32 request_id = 6;
-
- /*
- * If set, this message is intened to be a reply to a previously sent message with the defined id.
- */
- fixed32 reply_id = 7;
-
- /*
- * Defaults to false. If true, then what is in the payload should be treated as an emoji like giving
- * a message a heart or poop emoji.
- */
- fixed32 emoji = 8;
-
- /*
- * Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT.
- */
- optional uint32 bitfield = 9;
-}
-
-/*
- * Waypoint message, used to share arbitrary locations across the mesh
- */
-message Waypoint {
- /*
- * Id of the waypoint
- */
- uint32 id = 1;
-
- /*
- * latitude_i
- */
- optional sfixed32 latitude_i = 2;
-
- /*
- * longitude_i
- */
- optional sfixed32 longitude_i = 3;
-
- /*
- * Time the waypoint is to expire (epoch)
- */
- uint32 expire = 4;
-
- /*
- * If greater than zero, treat the value as a nodenum only allowing them to update the waypoint.
- * If zero, the waypoint is open to be edited by any member of the mesh.
- */
- uint32 locked_to = 5;
-
- /*
- * Name of the waypoint - max 30 chars
- */
- string name = 6;
-
- /*
- * Description of the waypoint - max 100 chars
- */
- string description = 7;
-
- /*
- * Designator icon for the waypoint in the form of a unicode emoji
- */
- fixed32 icon = 8;
-}
-
-/*
- * This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server
- */
-message MqttClientProxyMessage {
- /*
- * The MQTT topic this message will be sent /received on
- */
- string topic = 1;
-
- /*
- * The actual service envelope payload or text for mqtt pub / sub
- */
- oneof payload_variant {
- /*
- * Bytes
- */
- bytes data = 2;
-
- /*
- * Text
- */
- string text = 3;
- }
-
- /*
- * Whether the message should be retained (or not)
- */
- bool retained = 4;
-}
-
-/*
- * A packet envelope sent/received over the mesh
- * only payload_variant is sent in the payload portion of the LORA packet.
- * The other fields are either not sent at all, or sent in the special 16 byte LORA header.
- */
-message MeshPacket {
- /*
- * The priority of this message for sending.
- * Higher priorities are sent first (when managing the transmit queue).
- * This field is never sent over the air, it is only used internally inside of a local device node.
- * API clients (either on the local node or connected directly to the node)
- * can set this parameter if necessary.
- * (values must be <= 127 to keep protobuf field to one byte in size.
- * Detailed background on this field:
- * I noticed a funny side effect of lora being so slow: Usually when making
- * a protocol there isn’t much need to use message priority to change the order
- * of transmission (because interfaces are fairly fast).
- * But for lora where packets can take a few seconds each, it is very important
- * to make sure that critical packets are sent ASAP.
- * In the case of meshtastic that means we want to send protocol acks as soon as possible
- * (to prevent unneeded retransmissions), we want routing messages to be sent next,
- * then messages marked as reliable and finally 'background' packets like periodic position updates.
- * So I bit the bullet and implemented a new (internal - not sent over the air)
- * field in MeshPacket called 'priority'.
- * And the transmission queue in the router object is now a priority queue.
- */
- enum Priority {
- /*
- * Treated as Priority.DEFAULT
- */
- UNSET = 0;
-
- /*
- * TODO: REPLACE
- */
- MIN = 1;
-
- /*
- * Background position updates are sent with very low priority -
- * if the link is super congested they might not go out at all
- */
- BACKGROUND = 10;
-
- /*
- * This priority is used for most messages that don't have a priority set
- */
- DEFAULT = 64;
-
- /*
- * If priority is unset but the message is marked as want_ack,
- * assume it is important and use a slightly higher priority
- */
- RELIABLE = 70;
-
- /*
- * If priority is unset but the packet is a response to a request, we want it to get there relatively quickly.
- * Furthermore, responses stop relaying packets directed to a node early.
- */
- RESPONSE = 80;
-
- /*
- * Higher priority for specific message types (portnums) to distinguish between other reliable packets.
- */
- HIGH = 100;
-
- /*
- * Ack/naks are sent with very high priority to ensure that retransmission
- * stops as soon as possible
- */
- ACK = 120;
-
- /*
- * TODO: REPLACE
- */
- MAX = 127;
- }
-
- /*
- * Identify if this is a delayed packet
- */
- enum Delayed {
- /*
- * If unset, the message is being sent in real time.
- */
- NO_DELAY = 0;
-
- /*
- * The message is delayed and was originally a broadcast
- */
- DELAYED_BROADCAST = 1;
-
- /*
- * The message is delayed and was originally a direct message
- */
- DELAYED_DIRECT = 2;
- }
-
- /*
- * The sending node number.
- * Note: Our crypto implementation uses this field as well.
- * See [crypto](/docs/overview/encryption) for details.
- */
- fixed32 from = 1;
-
- /*
- * The (immediate) destination for this packet
- */
- fixed32 to = 2;
-
- /*
- * (Usually) If set, this indicates the index in the secondary_channels table that this packet was sent/received on.
- * If unset, packet was on the primary channel.
- * A particular node might know only a subset of channels in use on the mesh.
- * Therefore channel_index is inherently a local concept and meaningless to send between nodes.
- * Very briefly, while sending and receiving deep inside the device Router code, this field instead
- * contains the 'channel hash' instead of the index.
- * This 'trick' is only used while the payload_variant is an 'encrypted'.
- */
- uint32 channel = 3;
-
- /*
- * Internally to the mesh radios we will route SubPackets encrypted per [this](docs/developers/firmware/encryption).
- * However, when a particular node has the correct
- * key to decode a particular packet, it will decode the payload into a SubPacket protobuf structure.
- * Software outside of the device nodes will never encounter a packet where
- * "decoded" is not populated (i.e. any encryption/decryption happens before reaching the applications)
- * The numeric IDs for these fields were selected to keep backwards compatibility with old applications.
- */
-
- oneof payload_variant {
- /*
- * TODO: REPLACE
- */
- Data decoded = 4;
-
- /*
- * TODO: REPLACE
- */
- bytes encrypted = 5;
- }
-
- /*
- * A unique ID for this packet.
- * Always 0 for no-ack packets or non broadcast packets (and therefore take zero bytes of space).
- * Otherwise a unique ID for this packet, useful for flooding algorithms.
- * ID only needs to be unique on a _per sender_ basis, and it only
- * needs to be unique for a few minutes (long enough to last for the length of
- * any ACK or the completion of a mesh broadcast flood).
- * Note: Our crypto implementation uses this id as well.
- * See [crypto](/docs/overview/encryption) for details.
- */
- fixed32 id = 6;
-
- /*
- * The time this message was received by the esp32 (secs since 1970).
- * Note: this field is _never_ sent on the radio link itself (to save space) Times
- * are typically not sent over the mesh, but they will be added to any Packet
- * (chain of SubPacket) sent to the phone (so the phone can know exact time of reception)
- */
- fixed32 rx_time = 7;
-
- /*
- * *Never* sent over the radio links.
- * Set during reception to indicate the SNR of this packet.
- * Used to collect statistics on current link quality.
- */
- float rx_snr = 8;
-
- /*
- * If unset treated as zero (no forwarding, send to adjacent nodes only)
- * if 1, allow hopping through one node, etc...
- * For our usecase real world topologies probably have a max of about 3.
- * This field is normally placed into a few of bits in the header.
- */
- uint32 hop_limit = 9;
-
- /*
- * This packet is being sent as a reliable message, we would prefer it to arrive at the destination.
- * We would like to receive a ack packet in response.
- * Broadcasts messages treat this flag specially: Since acks for broadcasts would
- * rapidly flood the channel, the normal ack behavior is suppressed.
- * Instead, the original sender listens to see if at least one node is rebroadcasting this packet (because naive flooding algorithm).
- * If it hears that the odds (given typical LoRa topologies) the odds are very high that every node should eventually receive the message.
- * So FloodingRouter.cpp generates an implicit ack which is delivered to the original sender.
- * If after some time we don't hear anyone rebroadcast our packet, we will timeout and retransmit, using the regular resend logic.
- * Note: This flag is normally sent in a flag bit in the header when sent over the wire
- */
- bool want_ack = 10;
-
- /*
- * The priority of this message for sending.
- * See MeshPacket.Priority description for more details.
- */
- Priority priority = 11;
-
- /*
- * rssi of received packet. Only sent to phone for dispay purposes.
- */
- int32 rx_rssi = 12;
-
- /*
- * Describe if this message is delayed
- */
- Delayed delayed = 13 [deprecated = true];
-
- /*
- * Describes whether this packet passed via MQTT somewhere along the path it currently took.
- */
- bool via_mqtt = 14;
-
- /*
- * Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header.
- * When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled.
- */
- uint32 hop_start = 15;
-
- /*
- * Records the public key the packet was encrypted with, if applicable.
- */
- bytes public_key = 16;
-
- /*
- * Indicates whether the packet was en/decrypted using PKI
- */
- bool pki_encrypted = 17;
-}
-
-/*
- * Shared constants between device and phone
- */
-enum Constants {
- /*
- * First enum must be zero, and we are just using this enum to
- * pass int constants between two very different environments
- */
- ZERO = 0;
-
- /*
- * From mesh.options
- * note: this payload length is ONLY the bytes that are sent inside of the Data protobuf (excluding protobuf overhead). The 16 byte header is
- * outside of this envelope
- */
- DATA_PAYLOAD_LEN = 237;
-}
-
-/*
- * The bluetooth to device link:
- * Old BTLE protocol docs from TODO, merge in above and make real docs...
- * use protocol buffers, and NanoPB
- * messages from device to phone:
- * POSITION_UPDATE (..., time)
- * TEXT_RECEIVED(from, text, time)
- * OPAQUE_RECEIVED(from, payload, time) (for signal messages or other applications)
- * messages from phone to device:
- * SET_MYID(id, human readable long, human readable short) (send down the unique ID
- * string used for this node, a human readable string shown for that id, and a very
- * short human readable string suitable for oled screen) SEND_OPAQUE(dest, payload)
- * (for signal messages or other applications) SEND_TEXT(dest, text) Get all
- * nodes() (returns list of nodes, with full info, last time seen, loc, battery
- * level etc) SET_CONFIG (switches device to a new set of radio params and
- * preshared key, drops all existing nodes, force our node to rejoin this new group)
- * Full information about a node on the mesh
- */
-message NodeInfo {
- /*
- * The node number
- */
- uint32 num = 1;
-
- /*
- * The user info for this node
- */
- User user = 2;
-
- /*
- * This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true.
- * Position.time now indicates the last time we received a POSITION from that node.
- */
- Position position = 3;
-
- /*
- * Returns the Signal-to-noise ratio (SNR) of the last received message,
- * as measured by the receiver. Return SNR of the last received message in dB
- */
- float snr = 4;
-
- /*
- * TODO: REMOVE/INTEGRATE
- * Returns the last measured frequency error.
- * The LoRa receiver estimates the frequency offset between the receiver
- * center frequency and that of the received LoRa signal. This function
- * returns the estimates offset (in Hz) of the last received message.
- * Caution: this measurement is not absolute, but is measured relative to the
- * local receiver's oscillator. Apparent errors may be due to the
- * transmitter, the receiver or both. \return The estimated center frequency
- * offset in Hz of the last received message.
- * int32 frequency_error = 6;
- * enum RouteState {
- * Invalid = 0;
- * Discovering = 1;
- * Valid = 2;
- * }
- * Not needed?
- * RouteState route = 4;
- */
-
- /*
- * TODO: REMOVE/INTEGRATE
- * Not currently used (till full DSR deployment?) Our current preferred node node for routing - might be the same as num if
- * we are adjacent Or zero if we don't yet know a route to this node.
- * fixed32 next_hop = 5;
- */
-
- /*
- * Set to indicate the last time we received a packet from this node
- */
- fixed32 last_heard = 5;
- /*
- * The latest device metrics for the node.
- */
- DeviceMetrics device_metrics = 6;
-
- /*
- * local channel index we heard that node on. Only populated if its not the default channel.
- */
- uint32 channel = 7;
-
- /*
- * True if we witnessed the node over MQTT instead of LoRA transport
- */
- bool via_mqtt = 8;
-
- /*
- * Number of hops away from us this node is (0 if adjacent)
- */
- uint32 hops_away = 9;
-
- /*
- * True if node is in our favorites list
- * Persists between NodeDB internal clean ups
- */
- bool is_favorite = 10;
-}
-
-/*
- * Error codes for critical errors
- * The device might report these fault codes on the screen.
- * If you encounter a fault code, please post on the meshtastic.discourse.group
- * and we'll try to help.
- */
-enum CriticalErrorCode {
- /*
- * TODO: REPLACE
- */
- NONE = 0;
-
- /*
- * A software bug was detected while trying to send lora
- */
- TX_WATCHDOG = 1;
-
- /*
- * A software bug was detected on entry to sleep
- */
- SLEEP_ENTER_WAIT = 2;
-
- /*
- * No Lora radio hardware could be found
- */
- NO_RADIO = 3;
-
- /*
- * Not normally used
- */
- UNSPECIFIED = 4;
-
- /*
- * We failed while configuring a UBlox GPS
- */
- UBLOX_UNIT_FAILED = 5;
-
- /*
- * This board was expected to have a power management chip and it is missing or broken
- */
- NO_AXP192 = 6;
-
- /*
- * The channel tried to set a radio setting which is not supported by this chipset,
- * radio comms settings are now undefined.
- */
- INVALID_RADIO_SETTING = 7;
-
- /*
- * Radio transmit hardware failure. We sent data to the radio chip, but it didn't
- * reply with an interrupt.
- */
- TRANSMIT_FAILED = 8;
-
- /*
- * We detected that the main CPU voltage dropped below the minimum acceptable value
- */
- BROWNOUT = 9;
-
- /* Selftest of SX1262 radio chip failed */
- SX1262_FAILURE = 10;
-
- /*
- * A (likely software but possibly hardware) failure was detected while trying to send packets.
- * If this occurs on your board, please post in the forum so that we can ask you to collect some information to allow fixing this bug
- */
- RADIO_SPI_BUG = 11;
-
- /*
- * Corruption was detected on the flash filesystem but we were able to repair things.
- * If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field.
- */
- FLASH_CORRUPTION_RECOVERABLE = 12;
-
- /*
- * Corruption was detected on the flash filesystem but we were unable to repair things.
- * NOTE: Your node will probably need to be reconfigured the next time it reboots (it will lose the region code etc...)
- * If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field.
- */
- FLASH_CORRUPTION_UNRECOVERABLE = 13;
-}
-
-/*
- * Unique local debugging info for this node
- * Note: we don't include position or the user info, because that will come in the
- * Sent to the phone in response to WantNodes.
- */
-message MyNodeInfo {
- /*
- * Tells the phone what our node number is, default starting value is
- * lowbyte of macaddr, but it will be fixed if that is already in use
- */
- uint32 my_node_num = 1;
-
- /*
- * The total number of reboots this node has ever encountered
- * (well - since the last time we discarded preferences)
- */
- uint32 reboot_count = 8;
-
- /*
- * The minimum app version that can talk to this device.
- * Phone/PC apps should compare this to their build number and if too low tell the user they must update their app
- */
- uint32 min_app_version = 11;
-}
-
-/*
- * Debug output from the device.
- * To minimize the size of records inside the device code, if a time/source/level is not set
- * on the message it is assumed to be a continuation of the previously sent message.
- * This allows the device code to use fixed maxlen 64 byte strings for messages,
- * and then extend as needed by emitting multiple records.
- */
-message LogRecord {
- /*
- * Log levels, chosen to match python logging conventions.
- */
- enum Level {
- /*
- * Log levels, chosen to match python logging conventions.
- */
- UNSET = 0;
-
- /*
- * Log levels, chosen to match python logging conventions.
- */
- CRITICAL = 50;
-
- /*
- * Log levels, chosen to match python logging conventions.
- */
- ERROR = 40;
-
- /*
- * Log levels, chosen to match python logging conventions.
- */
- WARNING = 30;
-
- /*
- * Log levels, chosen to match python logging conventions.
- */
- INFO = 20;
-
- /*
- * Log levels, chosen to match python logging conventions.
- */
- DEBUG = 10;
-
- /*
- * Log levels, chosen to match python logging conventions.
- */
- TRACE = 5;
- }
-
- /*
- * Log levels, chosen to match python logging conventions.
- */
- string message = 1;
-
- /*
- * Seconds since 1970 - or 0 for unknown/unset
- */
- fixed32 time = 2;
-
- /*
- * Usually based on thread name - if known
- */
- string source = 3;
-
- /*
- * Not yet set
- */
- Level level = 4;
-}
-
-message QueueStatus {
- /* Last attempt to queue status, ErrorCode */
- int32 res = 1;
-
- /* Free entries in the outgoing queue */
- uint32 free = 2;
-
- /* Maximum entries in the outgoing queue */
- uint32 maxlen = 3;
-
- /* What was mesh packet id that generated this response? */
- uint32 mesh_packet_id = 4;
-}
-
-/*
- * Packets from the radio to the phone will appear on the fromRadio characteristic.
- * It will support READ and NOTIFY. When a new packet arrives the device will BLE notify?
- * It will sit in that descriptor until consumed by the phone,
- * at which point the next item in the FIFO will be populated.
- */
-message FromRadio {
- /*
- * The packet id, used to allow the phone to request missing read packets from the FIFO,
- * see our bluetooth docs
- */
- uint32 id = 1;
-
- /*
- * Log levels, chosen to match python logging conventions.
- */
- oneof payload_variant {
- /*
- * Log levels, chosen to match python logging conventions.
- */
- MeshPacket packet = 2;
-
- /*
- * Tells the phone what our node number is, can be -1 if we've not yet joined a mesh.
- * NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps.
- */
- MyNodeInfo my_info = 3;
-
- /*
- * One packet is sent for each node in the on radio DB
- * starts over with the first node in our DB
- */
- NodeInfo node_info = 4;
-
- /*
- * Include a part of the config (was: RadioConfig radio)
- */
- Config config = 5;
-
- /*
- * Set to send debug console output over our protobuf stream
- */
- LogRecord log_record = 6;
-
- /*
- * Sent as true once the device has finished sending all of the responses to want_config
- * recipient should check if this ID matches our original request nonce, if
- * not, it means your config responses haven't started yet.
- * NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps.
- */
- uint32 config_complete_id = 7;
-
- /*
- * Sent to tell clients the radio has just rebooted.
- * Set to true if present.
- * Not used on all transports, currently just used for the serial console.
- * NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps.
- */
- bool rebooted = 8;
-
- /*
- * Include module config
- */
- ModuleConfig moduleConfig = 9;
-
- /*
- * One packet is sent for each channel
- */
- Channel channel = 10;
-
- /*
- * Queue status info
- */
- QueueStatus queueStatus = 11;
-
- /*
- * File Transfer Chunk
- */
- XModem xmodemPacket = 12;
-
- /*
- * Device metadata message
- */
- DeviceMetadata metadata = 13;
-
- /*
- * MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT)
- */
- MqttClientProxyMessage mqttClientProxyMessage = 14;
-
- /*
- * File system manifest messages
- */
- FileInfo fileInfo = 15;
-
- /*
- * Notification message to the client
- */
- ClientNotification clientNotification = 16;
- }
-}
-
-/*
- * A notification message from the device to the client
- * To be used for important messages that should to be displayed to the user
- * in the form of push notifications or validation messages when saving
- * invalid configuration.
- */
-message ClientNotification {
- /*
- * The id of the packet we're notifying in response to
- */
- optional uint32 reply_id = 1;
-
- /*
- * Seconds since 1970 - or 0 for unknown/unset
- */
- fixed32 time = 2;
-
- /*
- * The level type of notification
- */
- LogRecord.Level level = 3;
- /*
- * The message body of the notification
- */
- string message = 4;
-}
-
-/*
- * Individual File info for the device
- */
-message FileInfo {
- /*
- * The fully qualified path of the file
- */
- string file_name = 1;
-
- /*
- * The size of the file in bytes
- */
- uint32 size_bytes = 2;
-}
-
-/*
- * Packets/commands to the radio will be written (reliably) to the toRadio characteristic.
- * Once the write completes the phone can assume it is handled.
- */
-message ToRadio {
- /*
- * Log levels, chosen to match python logging conventions.
- */
- oneof payload_variant {
- /*
- * Send this packet on the mesh
- */
- MeshPacket packet = 1;
-
- /*
- * Phone wants radio to send full node db to the phone, This is
- * typically the first packet sent to the radio when the phone gets a
- * bluetooth connection. The radio will respond by sending back a
- * MyNodeInfo, a owner, a radio config and a series of
- * FromRadio.node_infos, and config_complete
- * the integer you write into this field will be reported back in the
- * config_complete_id response this allows clients to never be confused by
- * a stale old partially sent config.
- */
- uint32 want_config_id = 3;
-
- /*
- * Tell API server we are disconnecting now.
- * This is useful for serial links where there is no hardware/protocol based notification that the client has dropped the link.
- * (Sending this message is optional for clients)
- */
- bool disconnect = 4;
-
- /*
- * File Transfer Chunk
- */
-
- XModem xmodemPacket = 5;
-
- /*
- * MQTT Client Proxy Message (for client / phone subscribed to MQTT sending to device)
- */
- MqttClientProxyMessage mqttClientProxyMessage = 6;
-
- /*
- * Heartbeat message (used to keep the device connection awake on serial)
- */
- Heartbeat heartbeat = 7;
- }
-}
-
-/*
- * Compressed message payload
- */
-message Compressed {
- /*
- * PortNum to determine the how to handle the compressed payload.
- */
- PortNum portnum = 1;
-
- /*
- * Compressed data.
- */
- bytes data = 2;
-}
-
-/*
- * Full info on edges for a single node
- */
-message NeighborInfo {
- /*
- * The node ID of the node sending info on its neighbors
- */
- uint32 node_id = 1;
- /*
- * Field to pass neighbor info for the next sending cycle
- */
- uint32 last_sent_by_id = 2;
-
- /*
- * Broadcast interval of the represented node (in seconds)
- */
- uint32 node_broadcast_interval_secs = 3;
- /*
- * The list of out edges from this node
- */
- repeated Neighbor neighbors = 4;
-}
-
-/*
- * A single edge in the mesh
- */
-message Neighbor {
- /*
- * Node ID of neighbor
- */
- uint32 node_id = 1;
-
- /*
- * SNR of last heard message
- */
- float snr = 2;
-
- /*
- * Reception time (in secs since 1970) of last message that was last sent by this ID.
- * Note: this is for local storage only and will not be sent out over the mesh.
- */
- fixed32 last_rx_time = 3;
-
- /*
- * Broadcast interval of this neighbor (in seconds).
- * Note: this is for local storage only and will not be sent out over the mesh.
- */
- uint32 node_broadcast_interval_secs = 4;
-}
-
-/*
- * Device metadata response
- */
-message DeviceMetadata {
- /*
- * Device firmware version string
- */
- string firmware_version = 1;
-
- /*
- * Device state version
- */
- uint32 device_state_version = 2;
-
- /*
- * Indicates whether the device can shutdown CPU natively or via power management chip
- */
- bool canShutdown = 3;
-
- /*
- * Indicates that the device has native wifi capability
- */
- bool hasWifi = 4;
-
- /*
- * Indicates that the device has native bluetooth capability
- */
- bool hasBluetooth = 5;
-
- /*
- * Indicates that the device has an ethernet peripheral
- */
- bool hasEthernet = 6;
-
- /*
- * Indicates that the device's role in the mesh
- */
- Config.DeviceConfig.Role role = 7;
-
- /*
- * Indicates the device's current enabled position flags
- */
- uint32 position_flags = 8;
-
- /*
- * Device hardware model
- */
- HardwareModel hw_model = 9;
-
- /*
- * Has Remote Hardware enabled
- */
- bool hasRemoteHardware = 10;
-}
-
-/*
- * A heartbeat message is sent to the node from the client to keep the connection alive.
- * This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI.
- */
-message Heartbeat {}
-
-/*
- * RemoteHardwarePins associated with a node
- */
-message NodeRemoteHardwarePin {
- /*
- * The node_num exposing the available gpio pin
- */
- uint32 node_num = 1;
-
- /*
- * The the available gpio pin for usage with RemoteHardware module
- */
- RemoteHardwarePin pin = 2;
-}
-
-message ChunkedPayload {
- /*
- * The ID of the entire payload
- */
- uint32 payload_id = 1;
-
- /*
- * The total number of chunks in the payload
- */
- uint32 chunk_count = 2;
-
- /*
- * The current chunk index in the total
- */
- uint32 chunk_index = 3;
-
- /*
- * The binary data of the current chunk
- */
- bytes payload_chunk = 4;
-}
-
-/*
- * Wrapper message for broken repeated oneof support
- */
-message resend_chunks {
- repeated uint32 chunks = 1;
-}
-
-/*
- * Responses to a ChunkedPayload request
- */
-message ChunkedPayloadResponse {
- /*
- * The ID of the entire payload
- */
- uint32 payload_id = 1;
-
- oneof payload_variant {
- /*
- * Request to transfer chunked payload
- */
- bool request_transfer = 2;
-
- /*
- * Accept the transfer chunked payload
- */
- bool accept_transfer = 3;
- /*
- * Request missing indexes in the chunked payload
- */
- resend_chunks resend_chunks = 4;
- }
-}
\ No newline at end of file
diff --git a/src/protos/meshtastic/module_config.options b/src/protos/meshtastic/module_config.options
deleted file mode 100644
index fdc46d5..0000000
--- a/src/protos/meshtastic/module_config.options
+++ /dev/null
@@ -1,28 +0,0 @@
-*CannedMessageConfig.allow_input_source max_size:16
-
-*MQTTConfig.address max_size:64
-*MQTTConfig.username max_size:64
-*MQTTConfig.password max_size:64
-*MQTTConfig.root max_size:16
-
-*AudioConfig.ptt_pin int_size:8
-*AudioConfig.i2s_ws int_size:8
-*AudioConfig.i2s_sd int_size:8
-*AudioConfig.i2s_din int_size:8
-*AudioConfig.i2s_sck int_size:8
-
-*ExternalNotificationConfig.output_vibra int_size:8
-*ExternalNotificationConfig.output_buzzer int_size:8
-*ExternalNotificationConfig.nag_timeout int_size:16
-
-*RemoteHardwareConfig.available_pins max_count:4
-*RemoteHardwarePin.name max_size:15
-*RemoteHardwarePin.gpio_pin int_size:8
-
-*AmbientLightingConfig.current int_size:8
-*AmbientLightingConfig.red int_size:8
-*AmbientLightingConfig.green int_size:8
-*AmbientLightingConfig.blue int_size:8
-
-*DetectionSensorConfig.monitor_pin int_size:8
-*DetectionSensorConfig.name max_size:20
\ No newline at end of file
diff --git a/src/protos/meshtastic/module_config.proto b/src/protos/meshtastic/module_config.proto
deleted file mode 100644
index f2c2805..0000000
--- a/src/protos/meshtastic/module_config.proto
+++ /dev/null
@@ -1,801 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "ModuleConfigProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * Module Config
- */
-message ModuleConfig {
- /*
- * MQTT Client Config
- */
- message MQTTConfig {
- /*
- * If a meshtastic node is able to reach the internet it will normally attempt to gateway any channels that are marked as
- * is_uplink_enabled or is_downlink_enabled.
- */
- bool enabled = 1;
-
- /*
- * The server to use for our MQTT global message gateway feature.
- * If not set, the default server will be used
- */
- string address = 2;
-
- /*
- * MQTT username to use (most useful for a custom MQTT server).
- * If using a custom server, this will be honoured even if empty.
- * If using the default server, this will only be honoured if set, otherwise the device will use the default username
- */
- string username = 3;
-
- /*
- * MQTT password to use (most useful for a custom MQTT server).
- * If using a custom server, this will be honoured even if empty.
- * If using the default server, this will only be honoured if set, otherwise the device will use the default password
- */
- string password = 4;
-
- /*
- * Whether to send encrypted or decrypted packets to MQTT.
- * This parameter is only honoured if you also set server
- * (the default official mqtt.meshtastic.org server can handle encrypted packets)
- * Decrypted packets may be useful for external systems that want to consume meshtastic packets
- */
- bool encryption_enabled = 5;
-
- /*
- * Whether to send / consume json packets on MQTT
- */
- bool json_enabled = 6;
-
- /*
- * If true, we attempt to establish a secure connection using TLS
- */
- bool tls_enabled = 7;
-
- /*
- * The root topic to use for MQTT messages. Default is "msh".
- * This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs
- */
- string root = 8;
-
- /*
- * If true, we can use the connected phone / client to proxy messages to MQTT instead of a direct connection
- */
- bool proxy_to_client_enabled = 9;
-
- /*
- * If true, we will periodically report unencrypted information about our node to a map via MQTT
- */
- bool map_reporting_enabled = 10;
-
- /*
- * Settings for reporting information about our node to a map via MQTT
- */
- MapReportSettings map_report_settings = 11;
- }
-
- /*
- * Settings for reporting unencrypted information about our node to a map via MQTT
- */
- message MapReportSettings {
- /*
- * How often we should report our info to the map (in seconds)
- */
- uint32 publish_interval_secs = 1;
-
- /*
- * Bits of precision for the location sent (default of 32 is full precision).
- */
- uint32 position_precision = 2;
- }
-
- /*
- * RemoteHardwareModule Config
- */
- message RemoteHardwareConfig {
- /*
- * Whether the Module is enabled
- */
- bool enabled = 1;
-
- /*
- * Whether the Module allows consumers to read / write to pins not defined in available_pins
- */
- bool allow_undefined_pin_access = 2;
-
- /*
- * Exposes the available pins to the mesh for reading and writing
- */
- repeated RemoteHardwarePin available_pins = 3;
- }
-
- /*
- * NeighborInfoModule Config
- */
- message NeighborInfoConfig {
- /*
- * Whether the Module is enabled
- */
- bool enabled = 1;
-
- /*
- * Interval in seconds of how often we should try to send our
- * Neighbor Info to the mesh
- */
- uint32 update_interval = 2;
- }
-
- /*
- * Detection Sensor Module Config
- */
- message DetectionSensorConfig {
- /*
- * Whether the Module is enabled
- */
- bool enabled = 1;
-
- /*
- * Interval in seconds of how often we can send a message to the mesh when a state change is detected
- */
- uint32 minimum_broadcast_secs = 2;
-
- /*
- * Interval in seconds of how often we should send a message to the mesh with the current state regardless of changes
- * When set to 0, only state changes will be broadcasted
- * Works as a sort of status heartbeat for peace of mind
- */
- uint32 state_broadcast_secs = 3;
- /*
- * Send ASCII bell with alert message
- * Useful for triggering ext. notification on bell
- */
- bool send_bell = 4;
-
- /*
- * Friendly name used to format message sent to mesh
- * Example: A name "Motion" would result in a message "Motion detected"
- * Maximum length of 20 characters
- */
- string name = 5;
-
- /*
- * GPIO pin to monitor for state changes
- */
- uint32 monitor_pin = 6;
-
- /*
- * Whether or not the GPIO pin state detection is triggered on HIGH (1)
- * Otherwise LOW (0)
- */
- bool detection_triggered_high = 7;
-
- /*
- * Whether or not use INPUT_PULLUP mode for GPIO pin
- * Only applicable if the board uses pull-up resistors on the pin
- */
- bool use_pullup = 8;
- }
-
- /*
- * Audio Config for codec2 voice
- */
- message AudioConfig {
- /*
- * Baudrate for codec2 voice
- */
- enum Audio_Baud {
- CODEC2_DEFAULT = 0;
- CODEC2_3200 = 1;
- CODEC2_2400 = 2;
- CODEC2_1600 = 3;
- CODEC2_1400 = 4;
- CODEC2_1300 = 5;
- CODEC2_1200 = 6;
- CODEC2_700 = 7;
- CODEC2_700B = 8;
- }
-
- /*
- * Whether Audio is enabled
- */
- bool codec2_enabled = 1;
-
- /*
- * PTT Pin
- */
- uint32 ptt_pin = 2;
-
- /*
- * The audio sample rate to use for codec2
- */
- Audio_Baud bitrate = 3;
-
- /*
- * I2S Word Select
- */
- uint32 i2s_ws = 4;
-
- /*
- * I2S Data IN
- */
- uint32 i2s_sd = 5;
-
- /*
- * I2S Data OUT
- */
- uint32 i2s_din = 6;
-
- /*
- * I2S Clock
- */
- uint32 i2s_sck = 7;
- }
-
- /*
- * Config for the Paxcounter Module
- */
- message PaxcounterConfig {
- /*
- * Enable the Paxcounter Module
- */
- bool enabled = 1;
-
- /*
- * Interval in seconds of how often we should try to send our
- * metrics to the mesh
- */
-
- uint32 paxcounter_update_interval = 2;
- }
-
- /*
- * Serial Config
- */
- message SerialConfig {
- /*
- * TODO: REPLACE
- */
- enum Serial_Baud {
- BAUD_DEFAULT = 0;
- BAUD_110 = 1;
- BAUD_300 = 2;
- BAUD_600 = 3;
- BAUD_1200 = 4;
- BAUD_2400 = 5;
- BAUD_4800 = 6;
- BAUD_9600 = 7;
- BAUD_19200 = 8;
- BAUD_38400 = 9;
- BAUD_57600 = 10;
- BAUD_115200 = 11;
- BAUD_230400 = 12;
- BAUD_460800 = 13;
- BAUD_576000 = 14;
- BAUD_921600 = 15;
- }
-
- /*
- * TODO: REPLACE
- */
- enum Serial_Mode {
- DEFAULT = 0;
- SIMPLE = 1;
- PROTO = 2;
- TEXTMSG = 3;
- NMEA = 4;
- // NMEA messages specifically tailored for CalTopo
- CALTOPO = 5;
- }
-
- /*
- * Preferences for the SerialModule
- */
- bool enabled = 1;
-
- /*
- * TODO: REPLACE
- */
- bool echo = 2;
-
- /*
- * RX pin (should match Arduino gpio pin number)
- */
- uint32 rxd = 3;
-
- /*
- * TX pin (should match Arduino gpio pin number)
- */
- uint32 txd = 4;
-
- /*
- * Serial baud rate
- */
- Serial_Baud baud = 5;
-
- /*
- * TODO: REPLACE
- */
- uint32 timeout = 6;
-
- /*
- * Mode for serial module operation
- */
- Serial_Mode mode = 7;
-
- /*
- * Overrides the platform's defacto Serial port instance to use with Serial module config settings
- * This is currently only usable in output modes like NMEA / CalTopo and may behave strangely or not work at all in other modes
- * Existing logging over the Serial Console will still be present
- */
- bool override_console_serial_port = 8;
- }
-
- /*
- * External Notifications Config
- */
- message ExternalNotificationConfig {
- /*
- * Enable the ExternalNotificationModule
- */
- bool enabled = 1;
-
- /*
- * When using in On/Off mode, keep the output on for this many
- * milliseconds. Default 1000ms (1 second).
- */
- uint32 output_ms = 2;
-
- /*
- * Define the output pin GPIO setting Defaults to
- * EXT_NOTIFY_OUT if set for the board.
- * In standalone devices this pin should drive the LED to match the UI.
- */
- uint32 output = 3;
-
- /*
- * Optional: Define a secondary output pin for a vibra motor
- * This is used in standalone devices to match the UI.
- */
- uint32 output_vibra = 8;
-
- /*
- * Optional: Define a tertiary output pin for an active buzzer
- * This is used in standalone devices to to match the UI.
- */
- uint32 output_buzzer = 9;
-
- /*
- * IF this is true, the 'output' Pin will be pulled active high, false
- * means active low.
- */
- bool active = 4;
-
- /*
- * True: Alert when a text message arrives (output)
- */
- bool alert_message = 5;
-
- /*
- * True: Alert when a text message arrives (output_vibra)
- */
- bool alert_message_vibra = 10;
-
- /*
- * True: Alert when a text message arrives (output_buzzer)
- */
- bool alert_message_buzzer = 11;
-
- /*
- * True: Alert when the bell character is received (output)
- */
- bool alert_bell = 6;
-
- /*
- * True: Alert when the bell character is received (output_vibra)
- */
- bool alert_bell_vibra = 12;
-
- /*
- * True: Alert when the bell character is received (output_buzzer)
- */
- bool alert_bell_buzzer = 13;
-
- /*
- * use a PWM output instead of a simple on/off output. This will ignore
- * the 'output', 'output_ms' and 'active' settings and use the
- * device.buzzer_gpio instead.
- */
- bool use_pwm = 7;
-
- /*
- * The notification will toggle with 'output_ms' for this time of seconds.
- * Default is 0 which means don't repeat at all. 60 would mean blink
- * and/or beep for 60 seconds
- */
- uint32 nag_timeout = 14;
-
- /*
- * When true, enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer
- * T-Watch S3 and T-Deck for example have this capability
- */
- bool use_i2s_as_buzzer = 15;
- }
-
- /*
- * Store and Forward Module Config
- */
- message StoreForwardConfig {
- /*
- * Enable the Store and Forward Module
- */
- bool enabled = 1;
-
- /*
- * TODO: REPLACE
- */
- bool heartbeat = 2;
-
- /*
- * TODO: REPLACE
- */
- uint32 records = 3;
-
- /*
- * TODO: REPLACE
- */
- uint32 history_return_max = 4;
-
- /*
- * TODO: REPLACE
- */
- uint32 history_return_window = 5;
- }
-
- /*
- * Preferences for the RangeTestModule
- */
- message RangeTestConfig {
- /*
- * Enable the Range Test Module
- */
- bool enabled = 1;
-
- /*
- * Send out range test messages from this node
- */
- uint32 sender = 2;
-
- /*
- * Bool value indicating that this node should save a RangeTest.csv file.
- * ESP32 Only
- */
- bool save = 3;
- }
-
- /*
- * Configuration for both device and environment metrics
- */
- message TelemetryConfig {
- /*
- * Interval in seconds of how often we should try to send our
- * device metrics to the mesh
- */
- uint32 device_update_interval = 1;
-
- /*
- * Interval in seconds of how often we should try to send our
- * environment measurements to the mesh
- */
-
- uint32 environment_update_interval = 2;
-
- /*
- * Preferences for the Telemetry Module (Environment)
- * Enable/Disable the telemetry measurement module measurement collection
- */
- bool environment_measurement_enabled = 3;
-
- /*
- * Enable/Disable the telemetry measurement module on-device display
- */
- bool environment_screen_enabled = 4;
-
- /*
- * We'll always read the sensor in Celsius, but sometimes we might want to
- * display the results in Fahrenheit as a "user preference".
- */
- bool environment_display_fahrenheit = 5;
-
- /*
- * Enable/Disable the air quality metrics
- */
- bool air_quality_enabled = 6;
-
- /*
- * Interval in seconds of how often we should try to send our
- * air quality metrics to the mesh
- */
- uint32 air_quality_interval = 7;
-
- /*
- * Interval in seconds of how often we should try to send our
- * air quality metrics to the mesh
- */
- bool power_measurement_enabled = 8;
-
- /*
- * Interval in seconds of how often we should try to send our
- * air quality metrics to the mesh
- */
- uint32 power_update_interval = 9;
-
- /*
- * Interval in seconds of how often we should try to send our
- * air quality metrics to the mesh
- */
- bool power_screen_enabled = 10;
-
-
-
-
-
-
-
-
-
- }
-
- /*
- * TODO: REPLACE
- */
- message CannedMessageConfig {
- /*
- * TODO: REPLACE
- */
- enum InputEventChar {
- /*
- * TODO: REPLACE
- */
- NONE = 0;
-
- /*
- * TODO: REPLACE
- */
- UP = 17;
-
- /*
- * TODO: REPLACE
- */
- DOWN = 18;
-
- /*
- * TODO: REPLACE
- */
- LEFT = 19;
-
- /*
- * TODO: REPLACE
- */
- RIGHT = 20;
-
- /*
- * '\n'
- */
- SELECT = 10;
-
- /*
- * TODO: REPLACE
- */
- BACK = 27;
-
- /*
- * TODO: REPLACE
- */
- CANCEL = 24;
- }
-
- /*
- * Enable the rotary encoder #1. This is a 'dumb' encoder sending pulses on both A and B pins while rotating.
- */
- bool rotary1_enabled = 1;
-
- /*
- * GPIO pin for rotary encoder A port.
- */
- uint32 inputbroker_pin_a = 2;
-
- /*
- * GPIO pin for rotary encoder B port.
- */
- uint32 inputbroker_pin_b = 3;
-
- /*
- * GPIO pin for rotary encoder Press port.
- */
- uint32 inputbroker_pin_press = 4;
-
- /*
- * Generate input event on CW of this kind.
- */
- InputEventChar inputbroker_event_cw = 5;
-
- /*
- * Generate input event on CCW of this kind.
- */
- InputEventChar inputbroker_event_ccw = 6;
-
- /*
- * Generate input event on Press of this kind.
- */
- InputEventChar inputbroker_event_press = 7;
-
- /*
- * Enable the Up/Down/Select input device. Can be RAK rotary encoder or 3 buttons. Uses the a/b/press definitions from inputbroker.
- */
- bool updown1_enabled = 8;
-
- /*
- * Enable/disable CannedMessageModule.
- */
- bool enabled = 9;
-
- /*
- * Input event origin accepted by the canned message module.
- * Can be e.g. "rotEnc1", "upDownEnc1" or keyword "_any"
- */
- string allow_input_source = 10;
-
- /*
- * CannedMessageModule also sends a bell character with the messages.
- * ExternalNotificationModule can benefit from this feature.
- */
- bool send_bell = 11;
- }
-
- /*
- Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels.
- Initially created for the RAK14001 RGB LED module.
- */
- message AmbientLightingConfig {
-
- /*
- * Sets LED to on or off.
- */
- bool led_state = 1;
-
- /*
- * Sets the current for the LED output. Default is 10.
- */
- uint32 current = 2;
-
- /*
- * Sets the red LED level. Values are 0-255.
- */
- uint32 red = 3;
-
- /*
- * Sets the green LED level. Values are 0-255.
- */
- uint32 green = 4;
-
- /*
- * Sets the blue LED level. Values are 0-255.
- */
- uint32 blue = 5;
- }
-
- /*
- * TODO: REPLACE
- */
- oneof payload_variant {
- /*
- * TODO: REPLACE
- */
- MQTTConfig mqtt = 1;
-
- /*
- * TODO: REPLACE
- */
- SerialConfig serial = 2;
-
- /*
- * TODO: REPLACE
- */
- ExternalNotificationConfig external_notification = 3;
-
- /*
- * TODO: REPLACE
- */
- StoreForwardConfig store_forward = 4;
-
- /*
- * TODO: REPLACE
- */
- RangeTestConfig range_test = 5;
-
- /*
- * TODO: REPLACE
- */
- TelemetryConfig telemetry = 6;
-
- /*
- * TODO: REPLACE
- */
- CannedMessageConfig canned_message = 7;
-
- /*
- * TODO: REPLACE
- */
- AudioConfig audio = 8;
-
- /*
- * TODO: REPLACE
- */
- RemoteHardwareConfig remote_hardware = 9;
-
- /*
- * TODO: REPLACE
- */
- NeighborInfoConfig neighbor_info = 10;
-
- /*
- * TODO: REPLACE
- */
- AmbientLightingConfig ambient_lighting = 11;
-
- /*
- * TODO: REPLACE
- */
- DetectionSensorConfig detection_sensor = 12;
-
- /*
- * TODO: REPLACE
- */
- PaxcounterConfig paxcounter = 13;
- }
-}
-
-/*
- * A GPIO pin definition for remote hardware module
- */
-message RemoteHardwarePin {
- /*
- * GPIO Pin number (must match Arduino)
- */
- uint32 gpio_pin = 1;
-
- /*
- * Name for the GPIO pin (i.e. Front gate, mailbox, etc)
- */
- string name = 2;
-
- /*
- * Type of GPIO access available to consumers on the mesh
- */
- RemoteHardwarePinType type = 3;
-}
-
-enum RemoteHardwarePinType {
- /*
- * Unset/unused
- */
- UNKNOWN = 0;
-
- /*
- * GPIO pin can be read (if it is high / low)
- */
- DIGITAL_READ = 1;
-
- /*
- * GPIO pin can be written to (high / low)
- */
- DIGITAL_WRITE = 2;
-}
\ No newline at end of file
diff --git a/src/protos/meshtastic/mqtt.options b/src/protos/meshtastic/mqtt.options
deleted file mode 100644
index 591e898..0000000
--- a/src/protos/meshtastic/mqtt.options
+++ /dev/null
@@ -1,8 +0,0 @@
-*ServiceEnvelope.packet type:FT_POINTER
-*ServiceEnvelope.channel_id type:FT_POINTER
-*ServiceEnvelope.gateway_id type:FT_POINTER
-
-*MapReport.long_name max_size:40
-*MapReport.short_name max_size:5
-*MapReport.firmware_version max_size:18
-*MapReport.num_online_local_nodes int_size:16
\ No newline at end of file
diff --git a/src/protos/meshtastic/mqtt.proto b/src/protos/meshtastic/mqtt.proto
deleted file mode 100644
index 17ebf0e..0000000
--- a/src/protos/meshtastic/mqtt.proto
+++ /dev/null
@@ -1,106 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-import "meshtastic/mesh.proto";
-import "meshtastic/config.proto";
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "MQTTProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * This message wraps a MeshPacket with extra metadata about the sender and how it arrived.
- */
-message ServiceEnvelope {
- /*
- * The (probably encrypted) packet
- */
- MeshPacket packet = 1;
-
- /*
- * The global channel ID it was sent on
- */
- string channel_id = 2;
-
- /*
- * The sending gateway node ID. Can we use this to authenticate/prevent fake
- * nodeid impersonation for senders? - i.e. use gateway/mesh id (which is authenticated) + local node id as
- * the globally trusted nodenum
- */
- string gateway_id = 3;
-}
-
-/*
- * Information about a node intended to be reported unencrypted to a map using MQTT.
- */
-message MapReport {
- /*
- * A full name for this user, i.e. "Kevin Hester"
- */
- string long_name = 1;
-
- /*
- * A VERY short name, ideally two characters.
- * Suitable for a tiny OLED screen
- */
- string short_name = 2;
-
- /*
- * Role of the node that applies specific settings for a particular use-case
- */
- Config.DeviceConfig.Role role = 3;
-
- /*
- * Hardware model of the node, i.e. T-Beam, Heltec V3, etc...
- */
- HardwareModel hw_model = 4;
-
- /*
- * Device firmware version string
- */
- string firmware_version = 5;
-
- /*
- * The region code for the radio (US, CN, EU433, etc...)
- */
- Config.LoRaConfig.RegionCode region = 6;
-
- /*
- * Modem preset used by the radio (LongFast, MediumSlow, etc...)
- */
- Config.LoRaConfig.ModemPreset modem_preset = 7;
-
- /*
- * Whether the node has a channel with default PSK and name (LongFast, MediumSlow, etc...)
- * and it uses the default frequency slot given the region and modem preset.
- */
- bool has_default_channel = 8;
-
- /*
- * Latitude: multiply by 1e-7 to get degrees in floating point
- */
- sfixed32 latitude_i = 9;
-
- /*
- * Longitude: multiply by 1e-7 to get degrees in floating point
- */
- sfixed32 longitude_i = 10;
-
- /*
- * Altitude in meters above MSL
- */
- int32 altitude = 11;
-
- /*
- * Indicates the bits of precision for latitude and longitude set by the sending node
- */
- uint32 position_precision = 12;
-
- /*
- * Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT)
- */
- uint32 num_online_local_nodes = 13;
-}
\ No newline at end of file
diff --git a/src/protos/meshtastic/paxcount.proto b/src/protos/meshtastic/paxcount.proto
deleted file mode 100644
index 47b2639..0000000
--- a/src/protos/meshtastic/paxcount.proto
+++ /dev/null
@@ -1,29 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "PaxcountProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * TODO: REPLACE
- */
-message Paxcount {
- /*
- * seen Wifi devices
- */
- uint32 wifi = 1;
-
- /*
- * Seen BLE devices
- */
- uint32 ble = 2;
-
- /*
- * Uptime in seconds
- */
- uint32 uptime = 3;
-}
diff --git a/src/protos/meshtastic/portnums.proto b/src/protos/meshtastic/portnums.proto
deleted file mode 100644
index b02651a..0000000
--- a/src/protos/meshtastic/portnums.proto
+++ /dev/null
@@ -1,216 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "Portnums";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * For any new 'apps' that run on the device or via sister apps on phones/PCs they should pick and use a
- * unique 'portnum' for their application.
- * If you are making a new app using meshtastic, please send in a pull request to add your 'portnum' to this
- * master table.
- * PortNums should be assigned in the following range:
- * 0-63 Core Meshtastic use, do not use for third party apps
- * 64-127 Registered 3rd party apps, send in a pull request that adds a new entry to portnums.proto to register your application
- * 256-511 Use one of these portnums for your private applications that you don't want to register publically
- * All other values are reserved.
- * Note: This was formerly a Type enum named 'typ' with the same id #
- * We have change to this 'portnum' based scheme for specifying app handlers for particular payloads.
- * This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically.
- */
-enum PortNum {
- /*
- * Deprecated: do not use in new code (formerly called OPAQUE)
- * A message sent from a device outside of the mesh, in a form the mesh does not understand
- * NOTE: This must be 0, because it is documented in IMeshService.aidl to be so
- * ENCODING: binary undefined
- */
- UNKNOWN_APP = 0;
-
- /*
- * A simple UTF-8 text message, which even the little micros in the mesh
- * can understand and show on their screen eventually in some circumstances
- * even signal might send messages in this form (see below)
- * ENCODING: UTF-8 Plaintext (?)
- */
- TEXT_MESSAGE_APP = 1;
-
- /*
- * Reserved for built-in GPIO/example app.
- * See remote_hardware.proto/HardwareMessage for details on the message sent/received to this port number
- * ENCODING: Protobuf
- */
- REMOTE_HARDWARE_APP = 2;
-
- /*
- * The built-in position messaging app.
- * Payload is a Position message.
- * ENCODING: Protobuf
- */
- POSITION_APP = 3;
-
- /*
- * The built-in user info app.
- * Payload is a User message.
- * ENCODING: Protobuf
- */
- NODEINFO_APP = 4;
-
- /*
- * Protocol control packets for mesh protocol use.
- * Payload is a Routing message.
- * ENCODING: Protobuf
- */
- ROUTING_APP = 5;
-
- /*
- * Admin control packets.
- * Payload is a AdminMessage message.
- * ENCODING: Protobuf
- */
- ADMIN_APP = 6;
-
- /*
- * Compressed TEXT_MESSAGE payloads.
- * ENCODING: UTF-8 Plaintext (?) with Unishox2 Compression
- * NOTE: The Device Firmware converts a TEXT_MESSAGE_APP to TEXT_MESSAGE_COMPRESSED_APP if the compressed
- * payload is shorter. There's no need for app developers to do this themselves. Also the firmware will decompress
- * any incoming TEXT_MESSAGE_COMPRESSED_APP payload and convert to TEXT_MESSAGE_APP.
- */
- TEXT_MESSAGE_COMPRESSED_APP = 7;
-
- /*
- * Waypoint payloads.
- * Payload is a Waypoint message.
- * ENCODING: Protobuf
- */
- WAYPOINT_APP = 8;
-
- /*
- * Audio Payloads.
- * Encapsulated codec2 packets. On 2.4 GHZ Bandwidths only for now
- * ENCODING: codec2 audio frames
- * NOTE: audio frames contain a 3 byte header (0xc0 0xde 0xc2) and a one byte marker for the decompressed bitrate.
- * This marker comes from the 'moduleConfig.audio.bitrate' enum minus one.
- */
- AUDIO_APP = 9;
-
- /*
- * Same as Text Message but originating from Detection Sensor Module.
- * NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9
- */
- DETECTION_SENSOR_APP = 10;
-
- /*
- * Provides a 'ping' service that replies to any packet it receives.
- * Also serves as a small example module.
- * ENCODING: ASCII Plaintext
- */
- REPLY_APP = 32;
-
- /*
- * Used for the python IP tunnel feature
- * ENCODING: IP Packet. Handled by the python API, firmware ignores this one and pases on.
- */
- IP_TUNNEL_APP = 33;
-
- /*
- * Paxcounter lib included in the firmware
- * ENCODING: protobuf
- */
- PAXCOUNTER_APP = 34;
-
- /*
- * Provides a hardware serial interface to send and receive from the Meshtastic network.
- * Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic
- * network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network.
- * Maximum packet size of 240 bytes.
- * Module is disabled by default can be turned on by setting SERIAL_MODULE_ENABLED = 1 in SerialPlugh.cpp.
- * ENCODING: binary undefined
- */
- SERIAL_APP = 64;
-
- /*
- * STORE_FORWARD_APP (Work in Progress)
- * Maintained by Jm Casler (MC Hamster) : jm@casler.org
- * ENCODING: Protobuf
- */
- STORE_FORWARD_APP = 65;
-
- /*
- * Optional port for messages for the range test module.
- * ENCODING: ASCII Plaintext
- * NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9
- */
- RANGE_TEST_APP = 66;
-
- /*
- * Provides a format to send and receive telemetry data from the Meshtastic network.
- * Maintained by Charles Crossan (crossan007) : crossan007@gmail.com
- * ENCODING: Protobuf
- */
- TELEMETRY_APP = 67;
-
- /*
- * Experimental tools for estimating node position without a GPS
- * Maintained by Github user a-f-G-U-C (a Meshtastic contributor)
- * Project files at https://github.com/a-f-G-U-C/Meshtastic-ZPS
- * ENCODING: arrays of int64 fields
- */
- ZPS_APP = 68;
-
- /*
- * Used to let multiple instances of Linux native applications communicate
- * as if they did using their LoRa chip.
- * Maintained by GitHub user GUVWAF.
- * Project files at https://github.com/GUVWAF/Meshtasticator
- * ENCODING: Protobuf (?)
- */
- SIMULATOR_APP = 69;
-
- /*
- * Provides a traceroute functionality to show the route a packet towards
- * a certain destination would take on the mesh.
- * ENCODING: Protobuf
- */
- TRACEROUTE_APP = 70;
-
- /*
- * Aggregates edge info for the network by sending out a list of each node's neighbors
- * ENCODING: Protobuf
- */
- NEIGHBORINFO_APP = 71;
-
- /*
- * ATAK Plugin
- * Portnum for payloads from the official Meshtastic ATAK plugin
- */
- ATAK_PLUGIN = 72;
-
- /*
- * Provides unencrypted information about a node for consumption by a map via MQTT
- */
- MAP_REPORT_APP = 73;
-
- /*
- * Private applications should use portnums >= 256.
- * To simplify initial development and testing you can use "PRIVATE_APP"
- * in your code without needing to rebuild protobuf files (via [regen-protos.sh](https://github.com/meshtastic/firmware/blob/master/bin/regen-protos.sh))
- */
- PRIVATE_APP = 256;
-
- /*
- * ATAK Forwarder Module https://github.com/paulmandal/atak-forwarder
- * ENCODING: libcotshrink
- */
- ATAK_FORWARDER = 257;
-
- /*
- * Currently we limit port nums to no higher than this value
- */
- MAX = 511;
-}
\ No newline at end of file
diff --git a/src/protos/meshtastic/remote_hardware.proto b/src/protos/meshtastic/remote_hardware.proto
deleted file mode 100644
index ba4a693..0000000
--- a/src/protos/meshtastic/remote_hardware.proto
+++ /dev/null
@@ -1,75 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "RemoteHardware";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * An example app to show off the module system. This message is used for
- * REMOTE_HARDWARE_APP PortNums.
- * Also provides easy remote access to any GPIO.
- * In the future other remote hardware operations can be added based on user interest
- * (i.e. serial output, spi/i2c input/output).
- * FIXME - currently this feature is turned on by default which is dangerous
- * because no security yet (beyond the channel mechanism).
- * It should be off by default and then protected based on some TBD mechanism
- * (a special channel once multichannel support is included?)
- */
-message HardwareMessage {
- /*
- * TODO: REPLACE
- */
- enum Type {
- /*
- * Unset/unused
- */
- UNSET = 0;
-
- /*
- * Set gpio gpios based on gpio_mask/gpio_value
- */
- WRITE_GPIOS = 1;
-
- /*
- * We are now interested in watching the gpio_mask gpios.
- * If the selected gpios change, please broadcast GPIOS_CHANGED.
- * Will implicitly change the gpios requested to be INPUT gpios.
- */
- WATCH_GPIOS = 2;
-
- /*
- * The gpios listed in gpio_mask have changed, the new values are listed in gpio_value
- */
- GPIOS_CHANGED = 3;
-
- /*
- * Read the gpios specified in gpio_mask, send back a READ_GPIOS_REPLY reply with gpio_value populated
- */
- READ_GPIOS = 4;
-
- /*
- * A reply to READ_GPIOS. gpio_mask and gpio_value will be populated
- */
- READ_GPIOS_REPLY = 5;
- }
-
- /*
- * What type of HardwareMessage is this?
- */
- Type type = 1;
-
- /*
- * What gpios are we changing. Not used for all MessageTypes, see MessageType for details
- */
- uint64 gpio_mask = 2;
-
- /*
- * For gpios that were listed in gpio_mask as valid, what are the signal levels for those gpios.
- * Not used for all MessageTypes, see MessageType for details
- */
- uint64 gpio_value = 3;
-}
diff --git a/src/protos/meshtastic/rtttl.options b/src/protos/meshtastic/rtttl.options
deleted file mode 100644
index 1ae0c2f..0000000
--- a/src/protos/meshtastic/rtttl.options
+++ /dev/null
@@ -1 +0,0 @@
-*RTTTLConfig.ringtone max_size:230
diff --git a/src/protos/meshtastic/rtttl.proto b/src/protos/meshtastic/rtttl.proto
deleted file mode 100644
index 11c8b92..0000000
--- a/src/protos/meshtastic/rtttl.proto
+++ /dev/null
@@ -1,19 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "RTTTLConfigProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * Canned message module configuration.
- */
-message RTTTLConfig {
- /*
- * Ringtone for PWM Buzzer in RTTTL Format.
- */
- string ringtone = 1;
-}
diff --git a/src/protos/meshtastic/storeforward.options b/src/protos/meshtastic/storeforward.options
deleted file mode 100644
index 8580aab..0000000
--- a/src/protos/meshtastic/storeforward.options
+++ /dev/null
@@ -1 +0,0 @@
-*StoreAndForward.text max_size:237
\ No newline at end of file
diff --git a/src/protos/meshtastic/storeforward.proto b/src/protos/meshtastic/storeforward.proto
deleted file mode 100644
index ef7de2c..0000000
--- a/src/protos/meshtastic/storeforward.proto
+++ /dev/null
@@ -1,218 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "StoreAndForwardProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * TODO: REPLACE
- */
-message StoreAndForward {
- /*
- * 001 - 063 = From Router
- * 064 - 127 = From Client
- */
- enum RequestResponse {
- /*
- * Unset/unused
- */
- UNSET = 0;
-
- /*
- * Router is an in error state.
- */
- ROUTER_ERROR = 1;
-
- /*
- * Router heartbeat
- */
- ROUTER_HEARTBEAT = 2;
-
- /*
- * Router has requested the client respond. This can work as a
- * "are you there" message.
- */
- ROUTER_PING = 3;
-
- /*
- * The response to a "Ping"
- */
- ROUTER_PONG = 4;
-
- /*
- * Router is currently busy. Please try again later.
- */
- ROUTER_BUSY = 5;
-
- /*
- * Router is responding to a request for history.
- */
- ROUTER_HISTORY = 6;
-
- /*
- * Router is responding to a request for stats.
- */
- ROUTER_STATS = 7;
-
- /*
- * Router sends a text message from its history that was a direct message.
- */
- ROUTER_TEXT_DIRECT = 8;
-
- /*
- * Router sends a text message from its history that was a broadcast.
- */
- ROUTER_TEXT_BROADCAST = 9;
-
- /*
- * Client is an in error state.
- */
- CLIENT_ERROR = 64;
-
- /*
- * Client has requested a replay from the router.
- */
- CLIENT_HISTORY = 65;
-
- /*
- * Client has requested stats from the router.
- */
- CLIENT_STATS = 66;
-
- /*
- * Client has requested the router respond. This can work as a
- * "are you there" message.
- */
- CLIENT_PING = 67;
-
- /*
- * The response to a "Ping"
- */
- CLIENT_PONG = 68;
-
- /*
- * Client has requested that the router abort processing the client's request
- */
- CLIENT_ABORT = 106;
- }
-
- /*
- * TODO: REPLACE
- */
- message Statistics {
- /*
- * Number of messages we have ever seen
- */
- uint32 messages_total = 1;
-
- /*
- * Number of messages we have currently saved our history.
- */
- uint32 messages_saved = 2;
-
- /*
- * Maximum number of messages we will save
- */
- uint32 messages_max = 3;
-
- /*
- * Router uptime in seconds
- */
- uint32 up_time = 4;
-
- /*
- * Number of times any client sent a request to the S&F.
- */
- uint32 requests = 5;
-
- /*
- * Number of times the history was requested.
- */
- uint32 requests_history = 6;
-
- /*
- * Is the heartbeat enabled on the server?
- */
- bool heartbeat = 7;
-
- /*
- * Maximum number of messages the server will return.
- */
- uint32 return_max = 8;
-
- /*
- * Maximum history window in minutes the server will return messages from.
- */
- uint32 return_window = 9;
- }
-
- /*
- * TODO: REPLACE
- */
- message History {
- /*
- * Number of that will be sent to the client
- */
- uint32 history_messages = 1;
-
- /*
- * The window of messages that was used to filter the history client requested
- */
- uint32 window = 2;
-
- /*
- * Index in the packet history of the last message sent in a previous request to the server.
- * Will be sent to the client before sending the history and can be set in a subsequent request to avoid getting packets the server already sent to the client.
- */
- uint32 last_request = 3;
- }
-
- /*
- * TODO: REPLACE
- */
- message Heartbeat {
- /*
- * Period in seconds that the heartbeat is sent out that will be sent to the client
- */
- uint32 period = 1;
-
- /*
- * If set, this is not the primary Store & Forward router on the mesh
- */
- uint32 secondary = 2;
- }
-
- /*
- * TODO: REPLACE
- */
- RequestResponse rr = 1;
-
- /*
- * TODO: REPLACE
- */
- oneof variant {
- /*
- * TODO: REPLACE
- */
- Statistics stats = 2;
-
- /*
- * TODO: REPLACE
- */
- History history = 3;
-
- /*
- * TODO: REPLACE
- */
- Heartbeat heartbeat = 4;
-
- /*
- * Text from history message.
- */
- bytes text = 5;
- }
-}
\ No newline at end of file
diff --git a/src/protos/meshtastic/telemetry.options b/src/protos/meshtastic/telemetry.options
deleted file mode 100644
index 2fe657c..0000000
--- a/src/protos/meshtastic/telemetry.options
+++ /dev/null
@@ -1,4 +0,0 @@
-# options for nanopb
-# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
-
-*EnvironmentMetrics.iaq int_size:16
\ No newline at end of file
diff --git a/src/protos/meshtastic/telemetry.proto b/src/protos/meshtastic/telemetry.proto
deleted file mode 100644
index 78c0e4f..0000000
--- a/src/protos/meshtastic/telemetry.proto
+++ /dev/null
@@ -1,560 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "TelemetryProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-/*
- * Key native device metrics such as battery level
- */
-message DeviceMetrics {
- /*
- * 0-100 (>100 means powered)
- */
- optional uint32 battery_level = 1;
-
- /*
- * Voltage measured
- */
- optional float voltage = 2;
-
- /*
- * Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise).
- */
- optional float channel_utilization = 3;
-
- /*
- * Percent of airtime for transmission used within the last hour.
- */
- optional float air_util_tx = 4;
-
- /*
- * How long the device has been running since the last reboot (in seconds)
- */
- optional uint32 uptime_seconds = 5;
-}
-
-/*
- * Weather station or other environmental metrics
- */
-message EnvironmentMetrics {
- /*
- * Temperature measured
- */
- optional float temperature = 1;
-
- /*
- * Relative humidity percent measured
- */
- optional float relative_humidity = 2;
-
- /*
- * Barometric pressure in hPA measured
- */
- optional float barometric_pressure = 3;
-
- /*
- * Gas resistance in MOhm measured
- */
- optional float gas_resistance = 4;
-
- /*
- * Voltage measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x)
- */
- optional float voltage = 5;
-
- /*
- * Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x)
- */
- optional float current = 6;
-
- /*
- * relative scale IAQ value as measured by Bosch BME680 . value 0-500.
- * Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here.
- */
- optional uint32 iaq = 7;
-
- /*
- * RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm.
- */
- optional float distance = 8;
-
- /*
- * VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
- */
- optional float lux = 9;
-
- /*
- * VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor.
- */
- optional float white_lux = 10;
-
- /*
- * Infrared lux
- */
- optional float ir_lux = 11;
-
- /*
- * Ultraviolet lux
- */
- optional float uv_lux = 12;
-
- /*
- * Wind direction in degrees
- * 0 degrees = North, 90 = East, etc...
- */
- optional uint32 wind_direction = 13;
-
- /*
- * Wind speed in m/s
- */
- optional float wind_speed = 14;
-
- /*
- * Weight in KG
- */
- optional float weight = 15;
-
- /*
- * Wind gust in m/s
- */
- optional float wind_gust = 16;
-
- /*
- * Wind lull in m/s
- */
- optional float wind_lull = 17;
-
- /*
- * Radiation in µR/h
- */
- optional float radiation = 18;
-
-}
-
-/*
- * Power Metrics (voltage / current / etc)
- */
-message PowerMetrics {
- /*
- * Voltage (Ch1)
- */
- optional float ch1_voltage = 1;
-
- /*
- * Current (Ch1)
- */
- optional float ch1_current = 2;
-
- /*
- * Voltage (Ch2)
- */
- optional float ch2_voltage = 3;
-
- /*
- * Current (Ch2)
- */
- optional float ch2_current = 4;
-
- /*
- * Voltage (Ch3)
- */
- optional float ch3_voltage = 5;
-
- /*
- * Current (Ch3)
- */
- optional float ch3_current = 6;
-}
-
-/*
- * Air quality metrics
- */
-message AirQualityMetrics {
- /*
- * Concentration Units Standard PM1.0
- */
- optional uint32 pm10_standard = 1;
-
- /*
- * Concentration Units Standard PM2.5
- */
- optional uint32 pm25_standard = 2;
-
- /*
- * Concentration Units Standard PM10.0
- */
- optional uint32 pm100_standard = 3;
-
- /*
- * Concentration Units Environmental PM1.0
- */
- optional uint32 pm10_environmental = 4;
-
- /*
- * Concentration Units Environmental PM2.5
- */
- optional uint32 pm25_environmental = 5;
-
- /*
- * Concentration Units Environmental PM10.0
- */
- optional uint32 pm100_environmental = 6;
-
- /*
- * 0.3um Particle Count
- */
- optional uint32 particles_03um = 7;
-
- /*
- * 0.5um Particle Count
- */
- optional uint32 particles_05um = 8;
-
- /*
- * 1.0um Particle Count
- */
- optional uint32 particles_10um = 9;
-
- /*
- * 2.5um Particle Count
- */
- optional uint32 particles_25um = 10;
-
- /*
- * 5.0um Particle Count
- */
- optional uint32 particles_50um = 11;
-
- /*
- * 10.0um Particle Count
- */
- optional uint32 particles_100um = 12;
-
- /*
- * 10.0um Particle Count
- */
- optional uint32 co2 = 13;
-}
-
-/*
- * Local device mesh statistics
- */
-message LocalStats {
- /*
- * How long the device has been running since the last reboot (in seconds)
- */
- uint32 uptime_seconds = 1;
- /*
- * Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise).
- */
- float channel_utilization = 2;
- /*
- * Percent of airtime for transmission used within the last hour.
- */
- float air_util_tx = 3;
-
- /*
- * Number of packets sent
- */
- uint32 num_packets_tx = 4;
-
- /*
- * Number of packets received (both good and bad)
- */
- uint32 num_packets_rx = 5;
-
- /*
- * Number of packets received that are malformed or violate the protocol
- */
- uint32 num_packets_rx_bad = 6;
-
- /*
- * Number of nodes online (in the past 2 hours)
- */
- uint32 num_online_nodes = 7;
-
- /*
- * Number of nodes total
- */
- uint32 num_total_nodes = 8;
-
- /*
- * Number of received packets that were duplicates (due to multiple nodes relaying).
- * If this number is high, there are nodes in the mesh relaying packets when it's unnecessary, for example due to the ROUTER/REPEATER role.
- */
- uint32 num_rx_dupe = 9;
-
- /*
- * Number of packets we transmitted that were a relay for others (not originating from ourselves).
- */
- uint32 num_tx_relay = 10;
-
- /*
- * Number of times we canceled a packet to be relayed, because someone else did it before us.
- * This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you.
- */
- uint32 num_tx_relay_canceled = 11;
-}
-
-/*
- * Health telemetry metrics
- */
-message HealthMetrics {
- /*
- * Heart rate (beats per minute)
- */
- optional uint32 heart_bpm = 1;
-
- /*
- * SpO2 (blood oxygen saturation) level
- */
- optional uint32 spO2 = 2;
-
- /*
- * Body temperature in degrees Celsius
- */
- optional float temperature = 3;
-}
-
-/*
- * Types of Measurements the telemetry module is equipped to handle
- */
-message Telemetry {
- /*
- * Seconds since 1970 - or 0 for unknown/unset
- */
- fixed32 time = 1;
-
- oneof variant {
- /*
- * Key native device metrics such as battery level
- */
- DeviceMetrics device_metrics = 2;
-
- /*
- * Weather station or other environmental metrics
- */
- EnvironmentMetrics environment_metrics = 3;
-
- /*
- * Air quality metrics
- */
- AirQualityMetrics air_quality_metrics = 4;
-
- /*
- * Power Metrics
- */
- PowerMetrics power_metrics = 5;
-
- /*
- * Local device mesh statistics
- */
- LocalStats local_stats = 6;
-
- /*
- * Health telemetry metrics
- */
- HealthMetrics health_metrics = 7;
- }
-}
-
-/*
- * Supported I2C Sensors for telemetry in Meshtastic
- */
-enum TelemetrySensorType {
- /*
- * No external telemetry sensor explicitly set
- */
- SENSOR_UNSET = 0;
-
- /*
- * High accuracy temperature, pressure, humidity
- */
- BME280 = 1;
-
- /*
- * High accuracy temperature, pressure, humidity, and air resistance
- */
- BME680 = 2;
-
- /*
- * Very high accuracy temperature
- */
- MCP9808 = 3;
-
- /*
- * Moderate accuracy current and voltage
- */
- INA260 = 4;
-
- /*
- * Moderate accuracy current and voltage
- */
- INA219 = 5;
-
- /*
- * High accuracy temperature and pressure
- */
- BMP280 = 6;
-
- /*
- * High accuracy temperature and humidity
- */
- SHTC3 = 7;
-
- /*
- * High accuracy pressure
- */
- LPS22 = 8;
-
- /*
- * 3-Axis magnetic sensor
- */
- QMC6310 = 9;
-
- /*
- * 6-Axis inertial measurement sensor
- */
- QMI8658 = 10;
-
- /*
- * 3-Axis magnetic sensor
- */
- QMC5883L = 11;
-
- /*
- * High accuracy temperature and humidity
- */
- SHT31 = 12;
-
- /*
- * PM2.5 air quality sensor
- */
- PMSA003I = 13;
-
- /*
- * INA3221 3 Channel Voltage / Current Sensor
- */
- INA3221 = 14;
-
- /*
- * BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280)
- */
- BMP085 = 15;
-
- /*
- * RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
- */
- RCWL9620 = 16;
-
- /*
- * Sensirion High accuracy temperature and humidity
- */
- SHT4X = 17;
-
- /*
- * VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
- */
- VEML7700 = 18;
-
- /*
- * MLX90632 non-contact IR temperature sensor.
- */
- MLX90632 = 19;
-
- /*
- * TI OPT3001 Ambient Light Sensor
- */
- OPT3001 = 20;
-
- /*
- * Lite On LTR-390UV-01 UV Light Sensor
- */
- LTR390UV = 21;
-
- /*
- * AMS TSL25911FN RGB Light Sensor
- */
- TSL25911FN = 22;
-
- /*
- * AHT10 Integrated temperature and humidity sensor
- */
- AHT10 = 23;
-
- /*
- * DFRobot Lark Weather station (temperature, humidity, pressure, wind speed and direction)
- */
- DFROBOT_LARK = 24;
-
- /*
- * NAU7802 Scale Chip or compatible
- */
- NAU7802 = 25;
-
- /*
- * BMP3XX High accuracy temperature and pressure
- */
- BMP3XX = 26;
-
- /*
- * ICM-20948 9-Axis digital motion processor
- */
- ICM20948 = 27;
-
- /*
- * MAX17048 1S lipo battery sensor (voltage, state of charge, time to go)
- */
- MAX17048 = 28;
-
- /*
- * Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor
- */
- CUSTOM_SENSOR = 29;
-
- /*
- * MAX30102 Pulse Oximeter and Heart-Rate Sensor
- */
- MAX30102 = 30;
-
- /*
- * MLX90614 non-contact IR temperature sensor
- */
- MLX90614 = 31;
-
- /*
- * SCD40/SCD41 CO2, humidity, temperature sensor
- */
- SCD4X = 32;
-
- /*
- * ClimateGuard RadSens, radiation, Geiger-Muller Tube
- */
- RADSENS = 33;
-
- /*
- * High accuracy current and voltage
- */
- INA226 = 34;
-
-}
-
-/*
- * NAU7802 Telemetry configuration, for saving to flash
- */
-message Nau7802Config {
- /*
- * The offset setting for the NAU7802
- */
- int32 zeroOffset = 1;
-
- /*
- * The calibration factor for the NAU7802
- */
- float calibrationFactor = 2;
-}
\ No newline at end of file
diff --git a/src/protos/meshtastic/xmodem.options b/src/protos/meshtastic/xmodem.options
deleted file mode 100644
index 3af6125..0000000
--- a/src/protos/meshtastic/xmodem.options
+++ /dev/null
@@ -1,6 +0,0 @@
-# options for nanopb
-# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
-
-*XModem.buffer max_size:128
-*XModem.seq int_size:16
-*XModem.crc16 int_size:16
diff --git a/src/protos/meshtastic/xmodem.proto b/src/protos/meshtastic/xmodem.proto
deleted file mode 100644
index 732780a..0000000
--- a/src/protos/meshtastic/xmodem.proto
+++ /dev/null
@@ -1,27 +0,0 @@
-syntax = "proto3";
-
-package meshtastic;
-
-option csharp_namespace = "Meshtastic.Protobufs";
-option go_package = "github.com/meshtastic/go/generated";
-option java_outer_classname = "XmodemProtos";
-option java_package = "com.geeksville.mesh";
-option swift_prefix = "";
-
-message XModem {
- enum Control {
- NUL = 0;
- SOH = 1;
- STX = 2;
- EOT = 4;
- ACK = 6;
- NAK = 21;
- CAN = 24;
- CTRLZ = 26;
- }
-
- Control control = 1;
- uint32 seq = 2;
- uint32 crc16 = 3;
- bytes buffer = 4;
-}
diff --git a/src/public/assets/css/styles.css b/src/public/assets/css/styles.css
new file mode 100644
index 0000000..8811cad
--- /dev/null
+++ b/src/public/assets/css/styles.css
@@ -0,0 +1,115 @@
+/* used to prevent ui flicker before vuejs loads */
+[v-cloak] {
+ display: none;
+}
+
+.icon-longfast {
+ background-color: #009016;
+ border-radius: 25px;
+ border: 1px solid #2C2D3C;
+}
+
+.icon-mediumfast {
+ background-color: #326be7;
+ border-radius: 25px;
+ border: 1px solid #2C2D3C;
+}
+
+.icon-shortslow {
+ background-color: #0077e6;
+ border-radius: 25px;
+ border: 1px solid #2C2D3C;
+}
+
+.icon-mqtt-connected {
+ background-color: #2563eb; /* Change to use same color as disconnected // #16a34a; */
+ border-radius: 25px;
+ border: 1px solid #2C2D3C;
+}
+
+.icon-mqtt-disconnected {
+ background-color: #2563eb;
+ border-radius: 25px;
+ border: 1px solid #2C2D3C;
+}
+
+.icon-offline {
+ background-color: #e2286c;
+ border-radius: 25px;
+ border: 1px solid #2C2D3C;
+}
+
+.icon-position-history {
+ background-color: #a855f7;
+ border-radius: 25px;
+ border: 1px solid #2C2D3C;
+}
+
+.icon-traceroute-start {
+ background-color: #16a34a; /* green */
+ border-radius: 25px;
+ border: 1px solid #2C2D3C;
+}
+
+.icon-traceroute-end {
+ background-color: #dc2626; /* red */
+ border-radius: 25px;
+ border: 1px solid #2C2D3C;
+}
+
+.waypoint-label {
+ font-size: 26px;
+ background-color: transparent;
+}
+
+.link {
+ color: #2563eb;
+}
+
+.link:hover {
+ text-decoration: underline;
+}
+
+.tooltip {
+ position: relative;
+ display: inline-block;
+}
+
+.tooltip .tooltip-text {
+ visibility: hidden;
+ width: 80px;
+ background-color: black;
+ color: #fff;
+ text-align: center;
+ padding: 4px 0;
+ border-radius: 6px;
+ position: absolute;
+ z-index: 10000;
+ top: 100%;
+ left: 50%;
+ margin-top: 8px;
+ margin-left: -40px; /* Use half of the width (120/2 = 60), to center the tooltip */
+}
+
+.tooltip .tooltip-text::after {
+ content: " ";
+ position: absolute;
+ bottom: 100%; /* At the top of the tooltip */
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px;
+ border-style: solid;
+ border-color: transparent transparent black transparent;
+}
+
+.tooltip:hover .tooltip-text {
+ visibility: visible;
+}
+
+.z-search {
+ z-index: 1001;
+}
+
+.z-sidebar {
+ z-index: 1002;
+}
\ No newline at end of file
diff --git a/src/public/assets/js/app.js b/src/public/assets/js/app.js
new file mode 100644
index 0000000..86b4542
--- /dev/null
+++ b/src/public/assets/js/app.js
@@ -0,0 +1,956 @@
+Vue.createApp({
+ data() {
+ return {
+
+ isShowingAnnouncement: this.shouldShowAnnouncement(),
+
+ configNodesMaxAgeInSeconds: window.getConfigNodesMaxAgeInSeconds(),
+ configNodesOfflineAgeInSeconds: window.getConfigNodesOfflineAgeInSeconds(),
+ configWaypointsMaxAgeInSeconds: window.getConfigWaypointsMaxAgeInSeconds(),
+ configConnectionsMaxDistanceInMeters: window.getConfigConnectionsMaxDistanceInMeters(),
+ configZoomLevelGoToNode: window.getConfigZoomLevelGoToNode(),
+ configAutoUpdatePositionInUrl: window.getConfigAutoUpdatePositionInUrl(),
+ configEnableMapAnimations: window.getConfigEnableMapAnimations(),
+ configTemperatureFormat: window.getConfigTemperatureFormat(),
+ configConnectionsTimePeriodInSeconds: window.getConfigConnectionsTimePeriodInSeconds(),
+ configConnectionsColoredLines: window.getConfigConnectionsColoredLines(),
+ configConnectionsBidirectionalOnly: window.getConfigConnectionsBidirectionalOnly(),
+ configConnectionsMinSnrDb: window.getConfigConnectionsMinSnrDb(),
+ configConnectionsBidirectionalMinSnr: window.getConfigConnectionsBidirectionalMinSnr(),
+
+ isShowingHardwareModels: false,
+ hardwareModelStats: null,
+
+ isShowingInfoModal: this.shouldShowInfoModal(),
+ isShowingMobileSearch: false,
+ isShowingSettings: false,
+
+ nodes: [],
+ searchText: "",
+
+ selectedNode: null,
+ selectedNodeDeviceMetrics: [],
+ selectedNodeEnvironmentMetrics: [],
+ selectedNodePowerMetrics: [],
+ selectedNodeMqttMetrics: [],
+ selectedNodeTraceroutes: [],
+
+ deviceMetricsTimeRange: "7d",
+ environmentMetricsTimeRange: "7d",
+ powerMetricsTimeRange: "7d",
+
+ isPositionHistoryModalExpanded: true,
+ positionHistoryDateTimeFrom: null,
+ positionHistoryDateTimeTo: null,
+ selectedNodePositionHistory: [],
+ selectedNodeToShowPositionHistory: null,
+ selectedNodePositionHistoryMarkers: [],
+ selectedNodePositionHistoryPolyLines: [],
+
+ selectedTraceRoute: null,
+ tracerouteEdges: [],
+
+ selectedNodeToShowConnections: null,
+
+ moment: window.moment,
+
+ };
+ },
+ mounted: function() {
+
+ // load data
+ this.loadHardwareModelStats();
+
+ // handle map click callback from outside of vue
+ window._onMapClick = () => {
+ this.searchText = "";
+ this.isShowingMobileSearch = false;
+ };
+
+ // handle node callback from outside of vue
+ window._onNodeClick = (node) => {
+ this.selectedNode = node;
+ this.loadNodeDeviceMetrics(node.node_id);
+ this.loadNodeEnvironmentMetrics(node.node_id);
+ this.loadNodePowerMetrics(node.node_id);
+ this.loadNodeMqttMetrics(node.node_id);
+ this.loadNodeTraceroutes(node.node_id);
+ //this.loadNodePositionHistory(node.node_id);
+ };
+
+ // handle node callback from outside of vue
+ window._onShowNodeConnectionsClick = (node) => {
+ this.selectedNodeToShowConnections = node;
+ };
+
+ // handle nodes updated callback from outside of vue
+ window._onNodesUpdated = (nodes) => {
+ this.nodes = nodes;
+ };
+
+ },
+ methods: {
+ getAnnouncementId: function() {
+ // change this when making a new announcement
+ return "1";
+ },
+ shouldShowAnnouncement: function() {
+ const lastSeenAnnouncementId = window.localStorage.getItem("last-seen-announcement-id");
+ return lastSeenAnnouncementId?.toString() !== this.getAnnouncementId();
+ },
+ dismissAnnouncement: function() {
+ window.localStorage.setItem("last-seen-announcement-id", this.getAnnouncementId());
+ this.isShowingAnnouncement = false;
+ },
+ shouldShowInfoModal: function() {
+ return !window.getConfigHasSeenInfoModal()
+ && !window.isMobile();
+ },
+ loadHardwareModelStats: function() {
+ window.axios.get('/api/v1/stats/hardware-models').then((response) => {
+ this.hardwareModelStats = response.data.hardware_model_stats;
+ }).catch((error) => {
+ // do nothing
+ });
+ },
+ loadNodeDeviceMetrics: function(nodeId) {
+
+ // calculate unix timestamps in milliseconds for supported time ranges
+ const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
+ const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000);
+ const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000);
+ const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10);
+
+ // determine how long back to load device metrics from
+ var timeFrom = threeDaysAgoInMilliseconds;
+ switch(this.deviceMetricsTimeRange){
+ case "1d": {
+ timeFrom = oneDayAgoInMilliseconds;
+ break;
+ }
+ case "3d": {
+ timeFrom = threeDaysAgoInMilliseconds;
+ break;
+ }
+ case "7d": {
+ timeFrom = sevenDaysAgoInMilliseconds;
+ break;
+ }
+ case "30d": {
+ timeFrom = thirtyDaysAgoInMilliseconds;
+ break;
+ }
+ }
+
+ window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, {
+ params: {
+ time_from: timeFrom,
+ },
+ }).then((response) => {
+ // reverse response, as it's newest to oldest, but we want oldest to newest
+ this.selectedNodeDeviceMetrics = response.data.device_metrics.reverse();
+ this.renderDeviceMetricCharts();
+ }).catch(() => {
+ this.selectedNodeDeviceMetrics = [];
+ this.renderDeviceMetricCharts();
+ });
+ },
+ loadNodeEnvironmentMetrics: function(nodeId) {
+
+ // calculate unix timestamps in milliseconds for supported time ranges
+ const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
+ const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000);
+ const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000);
+ const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10);
+
+ // determine how long back to load environment metrics from
+ var timeFrom = threeDaysAgoInMilliseconds;
+ switch(this.environmentMetricsTimeRange){
+ case "1d": {
+ timeFrom = oneDayAgoInMilliseconds;
+ break;
+ }
+ case "3d": {
+ timeFrom = threeDaysAgoInMilliseconds;
+ break;
+ }
+ case "7d": {
+ timeFrom = sevenDaysAgoInMilliseconds;
+ break;
+ }
+ case "30d": {
+ timeFrom = thirtyDaysAgoInMilliseconds;
+ break;
+ }
+ }
+
+ window.axios.get(`/api/v1/nodes/${nodeId}/environment-metrics`, {
+ params: {
+ time_from: timeFrom,
+ },
+ }).then((response) => {
+ // reverse response, as it's newest to oldest, but we want oldest to newest
+ this.selectedNodeEnvironmentMetrics = response.data.environment_metrics.reverse();
+ this.renderEnvironmentMetricCharts();
+ }).catch(() => {
+ this.selectedNodeEnvironmentMetrics = [];
+ this.renderEnvironmentMetricCharts();
+ });
+ },
+ loadNodePowerMetrics: function(nodeId) {
+
+ // calculate unix timestamps in milliseconds for supported time ranges
+ const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
+ const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000);
+ const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000);
+ const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10);
+
+ // determine how long back to load power metrics from
+ var timeFrom = threeDaysAgoInMilliseconds;
+ switch(this.powerMetricsTimeRange){
+ case "1d": {
+ timeFrom = oneDayAgoInMilliseconds;
+ break;
+ }
+ case "3d": {
+ timeFrom = threeDaysAgoInMilliseconds;
+ break;
+ }
+ case "7d": {
+ timeFrom = sevenDaysAgoInMilliseconds;
+ break;
+ }
+ case "30d": {
+ timeFrom = thirtyDaysAgoInMilliseconds;
+ break;
+ }
+ }
+
+ window.axios.get(`/api/v1/nodes/${nodeId}/power-metrics`, {
+ params: {
+ time_from: timeFrom,
+ },
+ }).then((response) => {
+ // reverse response, as it's newest to oldest, but we want oldest to newest
+ this.selectedNodePowerMetrics = response.data.power_metrics.reverse();
+ this.renderPowerMetricCharts();
+ }).catch(() => {
+ this.selectedNodePowerMetrics = [];
+ this.renderPowerMetricCharts();
+ });
+ },
+ loadNodeMqttMetrics: function(nodeId) {
+ this.selectedNodeMqttMetrics = [];
+ window.axios.get(`/api/v1/nodes/${nodeId}/mqtt-metrics`).then((response) => {
+ this.selectedNodeMqttMetrics = response.data.mqtt_metrics;
+ }).catch(() => {
+ // do nothing
+ });
+ },
+ loadNodeTraceroutes: function(nodeId) {
+ this.selectedNodeTraceroutes = [];
+ window.axios.get(`/api/v1/nodes/${nodeId}/traceroutes`, {
+ params: {
+ count: 5,
+ },
+ }).then((response) => {
+ this.selectedNodeTraceroutes = response.data.traceroutes;
+ }).catch(() => {
+ // do nothing
+ });
+ },
+ loadNodePositionHistory: function(nodeId) {
+ this.selectedNodePositionHistory = [];
+ window.axios.get(`/api/v1/nodes/${nodeId}/position-history`, {
+ params: {
+ // parse from datetime-local format, and send as unix timestamp in milliseconds
+ time_from: moment(this.positionHistoryDateTimeFrom, "YYYY-MM-DDTHH:mm").format("x"),
+ time_to: moment(this.positionHistoryDateTimeTo, "YYYY-MM-DDTHH:mm").format("x"),
+ },
+ }).then((response) => {
+ this.selectedNodePositionHistory = response.data.position_history;
+ if(this.selectedNodeToShowPositionHistory != null){
+ clearAllPositionHistory();
+ onPositionHistoryUpdated(response.data.position_history);
+ }
+
+ }).catch(() => {
+ // do nothing
+ });
+ },
+ renderDeviceMetricCharts: function() {
+ try {
+ this.updateDeviceMetricsChart();
+ } catch(e) {
+ console.log(e);
+ }
+ },
+ updateDeviceMetricsChart: function() {
+
+ // destroy existing chart
+ const chartElementId = "deviceMetricsChart";
+ const existingChart = window.Chart.getChart(chartElementId);
+ if(existingChart != null){
+ existingChart.destroy();
+ }
+
+ // get chart element
+ const chartElement = window.document.getElementById(chartElementId);
+ if(!chartElement){
+ return;
+ }
+
+ // create chart data
+ const labels = [];
+ const batteryMetrics = [];
+ const channelUtilizationMetrics = [];
+ const airUtilTxMetrics = [];
+ for(const deviceMetric of this.selectedNodeDeviceMetrics){
+ labels.push(moment(deviceMetric.created_at));
+ batteryMetrics.push(deviceMetric.battery_level);
+ channelUtilizationMetrics.push(deviceMetric.channel_utilization);
+ airUtilTxMetrics.push(deviceMetric.air_util_tx);
+ }
+
+ // create chart
+ new window.Chart(chartElement, {
+ type: 'line',
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: 'Battery Level',
+ borderColor: '#3b82f6',
+ backgroundColor: '#3b82f6',
+ pointStyle: false, // no points
+ fill: false,
+ data: batteryMetrics,
+ },
+ {
+ label: 'Channel Util',
+ borderColor: '#22c55e',
+ backgroundColor: '#22c55e',
+ showLine: false, // no lines between points
+ fill: false,
+ data: channelUtilizationMetrics,
+ },
+ {
+ label: 'Air Util TX',
+ borderColor: '#f97316',
+ backgroundColor: '#f97316',
+ showLine: false, // no lines between points
+ fill: false,
+ data: airUtilTxMetrics,
+
+ },
+ ],
+ },
+ options: {
+ responsive: true,
+ borderWidth: 2,
+ elements: {
+ point: {
+ radius: 2,
+ },
+ },
+ scales: {
+ x: {
+ position: 'top',
+ type: 'time',
+ time: {
+ unit: 'day',
+ displayFormats: {
+ day: 'MMM DD', // Jan 01
+ },
+ },
+ },
+ y: {
+ min: 0,
+ max: 101, // 101 is "Plugged In", need to include for tooltip to work
+ ticks: {
+ callback: (label) => `${label}%`,
+ },
+ },
+ },
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ mode: "index",
+ intersect: false,
+ callbacks: {
+ label: (item) => {
+ return `${item.dataset.label}: ${item.formattedValue}%`;
+ },
+ },
+ },
+ },
+ }
+ });
+
+ },
+ renderEnvironmentMetricCharts: function() {
+ try {
+ this.updateEnvironmentMetricsChart();
+ } catch(e) {
+ console.log(e);
+ }
+ },
+ updateEnvironmentMetricsChart: function() {
+
+ // destroy existing chart
+ const chartElementId = "environmentMetricsChart";
+ const existingChart = window.Chart.getChart(chartElementId);
+ if(existingChart != null){
+ existingChart.destroy();
+ }
+
+ // get chart element
+ const chartElement = window.document.getElementById(chartElementId);
+ if(!chartElement){
+ return;
+ }
+
+ // create chart data
+ const labels = [];
+ const temperatureMetrics = [];
+ const relativeHumidityMetrics = [];
+ const barometricPressureMetrics = [];
+ const iaqMetrics = [];
+ for(const deviceMetric of this.selectedNodeEnvironmentMetrics){
+ labels.push(moment(deviceMetric.created_at));
+ temperatureMetrics.push(deviceMetric.temperature);
+ relativeHumidityMetrics.push(deviceMetric.relative_humidity);
+ barometricPressureMetrics.push(deviceMetric.barometric_pressure);
+ iaqMetrics.push(deviceMetric.iaq);
+ }
+
+ // create chart
+ new window.Chart(chartElement, {
+ type: 'line',
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: 'Temperature',
+ suffix: '°C',
+ borderColor: '#3b82f6',
+ backgroundColor: '#3b82f6',
+ pointStyle: false, // no points
+ fill: false,
+ data: temperatureMetrics,
+ yAxisID: 'y',
+ },
+ {
+ label: 'Humidity',
+ suffix: '%',
+ borderColor: '#22c55e',
+ backgroundColor: '#22c55e',
+ pointStyle: false, // no points
+ fill: false,
+ data: relativeHumidityMetrics,
+ yAxisID: 'y',
+ },
+ {
+ label: 'Pressure',
+ suffix: 'hPa',
+ borderColor: '#f97316',
+ backgroundColor: '#f97316',
+ pointStyle: false, // no points
+ fill: false,
+ data: barometricPressureMetrics,
+ yAxisID: 'y1',
+
+ },
+ {
+ label: 'IAQ',
+ suffix: 'IAQ',
+ borderColor: '#f472b6',
+ backgroundColor: '#f472b6',
+ pointStyle: false, // no points
+ fill: false,
+ data: iaqMetrics,
+ yAxisID: 'yIAQ',
+
+ },
+ ],
+ },
+ options: {
+ responsive: true,
+ borderWidth: 2,
+ spanGaps: 1000 * 60 * 60 * 24, // only show lines between metrics with a 24 hour or less gap
+ elements: {
+ point: {
+ radius: 2,
+ },
+ },
+ scales: {
+ x: {
+ position: 'top',
+ type: 'time',
+ time: {
+ unit: 'day',
+ displayFormats: {
+ day: 'MMM DD', // Jan 01
+ },
+ },
+ },
+ y: {
+ min: -20,
+ max: 100,
+ },
+ y1: {
+ min: 800,
+ max: 1100,
+ ticks: {
+ stepSize: 10,
+ callback: (label) => `${label} hPa`,
+ },
+ position: 'right',
+ grid: {
+ drawOnChartArea: false, // only want the grid lines for one axis to show up
+ },
+ },
+ yIAQ: {
+ type: 'linear',
+ display: false,
+ },
+ },
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ mode: "index",
+ intersect: false,
+ callbacks: {
+ label: (item) => {
+ return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`;
+ },
+ },
+ },
+ },
+ }
+ });
+
+ },
+ renderPowerMetricCharts: function() {
+ try {
+ this.updatePowerMetricsChart();
+ } catch(e) {
+ console.log(e);
+ }
+ },
+ updatePowerMetricsChart: function() {
+
+ // destroy existing chart
+ const chartElementId = "powerMetricsChart";
+ const existingChart = window.Chart.getChart(chartElementId);
+ if(existingChart != null){
+ existingChart.destroy();
+ }
+
+ // get chart element
+ const chartElement = window.document.getElementById(chartElementId);
+ if(!chartElement){
+ return;
+ }
+
+ // create chart data
+ const labels = [];
+ const channel1VoltageReadings = [];
+ const channel2VoltageReadings = [];
+ const channel3VoltageReadings = [];
+ const channel1CurrentReadings = [];
+ const channel2CurrentReadings = [];
+ const channel3CurrentReadings = [];
+ for(const powerMetric of this.selectedNodePowerMetrics){
+ labels.push(moment(powerMetric.created_at));
+ channel1VoltageReadings.push(powerMetric.ch1_voltage);
+ channel2VoltageReadings.push(powerMetric.ch2_voltage);
+ channel3VoltageReadings.push(powerMetric.ch3_voltage);
+ channel1CurrentReadings.push(powerMetric.ch1_current);
+ channel2CurrentReadings.push(powerMetric.ch2_current);
+ channel3CurrentReadings.push(powerMetric.ch3_current);
+ }
+
+ // create chart
+ new window.Chart(chartElement, {
+ type: 'line',
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: 'Ch1 Voltage',
+ suffix: "V",
+ borderColor: '#3b82f6',
+ backgroundColor: '#3b82f6',
+ pointStyle: false, // no points
+ fill: false,
+ data: channel1VoltageReadings,
+ yAxisID: 'y',
+ },
+ {
+ label: 'Ch2 Voltage',
+ suffix: "V",
+ borderColor: '#22c55e',
+ backgroundColor: '#22c55e',
+ pointStyle: false, // no points
+ fill: false,
+ data: channel2VoltageReadings,
+ yAxisID: 'y',
+ },
+ {
+ label: 'Ch3 Voltage',
+ suffix: "V",
+ borderColor: '#f97316',
+ backgroundColor: '#f97316',
+ pointStyle: false, // no points
+ fill: false,
+ data: channel3VoltageReadings,
+ yAxisID: 'y',
+ },
+ {
+ label: 'Ch1 Current',
+ suffix: "mA",
+ borderColor: '#93c5fd',
+ backgroundColor: '#93c5fd',
+ pointStyle: false, // no points
+ fill: false,
+ data: channel1CurrentReadings,
+ yAxisID: 'y1',
+ },
+ {
+ label: 'Ch2 Current',
+ suffix: "mA",
+ borderColor: '#86efac',
+ backgroundColor: '#86efac',
+ pointStyle: false, // no points
+ fill: false,
+ data: channel2CurrentReadings,
+ yAxisID: 'y1',
+ },
+ {
+ label: 'Ch3 Current',
+ suffix: "mA",
+ borderColor: '#fdba74',
+ backgroundColor: '#fdba74',
+ pointStyle: false, // no points
+ fill: false,
+ data: channel3CurrentReadings,
+ yAxisID: 'y1',
+ },
+ ],
+ },
+ options: {
+ responsive: true,
+ borderWidth: 2,
+ spanGaps: 1000 * 60 * 60 * 3, // only show lines between metrics with a 3 hour or less gap
+ elements: {
+ point: {
+ radius: 2,
+ },
+ },
+ scales: {
+ x: {
+ position: 'top',
+ type: 'time',
+ time: {
+ unit: 'day',
+ displayFormats: {
+ day: 'MMM DD', // Jan 01
+ },
+ },
+ },
+ y: {
+ min: 0,
+ suggestedMax: 6,
+ ticks: {
+ callback: (label) => `${label}V`,
+ },
+ },
+ y1: {
+ suggestedMin: -50,
+ suggestedMax: 50,
+ ticks: {
+ stepSize: 50,
+ callback: (label) => `${label}mA`,
+ },
+ position: 'right',
+ grid: {
+ drawOnChartArea: false, // only want the grid lines for one axis to show up
+ },
+ },
+ },
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ mode: "index",
+ intersect: false,
+ callbacks: {
+ label: (item) => {
+ return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`;
+ },
+ },
+ },
+ },
+ }
+ });
+
+ },
+ showTraceRoute: function(traceroute) {
+ this.selectedTraceRoute = traceroute;
+ },
+ findNodeById: function(id) {
+ return window.findNodeById(id);
+ },
+ findNodeMarkerById: function(id) {
+ return window.findNodeMarkerById(id);
+ },
+ onSearchResultNodeClick: function(node) {
+
+ // clear search
+ this.searchText = "";
+
+ // hide search
+ this.isShowingMobileSearch = false;
+
+ // go to node
+ if(window.goToNode(node.node_id)){
+ return;
+ }
+
+ // fallback to showing node details since we can't go to the node
+ window.showNodeDetails(node.node_id);
+
+ },
+ dismissInfoModal: function() {
+ this.isShowingInfoModal = false;
+ window.setConfigHasSeenInfoModal(true);
+ },
+ getRegionFrequencyRange: function(regionName) {
+ return window.getRegionFrequencyRange(regionName);
+ },
+ showNodePositionHistory: function(nodeId) {
+
+ // find node
+ const node = findNodeById(nodeId);
+ if(!node){
+ return;
+ }
+
+ // update ui
+ this.selectedNode = null;
+ this.selectedNodeToShowPositionHistory = node;
+ this.isPositionHistoryModalExpanded = true;
+
+ // close node info tooltip as position history shows under it
+ window.closeAllTooltips();
+
+ // reset default time range when opening position history ui
+ // YYYY-MM-DDTHH:mm is the format expected by the datetime-local input type
+ this.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm');
+ this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
+
+ // load position history
+ this.loadNodePositionHistory(nodeId);
+
+ },
+ onPositionHistoryQuickRangeClick: function(range) {
+
+ // update position history time range
+ switch(range){
+ case "1h": {
+ this.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm');
+ this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
+ break;
+ }
+ case "24h": {
+ this.positionHistoryDateTimeFrom = moment().subtract(24, "hours").format('YYYY-MM-DDTHH:mm');
+ this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
+ break;
+ }
+ case "7d": {
+ this.positionHistoryDateTimeFrom = moment().subtract(7, "days").format('YYYY-MM-DDTHH:mm');
+ this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
+ break;
+ }
+ }
+
+ // reload position history
+ const node = this.selectedNodeToShowPositionHistory;
+ if(node){
+ this.loadNodePositionHistory(node.node_id);
+ }
+
+ },
+ getShareLinkForNode: function(nodeId) {
+ return window.location.origin + `/?node_id=${nodeId}`;
+ },
+ copyShareLinkForNode: function(nodeId) {
+
+ // make sure copy to clipboard is supported
+ if(!navigator.clipboard || !navigator.clipboard.writeText){
+ alert("Clipboard not supported. Site must be served via https on iOS.");
+ return;
+ }
+
+ // copy share link to clipboard
+ const url = this.getShareLinkForNode(nodeId);
+ navigator.clipboard.writeText(url);
+
+ // tell user we copied it
+ alert("Link copied to clipboard!");
+
+ },
+ dismissShowingNodeConnections: function() {
+ window._onHideNodeConnectionsClick();
+ this.selectedNodeToShowConnections = null;
+ },
+ dismissShowingNodePositionHistory: function() {
+ this.selectedNodePositionHistory = [];
+ this.selectedNodeToShowPositionHistory = null;
+ this.selectedNodePositionHistoryMarkers = [];
+ this.selectedNodePositionHistoryPolyLines = [];
+ cleanUpPositionHistory();
+ },
+ formatUptimeSeconds: function(secondsToFormat) {
+ secondsToFormat = Number(secondsToFormat);
+ var days = Math.floor(secondsToFormat / (3600 * 24));
+ var hours = Math.floor((secondsToFormat % (3600 * 24)) / 3600);
+ var minutes = Math.floor((secondsToFormat % 3600) / 60);
+ var seconds = Math.floor(secondsToFormat % 60);
+ var daysPlural = days === 1 ? 'day' : 'days';
+ return `${days} ${daysPlural} ${hours}h ${minutes}m ${seconds}s`;
+ },
+ formatTemperature: function(celsius) {
+ switch(this.configTemperatureFormat){
+ case "celsius": {
+ return `${Number(celsius).toFixed(0)}°C`;
+ }
+ case "fahrenheit": {
+ const fahrenheit = this.celsiusToFahrenheit(celsius);
+ return `${fahrenheit.toFixed(0)}°F`;
+ }
+ }
+ },
+ convertTemperature: function(celsius) {
+ switch(this.configTemperatureFormat){
+ case "celsius": {
+ return celsius;
+ }
+ case "fahrenheit": {
+ return this.celsiusToFahrenheit(celsius);
+ }
+ }
+ },
+ getTemperatureUnit: function() {
+ switch(this.configTemperatureFormat){
+ case "celsius": return "°C";
+ case "fahrenheit": return "°F";
+ }
+ },
+ celsiusToFahrenheit: function(celsius) {
+ return (celsius * 9/5) + 32;
+ },
+ getNodeColour(nodeId) {
+ // convert node id to a hex colour
+ return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0');
+ },
+ getNodeTextColour(nodeId) {
+
+ // extract rgb components
+ const r = (nodeId & 0xFF0000) >> 16;
+ const g = (nodeId & 0x00FF00) >> 8;
+ const b = nodeId & 0x0000FF;
+
+ // calculate brightness
+ const brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255;
+
+ // determine text color based on brightness
+ return brightness > 0.5 ? "#000000" : "#FFFFFF";
+
+ },
+ },
+ computed: {
+ searchedNodes() {
+
+ // search nodes
+ const nodes = this.nodes.filter((node) => {
+ const matchesId = node.node_id?.toLowerCase()?.includes(this.searchText.toLowerCase());
+ const matchesHexId = node.node_id_hex?.toLowerCase()?.includes(this.searchText.toLowerCase());
+ const matchesLongName = node.long_name?.toLowerCase()?.includes(this.searchText.toLowerCase());
+ const matchesShortName = node.short_name?.toLowerCase()?.includes(this.searchText.toLowerCase());
+ return matchesId || matchesHexId || matchesLongName || matchesShortName;
+ });
+
+ // order alphabetically by long name
+ nodes.sort((nodeA, nodeB) => {
+ const nodeALongName = nodeA.long_name || "";
+ const nodeBLongName = nodeB.long_name || "";
+ return nodeALongName.localeCompare(nodeBLongName);
+ });
+
+ // only return the first 500 results to avoid ui lag...
+ return nodes.slice(0, 500);
+
+ },
+ selectedNodeLatestPowerMetric() {
+ const [ latestPowerMetric ] = this.selectedNodePowerMetrics.slice(-1);
+ return latestPowerMetric;
+ },
+ },
+ watch: {
+ configNodesMaxAgeInSeconds() {
+ window.setConfigNodesMaxAgeInSeconds(this.configNodesMaxAgeInSeconds);
+ },
+ configNodesOfflineAgeInSeconds() {
+ window.setConfigNodesOfflineAgeInSeconds(this.configNodesOfflineAgeInSeconds);
+ },
+ configWaypointsMaxAgeInSeconds() {
+ window.setConfigWaypointsMaxAgeInSeconds(this.configWaypointsMaxAgeInSeconds);
+ },
+ configConnectionsMaxDistanceInMeters() {
+ window.setConfigConnectionsMaxDistanceInMeters(this.configConnectionsMaxDistanceInMeters);
+ },
+ configZoomLevelGoToNode() {
+ window.setConfigZoomLevelGoToNode(this.configZoomLevelGoToNode);
+ },
+ configAutoUpdatePositionInUrl() {
+ window.setConfigAutoUpdatePositionInUrl(this.configAutoUpdatePositionInUrl);
+ },
+ configEnableMapAnimations() {
+ window.setConfigEnableMapAnimations(this.configEnableMapAnimations);
+ },
+ configTemperatureFormat() {
+ window.setConfigTemperatureFormat(this.configTemperatureFormat);
+ },
+ configConnectionsTimePeriodInSeconds() {
+ window.setConfigConnectionsTimePeriodInSeconds(this.configConnectionsTimePeriodInSeconds);
+ },
+ configConnectionsColoredLines() {
+ window.setConfigConnectionsColoredLines(this.configConnectionsColoredLines);
+ },
+ configConnectionsBidirectionalOnly() {
+ window.setConfigConnectionsBidirectionalOnly(this.configConnectionsBidirectionalOnly);
+ },
+ configConnectionsMinSnrDb() {
+ window.setConfigConnectionsMinSnrDb(this.configConnectionsMinSnrDb);
+ },
+ configConnectionsBidirectionalMinSnr() {
+ window.setConfigConnectionsBidirectionalMinSnr(this.configConnectionsBidirectionalMinSnr);
+ },
+ deviceMetricsTimeRange() {
+ this.loadNodeDeviceMetrics(this.selectedNode.node_id);
+ },
+ environmentMetricsTimeRange() {
+ this.loadNodeEnvironmentMetrics(this.selectedNode.node_id);
+ },
+ powerMetricsTimeRange() {
+ this.loadNodePowerMetrics(this.selectedNode.node_id);
+ },
+ },
+}).mount('#app');
\ No newline at end of file
diff --git a/src/public/assets/js/config.js b/src/public/assets/js/config.js
new file mode 100644
index 0000000..5866a4c
--- /dev/null
+++ b/src/public/assets/js/config.js
@@ -0,0 +1,199 @@
+function getConfigHasSeenInfoModal() {
+ return localStorage.getItem("config_has_seen_info_modal") === "true";
+}
+
+function setConfigHasSeenInfoModal(value) {
+ return localStorage.setItem("config_has_seen_info_modal", value);
+}
+
+function getConfigAutoUpdatePositionInUrl() {
+ // use user preference, or enable by default
+ const value = localStorage.getItem("config_auto_update_position_in_url");
+ return value === "true" || value == null;
+}
+
+function setConfigAutoUpdatePositionInUrl(value) {
+ return localStorage.setItem("config_auto_update_position_in_url", value);
+}
+
+function getConfigEnableMapAnimations() {
+
+ const value = localStorage.getItem("config_enable_map_animations");
+
+ // enable animations by default
+ if(value === null){
+ return true;
+ }
+
+ return value === "true";
+
+}
+
+function setConfigEnableMapAnimations(value) {
+ return localStorage.setItem("config_enable_map_animations", value);
+}
+
+function getConfigTemperatureFormat() {
+ return localStorage.getItem("config_temperature_format") || "celsius";
+}
+
+function setConfigTemperatureFormat(format) {
+ return localStorage.setItem("config_temperature_format", format);
+}
+
+function getConfigMapSelectedTileLayer() {
+ return localStorage.getItem("config_map_selected_tile_layer") || "Thunderforest Neighbourhood";
+}
+
+function setConfigMapSelectedTileLayer(layer) {
+ return localStorage.setItem("config_map_selected_tile_layer", layer);
+}
+
+function getConfigMapEnabledOverlayLayers() {
+
+ try {
+ const value = localStorage.getItem("config_map_enabled_overlay_layers");
+ if(value){
+ return JSON.parse(value);
+ }
+ } catch(e) {}
+
+ // overlays enabled by default
+ return ["Legend", "Position History", "Traceroutes"];
+
+}
+
+function setConfigMapEnabledOverlayLayers(layers) {
+ return localStorage.setItem("config_map_enabled_overlay_layers", JSON.stringify(layers));
+}
+
+function getConfigNodesMaxAgeInSeconds() {
+ const value = localStorage.getItem("config_nodes_max_age_in_seconds");
+ return value != null ? parseInt(value) : null;
+}
+
+function setConfigNodesMaxAgeInSeconds(value) {
+ if(value != null){
+ return localStorage.setItem("config_nodes_max_age_in_seconds", value);
+ } else {
+ return localStorage.removeItem("config_nodes_max_age_in_seconds");
+ }
+}
+
+function getConfigNodesOfflineAgeInSeconds() {
+ const value = localStorage.getItem("config_nodes_offline_age_in_seconds");
+ return value != null ? parseInt(value) : 10800;
+}
+
+function setConfigNodesOfflineAgeInSeconds(value) {
+ if(value != null){
+ return localStorage.setItem("config_nodes_offline_age_in_seconds", value);
+ } else {
+ return localStorage.removeItem("config_nodes_offline_age_in_seconds");
+ }
+}
+
+function getConfigWaypointsMaxAgeInSeconds() {
+ const value = localStorage.getItem("config_waypoints_max_age_in_seconds");
+ return value != null ? parseInt(value) : null;
+}
+
+function setConfigWaypointsMaxAgeInSeconds(value) {
+ if(value != null){
+ return localStorage.setItem("config_waypoints_max_age_in_seconds", value);
+ } else {
+ return localStorage.removeItem("config_waypoints_max_age_in_seconds");
+ }
+}
+
+function getConfigConnectionsMaxDistanceInMeters() {
+ const value = localStorage.getItem("config_connections_max_distance_in_meters");
+ // default to 70km (70,000 meters)
+ return value != null ? parseInt(value) : 70000;
+}
+
+function setConfigConnectionsMaxDistanceInMeters(value) {
+ return localStorage.setItem("config_connections_max_distance_in_meters", value);
+}
+
+function getConfigZoomLevelGoToNode() {
+ const value = localStorage.getItem("config_zoom_level_go_to_node");
+ const parsedValue = value != null ? parseInt(value) : null;
+ return parsedValue || 15;
+}
+
+function setConfigZoomLevelGoToNode(value) {
+ return localStorage.setItem("config_zoom_level_go_to_node", value);
+}
+
+function getConfigConnectionsTimePeriodInSeconds() {
+ const value = localStorage.getItem("config_connections_time_period_in_seconds");
+ // default to 7 days if unset
+ return value != null ? parseInt(value) : 604800;
+}
+
+function setConfigConnectionsTimePeriodInSeconds(value) {
+ return localStorage.setItem("config_connections_time_period_in_seconds", value);
+}
+
+function getConfigConnectionsColoredLines() {
+ const value = localStorage.getItem("config_connections_colored_lines");
+ // disable colored lines by default
+ if(value === null){
+ return false;
+ }
+ return value === "true";
+}
+
+function setConfigConnectionsColoredLines(value) {
+ return localStorage.setItem("config_connections_colored_lines", value);
+}
+
+function getConfigConnectionsBidirectionalOnly() {
+ const value = localStorage.getItem("config_connections_bidirectional_only");
+ // disable bidirectional filter by default
+ if(value === null){
+ return false;
+ }
+ return value === "true";
+}
+
+function setConfigConnectionsBidirectionalOnly(value) {
+ return localStorage.setItem("config_connections_bidirectional_only", value);
+}
+
+function getConfigConnectionsMinSnrDb() {
+ const value = localStorage.getItem("config_connections_min_snr_db");
+ // default to null (unset)
+ if(value === null || value === ""){
+ return null;
+ }
+ const parsed = parseFloat(value);
+ return isNaN(parsed) ? null : parsed;
+}
+
+function setConfigConnectionsMinSnrDb(value) {
+ if(value === null || value === "" || value === undefined){
+ return localStorage.removeItem("config_connections_min_snr_db");
+ }
+ // Convert to string for localStorage (handles both number and string inputs)
+ const stringValue = typeof value === "number" ? value.toString() : String(value);
+ return localStorage.setItem("config_connections_min_snr_db", stringValue);
+}
+
+function getConfigConnectionsBidirectionalMinSnr() {
+ const value = localStorage.getItem("config_connections_bidirectional_min_snr");
+ // disable bidirectional minimum SNR by default
+ if(value === null){
+ return false;
+ }
+ return value === "true";
+}
+
+function setConfigConnectionsBidirectionalMinSnr(value) {
+ return localStorage.setItem("config_connections_bidirectional_min_snr", value);
+}
+
+function isMobile() {
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+}
\ No newline at end of file
diff --git a/src/public/assets/js/map.js b/src/public/assets/js/map.js
new file mode 100644
index 0000000..e25c067
--- /dev/null
+++ b/src/public/assets/js/map.js
@@ -0,0 +1,1692 @@
+// global state
+var nodes = [];
+var nodeMarkers = {};
+var selectedNodeOutlineCircle = null;
+var waypoints = [];
+
+// set map bounds to be a little more than full size to prevent panning off screen
+var bounds = [
+ [-100, 70], // top left
+ [100, 500], // bottom right
+];
+
+// create map positioned over NRW
+if(!isMobile()){
+ var map = L.map('map', {
+ maxBounds: bounds,
+ }).setView([
+ 51.1,
+ 366.82,
+ ], 9);
+} else {
+ var map = L.map('map', {
+ maxBounds: bounds,
+ }).setView([
+ 51.1,
+ 366.82,
+ ], 8);
+}
+
+// remove leaflet link
+map.attributionControl.setPrefix('');
+
+var openThunderforestLandscapeMapTileLayer = L.tileLayer('https://tiles.nixware.dev/landscape/{z}/{x}/{y}.png', {
+ maxZoom: 22,
+ attribution: 'Tiles © Gravitystorm Limited | Data from Meshtastic',
+});
+
+var openThunderforestAtlasMapTileLayer = L.tileLayer('https://tiles.nixware.dev/atlas/{z}/{x}/{y}.png', {
+ maxZoom: 22,
+ attribution: 'Tiles © Gravitystorm Limited | Data from Meshtastic',
+});
+
+var openThunderforestNeighbourhoodMapTileLayer = L.tileLayer('https://tiles.nixware.dev/neighbourhood/{z}/{x}/{y}.png', {
+ maxZoom: 22,
+ attribution: 'Tiles © Gravitystorm Limited | Data from Meshtastic',
+});
+
+var openStreetMapTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 22, // increase from 18 to 22
+ attribution: 'Tiles © OpenStreetMap | Data from Meshtastic',
+});
+
+var openTopoMapTileLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
+ maxZoom: 17, // open topo map doesn't have tiles closer than this
+ attribution: 'Tiles © OpenStreetMap | Data from Meshtastic',
+});
+
+var esriWorldImageryTileLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
+ maxZoom: 21, // esri doesn't have tiles closer than this
+ attribution: 'Tiles © Esri | Data from Meshtastic'
+});
+
+var googleSatelliteTileLayer = L.tileLayer('https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', {
+ maxZoom: 21,
+ subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
+ attribution: 'Tiles © Google | Data from Meshtastic'
+});
+
+var googleHybridTileLayer = L.tileLayer('https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
+ maxZoom: 21,
+ subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
+ attribution: 'Tiles © Google | Data from Meshtastic'
+});
+
+var tileLayers = {
+ "Thunderforest Neighbourhood": openThunderforestNeighbourhoodMapTileLayer,
+ "Thunderforest Landscape": openThunderforestLandscapeMapTileLayer,
+ "Thunderforest Atlas": openThunderforestAtlasMapTileLayer,
+ "OpenStreetMap": openStreetMapTileLayer,
+ "OpenTopoMap": openTopoMapTileLayer,
+ "Esri Satellite": esriWorldImageryTileLayer,
+ "Google Satellite": googleSatelliteTileLayer,
+ "Google Hybrid": googleHybridTileLayer,
+};
+
+// use tile layer based on config
+const selectedTileLayerName = getConfigMapSelectedTileLayer();
+const selectedTileLayer = tileLayers[selectedTileLayerName] || openThunderforestNeighbourhoodMapTileLayer;
+selectedTileLayer.addTo(map);
+
+// create layer groups
+var nodesLayerGroup = new L.LayerGroup();
+var backboneConnectionsLayerGroup = new L.LayerGroup();
+var nodeConnectionsLayerGroup = new L.LayerGroup();
+var nodesClusteredLayerGroup = L.markerClusterGroup({
+ showCoverageOnHover: false,
+ disableClusteringAtZoom: 10, // zoom level where node clustering is disabled
+});
+var nodesRouterLayerGroup = L.markerClusterGroup({
+ showCoverageOnHover: false,
+ disableClusteringAtZoom: 10, // zoom level where node clustering is disabled
+});
+var nodesBackboneLayerGroup = new L.LayerGroup();
+//var nodesMediumFastLayerGroup = new L.LayerGroup();
+var nodesShortSlowLayerGroup = new L.LayerGroup();
+var nodesLongFastLayerGroup = new L.LayerGroup();
+var waypointsLayerGroup = new L.LayerGroup();
+var nodePositionHistoryLayerGroup = new L.LayerGroup();
+var traceroutesLayerGroup = new L.LayerGroup();
+var connectionsLayerGroup = new L.LayerGroup();
+
+// create icons
+var iconMqttConnected = L.divIcon({
+ className: 'icon-mqtt-connected',
+ iconSize: [16, 16], // increase from 12px to 16px to make hover easier
+});
+
+var iconLongFast = L.divIcon({
+ className: 'icon-longfast',
+ iconSize: [16, 16], // increase from 12px to 16px to make hover easier
+});
+
+/*var iconMediumFast = L.divIcon({
+ className: 'icon-mediumfast',
+ iconSize: [16, 16], // increase from 12px to 16px to make hover easier
+});*/
+
+var iconShortSlow = L.divIcon({
+ className: 'icon-shortslow',
+ iconSize: [16, 16],
+});
+
+var iconMqttDisconnected = L.divIcon({
+ className: 'icon-mqtt-disconnected',
+ iconSize: [16, 16], // increase from 12px to 16px to make hover easier
+});
+
+var iconOffline = L.divIcon({
+ className: 'icon-offline',
+ iconSize: [16, 16], // increase from 12px to 16px to make hover easier
+});
+
+var iconPositionHistory = L.divIcon({
+ className: 'icon-position-history',
+ iconSize: [16, 16], // increase from 12px to 16px to make hover easier
+});
+
+var iconTracerouteStart = L.divIcon({
+ className: 'icon-traceroute-start',
+ iconSize: [16, 16],
+});
+
+var iconTracerouteEnd = L.divIcon({
+ className: 'icon-traceroute-end',
+ iconSize: [16, 16],
+});
+
+// create legend
+var legendLayerGroup = new L.LayerGroup();
+var legend = L.control({position: 'bottomleft'});
+legend.onAdd = function (map) {
+ var div = L.DomUtil.create('div', 'leaflet-control-layers');
+ div.style.backgroundColor = 'white';
+ div.style.padding = '12px';
+ div.innerHTML = `Legend
`
+ + ``
+ //+ ``
+ + ``
+ + ``
+ + ` Traceroute
`;
+ return div;
+};
+
+// handle baselayerchange to update tile layer preference
+map.on('baselayerchange', function(event) {
+ setConfigMapSelectedTileLayer(event.name);
+});
+
+// handle adding/remove legend on map (can't use L.Control as an overlay, so we toggle an empty L.LayerGroup)
+map.on('overlayadd overlayremove', function(event) {
+ if(event.name === "Legend"){
+ if(event.type === "overlayadd"){
+ map.addControl(legend);
+ } else if(event.type === "overlayremove"){
+ map.removeControl(legend);
+ }
+ }
+});
+
+// add layers to control ui
+L.control.groupedLayers(tileLayers, {
+ "Nodes": {
+ "All": nodesLayerGroup,
+ "Routers": nodesRouterLayerGroup,
+ "Backbone": nodesBackboneLayerGroup,
+ "ShortSlow": nodesShortSlowLayerGroup,
+ //"MediumFast": nodesMediumFastLayerGroup,
+ "LongFast": nodesLongFastLayerGroup,
+ "Clustered": nodesClusteredLayerGroup,
+ "None": new L.LayerGroup(),
+ },
+ "Overlays": {
+ "Legend": legendLayerGroup,
+ "Backbone Connections": backboneConnectionsLayerGroup,
+ "Connections": connectionsLayerGroup,
+ "Waypoints": waypointsLayerGroup,
+ "Position History": nodePositionHistoryLayerGroup,
+ "Traceroutes": traceroutesLayerGroup,
+ },
+}, {
+ // make the "Nodes" group exclusive (use radio inputs instead of checkbox)
+ exclusiveGroups: ["Nodes"],
+}).addTo(map);
+
+// enable base layers
+nodesLayerGroup.addTo(map);
+
+// enable overlay layers based on config
+const enabledOverlayLayers = getConfigMapEnabledOverlayLayers();
+if(enabledOverlayLayers.includes("Legend")){
+ legendLayerGroup.addTo(map);
+}
+if(enabledOverlayLayers.includes("Backbone Connection")){
+ backboneConnectionsLayerGroup.addTo(map);
+}
+if(enabledOverlayLayers.includes("Connections")){
+ connectionsLayerGroup.addTo(map);
+}
+if(enabledOverlayLayers.includes("Waypoints")){
+ waypointsLayerGroup.addTo(map);
+}
+if(enabledOverlayLayers.includes("Position History")){
+ nodePositionHistoryLayerGroup.addTo(map);
+}
+if(enabledOverlayLayers.includes("Traceroutes")){
+ traceroutesLayerGroup.addTo(map);
+}
+
+map.on('overlayadd', function(event) {
+ // update config when map overlay is added
+ const layerName = event.name;
+ const enabledOverlayLayers = getConfigMapEnabledOverlayLayers();
+ if(!enabledOverlayLayers.includes(layerName)){
+ enabledOverlayLayers.push(layerName);
+ }
+ setConfigMapEnabledOverlayLayers(enabledOverlayLayers);
+
+ // clear traceroutes layer when traceroutes overlay is added
+ if (layerName === "Traceroutes") {
+ traceroutesLayerGroup.clearLayers();
+ }
+});
+
+// update config when map overlay is removed
+map.on('overlayremove', function(event) {
+ const layerName = event.name;
+ const enabledOverlayLayers = getConfigMapEnabledOverlayLayers().filter(function(enabledOverlayLayer) {
+ return enabledOverlayLayer !== layerName;
+ });
+ setConfigMapEnabledOverlayLayers(enabledOverlayLayers);
+});
+
+// handle map clicks
+map.on('click', function() {
+
+ // remove outline when map clicked
+ clearNodeOutline();
+
+ // send callback to vue
+ window._onMapClick();
+
+});
+
+// close all tooltips and popups when clicking map
+map.on("click", function(event) {
+
+ // do nothing when clicking inside tooltip
+ const clickedElement = event.originalEvent.target;
+ if(elementOrAnyAncestorHasClass(clickedElement, "leaflet-tooltip")){
+ return;
+ }
+
+ closeAllTooltips();
+ closeAllPopups();
+
+});
+
+function isValidLatLng(lat, lng) {
+
+ if(isNaN(lat) || isNaN(lng)){
+ return false;
+ }
+
+ return true;
+
+}
+
+function findNodeById(id) {
+
+ // find node by id
+ var node = nodes.find((node) => node.node_id.toString() === id.toString());
+ if(node){
+ return node;
+ }
+
+ return null;
+
+}
+
+function findNodeMarkerById(id) {
+
+ // find node marker by id
+ var nodeMarker = nodeMarkers[id];
+ if(nodeMarker){
+ return nodeMarker;
+ }
+
+ return null;
+
+}
+
+function goToNode(id, animate, zoom){
+
+ // find node
+ var node = findNodeById(id);
+ if(!node){
+ alert("Could not find node: " + id);
+ return false;
+ }
+
+ // find node marker by id
+ var nodeMarker = findNodeMarkerById(id);
+ if(!nodeMarker){
+ return false;
+ }
+
+ // close all popups and tooltips
+ closeAllPopups();
+ closeAllTooltips();
+
+ // select node
+ showNodeOutline(id);
+
+ // fly to node marker
+ const shouldAnimate = animate != null ? animate : true;
+ map.flyTo(nodeMarker.getLatLng(), zoom || getConfigZoomLevelGoToNode(), {
+ animate: getConfigEnableMapAnimations() ? shouldAnimate : false,
+ });
+
+ // open tooltip for node
+ map.openTooltip(getTooltipContentForNode(node), nodeMarker.getLatLng(), {
+ interactive: true, // allow clicking buttons inside tooltip
+ permanent: true, // don't auto dismiss when clicking buttons inside tooltip
+ });
+
+ // successfully went to node
+ return true;
+
+}
+
+function goToRandomNode() {
+ if(nodes.length > 0){
+ const randomNode = nodes[Math.floor(Math.random() * nodes.length)];
+ if(randomNode){
+
+ // go to node
+ if(window.goToNode(randomNode.node_id)){
+ return;
+ }
+
+ // fallback to showing node details since we can't go to the node
+ window.showNodeDetails(randomNode.node_id);
+
+ }
+ }
+}
+
+function clearAllNodes() {
+ nodesLayerGroup.clearLayers();
+ nodesClusteredLayerGroup.clearLayers();
+ nodesRouterLayerGroup.clearLayers();
+ nodesBackboneLayerGroup.clearLayers();
+ nodesShortSlowLayerGroup.clearLayers();
+ //nodesMediumFastLayerGroup.clearLayers();
+ nodesLongFastLayerGroup.clearLayers();
+}
+
+function clearAllBackboneConnections() {
+ backboneConnectionsLayerGroup.clearLayers();
+}
+
+function clearAllWaypoints() {
+ waypointsLayerGroup.clearLayers();
+}
+
+function clearAllTraceroutes() {
+ traceroutesLayerGroup.clearLayers();
+}
+
+function clearAllConnections() {
+ connectionsLayerGroup.clearLayers();
+ backboneConnectionsLayerGroup.clearLayers();
+}
+
+function closeAllPopups() {
+ map.eachLayer(function(layer) {
+ if(layer.options.pane === "popupPane"){
+ layer.removeFrom(map);
+ }
+ });
+}
+
+function closeAllTooltips() {
+ map.eachLayer(function(layer) {
+ if(layer.options.pane === "tooltipPane"){
+ layer.removeFrom(map);
+ }
+ });
+}
+
+function clearAllPositionHistory() {
+ nodePositionHistoryLayerGroup.clearLayers();
+}
+
+function clearNodeOutline() {
+ if(selectedNodeOutlineCircle){
+ selectedNodeOutlineCircle.removeFrom(map);
+ selectedNodeOutlineCircle = null;
+ }
+}
+
+function showNodeOutline(id) {
+
+ // remove any existing node circle
+ clearNodeOutline();
+
+ // find node marker by id
+ const nodeMarker = nodeMarkers[id];
+ if(!nodeMarker){
+ return;
+ }
+
+ // find node by id
+ const node = findNodeById(id);
+ if(!node){
+ return;
+ }
+
+ // add position precision circle around node
+ if(node.position_precision != null && node.position_precision > 0 && node.position_precision < 32){
+ selectedNodeOutlineCircle = L.circle(nodeMarker.getLatLng(), {
+ radius: getPositionPrecisionInMeters(node.position_precision),
+ }).addTo(map);
+ }
+
+}
+
+function showNodeDetails(id) {
+
+ // find node
+ const node = findNodeById(id);
+ if(!node){
+ return;
+ }
+
+ // fire callback to vuejs handler
+ window._onNodeClick(node);
+
+}
+
+function getColourForSnr(snr) {
+ if(snr >= -4) return "#16a34a"; // good
+ if(snr > -8) return "#fff200"; // medium-good
+ if(snr > -12) return "#ff9f1c"; // medium
+ return "#dc2626"; // bad
+}
+
+function getSignalBarsIndicator(snrDb) {
+ if(snrDb == null) return '';
+
+ // Determine number of bars based on SNR
+ let bars = 0;
+ if(snrDb >= -4) bars = 4; // good
+ else if(snrDb > -8) bars = 3; // medium-good
+ else if(snrDb > -12) bars = 2; // medium
+ else bars = 1; // bad
+
+ const color = getColourForSnr(snrDb);
+
+ // Create 4 bars with increasing height
+ let indicator = '';
+
+ // Bar heights: 4px, 6px, 8px, 10px
+ const barHeights = [4, 6, 8, 10];
+ const barWidth = 2;
+
+ for (let i = 0; i < 4; i++) {
+ const height = barHeights[i];
+ const isActive = i < bars;
+ const barColor = isActive ? color : '#d1d5db'; // gray for inactive bars
+ indicator += ``;
+ }
+
+ indicator += '';
+ return indicator;
+}
+
+function cleanUpNodeConnections() {
+
+ // close tooltips and popups
+ closeAllPopups();
+ closeAllTooltips();
+
+ // setup node connections layer
+ nodeConnectionsLayerGroup.clearLayers();
+ nodeConnectionsLayerGroup.removeFrom(map);
+ nodeConnectionsLayerGroup.addTo(map);
+
+}
+
+function getTerrainProfileImage(node1, node2) {
+
+ // line colour between nodes
+ const lineColour = "0000FF"; // blue
+
+ // node 1 (left side of image)
+ const node1MarkerColour = "0000FF"; // blue
+ const node1Latitude = node1.latitude;
+ const node1Longitude = node1.longitude;
+ const node1ElevationMSL = node1.altitude ?? "";
+
+ // node 2 (right side of image)
+ const node2MarkerColour = "0000FF"; // blue
+ const node2Latitude = node2.latitude;
+ const node2Longitude = node2.longitude;
+ const node2ElevationMSL = node2.altitude ?? "";
+
+ // generate terrain profile image url
+ return "https://heywhatsthat.com/bin/profile-0904.cgi?" + new URLSearchParams({
+ src: "meshtastic.liamcottle.net",
+ axes: 1, // include grid lines and a scale
+ metric: 1, // show metric units
+ curvature: 1,
+ width: 500,
+ height: 200,
+ pt0: `${node1Latitude},${node1Longitude},${lineColour},${node1ElevationMSL},${node1MarkerColour}`,
+ pt1: `${node2Latitude},${node2Longitude},${lineColour},${node2ElevationMSL},${node2MarkerColour}`,
+ }).toString();
+
+}
+
+async function showNodeConnections(id) {
+
+ cleanUpNodeConnections();
+
+ // find node
+ const node = findNodeById(id);
+ if(!node){
+ return;
+ }
+
+ // find node marker
+ const nodeMarker = findNodeMarkerById(node.node_id);
+ if(!nodeMarker){
+ return;
+ }
+
+ // show overlay for node connections
+ window._onShowNodeConnectionsClick(node);
+
+ // Fetch connections for this node
+ const connectionsTimePeriodSec = getConfigConnectionsTimePeriodInSeconds();
+ const connectionsTimeFrom = connectionsTimePeriodSec ? (Date.now() - connectionsTimePeriodSec * 1000) : undefined;
+ const connectionsParams = new URLSearchParams();
+ connectionsParams.set('node_id', id);
+ if (connectionsTimeFrom) connectionsParams.set('time_from', connectionsTimeFrom);
+
+ try {
+ const response = await window.axios.get(`/api/v1/connections?${connectionsParams.toString()}`);
+ const connections = response.data.connections ?? [];
+
+ for (const connection of connections) {
+ // Convert to numbers for comparison since API returns strings
+ const nodeA = parseInt(connection.node_a, 10);
+ const nodeB = parseInt(connection.node_b, 10);
+
+ const otherNodeId = nodeA === id ? nodeB : nodeA;
+ const otherNode = findNodeById(otherNodeId);
+ const otherNodeMarker = findNodeMarkerById(otherNodeId);
+
+ if (!otherNode || !otherNodeMarker) continue;
+
+ // Apply bidirectional filter
+ const configConnectionsBidirectionalOnly = getConfigConnectionsBidirectionalOnly();
+ if(configConnectionsBidirectionalOnly){
+ const hasDirectionAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null;
+ const hasDirectionBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null;
+ if(!hasDirectionAB || !hasDirectionBA){
+ continue;
+ }
+ }
+
+ // Apply minimum SNR filter
+ const configConnectionsMinSnrDb = getConfigConnectionsMinSnrDb();
+ if(configConnectionsMinSnrDb != null){
+ const snrAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null ? connection.direction_ab.avg_snr_db : null;
+ const snrBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null ? connection.direction_ba.avg_snr_db : null;
+
+ const configConnectionsBidirectionalMinSnr = getConfigConnectionsBidirectionalMinSnr();
+ let hasSnrAboveThreshold;
+
+ if(configConnectionsBidirectionalMinSnr){
+ // Bidirectional mode: ALL existing directions must meet threshold
+ const directionsToCheck = [];
+ if(snrAB != null) directionsToCheck.push(snrAB);
+ if(snrBA != null) directionsToCheck.push(snrBA);
+
+ if(directionsToCheck.length === 0){
+ // No SNR data in either direction, skip
+ hasSnrAboveThreshold = false;
+ } else {
+ // All existing directions must be above threshold
+ hasSnrAboveThreshold = directionsToCheck.every(snr => snr > configConnectionsMinSnrDb);
+ }
+ } else {
+ // Default mode: EITHER direction has SNR above threshold
+ hasSnrAboveThreshold = (snrAB != null && snrAB > configConnectionsMinSnrDb) || (snrBA != null && snrBA > configConnectionsMinSnrDb);
+ }
+
+ if(!hasSnrAboveThreshold){
+ continue;
+ }
+ }
+
+ // Calculate distance
+ const distanceInMeters = nodeMarker.getLatLng().distanceTo(otherNodeMarker.getLatLng()).toFixed(2);
+ const configConnectionsMaxDistanceInMeters = getConfigConnectionsMaxDistanceInMeters();
+ if(configConnectionsMaxDistanceInMeters != null && parseFloat(distanceInMeters) > configConnectionsMaxDistanceInMeters){
+ continue;
+ }
+
+ let distance = `${distanceInMeters} meters`;
+ if (distanceInMeters >= 1000) {
+ const distanceInKilometers = (distanceInMeters / 1000).toFixed(2);
+ distance = `${distanceInKilometers} kilometers`;
+ }
+
+ // Determine line color
+ const configConnectionsColoredLines = getConfigConnectionsColoredLines();
+ const worstSnrDb = connection.worst_avg_snr_db;
+ const lineColor = configConnectionsColoredLines && worstSnrDb != null ? getColourForSnr(worstSnrDb) : '#2563eb';
+
+ // Create bidirectional line
+ const line = L.polyline([
+ nodeMarker.getLatLng(),
+ otherNodeMarker.getLatLng(),
+ ], {
+ color: lineColor,
+ opacity: 0.75,
+ weight: 3,
+ }).addTo(nodeConnectionsLayerGroup);
+
+ // Generate tooltip using standardized function
+ const tooltipNodeA = findNodeById(connection.node_a);
+ const tooltipNodeB = findNodeById(connection.node_b);
+ const tooltip = generateConnectionTooltip(connection, tooltipNodeA, tooltipNodeB, distance);
+
+ line.bindTooltip(tooltip, {
+ sticky: true,
+ opacity: 1,
+ interactive: true,
+ })
+ .bindPopup(tooltip)
+ .on('click', function(event) {
+ event.target.closeTooltip();
+ });
+ }
+ } catch (err) {
+ console.error('Error fetching connections:', err);
+ }
+
+}
+
+function clearMap() {
+ closeAllPopups();
+ closeAllTooltips();
+ clearAllNodes();
+ clearAllBackboneConnections();
+ clearAllWaypoints();
+ clearAllTraceroutes();
+ clearAllConnections();
+ clearNodeOutline();
+ cleanUpNodeConnections();
+}
+
+// returns true if the element or one of its parents has the class classname
+function elementOrAnyAncestorHasClass(element, className) {
+
+ // check if element contains class
+ if(element.classList && element.classList.contains(className)){
+ return true;
+ }
+
+ // check if parent node has the class
+ if(element.parentNode){
+ return elementOrAnyAncestorHasClass(element.parentNode, className);
+ }
+
+ // couldn't find the class
+ return false;
+
+}
+
+// escape strings for tooltips etc, to prevent html/script injection
+// not used in vuejs, as that auto escapes
+function escapeString(string) {
+ return string.replace(//g, ">");
+}
+
+
+function onNodesUpdated(updatedNodes) {
+
+ // clear nodes cache
+ nodes = [];
+
+ // get config
+ const now = moment();
+ const configNodesMaxAgeInSeconds = getConfigNodesMaxAgeInSeconds();
+ const configNodesOfflineAgeInSeconds = getConfigNodesOfflineAgeInSeconds();
+ const configConnectionsMaxDistanceInMeters = getConfigConnectionsMaxDistanceInMeters();
+
+ // add nodes
+ for(const node of updatedNodes){
+
+ // skip nodes older than configured node max age
+ if(configNodesMaxAgeInSeconds){
+ const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
+ if(lastUpdatedAgeInMillis > configNodesMaxAgeInSeconds * 1000){
+ continue;
+ }
+ }
+
+ // add to cache
+ nodes.push(node);
+
+ // skip nodes without position
+ if(!node.latitude || !node.longitude){
+ continue;
+ }
+
+ // fix lat long
+ node.latitude = node.latitude / 10000000;
+ node.longitude = node.longitude / 10000000;
+
+ // skip nodes with invalid position
+ if(!isValidLatLng(node.latitude, node.longitude)){
+ continue;
+ }
+
+ // wrap longitude for shortest path, everything to left of australia should be shown on the right
+ var longitude = parseFloat(node.longitude);
+ if(longitude <= 100){
+ longitude += 360;
+ }
+
+ // icon based on channel preset
+ var icon = iconLongFast;
+
+ if (node.channel_id == "ShortSlow") {
+ icon = iconShortSlow;
+ }
+
+ /*if (node.channel_id == "MediumFast") {
+ icon = iconMediumFast;
+ }*/
+
+ // use offline icon for nodes older than configured node offline age
+ if(configNodesOfflineAgeInSeconds){
+ const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
+ if(lastUpdatedAgeInMillis > configNodesOfflineAgeInSeconds * 1000){
+ icon = iconOffline;
+ }
+ }
+
+ // determine zIndexOffset: MediumFast (1000), LongFast (-1000), Offline (-2000)
+ var zIndexOffset = 1000;
+ if(icon == iconOffline){
+ zIndexOffset = -2000;
+ } else if(node.channel_id == 'LongFast'){
+ zIndexOffset = -1000;
+ }
+
+ // To not have overlapping nodes.
+ var latJitter = 0;
+ var lonJitter = 0;
+ // If position pression > 45m apply random jitter within a small circle to avoid diagonal-only displacement
+ if (node.position_precision < 19) {
+ const maxMeters = 40;
+ const r = maxMeters * Math.sqrt(Math.random());
+ const theta = 2 * Math.PI * Math.random();
+ const dy = r * Math.sin(theta);
+ const dx = r * Math.cos(theta);
+ const metersPerDegLat = 111320;
+ const metersPerDegLon = 111320 * Math.cos(node.latitude * Math.PI / 180);
+ latJitter = dy / metersPerDegLat;
+ lonJitter = metersPerDegLon ? (dx / metersPerDegLon) : 0;
+ }
+
+ // create node marker
+ const marker = L.marker([node.latitude + latJitter, longitude + lonJitter], {
+ icon: icon,
+ tagName: node.node_id,
+ // zIndex: offline (-2000) < has channel_id (-1000) < others (1000)
+ zIndexOffset: zIndexOffset,
+ }).on('click', function(event) {
+ // close tooltip on click to prevent tooltip and popup showing at same time
+ event.target.closeTooltip();
+ });
+
+ // add marker to node layer groups
+ marker.addTo(nodesLayerGroup);
+ nodesClusteredLayerGroup.addLayer(marker);
+
+ // add markers for routers and repeaters to routers layer group
+ if(node.role_name === "ROUTER"
+ || node.role_name === "ROUTER_CLIENT"
+ || node.role_name === "ROUTER_LATE"
+ || node.role_name === "REPEATER"){
+ nodesRouterLayerGroup.addLayer(marker);
+ }
+
+ // add markers for backbone to layer group
+ if(node.is_backbone) {
+ nodesBackboneLayerGroup.addLayer(marker);
+ }
+
+ if(node.channel_id == "ShortSlow") {
+ nodesShortSlowLayerGroup.addLayer(marker);
+ }
+
+ // add markers for MediumFast channel to layer group
+ /*if(node.channel_id == "MediumFast") {
+ nodesMediumFastLayerGroup.addLayer(marker);
+ }*/
+
+ // add markers for LongFast channel to layer group
+ if(node.channel_id == "LongFast") {
+ nodesLongFastLayerGroup.addLayer(marker);
+ }
+
+ // show tooltip on desktop only
+ if(!isMobile()){
+ marker.bindTooltip(getTooltipContentForNode(node), {
+ interactive: true,
+ });
+ }
+
+ // show node info tooltip when clicking node marker
+ marker.on("click", function(event) {
+
+ // close all other popups and tooltips
+ closeAllTooltips();
+ closeAllPopups();
+
+ // find node
+ const node = findNodeById(event.target.options.tagName);
+ if(!node){
+ return;
+ }
+
+ // show position precision outline
+ showNodeOutline(node.node_id);
+
+ // open tooltip for node
+ map.openTooltip(getTooltipContentForNode(node), event.target.getLatLng(), {
+ interactive: true, // allow clicking buttons inside tooltip
+ permanent: true, // don't auto dismiss when clicking buttons inside tooltip
+ });
+
+ });
+
+ // add to cache
+ nodeMarkers[node.node_id] = marker;
+
+ }
+
+ window._onNodesUpdated(nodes);
+
+}
+
+function onWaypointsUpdated(updatedWaypoints) {
+
+ // clear nodes cache
+ waypoints = [];
+
+ // get config
+ const now = moment();
+ const configWaypointsMaxAgeInSeconds = getConfigWaypointsMaxAgeInSeconds();
+
+ // add nodes
+ for(const waypoint of updatedWaypoints){
+
+ // skip waypoints older than configured waypoint max age
+ if(configWaypointsMaxAgeInSeconds){
+ const lastUpdatedAgeInMillis = now.diff(moment(waypoint.updated_at));
+ if(lastUpdatedAgeInMillis > configWaypointsMaxAgeInSeconds * 1000){
+ continue;
+ }
+ }
+
+ // skip expired waypoints
+ if(waypoint.expire < Date.now() / 1000){
+ continue;
+ }
+
+ // skip waypoints without position
+ if(!waypoint.latitude || !waypoint.longitude){
+ continue;
+ }
+
+ // fix lat long
+ waypoint.latitude = waypoint.latitude / 10000000;
+ waypoint.longitude = waypoint.longitude / 10000000;
+
+ // skip waypoints with invalid position
+ if(!isValidLatLng(waypoint.latitude, waypoint.longitude)){
+ continue;
+ }
+
+ // wrap longitude for shortest path, everything to left of australia should be shown on the right
+ var longitude = parseFloat(waypoint.longitude);
+ if(longitude <= 100){
+ longitude += 360;
+ }
+
+ // determine emoji to show as marker icon
+ const emoji = waypoint.icon === 0 ? 128205 : waypoint.icon;
+ const emojiText = String.fromCodePoint(emoji)
+
+ var tooltip = getTooltipContentForWaypoint(waypoint);
+
+ // create waypoint marker
+ const marker = L.marker([waypoint.latitude, longitude], {
+ icon: L.divIcon({
+ className: 'waypoint-label',
+ iconSize: [26, 26], // increase from 12px to 26px
+ html: emojiText,
+ }),
+ }).bindPopup(tooltip).on('click', function(event) {
+ // close tooltip on click to prevent tooltip and popup showing at same time
+ event.target.closeTooltip();
+ });
+
+ // show tooltip on desktop only
+ if(!isMobile()){
+ marker.bindTooltip(tooltip, {
+ interactive: true,
+ });
+ }
+
+ // add marker to waypoints layer groups
+ marker.addTo(waypointsLayerGroup);
+
+ // add to cache
+ waypoints.push(waypoint);
+
+ }
+
+}
+
+
+function generateConnectionTooltip(connection, nodeA, nodeB, distance) {
+ let tooltip = `Connection`;
+ tooltip += `
[${escapeString(nodeA.short_name)}] ${escapeString(nodeA.long_name)} <-> [${escapeString(nodeB.short_name)}] ${escapeString(nodeB.long_name)}`;
+ tooltip += `
Distance: ${distance}`;
+ tooltip += `
`;
+
+ // Direction A -> B
+ if (connection.direction_ab.avg_snr_db != null) {
+ tooltip += `
${escapeString(nodeA.short_name)} -> ${escapeString(nodeB.short_name)}:`;
+ tooltip += `
SNR: ${connection.direction_ab.avg_snr_db.toFixed(1)}dB ${getSignalBarsIndicator(connection.direction_ab.avg_snr_db)} (Average of ${connection.direction_ab.total_count} edges)`;
+ if (connection.direction_ab.last_5_edges.length > 0) {
+ tooltip += `
Last 5 edges:`;
+ for (const edge of connection.direction_ab.last_5_edges) {
+ const timeAgo = moment(new Date(edge.created_at)).fromNow();
+ const sourceIcon = edge.source === "TRACEROUTE_APP" ? "⇵" : (edge.source === "NEIGHBORINFO_APP" ? "✳" : "?");
+ tooltip += `
${edge.snr_db.toFixed(1)}dB ${getSignalBarsIndicator(edge.snr_db)} (${timeAgo} by:${sourceIcon})`;
+ }
+ } else {
+ tooltip += `
No recent edges`;
+ }
+ }
+
+ // Direction B -> A
+ if (connection.direction_ba.avg_snr_db != null) {
+ tooltip += `
${escapeString(nodeB.short_name)} -> ${escapeString(nodeA.short_name)}:`;
+ tooltip += `
SNR: ${connection.direction_ba.avg_snr_db.toFixed(1)}dB ${getSignalBarsIndicator(connection.direction_ba.avg_snr_db)} (Average of ${connection.direction_ba.total_count} edges)`;
+ if (connection.direction_ba.last_5_edges.length > 0) {
+ tooltip += `
Last 5 edges:`;
+ for (const edge of connection.direction_ba.last_5_edges) {
+ const timeAgo = moment(new Date(edge.created_at)).fromNow();
+ const sourceIcon = edge.source === "TRACEROUTE_APP" ? "⇵" : (edge.source === "NEIGHBORINFO_APP" ? "✳" : "?");
+ tooltip += `
${edge.snr_db.toFixed(1)}dB ${getSignalBarsIndicator(edge.snr_db)} (${timeAgo} by:${sourceIcon})`;
+ }
+ } else {
+ tooltip += `
No recent edges`;
+ }
+ }
+
+ // Add terrain profile image
+ const terrainImageUrl = getTerrainProfileImage(nodeA, nodeB);
+ tooltip += `
Terrain images from HeyWhatsThat.com`;
+ tooltip += `
`;
+
+ return tooltip;
+}
+
+function onConnectionsUpdated(connections) {
+ // Clear existing connections
+ clearAllConnections();
+
+ for (const connection of connections) {
+ // Find both node markers
+ const nodeAMarker = findNodeMarkerById(connection.node_a);
+ const nodeBMarker = findNodeMarkerById(connection.node_b);
+
+ // Skip if either node marker doesn't exist
+ if (!nodeAMarker || !nodeBMarker) {
+ continue;
+ }
+
+ // Find node objects for names and terrain profile
+ const nodeA = findNodeById(connection.node_a);
+ const nodeB = findNodeById(connection.node_b);
+
+ if (!nodeA || !nodeB) {
+ continue;
+ }
+
+ // Apply bidirectional filter
+ const configConnectionsBidirectionalOnly = getConfigConnectionsBidirectionalOnly();
+ if(configConnectionsBidirectionalOnly){
+ const hasDirectionAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null;
+ const hasDirectionBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null;
+ if(!hasDirectionAB || !hasDirectionBA){
+ continue;
+ }
+ }
+
+ // Apply minimum SNR filter
+ const configConnectionsMinSnrDb = getConfigConnectionsMinSnrDb();
+ if(configConnectionsMinSnrDb != null){
+ const snrAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null ? connection.direction_ab.avg_snr_db : null;
+ const snrBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null ? connection.direction_ba.avg_snr_db : null;
+
+ const configConnectionsBidirectionalMinSnr = getConfigConnectionsBidirectionalMinSnr();
+ let hasSnrAboveThreshold;
+
+ if(configConnectionsBidirectionalMinSnr){
+ // Bidirectional mode: ALL existing directions must meet threshold
+ const directionsToCheck = [];
+ if(snrAB != null) directionsToCheck.push(snrAB);
+ if(snrBA != null) directionsToCheck.push(snrBA);
+
+ if(directionsToCheck.length === 0){
+ // No SNR data in either direction, skip
+ hasSnrAboveThreshold = false;
+ } else {
+ // All existing directions must be above threshold
+ hasSnrAboveThreshold = directionsToCheck.every(snr => snr > configConnectionsMinSnrDb);
+ }
+ } else {
+ // Default mode: EITHER direction has SNR above threshold
+ hasSnrAboveThreshold = (snrAB != null && snrAB > configConnectionsMinSnrDb) || (snrBA != null && snrBA > configConnectionsMinSnrDb);
+ }
+
+ if(!hasSnrAboveThreshold){
+ continue;
+ }
+ }
+
+ // Calculate distance between nodes
+ const distanceInMeters = nodeAMarker.getLatLng().distanceTo(nodeBMarker.getLatLng()).toFixed(2);
+
+ // Apply distance filter
+ const configConnectionsMaxDistanceInMeters = getConfigConnectionsMaxDistanceInMeters();
+ if(configConnectionsMaxDistanceInMeters != null && parseFloat(distanceInMeters) > configConnectionsMaxDistanceInMeters){
+ continue;
+ }
+
+ let distance = `${distanceInMeters} meters`;
+ if (distanceInMeters >= 1000) {
+ const distanceInKilometers = (distanceInMeters / 1000).toFixed(2);
+ distance = `${distanceInKilometers} kilometers`;
+ }
+
+ // Determine line color based on worst average SNR (if colored lines enabled)
+ const configConnectionsColoredLines = getConfigConnectionsColoredLines();
+ const worstSnrDb = connection.worst_avg_snr_db;
+ const lineColor = configConnectionsColoredLines && worstSnrDb != null ? getColourForSnr(worstSnrDb) : '#2563eb';
+
+ // Create polyline (bidirectional, no arrows)
+ const line = L.polyline([
+ nodeAMarker.getLatLng(),
+ nodeBMarker.getLatLng(),
+ ], {
+ color: lineColor,
+ opacity: 0.75,
+ weight: 3,
+ }).addTo(connectionsLayerGroup);
+
+ // Generate tooltip
+ const tooltip = generateConnectionTooltip(connection, nodeA, nodeB, distance);
+
+ // Bind tooltip and popup
+ line.bindTooltip(tooltip, {
+ sticky: true,
+ opacity: 1,
+ interactive: true,
+ })
+ .bindPopup(tooltip)
+ .on('click', function(event) {
+ // close tooltip on click to prevent tooltip and popup showing at same time
+ event.target.closeTooltip();
+ });
+
+ // If both nodes are backbone nodes, also add to backbone layer group
+ if (nodeA.is_backbone && nodeB.is_backbone) {
+ const backboneLine = L.polyline([
+ nodeAMarker.getLatLng(),
+ nodeBMarker.getLatLng(),
+ ], {
+ color: lineColor,
+ opacity: 0.75,
+ weight: 3,
+ }).addTo(backboneConnectionsLayerGroup);
+
+ backboneLine.bindTooltip(tooltip, {
+ sticky: true,
+ opacity: 1,
+ interactive: true,
+ })
+ .bindPopup(tooltip)
+ .on('click', function(event) {
+ event.target.closeTooltip();
+ });
+ }
+ }
+}
+
+function onPositionHistoryUpdated(updatedPositionHistories) {
+
+ let positionHistoryLinesCords = [];
+
+ // add nodes
+ for(const positionHistory of updatedPositionHistories) {
+
+ // skip position history without position
+ if(!positionHistory.latitude || !positionHistory.longitude){
+ continue;
+ }
+
+ // find node this position is for
+ const node = findNodeById(positionHistory.node_id);
+ if(!node){
+ continue;
+ }
+
+ // fix lat long
+ positionHistory.latitude = positionHistory.latitude / 10000000;
+ positionHistory.longitude = positionHistory.longitude / 10000000;
+
+ // skip position history with invalid position
+ if(!isValidLatLng(positionHistory.latitude, positionHistory.longitude)){
+ continue;
+ }
+
+ // wrap longitude for shortest path, everything to left of australia should be shown on the right
+ var longitude = parseFloat(positionHistory.longitude);
+ if(longitude <= 100){
+ longitude += 360;
+ }
+
+ positionHistoryLinesCords.push([positionHistory.latitude, longitude]);
+
+ let tooltip = "";
+ if(positionHistory.type === "position"){
+ tooltip += `Position`;
+ } else if(positionHistory.type === "map_report"){
+ tooltip += `Map Report`;
+ }
+ tooltip += `
[${escapeString(node.short_name)}] ${escapeString(node.long_name)}`;
+ tooltip += `
${positionHistory.latitude}, ${positionHistory.longitude}`;
+ tooltip += `
Heard on: ${moment(new Date(positionHistory.created_at)).format("YYYY-MM-DD HH:mm")}`;
+
+ // add gateway info if available
+ if(positionHistory.gateway_id){
+ const gatewayNode = findNodeById(positionHistory.gateway_id);
+ const gatewayNodeInfo = gatewayNode ? `[${gatewayNode.short_name}] ${gatewayNode.long_name}` : "???";
+ tooltip += `
Heard by: ${gatewayNodeInfo}`;
+ }
+
+ // create position history marker
+ const marker = L.marker([positionHistory.latitude, longitude],{
+ icon: iconPositionHistory,
+ }).bindTooltip(tooltip).bindPopup(tooltip).on('click', function(event) {
+ // close tooltip on click to prevent tooltip and popup showing at same time
+ event.target.closeTooltip();
+ });
+
+ // add marker to position history layer group
+ marker.addTo(nodePositionHistoryLayerGroup);
+
+ }
+
+ // show lines between position history markers
+ L.polyline(positionHistoryLinesCords, {
+ color: "#a855f7",
+ opacity: 1,
+ }).addTo(nodePositionHistoryLayerGroup);
+
+}
+
+function cleanUpPositionHistory() {
+
+ // close tooltips and popups
+ closeAllPopups();
+ closeAllTooltips();
+
+ // setup node position history layer
+ nodePositionHistoryLayerGroup.clearLayers();
+ nodePositionHistoryLayerGroup.removeFrom(map);
+ nodePositionHistoryLayerGroup.addTo(map);
+
+}
+
+function setLoading(loading){
+ var reloadButton = document.getElementById("reload-button");
+ if(loading){
+ reloadButton.classList.add("animate-spin");
+ } else {
+ reloadButton.classList.remove("animate-spin");
+ }
+}
+
+async function reload(goToNodeId, zoom) {
+
+ // show loading
+ setLoading(true);
+
+ // clear previous data
+ clearMap();
+
+ // fetch nodes
+ await window.axios.get('/api/v1/nodes').then(async (response) => {
+
+ // update nodes
+ onNodesUpdated(response.data.nodes);
+
+ // hide loading
+ setLoading(false);
+
+ // go to node id if provided
+ if(goToNodeId){
+
+ // go to node
+ if(window.goToNode(goToNodeId, false, zoom)){
+ return;
+ }
+
+ // fallback to showing node details since we can't go to the node
+ window.showNodeDetails(goToNodeId);
+
+ }
+
+ });
+
+ // fetch waypoints (after awaiting nodes, so we can use nodes cache in waypoint tooltips)
+ await window.axios.get('/api/v1/waypoints').then(async (response) => {
+ onWaypointsUpdated(response.data.waypoints);
+ });
+
+ // fetch connections (edges)
+ const connectionsTimePeriodSec = getConfigConnectionsTimePeriodInSeconds();
+ const connectionsTimeFrom = connectionsTimePeriodSec ? (Date.now() - connectionsTimePeriodSec * 1000) : undefined;
+ const connectionsParams = new URLSearchParams();
+ if (connectionsTimeFrom) connectionsParams.set('time_from', connectionsTimeFrom);
+ await window.axios.get(`/api/v1/connections?${connectionsParams.toString()}`).then(async (response) => {
+ onConnectionsUpdated(response.data.connections ?? []);
+ }).catch(() => {
+ onConnectionsUpdated([]);
+ });
+
+}
+
+function getRegionFrequencyRange(regionName) {
+
+ // determine lora frequency range based on region_name
+ // https://github.com/meshtastic/firmware/blob/a4c22321fca6fc8da7bab157c3812055603512ba/src/mesh/RadioInterface.cpp#L21
+ const regionNameToLoraFrequencyRange = {
+ "US": "902-928 MHz",
+ "EU_433": "433-434 MHz",
+ "EU_868": "869.4-869.65 MHz",
+ "CN": "470-510 MHz",
+ "JP": "920.8-927.8 MHz",
+ "ANZ": "915-928 MHz",
+ "RU": "868.7-869.2 MHz",
+ "KR": "920-923 MHz",
+ "TW": "920-925 MHz",
+ "IN": "865-867 MHz",
+ "NZ_865": "864-868 MHz",
+ "TH": "920-925 MHz",
+ "UA_433": "433-434.7 MHz",
+ "UA_868": "868-868.6 MHz",
+ "MY_433": "433-435 MHz",
+ "MY_919": "919-924 MHz",
+ "SG_923": "917-925 MHz",
+ "LORA_24": "2.4-2.4835 GHz",
+ "UNSET": "902-928 MHz",
+ }
+
+ return regionNameToLoraFrequencyRange[regionName] ?? null;
+
+}
+
+function getPositionPrecisionInMeters(positionPrecision) {
+ switch(positionPrecision){
+ case 2: return 5976446;
+ case 3: return 2988223;
+ case 4: return 1494111;
+ case 5: return 747055;
+ case 6: return 373527;
+ case 7: return 186763;
+ case 8: return 93381;
+ case 9: return 46690;
+ case 10: return 23345;
+ case 11: return 11672; // Android LOW_PRECISION
+ case 12: return 5836;
+ case 13: return 2918;
+ case 14: return 1459;
+ case 15: return 729;
+ case 16: return 364; // Android MED_PRECISION
+ case 17: return 182;
+ case 18: return 91;
+ case 19: return 45;
+ case 20: return 22;
+ case 21: return 11;
+ case 22: return 5;
+ case 23: return 2;
+ case 24: return 1;
+ case 32: return 0; // Android HIGH_PRECISION
+ }
+ return null;
+}
+
+function formatPositionPrecision(positionPrecision) {
+
+ // get position precision in meters
+ const positionPrecisionInMeters = getPositionPrecisionInMeters(positionPrecision);
+ if(positionPrecisionInMeters == null){
+ return "?";
+ }
+
+ // format kilometers
+ if(positionPrecisionInMeters > 1000){
+ const positionPrecisionInKilometers = Math.ceil(positionPrecisionInMeters / 1000);
+ return `±${positionPrecisionInKilometers}km`;
+ }
+
+ // format meters
+ return `±${positionPrecisionInMeters}m`;
+
+}
+
+function getTooltipContentForNode(node) {
+
+ var loraFrequencyRange = getRegionFrequencyRange(node.region_name);
+
+ var tooltip = `
` +
+ `${escapeString(node.long_name)}` +
+ `
Short Name: ${escapeString(node.short_name)}` +
+ (node.num_online_local_nodes != null ? `
Local Nodes Online: ${node.num_online_local_nodes}` : '') +
+ (node.position_precision != null && node.position_precision !== 32 ? `
Position Precision: ${formatPositionPrecision(node.position_precision)}` : '') +
+ `
Role: ${node.role_name}` +
+ `
Hardware: ${node.hardware_model_name}` +
+ (node.firmware_version != null ? `
Firmware: ${node.firmware_version}` : '') +
+ `
OK to MQTT: ${node.ok_to_mqtt}`;
+
+ if(node.battery_level){
+ if(node.battery_level > 100){
+ tooltip += `
Battery: ${node.battery_level > 100 ? 'Plugged In' : node.battery_level}`;
+ } else {
+ tooltip += `
Battery: ${node.battery_level}%`;
+ }
+ }
+
+ if(node.voltage){
+ tooltip += `
Voltage: ${Number(node.voltage).toFixed(2)}V`;
+ }
+
+ if(node.channel_utilization){
+ tooltip += `
Ch Util: ${Number(node.channel_utilization).toFixed(2)}%`;
+ }
+
+ if(node.air_util_tx){
+ tooltip += `
Air Util: ${Number(node.air_util_tx).toFixed(2)}%`;
+ }
+
+ // ignore alt above 42949000 due to https://github.com/meshtastic/firmware/issues/3109
+ if(node.altitude && node.altitude < 42949000){
+ tooltip += `
Altitude: ${node.altitude}m`;
+ }
+
+ // bottom info
+ tooltip += `
ID: ${node.node_id}`;
+ tooltip += `
Hex ID: ${node.node_id_hex}`;
+ tooltip += `
Updated: ${moment(new Date(node.updated_at)).fromNow()}`;
+ tooltip += (node.mqtt_connection_state_updated_at ? `
MQTT Updated: ${moment(new Date(node.mqtt_connection_state_updated_at)).fromNow()}` : '');
+ tooltip += (node.neighbours_updated_at ? `
Neighbours Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '');
+ tooltip += (node.position_updated_at ? `
Position Updated: ${moment(new Date(node.position_updated_at)).fromNow()}` : '');
+
+ // show details button
+ tooltip += `
`;
+ tooltip += `
`;
+ tooltip += ``;
+
+ return tooltip;
+
+}
+
+function getTooltipContentForWaypoint(waypoint) {
+
+ // get from node name
+ var fromNode = findNodeById(waypoint.from);
+
+ var tooltip = `${escapeString(waypoint.name)}` +
+ (waypoint.description ? `
${escapeString(waypoint.description)}` : '') +
+ `
Expires: ${moment(new Date(waypoint.expire * 1000)).fromNow()}` +
+ `
Lat/Lng: ${waypoint.latitude}, ${waypoint.longitude}` +
+ `
From ID: ${waypoint.from}` +
+ `
From Hex ID: !${Number(waypoint.from).toString(16)}`;
+
+ // show node name this waypoint is from, if possible
+ if(fromNode != null){
+ tooltip += `
From Node: ${escapeString(fromNode.long_name) || 'Unnamed Node'}`;
+ } else {
+ tooltip += `
From Node: ???`;
+ }
+
+ // bottom info
+ tooltip += `
ID: ${waypoint.waypoint_id}`;
+ tooltip += `
Updated: ${moment(new Date(waypoint.updated_at)).fromNow()}`;
+
+ return tooltip;
+
+}
+
+window._onHideNodeConnectionsClick = function() {
+ cleanUpNodeConnections();
+};
+
+// parse url params
+var queryParams = new URLSearchParams(location.search);
+var queryNodeId = queryParams.get('node_id');
+var queryLat = queryParams.get('lat');
+var queryLng = queryParams.get('lng');
+var queryZoom = queryParams.get('zoom');
+
+// go to lat/lng if provided
+if(queryLat && queryLng){
+ const zoomLevel = queryZoom || getConfigZoomLevelGoToNode();
+ map.flyTo([queryLat, queryLng], zoomLevel, {
+ animate: false,
+ });
+}
+
+// auto update url when lat/lng/zoom changes
+map.on("moveend zoomend", function() {
+
+ // check if user enabled auto updating position in url
+ const autoUpdatePositionInUrl = getConfigAutoUpdatePositionInUrl();
+ if(!autoUpdatePositionInUrl){
+ return;
+ }
+
+ // get map info
+ const latLng = map.getCenter();
+ const zoom = map.getZoom();
+
+ // construct new url
+ const url = new URL(window.location.href);
+ url.searchParams.set("lat", latLng.lat);
+ url.searchParams.set("lng", latLng.lng);
+ url.searchParams.set("zoom", zoom);
+
+ // update current url
+ if(window.history.replaceState){
+ window.history.replaceState(null, null, url.toString());
+ }
+
+});
+
+// reload and go to provided node id
+reload(queryNodeId, queryZoom);
+
+// WebSocket connection for real-time messages
+var ws = null;
+var tracerouteCooldown = {}; // Track last traceroute time per from node (for 20s cooldown)
+var activeTracerouteKeys = new Set(); // Track active traceroute visualizations to prevent duplicates
+var tracerouteLines = {}; // Track lines for each traceroute route key for cleanup
+
+function connectWebSocket() {
+ // Determine WebSocket URL - use same hostname as current page, port 8081
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ // Heuristic: if running on localhost, use port 8081; otherwise use /ws path via Nginx
+ const isLocalhost = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
+ const wsUrl = isLocalhost
+ ? `${wsProtocol}//${location.hostname}:8081`
+ : `${wsProtocol}//${location.host}/ws`;
+
+ console.log('Connecting to WebSocket:', wsUrl);
+ ws = new WebSocket(wsUrl);
+
+ ws.onopen = function() {
+ console.log('WebSocket connected');
+ };
+
+ ws.onmessage = function(event) {
+ try {
+ const message = JSON.parse(event.data);
+ handleWebSocketMessage(message);
+ } catch (e) {
+ console.error('Error parsing WebSocket message:', e);
+ }
+ };
+
+ ws.onerror = function(error) {
+ console.error('WebSocket error:', error);
+ };
+
+ ws.onclose = function() {
+ console.log('WebSocket disconnected, reconnecting in 5 seconds...');
+ setTimeout(connectWebSocket, 5000);
+ };
+}
+
+function handleWebSocketMessage(message) {
+ if (message.type === 'traceroute') {
+ handleTraceroute(message.data);
+ }
+}
+
+function handleTraceroute(data) {
+ // Only visualize traceroutes where want_response is false (the reply coming back)
+ if (data.want_response) {
+ return;
+ }
+
+ // When want_response is false, from and to are swapped from the original request
+ // The path goes from 'to' (original sender) through route to 'from' (original destination)
+ const originalSender = data.to; // This was the original sender
+ const originalDestination = data.from; // This was the original destination
+ const route = data.route || [];
+ const snrTowards = data.snr_towards || [];
+
+ // Deduplicate: ignore traceroutes from the same original sender for 20 seconds
+ const now = Date.now();
+ if (tracerouteCooldown[originalSender] && (now - tracerouteCooldown[originalSender]) < 20000) {
+ return; // Still in cooldown period
+ }
+
+ // Create unique key for this traceroute path to prevent duplicate visualizations
+ // Use original sender (to), original destination (from), and route to create unique key
+ // (ignoring gateway_id since multiple gateways can receive same route)
+ const routeKey = `${originalSender}-${originalDestination}-${route.join('-')}`;
+ if (activeTracerouteKeys.has(routeKey)) {
+ return; // Already visualizing this route
+ }
+
+ // Mark as active and set cooldown
+ activeTracerouteKeys.add(routeKey);
+ tracerouteCooldown[originalSender] = now;
+
+ // Build the complete path: to (original sender) -> route[0] -> route[1] -> ... -> from (original destination)
+ const path = [originalSender]; // Start from original sender
+ if (route.length > 0) {
+ path.push(...route);
+ }
+ path.push(originalDestination); // End at original destination
+
+ // Visualize the traceroute with animated hops
+ visualizeTraceroute(path, snrTowards, routeKey);
+}
+
+function visualizeTraceroute(path, snrTowards, routeKey) {
+ // Verify all nodes in path exist on map
+ const pathMarkers = [];
+ for (const nodeId of path) {
+ const marker = findNodeMarkerById(nodeId);
+ if (!marker) {
+ // Node not on map, skip this traceroute
+ activeTracerouteKeys.delete(routeKey);
+ return;
+ }
+ pathMarkers.push(marker);
+ }
+
+ // Store lines and overlays for this route key for cleanup
+ const routeElements = {
+ lines: [],
+ startOverlay: null,
+ endOverlay: null,
+ };
+ tracerouteLines[routeKey] = routeElements;
+
+ // Color starting node (first in path) green and destination node (last in path) red
+ const startMarker = pathMarkers[0];
+ const endMarker = pathMarkers[pathMarkers.length - 1];
+
+ const startOverlay = L.marker(startMarker.getLatLng(), {
+ icon: iconTracerouteStart,
+ zIndexOffset: 10000, // Ensure it's on top
+ }).addTo(traceroutesLayerGroup);
+
+ const endOverlay = L.marker(endMarker.getLatLng(), {
+ icon: iconTracerouteEnd,
+ zIndexOffset: 10000, // Ensure it's on top
+ }).addTo(traceroutesLayerGroup);
+
+ // Store overlays for cleanup
+ routeElements.startOverlay = startOverlay;
+ routeElements.endOverlay = endOverlay;
+
+ // Animate each hop sequentially
+ let hopIndex = 0;
+ const animateNextHop = () => {
+ if (hopIndex >= pathMarkers.length - 1) {
+ // All hops animated, cleanup after delay
+ setTimeout(() => {
+ if (tracerouteLines[routeKey]) {
+ const routeElements = tracerouteLines[routeKey];
+ // Remove all lines
+ if (routeElements.lines) {
+ routeElements.lines.forEach(line => {
+ line.remove();
+ });
+ }
+ // Remove node overlays
+ if (routeElements.startOverlay) {
+ routeElements.startOverlay.remove();
+ }
+ if (routeElements.endOverlay) {
+ routeElements.endOverlay.remove();
+ }
+ delete tracerouteLines[routeKey];
+ }
+ activeTracerouteKeys.delete(routeKey);
+ }, 2500);
+ return;
+ }
+
+ const fromMarker = pathMarkers[hopIndex];
+ const toMarker = pathMarkers[hopIndex + 1];
+ const snr = hopIndex < snrTowards.length ? snrTowards[hopIndex] : null;
+
+ // Use orange color for all traceroute lines
+ const lineColor = '#f97316'; // orange
+
+ // Create animated polyline for this hop with orange dotted style
+ const line = L.polyline([fromMarker.getLatLng(), toMarker.getLatLng()], {
+ color: lineColor,
+ weight: 4,
+ opacity: 0, // Start invisible
+ // dashArray: '10, 5', // Dotted line style
+ zIndexOffset: 10000,
+ }).addTo(traceroutesLayerGroup);
+
+ // Fade in animation
+ line.setStyle({ opacity: 1.0 });
+ tracerouteLines[routeKey].lines.push(line);
+
+ // Animate next hop after 700ms delay
+ hopIndex++;
+ setTimeout(animateNextHop, 700);
+ };
+
+ // Start animation
+ animateNextHop();
+}
+
+// Connect WebSocket when page loads
+connectWebSocket();
\ No newline at end of file
diff --git a/src/public/images/devices/HELTEC_MESH_POCKET.png b/src/public/images/devices/HELTEC_MESH_POCKET.png
new file mode 100644
index 0000000..ae7a797
Binary files /dev/null and b/src/public/images/devices/HELTEC_MESH_POCKET.png differ
diff --git a/src/public/images/devices/HELTEC_MESH_SOLAR.png b/src/public/images/devices/HELTEC_MESH_SOLAR.png
new file mode 100644
index 0000000..a6575ef
Binary files /dev/null and b/src/public/images/devices/HELTEC_MESH_SOLAR.png differ
diff --git a/src/public/images/devices/HELTEC_V2_0.png b/src/public/images/devices/HELTEC_V2_0.png
old mode 100755
new mode 100644
index bbdab59..6dd4c49
Binary files a/src/public/images/devices/HELTEC_V2_0.png and b/src/public/images/devices/HELTEC_V2_0.png differ
diff --git a/src/public/images/devices/HELTEC_V2_1.png b/src/public/images/devices/HELTEC_V2_1.png
old mode 100755
new mode 100644
index 6735a1b..6dd4c49
Binary files a/src/public/images/devices/HELTEC_V2_1.png and b/src/public/images/devices/HELTEC_V2_1.png differ
diff --git a/src/public/images/devices/HELTEC_V3.png b/src/public/images/devices/HELTEC_V3.png
old mode 100755
new mode 100644
index 2c83862..6dd4c49
Binary files a/src/public/images/devices/HELTEC_V3.png and b/src/public/images/devices/HELTEC_V3.png differ
diff --git a/src/public/images/devices/HELTEC_V4.png b/src/public/images/devices/HELTEC_V4.png
new file mode 100644
index 0000000..6dd4c49
Binary files /dev/null and b/src/public/images/devices/HELTEC_V4.png differ
diff --git a/src/public/images/devices/HELTEC_VISION_MASTER_E213.png b/src/public/images/devices/HELTEC_VISION_MASTER_E213.png
new file mode 100644
index 0000000..5000861
Binary files /dev/null and b/src/public/images/devices/HELTEC_VISION_MASTER_E213.png differ
diff --git a/src/public/images/devices/LILYGO_TBEAM_S3_CORE.png b/src/public/images/devices/LILYGO_TBEAM_S3_CORE.png
old mode 100755
new mode 100644
index 82a05f6..c763601
Binary files a/src/public/images/devices/LILYGO_TBEAM_S3_CORE.png and b/src/public/images/devices/LILYGO_TBEAM_S3_CORE.png differ
diff --git a/src/public/images/devices/NRF52_PROMICRO_DIY.png b/src/public/images/devices/NRF52_PROMICRO_DIY.png
new file mode 100644
index 0000000..3ece7f1
Binary files /dev/null and b/src/public/images/devices/NRF52_PROMICRO_DIY.png differ
diff --git a/src/public/images/devices/PORTDUINO.png b/src/public/images/devices/PORTDUINO.png
new file mode 100644
index 0000000..0ccea0d
Binary files /dev/null and b/src/public/images/devices/PORTDUINO.png differ
diff --git a/src/public/images/devices/RP2040_LORA.png b/src/public/images/devices/RP2040_LORA.png
old mode 100755
new mode 100644
index aacbe84..e83fa3a
Binary files a/src/public/images/devices/RP2040_LORA.png and b/src/public/images/devices/RP2040_LORA.png differ
diff --git a/src/public/images/devices/RPI_PICO.png b/src/public/images/devices/RPI_PICO.png
old mode 100755
new mode 100644
index 4e25730..ed0ad1c
Binary files a/src/public/images/devices/RPI_PICO.png and b/src/public/images/devices/RPI_PICO.png differ
diff --git a/src/public/images/devices/SEEED_SOLAR_NODE.png b/src/public/images/devices/SEEED_SOLAR_NODE.png
new file mode 100644
index 0000000..7f894bc
Binary files /dev/null and b/src/public/images/devices/SEEED_SOLAR_NODE.png differ
diff --git a/src/public/images/devices/SEEED_WIO_TRACKER_L1.png b/src/public/images/devices/SEEED_WIO_TRACKER_L1.png
new file mode 100644
index 0000000..e432076
Binary files /dev/null and b/src/public/images/devices/SEEED_WIO_TRACKER_L1.png differ
diff --git a/src/public/images/devices/SEEED_XIAO_S3.png b/src/public/images/devices/SEEED_XIAO_S3.png
new file mode 100644
index 0000000..cceedfd
Binary files /dev/null and b/src/public/images/devices/SEEED_XIAO_S3.png differ
diff --git a/src/public/images/devices/STATION_G2.png b/src/public/images/devices/STATION_G2.png
new file mode 100644
index 0000000..290f5eb
Binary files /dev/null and b/src/public/images/devices/STATION_G2.png differ
diff --git a/src/public/images/devices/TBEAM.png b/src/public/images/devices/TBEAM.png
old mode 100755
new mode 100644
index 07e1ea9..0968f5b
Binary files a/src/public/images/devices/TBEAM.png and b/src/public/images/devices/TBEAM.png differ
diff --git a/src/public/images/devices/THINKNODE_M1.png b/src/public/images/devices/THINKNODE_M1.png
new file mode 100644
index 0000000..7995026
Binary files /dev/null and b/src/public/images/devices/THINKNODE_M1.png differ
diff --git a/src/public/images/devices/TLORA_T3_S3.png b/src/public/images/devices/TLORA_T3_S3.png
index ab9a716..f241e32 100644
Binary files a/src/public/images/devices/TLORA_T3_S3.png and b/src/public/images/devices/TLORA_T3_S3.png differ
diff --git a/src/public/images/devices/TLORA_V2_1_1P6.png b/src/public/images/devices/TLORA_V2_1_1P6.png
old mode 100755
new mode 100644
index a85f673..9e50235
Binary files a/src/public/images/devices/TLORA_V2_1_1P6.png and b/src/public/images/devices/TLORA_V2_1_1P6.png differ
diff --git a/src/public/images/devices/T_DECK.png b/src/public/images/devices/T_DECK.png
old mode 100755
new mode 100644
index 1d3f495..89ae6e3
Binary files a/src/public/images/devices/T_DECK.png and b/src/public/images/devices/T_DECK.png differ
diff --git a/src/public/images/devices/T_ECHO.png b/src/public/images/devices/T_ECHO.png
old mode 100755
new mode 100644
index 076c748..baa3fc5
Binary files a/src/public/images/devices/T_ECHO.png and b/src/public/images/devices/T_ECHO.png differ
diff --git a/src/public/images/devices/T_ETH_ELITE.png b/src/public/images/devices/T_ETH_ELITE.png
new file mode 100644
index 0000000..3a25fbc
Binary files /dev/null and b/src/public/images/devices/T_ETH_ELITE.png differ
diff --git a/src/public/images/devices/XIAO_NRF52_KIT.png b/src/public/images/devices/XIAO_NRF52_KIT.png
new file mode 100644
index 0000000..f3cc2eb
Binary files /dev/null and b/src/public/images/devices/XIAO_NRF52_KIT.png differ
diff --git a/src/public/index.html b/src/public/index.html
index 92ce175..ff76890 100644
--- a/src/public/index.html
+++ b/src/public/index.html
@@ -3,15 +3,15 @@
- Meshtastic Map
-
+ DL4AX Meshtastic Map
+
-
-
+
+
@@ -48,95 +48,8 @@
-
+
+
@@ -145,32 +58,8 @@
-
-
-
-
-
-
Service Announcement
-
- Changes were made to mqtt.meshtastic.org. Uplink your nodes to to continue showing on this map.
-
-
-
-
-
-
-
-
-
+
@@ -184,16 +73,8 @@
-
-

-
-
-
-
-
Meshtastic Map
-
- Created by
Liam Cottle
-
+
+
@@ -231,16 +112,6 @@
-
-
-
-
-
-
LoRa Config
-
-
-
- -
-
Region
-
- {{ selectedNode.region_name }} ({{ getRegionFrequencyRange(selectedNode.region_name) }})
- ???
-
-
-
-
- -
-
Modem Preset
-
- {{ selectedNode.modem_preset_name }}
- ???
-
-
-
-
- -
-
Has Default Channel
-
- {{ selectedNode.has_default_channel ? "Yes" : "No" }}
- ???
-
-
-
-
-
-
@@ -805,6 +442,7 @@
+
@@ -895,6 +533,7 @@
+
@@ -919,6 +558,10 @@
Pressure
+
@@ -964,6 +607,7 @@
+
@@ -1402,29 +1046,6 @@
-
-
-
-
Nodes that have not uplinked to MQTT in this time will show as blue icons. Reload to update map.
-
-
-
@@ -1471,11 +1092,66 @@
-
+
-
-
Neighbours further than this are hidden. Reload to update map.
-
+
+
Edges from traceroutes and neighbour info within this time period are shown in the Connections layer. Reload to update map.
+
+
+
+
+
+
+
+
+
+
+
+
Colors the connection lines by the average SNR in the worst direction. Reload to update map.
+
+
+
+
+
+
+
+
+
+
+
Only show connections where data flows in both directions. Reload to update map.
+
+
+
+
+
+
Only show connections where at least one direction has SNR above this threshold. Leave empty to show all connections. Reload to update map.
+
+
+
+
+
+
+
+
If checked, all existing directions must meet the minimum SNR threshold (both directions if bidirectional, single direction if unidirectional).
+
+
+
+
+
+
Connections further than this are hidden. Reload to update map.
+
@@ -1490,8 +1166,8 @@
Metrics will be shown in the selected format.
@@ -1516,7 +1192,6 @@
Map will animate flying to nodes.
-
@@ -1526,7 +1201,7 @@
-
+