Resources for Feather 32u4 LoRa Relay

A year or so back I purchased a couple Feather 32u4 LoRa RFM9x boards and managed to get them talking with copious amounts of help from online tutorials. The idea was to have a number of sensors around my rather remote Oregon Coast forest property that would let me know when animals, people or vehicles were traveling through. With one set as a transmitter and using a battery, I walked the property and, by pushing a button, it connected to the unit in the house, which sent back a confirmation that blinked the LED on the transmitting unit several times for confirmation.

Our small 'mom & pop' farm would like to offer camping to hikers along the new C2C trail which passes across our forest road some 2.4 miles (by road) above us. Because of the 2.4 mile distance off the C2C trail that hikers would have to travel to stay here, we would like to offer a pickup service to the hikers at the trail kiosk. It is located just about one mile away, line-of-sight. The idea is to have a simple interface of a few toggle switches that they would set with a code, and then use a switch press to send the packet to us. Our end would receive it and reply in order to light or blink an LED to let them know the notice was sent.

My current setup Feather unit sends a number from the remote transmitter (or sensor) which is received at the house and the number is used to set levels on pin outputs, in binary, which would light LEDs and/or set off an audible indicator to know which sensor/transmitter had fired. The terminal output of the house unit showed the following info:

"4/8/2023 2:26:10 PM",HOUSE RECEIVER:
"4/8/2023 2:26:10 PM",Received: 02 Vbat= 3.860 Attempt 3886 with RSSI= -57
"4/8/2023 2:26:10 PM",Sent reply: And hello back to you, 02
"4/8/2023 2:26:11 PM",Pinouts for sensor# 02: LOW LOW HIGH LOW

and indicates the sender number (also battery level, attempts, power and the lines that should go high or low). The reply back to the sending unit blinks the LED on the transmitter board for confirmation. I think I have much of the needed code in place such that lines on the trail kiosk transmitter could be used send the code (instead of the sensor number hard coded) using toggle switches, and if it is a known code, the house unit could reply back to blink the LEDs for the sender and alert us to go pick them up.

Of course this is hilly terrain, so there is no visual line-of-sight, but there is a ridge half way that seems like it could "see" both our property and the trail kiosk. So I know I will need at least one, and worst case (if the ridge top is too broad), two repeaters to get a signal back and forth. My one remote unit hooked up to a PIR sensor transmitted for about two months without intervention, so changing batteries on the ridge top unit(s) every two months would not be often enough to worry about any additional accessories like setting up any solar panels for the units. Also the simple 4" wire antennas were adequate for up to almost a quarter mile through trees and even behind hills, so if I can get the ridge unit to be line-of-sight, I may not even need fancier antennas.

I know that in the distant past, I had seen more than one example of setting, specifically, the Feather 32u4 Lora as a repeater, but haven't re-found them. Can anyone offer any suggestions, a reasonable path forward, or locations of any blogs or articles that explain how my repeater could be set up? Also, those Feather boards are my one and only foray into the Arduino IDE interface so I have very little practical experience otherwise ... so any other suggestions about my current code attempt would not go unappreciated!

Thank you for your time and consideration. Have a great day!

Bob

My current transmit and receive codes are below if interested ... computer crash since, hopefully backups are identical :slightly_smiling_face: ...

Receiver ...

// HOUSE RECEIVER
// Feather9x_RX


#include <SPI.h>
#include <RH_RF95.h>

#define LED 13
#define RFM95_CS 8   // for Feather32u4 RFM9x
#define RFM95_RST 4  // for Feather32u4 RFM9x
#define RFM95_INT 7  // for Feather32u4 RFM9x

#define RF95_FREQ 915.0

RH_RF95 rf95(RFM95_CS, RFM95_INT);  // Singleton instance of the radio driver


void setup() {

  pinMode(LED, OUTPUT);
  pinMode(RFM95_RST, OUTPUT);

  Serial.begin(115200);
  while (!Serial) {
    delay(1);
  }

  delay(10);

  Serial.println("Feather LoRa HOUSE RECIEVER online.");

  // manual reset
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(20);

  while (!rf95.init()) {
    Serial.println("LoRa radio init failed");
    Serial.println("Uncomment '#define SERIAL_DEBUG' in RH_RF95.cpp for detailed debug info");
    while (1)
      ;
  }
  Serial.println("LoRa radio init OK!");

  if (!rf95.setFrequency(RF95_FREQ)) {
    Serial.println("setFrequency failed");
    while (1)
      ;
  }
  Serial.print("Set Freq to: ");
  Serial.println(RF95_FREQ);
  rf95.setTxPower(23, false);
  Serial.println("Set Tx power to 23. ");
}

void loop() {

  if (rf95.available()) {  // Should be a message for us now
    uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
    uint8_t len = sizeof(buf);

    if (rf95.recv(buf, &len)) {  // RH_RF95::printBuffer("Received: ", buf, len);
      Serial.print("Got: ");
      Serial.print((char*)buf);

      Serial.print("  -  with RSSI= ");
      Serial.println(rf95.lastRssi(), DEC);
      Serial.println();

      uint8_t data[] = "And hello back to you";
      rf95.send(data, sizeof(data));
      rf95.waitPacketSent();
      Serial.println("  Sent this reply:  And hello back to you.");
      Serial.println();

      unsigned long currentMillis = millis();
      unsigned long seconds = currentMillis / 1000;
      unsigned long minutes = seconds / 60;
      unsigned long hours = minutes / 60;
      unsigned long days = hours / 24;

      Serial.print("    Uptime: ");
      currentMillis %= 1000;
      seconds %= 60;
      minutes %= 60;
      hours %= 24;
      Serial.print(days);
      Serial.print(' ');
      if (hours < 10)
        Serial.print('0');
      Serial.print(hours);
      Serial.print(':');
      if (minutes < 10)
        Serial.print('0');
      Serial.print(minutes);
      Serial.print(':');
      if (seconds < 10)
        Serial.print('0');
      Serial.println(seconds);
      Serial.println();
      Serial.println();
      Serial.println();

      //blink led three times ...
      digitalWrite(LED, HIGH);
      delay(300);  // Wait 1/2 seconds
      digitalWrite(LED, LOW);
      delay(250);  // Wait 1/2 seconds
      digitalWrite(LED, HIGH);
      delay(300);  // Wait 1/2 seconds
      digitalWrite(LED, LOW);
      delay(250);  // Wait 1/2 seconds
      digitalWrite(LED, HIGH);
      delay(300);  // Wait 1/2 seconds
      digitalWrite(LED, LOW);

    } else {
      Serial.println("Receive failed ");
    }
  }
}

Transmitter ...

// SENSOR TRANSMITTER - Feather9x_TX

#include <SPI.h>
#include <RH_RF95.h>
#include <Adafruit_SleepyDog.h>


// for feather32u4
#define RFM95_CS 8
#define RFM95_RST 4
#define RFM95_INT 7
#define RF95_FREQ 915.0
#define VBATPIN A9
#define LED 13
#define SENSE 3

RH_RF95 rf95(RFM95_CS, RFM95_INT);

void setup() {
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, HIGH);

  //pinMode(SENSE, INPUT);
  //pinMode(SENSE, INPUT_PULLUP);
  //digitalWrite(SENSE, LOW);

  Serial.begin(115200);
  while (!Serial) {
    delay(1);
  }

  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  while (!rf95.init()) {
    Serial.println("LoRa radio init failed");
    Serial.println("Uncomment '#define SERIAL_DEBUG' in RH_RF95.cpp for detailed debug info");
    while (1)
      ;
  }

  if (!rf95.setFrequency(RF95_FREQ)) {
    Serial.println("setFrequency failed");
    while (1)
      ;
  }


  Serial.print("Set Freq to: ");
  Serial.println(RF95_FREQ);
  rf95.setTxPower(23, false);
  Serial.println("Set Tx power to 23. ");


  //attachInterrupt(digitalPinToInterrupt(SENSE), sendIt, RISING);
  Serial.println("Delaying 30 seconds ...");
  delay(30000);  // keep this to make it easier to upload changes ... gives 30 secs before running loop ...
}

int16_t packetnum = 0;  // packet counter, we increment per transmission


void loop() {

  Serial.println("Beginning loop: Waiting 5 seconds ...\n\n\n ");
  delay(5000);
  //rf95.sleep();
  //Watchdog.sleep(60000);
  // We resume here after the Interrupt

  float measuredvbat = analogRead(VBATPIN);
  measuredvbat *= 2;     // we divided by 2, so multiply back
  measuredvbat *= 3.3;   // Multiply by 3.3V, our reference voltage
  measuredvbat /= 1024;  // convert to voltage

  double integer;
  float fractional = modf(measuredvbat, &integer);

  fractional *= 1000;
  int wfrac = fractional;

  char radiopacket[38] = "10 Vbat=       Attempt #             ";

  int r = 16;
  while (r > 15) {
    r = rand() % 16;
    r++;
  }
  char sen[2];

  itoa(r, sen, 10);
  if (strlen(sen) == 2) {
    strncpy(radiopacket, sen, strlen(sen));
  } else {
    strncpy(radiopacket, "0", 1);
    strncpy(radiopacket + 1, sen, strlen(sen));
  }

  char vbat[1];
  itoa(integer, vbat, 10);
  strncpy(radiopacket + 9, vbat, strlen(vbat));

  Serial.print("REMOTE SENSOR: random sensor= ");
  Serial.println(r);
  Serial.print("VBat: ");
  Serial.print(measuredvbat);
  Serial.print(", Integer = ");
  Serial.print(vbat);
  Serial.print(", Fraction = ");
  Serial.println(wfrac);

  //if(senseState == HIGH){
  //   Serial.println(" INT BUTTON PRESSED >>> ");
  //   senseState = LOW ;
  //}

  strncpy(radiopacket + 10, ".", 1);

  char vbat2[3];
  itoa(wfrac, vbat2, 10);
  if (wfrac < 10) {
    strncpy(radiopacket + 11, "00", 3);
    strncpy(radiopacket + 13, vbat2, strlen(vbat2));
  } else if (wfrac < 100) {
    strncpy(radiopacket + 11, "0", 2);
    strncpy(radiopacket + 12, vbat2, strlen(vbat2));
  } else {
    strncpy(radiopacket + 11, vbat2, strlen(vbat2));
  }

  itoa(packetnum++, radiopacket + 23, 10);
  radiopacket[37] = 0;

  Serial.print("Sending ");
  Serial.println(radiopacket);
  delay(10);
  rf95.send((uint8_t *)radiopacket, sizeof(radiopacket));
  rf95.waitPacketSent();
  delay(10);

  uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
  uint8_t len = sizeof(buf);

  if (rf95.waitAvailableTimeout(5000)) {
    // Should be a reply message for us now
    if (rf95.recv(buf, &len)) {
      Serial.print("Got reply: ");
      Serial.println((char *)buf);
      Serial.print("RSSI: ");
      Serial.println(rf95.lastRssi(), DEC);
      Serial.println("Waiting 10 seconds after valid reply to send again ...");
      delay(10000);  // Wait 10 seconds between transmits, could also 'sleep' here!
    } else {
      Serial.println("Receive failed");
    }
  } else {
    Serial.println("Five seconds with no listener reply, transmitting again ...");
  }


  if (rf95.waitAvailableTimeout(1000)) {  // Should be a reply message for us now
    if (rf95.recv(buf, &len)) {
      Serial.print("Got reply: ");
      Serial.print((char *)buf);
      Serial.print("  with RSSI= ");
      Serial.println(rf95.lastRssi(), DEC);
      //      Serial.println("Waiting 10 seconds for next attempt ... \n\n");
      digitalWrite(LED, HIGH);
      delay(200);
      digitalWrite(LED, LOW);
      delay(200);
      digitalWrite(LED, HIGH);
      delay(200);
      digitalWrite(LED, LOW);
      //delay(5000);
    } else {
      Serial.println("Receive failed.");
    }
  } else {
    Serial.println("No reply. \n\n\n");
    if (rf95.available()) {
      digitalWrite(LED, HIGH);
      delay(100);
      digitalWrite(LED, LOW);
      delay(100);
      digitalWrite(LED, HIGH);
      delay(100);
      digitalWrite(LED, LOW);
      delay(100);
      digitalWrite(LED, HIGH);
      delay(100);
      digitalWrite(LED, LOW);
      delay(100);
      digitalWrite(LED, HIGH);
      delay(100);
      digitalWrite(LED, LOW);
      delay(100);
      digitalWrite(LED, HIGH);
      delay(100);
      digitalWrite(LED, LOW);
      delay(100);
    }
    digitalWrite(LED, HIGH);
    delay(600);
    digitalWrite(LED, LOW);
    //delay(5000) ;
  }


  //attachInterrupt(digitalPinToInterrupt(SENSE), sendIt, RISING);
}



void sendIt() {
  detachInterrupt(digitalPinToInterrupt(SENSE));
}

You can set up a mesh network using RadioHead RHMesh, which forms a standalone network with automatic route discovery and message forwarding using intermediate nodes. You will need at least 3 nodes, for non-line-of-sight communications.

I'll shortly be posting some examples on Github that do just what you want, but in the meantime you can see how RHMesh works by studying this tutorial: LoRa Mesh Networking with Simple Arduino-Based Modules | Project Lab

A version of the nootropic design code that runs on the Feather M0 LoRa is appended below, and with some modifications could also do what you want. There is no provision for external text message entry in the code below. It works very, very well, and the RadioHead library is a superb, truly professional contribution.

BTW I'm in Eugene, and plan to hike parts of the C2C.

// self-organizing standalone LoRa mesh network for Feather M0 LoRa modules
// S. James Remington 2/2024
// 
// heavily modified from example code at https://nootropicdesign.com/projectlab/2018/10/20/lora-mesh-networking/
// github source https://github.com/nootropicdesign/lora-mesh
// start, remove ATmega-specific stuff, fixed some errors
// added Feather M0 specific pin defs and setup
// rewrote local node network report for simpler display
// added example of arbitrary node to node messages

// This version features extensive console serial debugging info, and is intended to test network function
// under a variety of circumstances, such as network response when nodes drop out or come on line.
// Note that the RadioHead library has a built in feature to predetermine several differen node connectivities.
// See RHRouter.h, .cpp source code for RH_TEST_NETWORK options.

// Need to hard code node address on the Feather M0 (no EEPROM)

#include <SPI.h>
#include <RHRouter.h>
#include <RHMesh.h>
#include <RH_RF95.h>
#define RH_HAVE_SERIAL   //for debug print options in RadioHead mesh and router code

//RH_TEST_NETWORK 1  //if defined in RHRouter.h => Test Network 1-2-3-4

// for feather m0
#define RFM95_CS 8
#define RFM95_RST 4
#define RFM95_INT 3
#define VBATPIN A7
#define LED 13

#define N_NODES 4  //max 10 given RHRouter table dimensions
// *** hard coded node address *** (no EEPROM on Feather M0)
#define THIS_NODE 1  //white antenna

uint8_t nodeId;
uint8_t routes[N_NODES]; // full routing table for mesh
int16_t rssi[N_NODES]; // signal strength info
uint16_t packet_num = 0;  //optional packet ID, included in message

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

// Class to manage message delivery and receipt, using the driver declared above
RHMesh *manager;

// message buffers
char buf[RH_MESH_MAX_MESSAGE_LEN];
char report[80];  //network node info report

//place holder
byte readNodeId(void) {
  return THIS_NODE;
}

void setup() {
  randomSeed(analogRead(0) + analogRead(1) + analogRead(2)); //for now

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, 0); //red LED off
  
  Serial.begin(115200);
  // while (!Serial) ; // Wait for serial port to be available
  delay(2000);  //no hanging for battery powered node

  nodeId = readNodeId();
  if (nodeId > 10) { //RHrouter table dimensioned to 10
    Serial.print("NodeId invalid: ");
    Serial.println(nodeId);
    while(1);  //hang
  }
  Serial.print("Initializing node ");
  Serial.println(nodeId);

  manager = new RHMesh(rf95, nodeId);

  // manual reset
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  if (!manager->init()) {
    Serial.println("Init failed");
    //   rf95.printRegisters();            //uncomment to print radio registers for debugging
    while (1); //hang
  } else {
    Serial.println("Init OK");
  }
  rf95.setTxPower(0, false);  //dBm, 0 for bench tests, 20 max
  rf95.setFrequency(915.0);
  rf95.setCADTimeout(500);

  // library predefined radio configurations: see RH_RF95.h source code for the details
  // Bw125Cr45Sf128 (the chip default, medium range, CRC on, AGC enabled)
  // Bw500Cr45Sf128
  // Bw31_25Cr48Sf512
  // Bw125Cr48Sf4096  long range, CRC and AGC on, low data rate
  /*

    // possible long range configuration
      if (!rf95.setModemConfig(RH_RF95::Bw125Cr48Sf4096)) {
        Serial.println("Set config failed");
        while (1); //hang
        }
  */
  Serial.println("RF95 ready");
  Serial.print("Max msglen: ");
  Serial.println(RH_MESH_MAX_MESSAGE_LEN);  //default 249

  // initialize tables for connectivity report
  for (uint8_t i = 0 ; i < N_NODES; i++) {
    routes[i] = 0;  //next hop or final dest
    rssi[i] = 0;
  }
}

// translate error number to message
const char* getErrorString(uint8_t error) {
  switch (error) {
    case 1: return "invalid length";
      break;
    case 2: return "no route";
      break;
    case 3: return "timeout";
      break;
    case 4: return "no reply";
      break;
    case 5: return "unable to deliver";
      break;
  }
  return "unknown error";
}
// table informs only of next hop
void updateRoutingTable() {
  for (uint8_t n = 1; n <= N_NODES; n++) {
    RHRouter::RoutingTableEntry *route = manager->getRouteTo(n);
    if ( (n == nodeId) || route == NULL) { //fixed NULL pointer access sjr
      routes[n - 1] = 255;
      rssi[n - 1] = 0;
    }
    else {
      routes[n - 1] = route->next_hop;
    }
  }
}


// ASCII .csv format report of routing info: (node,rssi) pairs
// return 0: normal completion
// return 1: string terminated early, anticipated buffer overflow.

uint8_t getRouteInfoString(char *p, size_t len) {
  char stemp[8]; //temp buffer for int conversion

  p[0] = '\0'; //initialize string

  for (uint8_t n = 1; n <= N_NODES; n++) {
    itoa(routes[n - 1], stemp, 10); //target node number
    if (strlen(p) + strlen(stemp) < len - 1) strcat(p, stemp);
    else return 1; //buffer overflow avoided

    if (strlen(p) + 1 < len - 1) strcat(p, ",");
    else return 1;

    itoa(rssi[n - 1], stemp, 10); //rssi info
    if (strlen(p) + strlen(stemp) < len - 1) strcat(p, stemp);
    else return 1;

    if (n < N_NODES) {
      if (strlen(p) + 1 < len - 1) strcat(p, ",");
      else return 1;
    }
  }
  return 0;  //no output buffer overflow
}

// log network hop/rssi table status info: this_node,(node,rssi) pairs
void printNodeInfo(uint8_t node, char *s) {
  Serial.print(node);
  Serial.print(',');
  Serial.print(s);
  //  Serial.print(',');
  //  Serial.print(freeMemory()); //check for leaks (none detected in trials)
  Serial.println();
}


void loop() {

  uint8_t flags = 0; //sendToWait() optional message flags

  for (uint8_t n = 1; n <= N_NODES; n++) {
    if (n == nodeId) continue; // self

    updateRoutingTable();
    if (getRouteInfoString(report, sizeof(report))) {  //non zero = error
      //      Serial.println("getRouteInfoString: result truncated");
    }

    // Example of message with source, target, packet number and battery voltage
    unsigned long vbat = 200000UL * analogRead(VBATPIN) / (33UL * 1024); //mV
    // packet number is stored and incremented on a per node basis, 
    snprintf(buf, sizeof (buf), "%d>%d#%u: %lu mV", nodeId, n, packet_num++, vbat);

    // log packet to be sent on console
    Serial.print(">");
    Serial.print(n);
    Serial.print(" {");  //added braces as message delimiters
    Serial.print(buf);
    Serial.print("}");

    // send an acknowledged message to the target node
    // Note: sendtoWait times out after preset number of retries fails, see RHReliableDatagram.h
    uint8_t error = manager->sendtoWait((uint8_t *)buf, strlen(buf), n, flags);

    if (error != RH_ROUTER_ERROR_NONE) {  //print relevant error message, if any, or OK
      Serial.print(" !");
      Serial.println(getErrorString(error));
    }
    else {
      Serial.println(" OK");

      // we received an acknowledgement from the next hop for the node we tried to send to.
      RHRouter::RoutingTableEntry *route = manager->getRouteTo(n);
      if (route != NULL ) {
        if (route->next_hop != 0) rssi[route->next_hop - 1] = rf95.lastRssi();
      }
    }

    // network info report
    printNodeInfo(nodeId, report);
    //   manager->printRoutingTable(); // complete RadioHead routing table, less informative

    // listen for incoming messages. To reduce collisions, wait a random amount of time
    // then transmit again to the next node

    uint16_t waitTime = random(1000, 5000);  //milliseconds
    uint8_t len = sizeof(buf);
    uint8_t from;
    uint8_t hops;
    //blocking call:
    if (manager->recvfromAckTimeout((uint8_t *)buf, &len, waitTime, &from, NULL, NULL, &flags, &hops))
    {
      if (len < sizeof (buf)) buf[len] = 0; // null terminate ASCII string
      else buf[len - 1] = 0;
      
      // Get sender rssi. we received data from node 'from', but it may have arrived via an intermediate node
      // SJR note: the following gets the address of the first hop to "from", which may not be the 
      // same as the node delivering this message. But the rssi information is not critical.
      uint8_t via = from;
      RHRouter::RoutingTableEntry *route = manager->getRouteTo(from);
      if (route != NULL ) {
        via = route->next_hop;
        if (via != 0) rssi[via - 1] = rf95.lastRssi();
      }

      // log ASCII message received, including last intermediate node
      if (via != from) {
        Serial.print("<");
        Serial.print(via); //last hop
      }
      Serial.print("<");
      Serial.print(from);
 //     Serial.print(" f:");
 //     Serial.print(flags, HEX);
      Serial.print(" h:");
      Serial.print(hops);
      Serial.print(" \""); //added quotes
      Serial.print(buf);
      Serial.println("\"");

    }  //end recvfromAckTimeout
  }  //end for n: N_NODES
}
/*
  // monitor free memory

  #ifdef __arm__
  // should use uinstd.h to define sbrk but Due causes a conflict
  extern "C" char* sbrk(int incr);
  #else  // __ARM__
  extern char *__brkval;
  #endif  // __arm__

  int freeMemory() {
  char top;
  #ifdef __arm__
  return &top - reinterpret_cast<char*>(sbrk(0));
  #elif defined(CORE_TEENSY) || (ARDUINO > 103 && ARDUINO != 151)
  return &top - __brkval;
  #else  // __arm__
  return __brkval ? &top - __brkval : &top - __malloc_heap_start;
  #endif  // __arm__
  }
*/

@jremington - Thank you. I had seen references to mesh networks with LoRa but guessed they would be way too complicated for anything I tried to install. I did read through the link you provided and it makes it seem much more 'do-able' with the routing table examples, etc. Stepping through the code you provided and trying to make sense of that may take a lot longer :). Can contact at ottf.us which jumps to our site if you do manage to get out to hike the trail.

If you have questions about the posted code, either the nooptropic design example or that above, I'll be happy to answer them. It took me a while to understand what it did, but that is now very clear, and elegant.

You should also study the simple "reliable datagram" server and client examples in the RadioHead library, which give examples of how to use node addresses, with acknowledged delivery. It forms a layer below RHMesh. You don't need RHMesh to form a 1-2-3 node-repeater-node link, if you use fixed node addresses.

There is also Meshatastic, which might fit your requirements;

Lots of plastic cases available for the handheld units too.

Popular in America I believe.

As for a simple relay, positioned at a high location and receiving and re-transmitting packets the code for that is fairly easy.

@srnet Thank you. I read through some of the documentation for the open source Meshtastic. It appears to have some distinct advantages for someone like me. Let me know if any of the following is incorrect or would need additional dependencies.

I wouldn't need to figure out any code as there is a method to flash any of the Meshtastic devices. Meshtastic interfaces using Bluetooth with a phone, so I wouldn't have to create any hardware interfaces for either the hikers or myself. Of course, it also requires that one of the hikers has a phone device with them, which is probably not much of a hurdle these days. The communicating phones would require an app, but the Meshtastic flashed LoRa devices can communicate with either Android or IOS phones, without having to be set up specifically for either one. Basically every device is a relay once flashed so they are interchangeable except for the unique ID for each. I'm guessing that all of the hardware options shown can be used without additional components. Then the base price for each 'node' would be similar to the price of a Feather that I've already used, so there would be no excessive costs compared to trying to implement Feather boards or something similar by myself.

My biggest concern would be power. I haven't found average power consumption, so I don't know how often I would have to replace a battery for the remote relay and kiosk end devices. I saw that if you went with something like the "WisBlock" devices shown, there are solar panel enclosures shown as available. If that panel is actually sufficient, it could eliminate that worry for an additional $25-$35 per unit. But that would only be feasible depending on sunlight hours available at the remote locations. The ridge top should be fine as it could be set on a pole or tree. The kiosk is a shaded area and I believe it is mostly on the north side of a hill, so would get little direct sunlight. Since the devices connect by Bluetooth to the phone, I assume the device would need to be fairly close to the kiosk also which might make solar power problematic.

Does anyone have any real world experience with any of the Meshtastic supported hardware that could guess at battery life for a remote unit? It would not need GPS of course, and it would only broadcast, at most, once or twice in a day and most likely once or twice in a week. As mentioned in my original post, the Feather boards can be quite frugal with sleep modes and can last a couple months on a small poly battery.

Thank you for your time!

Bob

Check (or post on) the Meshtastic forum. I think there are several smaller group discussion fora, as well.

It will take an hour or two, but I'll put together and post a relay code example using the RadioHead library.

No, but for a reamote relay I dont see a problem. The meshtastic lot do appear to use solar powered relays, solar panels are not expensive these days, so just use one big enough.

As well as the Meshtastic forum there is a fair bit of activity in the Reddit group.

Here are tested and working examples of a client and repeater for the Feather M0 LoRa, using RHReliableDatagram. At the moment the repeater just echos back to the sending client, but obviously, it could send it on to another client, or to both using two calls.

Repeater (node 1)

// modified from example rf95_reliable_datagram_server.pde
// -*- mode: C++ -*-
// Example sketch showing how to create a simple addressed, reliable messaging repeater
// with the RHReliableDatagram class, using the RH_RF95 driver to control a RF95 radio.
// It is designed to work with the other example rf95_reliable_datagram_client
// Tested with Feather M0 LoRa 

#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>

#define REPEATER_ADDRESS 1

// for feather m0
#define RFM95_CS 8
#define RFM95_RST 4
#define RFM95_INT 3
#define VBATPIN A7

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

// Class to manage message delivery and receipt, using the driver declared above
RHReliableDatagram manager(rf95, REPEATER_ADDRESS);


void setup() 
{
   Serial.begin(115200);
  while (!Serial) ; // Wait for serial port to be available

    // reset radio
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);
  
  if (!manager.init())
    Serial.println("init failed");
    
  rf95.setTxPower(0, false);  //0 dBm for bench tests, 20 max
  rf95.setFrequency(915.0);
  rf95.setCADTimeout(500);

  // predefined configurations: see RH_RF95.h source code for the details
  // Bw125Cr45Sf128 (default, medium range, CRC on, AGC enabled)
  // Bw500Cr45Sf128
  // Bw31_25Cr48Sf512
  // Bw125Cr48Sf4096  long range, CRC and AGC on, low data rate
  /*

    // example long range configuration
      if (!rf95.setModemConfig(RH_RF95::Bw125Cr48Sf4096)) {
        Serial.println("Set config failed");
        while (1); //hang
        }
  */
}

uint8_t data[] = "And hello back to you";
uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];

void loop()
{
  if (manager.available())
  {
    // Wait for a message addressed to us from the client
    uint8_t len = sizeof(buf);
    uint8_t from;
    if (manager.recvfromAck(buf, &len, &from))
    {
      Serial.print("message from: ");
      Serial.print(from);
      Serial.print(": ");
      Serial.println((char*)buf);

      // Send a reply back to the originator client
      if (!manager.sendtoWait(data, sizeof(data), from)) {
        Serial.print("sendtoWait failed to node ");
        Serial.println(from);
      }
    }
  }
}

Client (node 2)

// modified from example rf95_reliable_datagram_client
// -*- mode: C++ -*-
// Example sketch showing how to create a simple addressed, reliable messaging client
// with the RHReliableDatagram class, using the RH_RF95 driver to control a RF95 radio.
// It is designed to work with the other example rf95_reliable_datagram_server
// Tested with Feather M0 LoRa

#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>

#define REPEATER_ADDRESS 1
#define CLIENT_ADDRESS 2

//client 2 yellow antenna COM14
//client 3 red antenna COM9

// for feather m0
#define RFM95_CS 8
#define RFM95_RST 4
#define RFM95_INT 3
#define VBATPIN A7

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

// Class to manage message delivery and receipt, using the driver declared above
RHReliableDatagram manager(rf95, CLIENT_ADDRESS);

void setup() 
{
 
  Serial.begin(115200);
  while (!Serial) ; // Wait for serial port to be available

    // reset radio
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);
  
  if (!manager.init()) {
    Serial.println("Init failed");
    //   rf95.printRegisters();    //uncomment to print radio registers for debugging
    while (1); //hang
  } else {
    Serial.println("Init OK");
  }
    
  rf95.setTxPower(0, false);  //0 dBm for bench tests, 20 max
  rf95.setFrequency(915.0);
  rf95.setCADTimeout(500);

  // predefined configurations: see RH_RF95.h source code for the details
  // Bw125Cr45Sf128 (default, medium range, CRC on, AGC enabled)
  // Bw500Cr45Sf128
  // Bw31_25Cr48Sf512
  // Bw125Cr48Sf4096  long range, CRC and AGC on, low data rate
  /*

    // example long range configuration
      if (!rf95.setModemConfig(RH_RF95::Bw125Cr48Sf4096)) {
        Serial.println("Set config failed");
        while (1); //hang
        }
  */
}

uint8_t data[] = "Hello World!";
uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];

void loop()
{
  Serial.println("Sending to rf95_reliable_datagram_server");
    
  // Send a message to manager_server (repeater)
  if (manager.sendtoWait(data, sizeof(data), REPEATER_ADDRESS))
  {
    // Now wait for a reply from the repeater
    uint8_t len = sizeof(buf);
    uint8_t from;   
    if (manager.recvfromAckTimeout(buf, &len, 2000, &from))
    {
      Serial.print("got reply from : 0x");
      Serial.print(from, HEX);
      Serial.print(": ");
      Serial.println((char*)buf);
    }
    else
    {
      Serial.println("No reply, is rf95_reliable_datagram_server running?");
    }
  }
  else
    Serial.println("sendtoWait failed");
  delay(500);
}

@jremington @srnet Thank you both for all of the time you have given me! Unexpected and awesome considering I'm not a regular on the forum!

As for larger solar panels, I don't think they would be a good solution. A unit on top of the ridge is probably relatively safe, but the kiosk is right on a road that is open to the public. Signs out here tend to get shot up, vehicles parked too long tend to get trashed, etc. I wouldn't expect an easily visible unit to last very long, whereas a small unit might mostly go unnoticed. And if Meshtastic uses standard Bluetooth, it likely would not connect far enough above in a tree for a larger hidden solar panel.

I will spend some time later trying to peruse the Meshtastic forum and/or Reddit to see if anyone mentions a similar setup or battery life. Unless I find a simpler, more frugal solution with Meshtastic, a Feather based mesh or relay may be the most practical even if a bit more difficult for me.

@jremington Your code offering is above and beyond. Thank you!!!! I still have the two feather LoRa units I originally set up as mentioned above. I can spend some time to see if I can get them to work with your code. I'm not sure, but it probably would only take a couple changes to pin numbers to work the same on my Feather32u4 since I did just see a reference to using RHReliableDatagram on another?

Please don't think I'm uninterested if this takes me a while. Although retired from a full time job, lots of farm chores, repairs and (hopefully occasionally) improvements keep me busy ... and often times busier than when I had a full time job.

And, of course, you are both welcome to stay and use the facilities if you ever get by here!

Thank you for your time and consideration. Have a great day!

Bob

The example on this Adafruit Feather 32u4 LoRa getting started page shows you which pin changes to make.

The example also uses RadioHead, but "unreliable, unaddressed" datagrams. In other words, there is no built-in means of directing a message to a particular node, determining whether the message was actually received, or by which node.

Take your time and have fun!

@jremington Yes, I did have those pins from my earlier sensor attempts and had also used the RadioHead library. Basically was asking do you know if RHReliableDatagram works OK on the 32u4 as well as the M0?

Thank you.

Bob

The results should be identical for the two processors, but I don't have the 32u4 to check. Since the nooptropics design RHMesh code ran equally well on the ATmega328 and the Feather M0, I strongly doubt that you will have any problems.

I recommend to just change the radio pin numbers in my Reliable Datagram examples to the following, and try them.

//#if defined (__AVR_ATmega32U4__)  // Feather 32u4 w/Radio
  #define RFM95_CS    8
  #define RFM95_INT   7
  #define RFM95_RST   4

@jremington - if I'm not overstaying my welcome, can I pester you one more time before I wander off and try more on my own?

I spent a little time installing the library and compiling, verifying and saving your code but I haven't yet downloaded it to the Feathers.

Is this correct?:

the recvfromAck() function from the library says it "processes and possibly routes any received messages addressed to other nodes and delivers any messages addressed to this node".

Seems straightforward that:
The function knows this node number from the "RHReliableDatagram manager(rf95, CLIENT_ADDRESS)" class setup in your code.
If there is a message to this node (which will return true from the function) it will print it on the serial line and send a message back to the calling node, hard coded as "And hello back to you".  

I am less sure how, or if, it "processes and possibly routes any received messages addressed to other nodes".
Will that same function also send out any message received for a different node but not return true?

Thanks again!

Bob

I don't understand that comment, either, and couldn't guess how it would work.

I had some time, and realized that the server/client examples are not properly structured for a repeater function. So, I restructured both the way I understand the send and receive functions to work, and the result does do what I want with three nodes.

There are a few more failed transmissions than I would like to see, so the loop timing may need to be adjusted a bit to avoid collisions (at the moment, that is set largely by the range of the random timeout value set the receive function in the client nodes).

This very simple repeater code just acknowledges a message from one node and sends it (reliably) to the other.

Valuable lesson learned: RHMesh really does simplify the entire process. You simply send a message from one node to another, and the network does all the routing and acknowledging.

Repeater:

// modified from example rf95_reliable_datagram_server.pde
// -*- mode: C++ -*-
// Example sketch showing how to create a simple addressed, reliable messaging repeater
// with the RHReliableDatagram class, using the RH_RF95 driver to control a RF95 radio.
// It is designed to work with the other example rf95_reliable_datagram_client
// Tested with Feather M0 LoRa

// Simple example restructured to accommodate two client nodes (see client node list below)
// To generalize this to multiple nodes, the client message would have to contain the target node

#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>

#define REPEATER_ADDRESS 1
// client node list
int client_nodes[2] = {2, 3}; 

// for feather m0
#define RFM95_CS 8
#define RFM95_RST 4
#define RFM95_INT 3
#define VBATPIN A7

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

// Class to manage message delivery and receipt, using the driver declared above
RHReliableDatagram manager(rf95, REPEATER_ADDRESS);


void setup()
{
  Serial.begin(115200);
  while (!Serial) ; // Wait for serial port to be available

  // reset radio
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  if (!manager.init())
    Serial.println("init failed");

  rf95.setTxPower(0, false);  //0 dBm for bench tests, 20 max
  rf95.setFrequency(915.0);
  rf95.setCADTimeout(500);

  // predefined configurations: see RH_RF95.h source code for the details
  // Bw125Cr45Sf128 (default, medium range, CRC on, AGC enabled)
  // Bw500Cr45Sf128
  // Bw31_25Cr48Sf512
  // Bw125Cr48Sf4096  long range, CRC and AGC on, low data rate
  /*
    // example long range configuration
      if (!rf95.setModemConfig(RH_RF95::Bw125Cr48Sf4096)) {
        Serial.println("Set config failed");
        while (1); //hang
        }
  */
}

uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];

void loop()
{
  if (manager.available())
  {
    // Wait for a message addressed to us from the client
    uint8_t len = sizeof(buf);
    uint8_t from;
    if (manager.recvfromAck(buf, &len, &from))
    {
      Serial.print("From ");
      Serial.print(from);
      Serial.print(": ");
      Serial.println((char*)buf);
     
      // forward the message to other client

      int forward = 0; //default = unrecognized node
      if (from == client_nodes[0]) forward = client_nodes[1];
      if (from == client_nodes[1]) forward = client_nodes[0];

      if (forward)
        if (manager.sendtoWait(buf, sizeof(buf), forward)) {
          Serial.print("Successfully forwarded to ");
          Serial.println(forward);
        }
        else {
          Serial.print("Forwarding failed to node ");
          Serial.println(forward);
        }
    }
  }
}

Client node 2

// modified from example rf95_reliable_datagram_client
// -*- mode: C++ -*-
// Example sketch showing how to create a simple addressed, reliable messaging client
// with the RHReliableDatagram class, using the RH_RF95 driver to control a RF95 radio.
// It is designed to work with the other example rf95_reliable_datagram_server
// Tested with Feather M0 LoRa

#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>

#define REPEATER_ADDRESS 1
#define CLIENT_ADDRESS 2

//client 2 yellow antenna COM14
//client 3 red antenna COM9

// for feather m0
#define RFM95_CS 8
#define RFM95_RST 4
#define RFM95_INT 3
#define VBATPIN A7

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

// Class to manage message delivery and receipt, using the driver declared above
RHReliableDatagram manager(rf95, CLIENT_ADDRESS);

void setup()
{

  Serial.begin(115200);
  while (!Serial) ; // Wait for serial port to be available

  // reset radio
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  if (!manager.init()) {
    Serial.println("Init failed");
    //   rf95.printRegisters();    //uncomment to print radio registers for debugging
    while (1); //hang
  } else {
    Serial.println("Init OK");
  }

  rf95.setTxPower(0, false);  //0 dBm for bench tests, 20 max
  rf95.setFrequency(915.0);
  rf95.setCADTimeout(500);

  // predefined configurations: see RH_RF95.h source code for the details
  // Bw125Cr45Sf128 (default, medium range, CRC on, AGC enabled)
  // Bw500Cr45Sf128
  // Bw31_25Cr48Sf512
  // Bw125Cr48Sf4096  long range, CRC and AGC on, low data rate
  /*

    // example long range configuration
      if (!rf95.setModemConfig(RH_RF95::Bw125Cr48Sf4096)) {
        Serial.println("Set config failed");
        while (1); //hang
        }
  */
}

char buf[RH_RF95_MAX_MESSAGE_LEN];

void loop()
{
  Serial.print("To repeater: ");
  snprintf(buf, sizeof(buf), "Message from node %d", CLIENT_ADDRESS);
  Serial.print(buf);

  // Send message to the repeater. Times out after 3 retries, by default

  if (manager.sendtoWait((uint8_t *)buf, sizeof(buf), REPEATER_ADDRESS))
    Serial.println();
  else
    Serial.println(" !failed");

  // check for forwarded messages from repeater, with random wait times to avoid collisions

  uint16_t waitTime = random(1000, 3000);  //milliseconds
  // these to be updated by recvfromAckTimeout()
  uint8_t len = sizeof(buf);
  uint8_t from;

  //blocking call, times out after waitTime
  if (manager.recvfromAckTimeout((uint8_t *)buf, &len, waitTime, &from)) {
    //arguments: (uint8_t *buf, uint8_t *len, uint16_t timeout, uint8_t *source=NULL, uint8_t *dest=NULL,
    // uint8_t *id=NULL, uint8_t *flags=NULL, uint8_t* hops = NULL)

    // log ASCII message received
    if (len < sizeof (buf)) buf[len] = 0; // null terminate ASCII string
    else buf[len - 1] = 0;
    if (from == REPEATER_ADDRESS) {
      Serial.print("forwarded message: ");
      Serial.print(buf);
      Serial.println();
    }
  } //end recvfromAckTimeout
}

Client Node 3

// modified from example rf95_reliable_datagram_client
// -*- mode: C++ -*-
// Example sketch showing how to create a simple addressed, reliable messaging client
// with the RHReliableDatagram class, using the RH_RF95 driver to control a RF95 radio.
// It is designed to work with the other example rf95_reliable_datagram_server
// Tested with Feather M0 LoRa

// Restructured client example to allow multiple client nodes. Repeater only works with two at the moment

#include <RHReliableDatagram.h>
#include <RH_RF95.h>
#include <SPI.h>

#define REPEATER_ADDRESS 1
#define CLIENT_ADDRESS 3

//client 2 yellow antenna COM14
//client 3 red antenna COM9

// for feather m0
#define RFM95_CS 8
#define RFM95_RST 4
#define RFM95_INT 3
#define VBATPIN A7

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

// Class to manage message delivery and receipt, using the driver declared above
RHReliableDatagram manager(rf95, CLIENT_ADDRESS);

void setup()
{

  Serial.begin(115200);
  while (!Serial) ; // Wait for serial port to be available

  // reset radio
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  if (!manager.init()) {
    Serial.println("Init failed");
    //   rf95.printRegisters();    //uncomment to print radio registers for debugging
    while (1); //hang
  } else {
    Serial.println("Init OK");
  }

  rf95.setTxPower(0, false);  //0 dBm for bench tests, 20 max
  rf95.setFrequency(915.0);
  rf95.setCADTimeout(500);

  // predefined configurations: see RH_RF95.h source code for the details
  // Bw125Cr45Sf128 (default, medium range, CRC on, AGC enabled)
  // Bw500Cr45Sf128
  // Bw31_25Cr48Sf512
  // Bw125Cr48Sf4096  long range, CRC and AGC on, low data rate
  /*

    // example long range configuration
      if (!rf95.setModemConfig(RH_RF95::Bw125Cr48Sf4096)) {
        Serial.println("Set config failed");
        while (1); //hang
        }
  */
}

char buf[RH_RF95_MAX_MESSAGE_LEN];

void loop()
{
  Serial.print("To repeater: ");
  snprintf(buf, sizeof(buf), "Message from node %d", CLIENT_ADDRESS);
  Serial.print(buf);

  // Send message to the repeater. Times out after 3 retries, by default

  if (manager.sendtoWait((uint8_t *)buf, sizeof(buf), REPEATER_ADDRESS))
    Serial.println();
  else
    Serial.println(" !failed");

  // check for forwarded messages from repeater, with random wait times to avoid collisions

  uint16_t waitTime = random(1000, 3000);  //milliseconds
  // these to be updated by recvfromAckTimeout()
  uint8_t len = sizeof(buf);
  uint8_t from;

  //blocking call, times out after waitTime
  if (manager.recvfromAckTimeout((uint8_t *)buf, &len, waitTime, &from)) {
    //arguments: (uint8_t *buf, uint8_t *len, uint16_t timeout, uint8_t *source=NULL, uint8_t *dest=NULL,
    // uint8_t *id=NULL, uint8_t *flags=NULL, uint8_t* hops = NULL)

    // log ASCII message received
    if (len < sizeof (buf)) buf[len] = 0; // null terminate ASCII string
    else buf[len - 1] = 0;
    if (from == REPEATER_ADDRESS) {
      Serial.print("forwarded message: ");
      Serial.print(buf);
      Serial.println();
    }
  } //end recvfromAckTimeout
}

OK, thanks. The three node example seems a bit more up front for me. I'll test these two Feathers and if no issues I'll order another one or two for testing. I'm sure I really should read through the entire library docs in the meantime, even if I don't understand all of it, to get a better feel for the hidden code :slightly_smiling_face:.

Thank you!

Bob

@bobf

I decided to look into establishing a fixed node repeater network using RHRouter, and it is quite a bit easier than the example using RHReliableDatagram. I started with the only example I could find, which was for the RF22 radio, and it works fine for four nodes.

The two intermediate nodes silently repeat messages between the client and the endpoint, which in this example, is called server3. It is one-way (originator <-> automatic response) at the moment, but I plan to turn it into a two way.

To avoid cluttering the forum, the code is posted in this github repository, along with the three-node example of RHReliableDatagram posted above. If you test these with the ATmega32u4, please let me know if they work as expected.

Got it. Did order a couple more 32u4 Feathers. Will let you know when I get to try them. Thanks for the update!

Bob

Sounds interesting.

For the mesh repeater can it go into deep sleep mode on the Arduino side ?

An SX127x will when in receive mode use around 15mA, so when a packet arrives the receiver could wake up.

Important question! I have no experience with the ATSAMD21 sleep modes, and will investigate.