Browse Source

Merge branch 'dev' into rep-room-acl

Scott Powell 11 tháng trước cách đây
mục cha
commit
9df6e8a6b6

+ 38 - 0
boards/nrf52840_s140_v6_extrafs.ld

@@ -0,0 +1,38 @@
+/* Linker script to configure memory regions. */
+
+SEARCH_DIR(.)
+GROUP(-lgcc -lc -lnosys)
+
+MEMORY
+{
+  FLASH (rx)     : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000
+
+  /* SRAM required by Softdevice depend on
+   * - Attribute Table Size (Number of Services and Characteristics)
+   * - Vendor UUID count
+   * - Max ATT MTU
+   * - Concurrent connection peripheral + central + secure links
+   * - Event Len, HVN queue, Write CMD queue
+   */ 
+  RAM (rwx) :  ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
+}
+
+SECTIONS
+{
+  . = ALIGN(4);
+  .svc_data :
+  {
+    PROVIDE(__start_svc_data = .);
+    KEEP(*(.svc_data))
+    PROVIDE(__stop_svc_data = .);
+  } > RAM
+  
+  .fs_data :
+  {
+    PROVIDE(__start_fs_data = .);
+    KEEP(*(.fs_data))
+    PROVIDE(__stop_fs_data = .);
+  } > RAM
+} INSERT AFTER .data;
+
+INCLUDE "nrf52_common.ld"

+ 38 - 0
boards/nrf52840_s140_v7_extrafs.ld

@@ -0,0 +1,38 @@
+/* Linker script to configure memory regions. */
+
+SEARCH_DIR(.)
+GROUP(-lgcc -lc -lnosys)
+
+MEMORY
+{
+  FLASH (rx)     : ORIGIN = 0x27000, LENGTH = 0xD4000 - 0x27000
+
+  /* SRAM required by Softdevice depend on
+   * - Attribute Table Size (Number of Services and Characteristics)
+   * - Vendor UUID count
+   * - Max ATT MTU
+   * - Concurrent connection peripheral + central + secure links
+   * - Event Len, HVN queue, Write CMD queue
+   */ 
+  RAM (rwx) :  ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
+}
+
+SECTIONS
+{
+  . = ALIGN(4);
+  .svc_data :
+  {
+    PROVIDE(__start_svc_data = .);
+    KEEP(*(.svc_data))
+    PROVIDE(__stop_svc_data = .);
+  } > RAM
+  
+  .fs_data :
+  {
+    PROVIDE(__start_fs_data = .);
+    KEEP(*(.fs_data))
+    PROVIDE(__stop_fs_data = .);
+  } > RAM
+} INSERT AFTER .data;
+
+INCLUDE "nrf52_common.ld"

+ 13 - 9
docs/faq.md

@@ -63,10 +63,11 @@ author: https://github.com/LitBomb<!-- omit from toc -->
   - [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago)
   - [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed)
   - [6.3. Q: How to connect to a repeater via BLE (Bluetooth)?](#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth)
-  - [6.4. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
-  - [6.5. Q: My Heltec V3 keeps disconnecting from my smartphone.  It can't hold a solid Bluetooth connection.](#65-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
-  - [6.6. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#66-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
-  - [6.7. Q: WebFlasher fails on Linux with failed to open](#67-q-webflasher-fails-on-linux-with-failed-to-open)
+  - [6.4. Q: My companion isn't showing up over Bluetooth?](#64-q-my-companion-isnt-showing-up-over-bluetooth)
+  - [6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
+  - [6.6. Q: My Heltec V3 keeps disconnecting from my smartphone.  It can't hold a solid Bluetooth connection.](#65-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
+  - [6.7. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#66-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
+  - [6.8. Q: WebFlasher fails on Linux with failed to open](#67-q-webflasher-fails-on-linux-with-failed-to-open)
 - [7. Other Questions:](#7-other-questions)
   - [7.1 Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app)
   - [7.2 Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air)
@@ -563,15 +564,19 @@ You can get the epoch time on <https://www.epochconverter.com/> and use it to se
 ### 6.3. Q: How to connect to a repeater via BLE (Bluetooth)?
 **A:** You can't connect to a device running repeater firmware  via Bluetooth.  Devices running the BLE companion firmware you can connect to it via Bluetooth using the android app
 
-### 6.4. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?
+### 6.4. Q: My companion isn't showing up over Bluetooth?
+
+**A:** make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.
+
+### 6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?
 
 **A:** the default Bluetooth pairing code is `123456`
 
-### 6.5. Q: My Heltec V3 keeps disconnecting from my smartphone.  It can't hold a solid Bluetooth connection.
+### 6.6. Q: My Heltec V3 keeps disconnecting from my smartphone.  It can't hold a solid Bluetooth connection.
 
 **A:** Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity.  It has a very short range, only a few feet.  It is possible to remove the coil antenna  and replace it with a 31mm wire.  The BT range is much improved with the modification.
 
-### 6.6. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?
+### 6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?
 
 **A:** 
 1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode:
@@ -591,8 +596,7 @@ You can get the epoch time on <https://www.epochconverter.com/> and use it to se
 
 Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode.  If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by hold down the user button of the device within 8 seconds of boot.  Then you can use the 'Console' on flasher.meshcore.co.uk 
 
-
-### 6.7. Q: WebFlasher fails on Linux with failed to open
+### 6.8. Q: WebFlasher fails on Linux with failed to open
 
 **A:** If the usb port doesn't have the right ownership for this task, the process fails with the following error:
 `NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.`

+ 111 - 1
examples/companion_radio/ui-new/UITask.cpp

@@ -75,6 +75,9 @@ class HomeScreen : public UIScreen {
     RADIO,
     BLUETOOTH,
     ADVERT,
+#if UI_SENSORS_PAGE == 1
+    SENSORS,
+#endif
     SHUTDOWN,
     Count    // keep as last
   };
@@ -113,9 +116,37 @@ class HomeScreen : public UIScreen {
     display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
   }
 
+  CayenneLPP sensors_lpp;
+  int sensors_nb = 0;
+  bool sensors_scroll = false;
+  int sensors_scroll_offset = 0;
+  int next_sensors_refresh = 0;
+
+  void refresh_sensors() {
+    if (millis() > next_sensors_refresh) {
+      sensors_lpp.reset();
+      sensors_nb = 0;
+      sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
+      sensors.querySensors(0xFF, sensors_lpp);
+      LPPReader reader (sensors_lpp.getBuffer(), sensors_lpp.getSize());
+      uint8_t channel, type;
+      while(reader.readHeader(channel, type)) {
+        reader.skipData(type);
+        sensors_nb ++;
+      }
+      sensors_scroll = sensors_nb > UI_RECENT_LIST_SIZE;
+#if AUTO_OFF_MILLIS > 0
+      next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec
+#else
+      next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min
+#endif
+    }
+  }
+
 public:
   HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
-     : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _shutdown_init(false) { }
+     : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), 
+       _shutdown_init(false), sensors_lpp(200) {  }
 
   void poll() override {
     if (_shutdown_init && !_task->isButtonPressed()) {  // must wait for USR button to be released
@@ -211,6 +242,78 @@ public:
       display.setColor(DisplayDriver::GREEN);
       display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32);
       display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL);
+#if UI_SENSORS_PAGE == 1
+    } else if (_page == HomePage::SENSORS) {
+      int y = 18;
+      refresh_sensors();
+      char buf[30];
+      char name[30];
+      LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize());
+
+      for (int i = 0; i < sensors_scroll_offset; i++) {
+        uint8_t channel, type;
+        r.readHeader(channel, type);
+        r.skipData(type);
+      }
+
+      for (int i = 0; i < (sensors_scroll?UI_RECENT_LIST_SIZE:sensors_nb); i++) {
+        uint8_t channel, type;
+        if (!r.readHeader(channel, type)) { // reached end, reset
+          r.reset();
+          r.readHeader(channel, type);
+        }
+
+        display.setCursor(0, y);
+        float v;
+        switch (type) {
+          case LPP_GPS: // GPS
+            float lat, lon, alt;
+            r.readGPS(lat, lon, alt);
+            strcpy(name, "gps"); sprintf(buf, "%.4f %.4f", lat, lon);
+            break;
+          case LPP_VOLTAGE:
+            r.readVoltage(v);
+            strcpy(name, "voltage"); sprintf(buf, "%6.2f", v);
+            break;
+          case LPP_CURRENT:
+            r.readCurrent(v);
+            strcpy(name, "current"); sprintf(buf, "%.3f", v);
+            break;
+          case LPP_TEMPERATURE:
+            r.readTemperature(v);
+            strcpy(name, "temperature"); sprintf(buf, "%.2f", v);
+            break;
+          case LPP_RELATIVE_HUMIDITY:
+            r.readRelativeHumidity(v);
+            strcpy(name, "humidity"); sprintf(buf, "%.2f", v);
+            break;
+          case LPP_BAROMETRIC_PRESSURE:
+            r.readPressure(v);
+            strcpy(name, "pressure"); sprintf(buf, "%.2f", v);
+            break;
+          case LPP_ALTITUDE:
+            r.readAltitude(v);
+            strcpy(name, "altitude"); sprintf(buf, "%.0f", v);
+            break;
+          case LPP_POWER:
+            r.readPower(v);
+            strcpy(name, "power"); sprintf(buf, "%6.2f", v);
+            break;
+          default:
+            r.skipData(type);
+            strcpy(name, "unk"); sprintf(buf, "");
+        }
+        display.setCursor(0, y);
+        display.print(name);
+        display.setCursor(
+          display.width()-display.getTextWidth(buf)-1, y
+        );
+        display.print(buf);
+        y = y + 12;
+      }
+      if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
+      else sensors_scroll_offset = 0;
+#endif
     } else if (_page == HomePage::SHUTDOWN) {
       display.setColor(DisplayDriver::GREEN);
       display.setTextSize(1);
@@ -255,6 +358,13 @@ public:
       }
       return true;
     }
+#if UI_SENSORS_PAGE == 1
+    if (c == KEY_ENTER && _page == HomePage::SENSORS) {
+      _task->toggleGPS();
+      next_sensors_refresh=0;
+      return true;
+    }
+#endif
     if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
       _shutdown_init = true;  // need to wait for button to be released
       return true;

+ 1 - 0
examples/companion_radio/ui-new/UITask.h

@@ -6,6 +6,7 @@
 #include <helpers/SensorManager.h>
 #include <helpers/BaseSerialInterface.h>
 #include <Arduino.h>
+#include <helpers/sensors/LPPDataHelpers.h>
 
 #ifdef PIN_BUZZER
   #include <helpers/ui/buzzer.h>

+ 7 - 1
src/helpers/sensors/EnvironmentSensorManager.cpp

@@ -252,7 +252,7 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
   next_available_channel = TELEM_CHANNEL_SELF + 1;
 
   if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {
-    telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, 0.0f); // allow lat/lon via telemetry even if no GPS is detected
+    telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); // allow lat/lon via telemetry even if no GPS is detected
   }
 
   if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
@@ -577,17 +577,23 @@ void EnvironmentSensorManager::loop() {
       node_lat = ((double)ublox_GNSS.getLatitude())/10000000.;
       node_lon = ((double)ublox_GNSS.getLongitude())/10000000.;
       MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
+      node_altitude = ((double)ublox_GNSS.getAltitude()) / 1000.0;
+      MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
     }
     else if (serialGPSFlag && _location->isValid()) {
       node_lat = ((double)_location->getLatitude())/1000000.;
       node_lon = ((double)_location->getLongitude())/1000000.;
       MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
+      node_altitude = ((double)_location->getAltitude()) / 1000.0;
+      MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
     }
     #else
     if (_location->isValid()) {
       node_lat = ((double)_location->getLatitude())/1000000.;
       node_lon = ((double)_location->getLongitude())/1000000.;
       MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
+      node_altitude = ((double)_location->getAltitude()) / 1000.0;
+      MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
     }
     #endif
     }

+ 223 - 0
src/helpers/sensors/LPPDataHelpers.h

@@ -0,0 +1,223 @@
+#pragma once
+
+#include <stdint.h>
+
+#define LPP_DIGITAL_INPUT 0         // 1 byte
+#define LPP_DIGITAL_OUTPUT 1        // 1 byte
+#define LPP_ANALOG_INPUT 2          // 2 bytes, 0.01 signed
+#define LPP_ANALOG_OUTPUT 3         // 2 bytes, 0.01 signed
+#define LPP_GENERIC_SENSOR 100      // 4 bytes, unsigned
+#define LPP_LUMINOSITY 101          // 2 bytes, 1 lux unsigned
+#define LPP_PRESENCE 102            // 1 byte, bool
+#define LPP_TEMPERATURE 103         // 2 bytes, 0.1°C signed
+#define LPP_RELATIVE_HUMIDITY 104   // 1 byte, 0.5% unsigned
+#define LPP_ACCELEROMETER 113       // 2 bytes per axis, 0.001G
+#define LPP_BAROMETRIC_PRESSURE 115 // 2 bytes 0.1hPa unsigned
+#define LPP_VOLTAGE 116             // 2 bytes 0.01V unsigned
+#define LPP_CURRENT 117             // 2 bytes 0.001A unsigned
+#define LPP_FREQUENCY 118           // 4 bytes 1Hz unsigned
+#define LPP_PERCENTAGE 120          // 1 byte 1-100% unsigned
+#define LPP_ALTITUDE 121            // 2 byte 1m signed
+#define LPP_CONCENTRATION 125       // 2 bytes, 1 ppm unsigned
+#define LPP_POWER 128               // 2 byte, 1W, unsigned
+#define LPP_DISTANCE 130            // 4 byte, 0.001m, unsigned
+#define LPP_ENERGY 131              // 4 byte, 0.001kWh, unsigned
+#define LPP_DIRECTION 132           // 2 bytes, 1deg, unsigned
+#define LPP_UNIXTIME 133            // 4 bytes, unsigned
+#define LPP_GYROMETER 134           // 2 bytes per axis, 0.01 °/s
+#define LPP_COLOUR 135              // 1 byte per RGB Color
+#define LPP_GPS 136                 // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
+#define LPP_SWITCH 142              // 1 byte, 0/1
+#define LPP_POLYLINE 240            // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
+
+// Multipliers
+#define LPP_DIGITAL_INPUT_MULT 1
+#define LPP_DIGITAL_OUTPUT_MULT 1
+#define LPP_ANALOG_INPUT_MULT 100
+#define LPP_ANALOG_OUTPUT_MULT 100
+#define LPP_GENERIC_SENSOR_MULT 1
+#define LPP_LUMINOSITY_MULT 1
+#define LPP_PRESENCE_MULT 1
+#define LPP_TEMPERATURE_MULT 10
+#define LPP_RELATIVE_HUMIDITY_MULT 2
+#define LPP_ACCELEROMETER_MULT 1000
+#define LPP_BAROMETRIC_PRESSURE_MULT 10
+#define LPP_VOLTAGE_MULT 100
+#define LPP_CURRENT_MULT 1000
+#define LPP_FREQUENCY_MULT 1
+#define LPP_PERCENTAGE_MULT 1
+#define LPP_ALTITUDE_MULT 1
+#define LPP_POWER_MULT 1
+#define LPP_DISTANCE_MULT 1000
+#define LPP_ENERGY_MULT 1000
+#define LPP_DIRECTION_MULT 1
+#define LPP_UNIXTIME_MULT 1
+#define LPP_GYROMETER_MULT 100
+#define LPP_GPS_LAT_LON_MULT 10000
+#define LPP_GPS_ALT_MULT 100
+#define LPP_SWITCH_MULT 1
+#define LPP_CONCENTRATION_MULT 1
+#define LPP_COLOUR_MULT 1
+
+#define LPP_ERROR_OK 0
+#define LPP_ERROR_OVERFLOW 1
+#define LPP_ERROR_UNKOWN_TYPE 2
+
+class LPPReader {
+  const uint8_t* _buf;
+  uint8_t _len;
+  uint8_t _pos;
+
+  float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
+    uint32_t value = 0;
+    for (uint8_t i = 0; i < size; i++) {
+      value = (value << 8) + buffer[i];
+    }
+
+    int sign = 1;
+    if (is_signed) {
+      uint32_t bit = 1ul << ((size * 8) - 1);
+      if ((value & bit) == bit) {
+        value = (bit << 1) - value;
+        sign = -1;
+      }
+    }
+    return sign * ((float) value / multiplier);
+  }
+
+public:
+  LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
+
+  void reset() {
+    _pos = 0;
+  }
+
+  bool readHeader(uint8_t& channel, uint8_t& type) {
+    if (_pos + 2 < _len) {
+      channel = _buf[_pos++];
+      type = _buf[_pos++];
+
+      return channel != 0;   // channel 0 is End-of-data
+    }
+    return false;  // end-of-buffer
+  }
+
+  bool readGPS(float& lat, float& lon, float& alt) {
+    lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
+    lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
+    alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
+    return _pos <= _len;
+  }
+  bool readVoltage(float& voltage) {
+    voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
+    return _pos <= _len;
+  }
+  bool readCurrent(float& amps) {
+    amps = getFloat(&_buf[_pos], 2, 1000, false); _pos += 2;
+    return _pos <= _len;
+  }
+  bool readPower(float& watts) {
+    watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
+    return _pos <= _len;
+  }
+  bool readTemperature(float& degrees_c) {
+    degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
+    return _pos <= _len;
+  }
+  bool readPressure(float& pa) {
+    pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
+    return _pos <= _len;
+  }
+  bool readRelativeHumidity(float& pct) {
+    pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
+    return _pos <= _len;
+  }
+  bool readAltitude(float& m) {
+    m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
+    return _pos <= _len;
+  }
+
+  void skipData(uint8_t type) {
+    switch (type) {
+      case LPP_GPS:
+        _pos += 9; break;
+      case LPP_POLYLINE:
+        _pos += 8; break;  // TODO: this is MINIMIUM
+      case LPP_GYROMETER:
+      case LPP_ACCELEROMETER:
+        _pos += 6; break;
+      case LPP_GENERIC_SENSOR:
+      case LPP_FREQUENCY:
+      case LPP_DISTANCE:
+      case LPP_ENERGY:
+      case LPP_UNIXTIME:
+        _pos += 4; break;
+      case LPP_COLOUR:
+        _pos += 3; break;
+      case LPP_ANALOG_INPUT:
+      case LPP_ANALOG_OUTPUT:
+      case LPP_LUMINOSITY:
+      case LPP_TEMPERATURE:
+      case LPP_CONCENTRATION:
+      case LPP_BAROMETRIC_PRESSURE:
+      case LPP_ALTITUDE:
+      case LPP_VOLTAGE:
+      case LPP_CURRENT:
+      case LPP_DIRECTION:
+      case LPP_POWER:
+        _pos += 2; break;
+      default:
+        _pos++;
+    }
+  }
+};
+
+class LPPWriter {
+  uint8_t* _buf;
+  uint8_t _max_len;
+  uint8_t _len;
+
+  void write(uint16_t value) {
+    _buf[_len++] = (value >> 8) & 0xFF;  // MSB
+    _buf[_len++] = value & 0xFF;         // LSB
+  }
+
+public:
+  LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { }
+
+  bool writeVoltage(uint8_t channel, float voltage) {
+    if (_len + 4 <= _max_len) {
+      _buf[_len++] = channel;
+      _buf[_len++] = LPP_VOLTAGE;
+      uint16_t value = voltage * 100;
+      write(value);
+      return true;
+    }
+    return false;
+  }
+
+  bool writeGPS(uint8_t channel, float lat, float lon, float alt) {
+    if (_len + 11 <= _max_len) {
+      _buf[_len++] = channel;
+      _buf[_len++] = LPP_GPS;
+
+      int32_t lati = lat * 10000;  // we lose some precision :-(
+      int32_t loni = lon * 10000;
+      int32_t alti = alt * 100;
+
+      _buf[_len++] = lati >> 16;
+      _buf[_len++] = lati >> 8;
+      _buf[_len++] = lati;
+      _buf[_len++] = loni >> 16;
+      _buf[_len++] = loni >> 8;
+      _buf[_len++] = loni;
+      _buf[_len++] = alti >> 16;
+      _buf[_len++] = alti >> 8;
+      _buf[_len++] = alti;
+      return true;
+    }
+    return false;
+  }
+
+  uint8_t length() { return _len; }
+};

+ 4 - 0
variants/heltec_mesh_solar/platformio.ini

@@ -57,6 +57,8 @@ build_flags =
 
 [env:Heltec_mesh_solar_companion_radio_ble]
 extends = Heltec_mesh_solar
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Heltec_mesh_solar.build_flags}
   -D MAX_CONTACTS=350
@@ -75,6 +77,8 @@ lib_deps =
 
 [env:Heltec_mesh_solar_companion_radio_usb]
 extends = Heltec_mesh_solar
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Heltec_mesh_solar.build_flags}
   -D MAX_CONTACTS=350

+ 8 - 0
variants/heltec_t114/platformio.ini

@@ -70,6 +70,8 @@ build_flags =
 
 [env:Heltec_t114_without_display_companion_radio_ble]
 extends = Heltec_t114
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Heltec_t114.build_flags}
   -I examples/companion_radio/ui-new
@@ -90,6 +92,8 @@ lib_deps =
 
 [env:Heltec_t114_without_display_companion_radio_usb]
 extends = Heltec_t114
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Heltec_t114.build_flags}
   -I examples/companion_radio/ui-new
@@ -158,6 +162,8 @@ build_flags =
 
 [env:Heltec_t114_companion_radio_ble]
 extends = Heltec_t114_with_display
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Heltec_t114_with_display.build_flags}
   -I examples/companion_radio/ui-new
@@ -178,6 +184,8 @@ lib_deps =
 
 [env:Heltec_t114_companion_radio_usb]
 extends = Heltec_t114_with_display
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Heltec_t114_with_display.build_flags}
   -I examples/companion_radio/ui-new

+ 4 - 0
variants/ikoka_stick_nrf/platformio.ini

@@ -101,6 +101,8 @@ build_src_filter = ${nrf52840_xiao.build_src_filter}
 
 [ikoka_stick_nrf_companion_radio_ble]
 extends = ikoka_stick_nrf_baseboard
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags =
   ${ikoka_stick_nrf_baseboard.build_flags}
   -D MAX_CONTACTS=350
@@ -121,6 +123,8 @@ lib_deps =
 
 [ikoka_stick_nrf_companion_radio_usb]
 extends = ikoka_stick_nrf_baseboard
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags =
   ${ikoka_stick_nrf_baseboard.build_flags}
   -D MAX_CONTACTS=350

+ 8 - 0
variants/lilygo_techo/platformio.ini

@@ -78,6 +78,8 @@ build_flags =
 
 [env:LilyGo_T-Echo_companion_radio_ble]
 extends = LilyGo_T-Echo
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${LilyGo_T-Echo.build_flags}
   -I src/helpers/ui
@@ -88,6 +90,8 @@ build_flags =
   -D BLE_PIN_CODE=123456
 ;  -D BLE_DEBUG_LOGGING=1
   -D OFFLINE_QUEUE_SIZE=256
+  -D UI_RECENT_LIST_SIZE=9
+  -D UI_SENSORS_PAGE=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
   -D AUTO_SHUTDOWN_MILLIVOLTS=3300
@@ -101,6 +105,8 @@ lib_deps =
 
 [env:LilyGo_T-Echo_companion_radio_usb]
 extends = LilyGo_T-Echo
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${LilyGo_T-Echo.build_flags}
   -I src/helpers/ui
@@ -109,7 +115,9 @@ build_flags =
   -D MAX_GROUP_CHANNELS=40
   -D OFFLINE_QUEUE_SIZE=256
   -D UI_RECENT_LIST_SIZE=9
+  -D UI_SENSORS_PAGE=1
   -D AUTO_SHUTDOWN_MILLIVOLTS=3300
+  -D QSPIFLASH=1
 build_src_filter = ${LilyGo_T-Echo.build_src_filter}
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>

+ 4 - 0
variants/mesh_pocket/platformio.ini

@@ -67,6 +67,8 @@ build_flags =
 
 [env:Mesh_pocket_companion_radio_ble]
 extends = Mesh_pocket
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Mesh_pocket.build_flags}
   -I examples/companion_radio/ui-new
@@ -89,6 +91,8 @@ lib_deps =
 
 [env:Mesh_pocket_companion_radio_usb]
 extends = Mesh_pocket
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Mesh_pocket.build_flags}
   -I examples/companion_radio/ui-new

+ 4 - 0
variants/minewsemi_me25ls01/platformio.ini

@@ -50,6 +50,8 @@ lib_deps = ${nrf52840_me25ls01.lib_deps}
 
 [env:Minewsemi_me25ls01_companion_radio_ble]
 extends = me25ls01
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags = ${me25ls01.build_flags}
   -I examples/companion_radio/ui-orig
   -D MAX_CONTACTS=350
@@ -147,6 +149,8 @@ lib_deps = ${me25ls01.lib_deps}
 
 [env:Minewsemi_me25ls01_companion_radio_usb]
 extends = me25ls01
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags = ${me25ls01.build_flags}
   -I examples/companion_radio/ui-orig
   -D MAX_CONTACTS=350

+ 2 - 2
variants/nano_g2_ultra/nano-g2.h

@@ -52,8 +52,8 @@ public:
   void powerOff() override {
     // put GPS chip to sleep
     digitalWrite(PIN_GPS_STANDBY, LOW);
-// unset buzzer to prevent notification circuit activating on hibernate
-#undef PIN_BUZZER
+    // TODO: unset buzzer to prevent notification circuit activating on hibernate
+    // needs to be set as silent or somehow stop using macros for pins
 
     nrf_gpio_cfg_sense_input(digitalPinToInterrupt(PIN_USER_BTN), NRF_GPIO_PIN_NOPULL,
                              NRF_GPIO_PIN_SENSE_LOW);

+ 5 - 0
variants/nano_g2_ultra/platformio.ini

@@ -31,6 +31,8 @@ upload_protocol = nrfutil
 
 [env:Nano_G2_Ultra_companion_radio_ble]
 extends = Nano_G2_Ultra
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Nano_G2_Ultra.build_flags}
   -I src/helpers/ui
@@ -62,12 +64,15 @@ lib_deps =
 
 [env:Nano_G2_Ultra_companion_radio_usb]
 extends = Nano_G2_Ultra
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${Nano_G2_Ultra.build_flags}
   -I src/helpers/ui
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
+  -D QSPIFLASH=1
   -D OFFLINE_QUEUE_SIZE=256
   -D DISPLAY_CLASS=SH1106Display
   -D PIN_BUZZER=4

+ 4 - 0
variants/promicro/platformio.ini

@@ -86,6 +86,8 @@ lib_deps = ${Faketec.lib_deps}
 
 [env:Faketec_companion_radio_usb]
 extends = Faketec
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags = ${Faketec.build_flags}
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350
@@ -104,6 +106,8 @@ lib_deps = ${Faketec.lib_deps}
 
 [env:Faketec_companion_radio_ble]
 extends = Faketec
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags = ${Faketec.build_flags}
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350

+ 4 - 0
variants/rak4631/platformio.ini

@@ -64,6 +64,8 @@ build_src_filter = ${rak4631.build_src_filter}
 
 [env:RAK_4631_companion_radio_usb]
 extends = rak4631
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${rak4631.build_flags}
   -I examples/companion_radio/ui-new
@@ -83,6 +85,8 @@ lib_deps =
 
 [env:RAK_4631_companion_radio_ble]
 extends = rak4631
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${rak4631.build_flags}
   -I examples/companion_radio/ui-new

+ 4 - 2
variants/rak_wismesh_tag/RAKWismeshTagBoard.h

@@ -62,7 +62,8 @@ public:
     digitalWrite(LED_PIN, HIGH);
     #endif
     #ifdef BUTTON_PIN
-    while(digitalRead(BUTTON_PIN));
+    // wismesh tag uses LOW to indicate button is pressed, wait until it goes HIGH to indicate it was released
+    while(digitalRead(BUTTON_PIN) == LOW);
     #endif
     #ifdef LED_GREEN
     digitalWrite(LED_GREEN, LOW);
@@ -72,7 +73,8 @@ public:
     #endif
 
     #ifdef BUTTON_PIN
-    nrf_gpio_cfg_sense_input(digitalPinToInterrupt(BUTTON_PIN), NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_HIGH);
+    // configure button press to wake up when in powered off state
+    nrf_gpio_cfg_sense_input(digitalPinToInterrupt(BUTTON_PIN), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
     #endif
 
     sd_power_system_off();

+ 4 - 0
variants/rak_wismesh_tag/platformio.ini

@@ -67,6 +67,8 @@ build_src_filter = ${rak_wismesh_tag.build_src_filter}
 
 [env:RAK_WisMesh_Tag_companion_radio_usb]
 extends = rak_wismesh_tag
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${rak_wismesh_tag.build_flags}
   -I examples/companion_radio/ui-orig
@@ -83,6 +85,8 @@ lib_deps =
 
 [env:RAK_WisMesh_Tag_companion_radio_ble]
 extends = rak_wismesh_tag
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${rak_wismesh_tag.build_flags}
   -I examples/companion_radio/ui-orig

+ 4 - 0
variants/sensecap_solar/platformio.ini

@@ -74,6 +74,8 @@ build_src_filter = ${SenseCap_Solar.build_src_filter}
 
 [env:SenseCap_Solar_companion_radio_ble]
 extends = SenseCap_Solar
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags =
   ${SenseCap_Solar.build_flags}
   -D MAX_CONTACTS=350
@@ -92,6 +94,8 @@ lib_deps =
 
 [env:SenseCap_Solar_companion_radio_usb]
 extends = SenseCap_Solar
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags =
   ${SenseCap_Solar.build_flags}
   -D MAX_CONTACTS=350

+ 6 - 2
variants/t1000-e/platformio.ini

@@ -68,10 +68,12 @@ lib_deps = ${t1000-e.lib_deps}
 
 [env:t1000e_companion_radio_usb]
 extends = t1000-e
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags = ${t1000-e.build_flags}
   -I examples/companion_radio/ui-orig
-  -D MAX_CONTACTS=100
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
   -D OFFLINE_QUEUE_SIZE=256
@@ -89,6 +91,8 @@ lib_deps = ${t1000-e.lib_deps}
 
 [env:t1000e_companion_radio_ble]
 extends = t1000-e
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags = ${t1000-e.build_flags}
   -I examples/companion_radio/ui-orig
   -D MAX_CONTACTS=350

+ 4 - 0
variants/thinknode_m1/platformio.ini

@@ -67,6 +67,8 @@ lib_deps =
 
 [env:ThinkNode_M1_companion_radio_ble]
 extends = ThinkNode_M1
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${ThinkNode_M1.build_flags}
   -I src/helpers/ui
@@ -99,6 +101,8 @@ lib_deps =
 
 [env:ThinkNode_M1_companion_radio_usb]
 extends = ThinkNode_M1
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
 build_flags =
   ${ThinkNode_M1.build_flags}
   -I src/helpers/ui

+ 7 - 2
variants/wio-tracker-l1/platformio.ini

@@ -55,13 +55,16 @@ lib_deps = ${WioTrackerL1.lib_deps}
 
 [env:WioTrackerL1_companion_radio_usb]
 extends = WioTrackerL1
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags = ${WioTrackerL1.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=100
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=SH1106Display
   -D OFFLINE_QUEUE_SIZE=256
   -D PIN_BUZZER=12
+  -D QSPIFLASH=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
 build_src_filter = ${WioTrackerL1.build_src_filter}
@@ -77,6 +80,8 @@ lib_deps = ${WioTrackerL1.lib_deps}
 
 [env:WioTrackerL1_companion_radio_ble]
 extends = WioTrackerL1
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags = ${WioTrackerL1.build_flags}
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350

+ 5 - 0
variants/xiao_nrf52/platformio.ini

@@ -57,6 +57,8 @@ upload_protocol = nrfutil
 
 [env:Xiao_nrf52_companion_radio_ble]
 extends = Xiao_nrf52
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags =
   ${Xiao_nrf52.build_flags}
   -D MAX_CONTACTS=350
@@ -76,10 +78,13 @@ lib_deps =
 
 [env:Xiao_nrf52_companion_radio_usb]
 extends = Xiao_nrf52
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
 build_flags =
   ${Xiao_nrf52.build_flags}
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
+  -D QSPIFLASH=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Xiao_nrf52.build_src_filter}