Grove Wio E5 LoRaWAN configuration AT commands for registration to TTN (Australia)

Hi,
We are trying to complete a university project that is a basic water level detector, using an Arduino Uno with a Grove Shield to send data through a Grove Wio E5 LoRa module. We are not making or configuring our own gateway, we are attempting to just use a TTN (The Things Network) community gateway. In fact I can see 2 gateways that seem to be operated by our university on TTN map, so I was planning to use those.

Using the example code from the Wio E5’s product page, I had to make several adjustments for it to work as the code was originally created for the Seeeduino Xiao. The most notable change was moving the Wio E5 to from a UART header to a GPIO header and converting the GPIO header to a software serial port, as the uno only has 1 serial port (the combined USB port and UART header). Initially grabbing the DevEui, etc needed both PC and Wio module connected. (technically now that I have the addresses written down, I could change it back to use hardware serial). The connection to the Wio seems to be working fine. The AT commands are being sent successfully over the software serial port, and replies are being sent back from the wio and displayed successfully.

However, the module keeps failing to join the network. The example code has no other configuration and no explanatory comments, yet based on the instructions on the page, it should just work. My only guess is I havn’t set up channels and etc, correctly for Australian standards. Reading the Wio’s AT command document, it is extremely confusing to me, and I don’t even understand what I’m supposed to do.

Is anyone able to tell what configurations I am missing in terms of Australian parameters?
Also, if the AT command document has the relevant info, I would appreciate if anyone could point me to the appropriate sections and help explain them (I found them so cryptic).

Current Config

        at_send_check_response("+ID: AppEui", 1000, "AT+ID=AppEui\r\n");                                                
        at_send_check_response("+ID: DevEui", 1000, "AT+ID=DevEui\r\n");
        at_send_check_response("+ID: DevAddr", 1000, "AT+ID=DevAddr\r\n");
        at_send_check_response("+MODE: LWOTAA", 1000, "AT+MODE=LWOTAA\r\n");
        at_send_check_response("+DR: AU915", 1000, "AT+DR=AU915\r\n");
        at_send_check_response("+CH: NUM", 1000, "AT+CH=NUM,8-15\r\n");
        at_send_check_response("+KEY: APPKEY", 1000, "AT+KEY=APPKEY,\"2B7E151628AED2A6ABF7158809CF4F3C\"\r\n");
        at_send_check_response("+CLASS: A", 1000, "AT+CLASS=A\r\n");
        at_send_check_response("+PORT: 8", 1000, "AT+PORT=8\r\n");

Error msg
Whenever join command is sent, we get these errors
+JOIN: Join failed

+JOIN: LoRaWAN modem is busy

Full output
Actually for some reason, the output alternates between these 2 patterns.

Water Height: 399.00 cm
AT+JOIN
+JOIN: Start
+JOIN: NORMAL
AT+ID=AppEui
+ID: AppEui, 52:69:73:69:6E:67:48:46
AT+ID=DevEui
+ID: DevEui, 2C:F7:F1:20:43:30:98:52
AT+ID=DevAddr
+ID: DevAddr, 42:01:43:6A
JOIN failed!

Water Height: 399.00 cm
AT+JOIN
+JOIN: LoRaWAN modem is busy
AT+ID=AppEui
+ID: AppEui, 52:69:73:69:6E:67:48:46
AT+ID=DevEui
+ID: DevEui, 2C:F7:F1:20:43:30:98:52
AT+ID=DevAddr
+ID: DevAddr, 42:01:43:6A
+JOIN: Join failed
+JOIN: Done
JOIN failed!

Links
LoRa module being used

AT commands document for the Wio E5

Full Code


//----- Include Library -----
# include <Ultrasonic.h>
# include <Arduino.h>
# include <SoftwareSerial.h>      

//# include <U8x8lib.h>
//# include "DHT.h"






//----- variables -----
static char recv_buf[512];      // buffer used to store data received from the serial device.
static bool is_exist = false;
static bool is_join = false;
static int led = 0;
static int offsetDistance = 0;   // sensor should be placed 4m (max range of the sensor) above the riverbed. 
                                  // If sensor is placed a set distance higher, adjust the offset to make sure water height calculation is accurate

const byte rxPin = 2;
const byte txPin = 3;

const byte ultrasonicPin = 4;

//----- what pin the components are connected to -----

SoftwareSerial SoftSerial (rxPin, txPin);       // Set up a new SoftwareSerial object

Ultrasonic ultrasonic(ultrasonicPin);     // ultrasonic sensor connected to pin 8

//----- functions -----

// at_send_check_response() function takes three arguments: the expected response from the module (p_ack), the maximum response time in milliseconds (timeout_ms), and the command to be sent (p_cmd). 
// Sends command to wio, if the response from the module matches the expected response within the given time, the function returns true, and ret is set to true.
static int at_send_check_response(char *p_ack, int timeout_ms, char*p_cmd, ...)   // ... indicates that the function can accept any number of arguments
{
    int ch;
    int num = 0;
    int index = 0;
    int startMillis = 0;
    va_list args;                               // make a va_list called args to handle variable-length argument lists. va_list typically used in functions that accept a variable number of arguments, such as printf() and scanf()
    memset(recv_buf, 0, sizeof(recv_buf));      // sets all elements of recv_buf to 0
    va_start(args, p_cmd);                      // starts the va_list using va_start, storing p_cmd in args.

    char cmd[200]; // create a character array to hold the formatted command
    sprintf(cmd, p_cmd, args); // format the command and store it in the array
    SoftSerial.print(cmd); // print the array
    
    Serial.print(cmd);  //-----         // prints the string value of p_cmd with the optional arguments to wio
    
    va_end(args);                               // ends the args va_list
    delay(200);
    startMillis = millis();                     // sets the startMillis variable to the current value of the millis() function, which returns the number of milliseconds since the program started running

    if (p_ack == NULL)                          // if p_ack is not NULL, the function proceeds with sending the command and checking for the response
    {
        return 0;
    }

    do                                          // do while the difference between the current value of millis() and startMillis is less than timeout_ms. difference between a "while loop" and a "do-while loop" is that a "do-while loop" guarantees that the instructions inside the loop will be executed at least once
    {
        while (SoftSerial.available() > 0)                  // while serial1 connected?      
        {
            ch = SoftSerial.read();                             // reads the characters available from wio one at a time, storing each character in ch
            recv_buf[index++] = ch;                         // append characters to the recv_buf array
            Serial.print((char)ch);                       // also print each character to pc
            delay(2);
        }

        if (strstr(recv_buf, p_ack) != NULL)             // check whether a certain response string, p_ack, is received from the serial device. strstr function is used to check whether p_ack is a substring of recv_buf
        {
            return 1;                                    // If p_ack is found in recv_buf, the function returns 1, indicating success. Otherwise, the function continues to loop until the timeout period has elapsed. If the timeout period has elapsed and p_ack is not found in recv_buf, the function returns 0, indicating failure.
        }

    } while (millis() - startMillis < timeout_ms);   // end of do while    
    return 0;                                        // return 0 when finish
 }

// recv_parse() function takes a character array p_msg as input and extracts specific integer values from it based on certain substrings. 
// It uses the strstr() function to search for the substrings "RX", "RSSI", and "SNR" in p_msg.
// If the corresponding substring is found, it uses the sscanf() function to extract an integer value from the substring.
static void recv_prase(char *p_msg)        // char *p_msg is pointer to a character array 
{
    if (p_msg == NULL)                        // checking if the p_msg pointer is NULL, which indicates that it points to nothing. 
    {
        return;                                   // the function simply returns without doing anything 
    }
    char*p_start = NULL;                   // pointer
    int data = 0;                          // these variables will hold data that will be extracted from the p_msg 
    int rssi = 0;                          //
    int snr = 0;                           //

    p_start = strstr(p_msg, "RX");                                    // strstr function to search for the substring "RX" in the p_msg character array
    if (p_start && (1 == sscanf(p_start, "RX: \"%d\"\r\n", &data)))   // If found, p_start is set to point to the beginning of the substring. sscanf function to extract an integer value from the p_start substring. The format string used by sscanf is "RX: "%d"\r\n", which specifies that sscanf should look for the string "RX: " followed by a quoted integer value, followed by a carriage return and newline sequence. 
    {                                                                 // If sscanf successfully extracts an integer value, it assigns that value to the data variable
        Serial.println(data);
        //u8x8.setCursor(2, 4);
        //u8x8.print("led :");
        led = !!data;
        //u8x8.print(led);
        if (led)
        {
            digitalWrite(LED_BUILTIN, LOW);
        }
        else
        {
            digitalWrite(LED_BUILTIN, HIGH);
        }
    }

    p_start = strstr(p_msg, "RSSI");
    if (p_start && (1 == sscanf(p_start, "RSSI %d,", &rssi)))         // same process for the "RSSI" substring using the sscanf function to extract integer values and print them to the Serial monitor. However, it doesn't perform any further actions based on these values.
    {
        //u8x8.setCursor(0, 6);
        //u8x8.print("                ");
        //u8x8.setCursor(2, 6);
        //u8x8.print("rssi:");
        //u8x8.print(rssi);
        Serial.print("rssi:");
        Serial.print(rssi);
    }
    p_start = strstr(p_msg, "SNR");
    if (p_start && (1 == sscanf(p_start, "SNR %d", &snr)))            // same process for the "SNR" substring using the sscanf function to extract integer values and print them to the Serial monitor. However, it doesn't perform any further actions based on these values.
    {
        //u8x8.setCursor(0, 7);
        //u8x8.print("                ");
        //u8x8.setCursor(2, 7);
        //u8x8.print("snr :");
        //u8x8.print(snr);
        Serial.print("snr :");
        Serial.print(snr);
    }
}


//----- setup() -----
void setup(void)
{
    //u8x8.begin();
    //u8x8.setFlipMode(1);
    //u8x8.setFont(u8x8_font_chroma48medium8_r);

    Serial.begin(9600);               // for USB PC
    
    pinMode(rxPin, INPUT);            // Define pin modes for TX and RX
    pinMode(txPin, OUTPUT);           //
    SoftSerial.begin(9600);           // SoftwareSerial object for wio
    
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, HIGH);


    Serial.print("E5 LORAWAN TEST\r\n");
    //u8x8.setCursor(0, 0);
    //at_send_check_response("9600", 1000, "AT+IPR\r\n")
    
    if (at_send_check_response("+AT: OK", 100, "AT\r\n"))             // checks if the response received from the E5 module is "+AT: OK" after sending the "AT" command. If it is received within 100 milliseconds, the code in the if statement will be executed.
    {
        is_exist = true;
        //at_send_check_response("+ID: AppEui", 1000, "AT+ID\r\n");                                                       // this line theoretically gives DevAddr, DevEUI and APPEUI, but APPEUI is being cut off for some reason
        at_send_check_response("+ID: AppEui", 1000, "AT+ID=AppEui\r\n");                                                // so these 3 lines are asking for the IDs individually as a bandaid
        at_send_check_response("+ID: DevEui", 1000, "AT+ID=DevEui\r\n");
        at_send_check_response("+ID: DevAddr", 1000, "AT+ID=DevAddr\r\n");
        at_send_check_response("+MODE: LWOTAA", 1000, "AT+MODE=LWOTAA\r\n");
        at_send_check_response("+DR: AU915", 1000, "AT+DR=AU915\r\n");
        at_send_check_response("+CH: NUM", 1000, "AT+CH=NUM,8-15\r\n");
        at_send_check_response("+KEY: APPKEY", 1000, "AT+KEY=APPKEY,\"2B7E151628AED2A6ABF7158809CF4F3C\"\r\n");
        at_send_check_response("+CLASS: A", 1000, "AT+CLASS=A\r\n");
        at_send_check_response("+PORT: 8", 1000, "AT+PORT=8\r\n");
        delay(200);
        //u8x8.setCursor(5, 0);
        //u8x8.print("LoRaWAN");
        is_join = true;
    }
    else                                                              // If the "+AT: OK" response is not received within 100 milliseconds, the code in the else statement will be executed.
    {
        is_exist = false;
        Serial.print("No E5 module found.\r\n");
        //u8x8.setCursor(0, 1);
        //u8x8.print("unfound E5 !");
    }

    //dht.begin();

    //u8x8.setCursor(0, 2);
    //u8x8.setCursor(2, 2);
    //u8x8.print("temp:");

    //u8x8.setCursor(2, 3);
    //u8x8.print("humi:");

    //u8x8.setCursor(2, 4);
    //u8x8.print("led :");
    //u8x8.print(led);
}

//----- loop() -----

void loop(void)
{
    // float temp = 69;
    // float humi = 420;
    // will be a value between 0-400cm
    float WaterHeight = 400 + offsetDistance - ultrasonic.MeasureInCentimeters();   //max range - 
    Serial.print("Water Height: ");
    Serial.print(WaterHeight);
    Serial.println(" cm\t");
    // Serial.print("Humidity: ");
    // Serial.print(humi);
    // Serial.print(" %\t");
    // Serial.print("Temperature: ");
    // Serial.print(temp);
    // Serial.println(" *C");

    // u8x8.setCursor(0, 2);
    // u8x8.print("      ");
    // u8x8.setCursor(2, 2);
    // u8x8.print("temp:");
    // u8x8.print(temp);
    // u8x8.setCursor(2, 3);
    // u8x8.print("humi:");
    // u8x8.print(humi);

    if (is_exist)                                             // if the LoRaWAN module is_exist
    {
        int ret = 0;
        if (is_join)                                                  // if network is_join is true...
        {

            ret = at_send_check_response("+JOIN: Network joined", 12000, "AT+JOIN\r\n");         // ...sends the AT+JOIN command to join the LoRaWAN network
            if (ret)
            {
                is_join = false;
            }
            else
            {
                //at_send_check_response("+ID: AppEui", 1000, "AT+ID\r\n");                                                       // this line theoretically gives DevAddr, DevEUI and APPEUI, but APPEUI is being cut off for some reason
                at_send_check_response("+ID: AppEui", 1000, "AT+ID=AppEui\r\n");                                                // so these 3 lines are asking for the IDs individually as a bandaid
                at_send_check_response("+ID: DevEui", 1000, "AT+ID=DevEui\r\n");
                at_send_check_response("+ID: DevAddr", 1000, "AT+ID=DevAddr\r\n");
                Serial.print("JOIN failed!\r\n\r\n");
                delay(5000);
            }
        }
        else
        {
            char cmd[128];                                                                      // command string cmd using the  
            sprintf(cmd, "AT+CMSGHEX=\"%04X\"\r\n", (float)WaterHeight);                  // sprintf function to format the temperature and humidity readings into a string of hexadecimal digits and store in cmd. The AT+CMSGHEX command is used to send the data over LoRaWAN,
            ret = at_send_check_response("Done", 5000, cmd);                                    // ret stores the output of at_send_check_response function
            
            if (ret)                                                                            // if the send was successful.
            {
                recv_prase(recv_buf);                                                                   // recv_prase function is called to parse the received data.                                                          
            }
            else                                                                                // else if send failed
            {
                Serial.print("Send failed!\r\n\r\n");                                                   //  an error message is printed to the serial console.
            }
            delay(5000);
        }
    }
    else                                                      // If the module is not found...
    {
        delay(1000);                                                  // ...it just waits for a second and repeats the loop.
    }
}

Hi @Raging_Wasabi , Are you able to fix the JOIN error? If yes, could you please share the details here. Thanks.