Grove Wio E5 configuration AT commands join fail to TTN

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
Currently, these are the commands being sent to the module

        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 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.
    }
}

How have you configured on the TTN Device/Application console? I see calling out OTAA so dev addr should be a correctly assigned TTN Addr - 46xxxxx isnt it! Have you hard coded a dev addr into the e5 module?

just remember in AU you have two channel plans AU_915_928 and AS_923, just check what the gateways are set at as well

I’ve already configured the AppEui and DevEui by copy pasting from the device into TTN

You’re right though about the DevAddr though, the wio instructions skipped it and I never thought about where it comes into play either. By the way, what did you mean by “46xxxxx isnt it!”? was the 42xxxxx number it had from the start just a placeholder value? And what I’m understanding now is that you need to register the device to TTN first with the AppEui and DevEui, then TTN will give a DevAddr for us to hardcode into our device?

Anyway, I’ve now copied the devaddr from TTN to the device, but right now it is still failing, but is appearing in the live feed which i didn’t notice before (i only checked like once or twice before though).

updated AT configuration commands

        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,""260DB15A""\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");

Sorry typo, of course meant 42xxxxx - had in mind 26xxxx IS ttn and conflated the two :slight_smile:

How do you have the module configured…you shoudn’t (need to) copy across as using OTAA this is automagically applied as part of the Join process

You normally only embed DevAddr for ABP copying assigned dev addr from console - sounds like you are tryng to mix modes and failing…

I’m just following the example that seeedstudio gave. The commands above are the entirety of the configuration.

Their instructions were

Note down DevEui and AppEUi

+ Add end device|

Click Manually , to enter the registration credentials manually

Select the Frequency plan according to your region. Also make sure you use the same frequency as the gateway in which you will connect this device to. Select MAC V1.0.2 as the LoRaWAN® version and PHY V1.0.2 REV B as the Regional Parameters version . These settings are according to the LoraWAN® stack of Wio-E5.

Copy and paste the previously obtained information from step 8 into DevEUI and AppEUI fields. For AppKey field, use: 2B7E151628AED2A6ABF7158809CF4F3C.

Finally click Register end device

Yep that is for where registering a device that isnt covered by the TTN Device repository…

Dont see anything there where ist says effectively

When using OTAA the LNS recognises the DevEui and AppEui as being ones that matach the registration details you have provided if copied and entered correctly, then as part of the join process the LNS provides the DevAddr over the air (its in the name :wink: ) as part of the authenticated ‘activation’ process… There is no hardcoding involved.

I ask again (and for last time at this point!)

How do you have the module configured
sorry, i’m not sure exactly what you mean, but heres a screenshot of the TTN console


image

Freq Plan AU FSB 8 → does that match that of either of the two Uni GW’s? Note TTN generally uses FSB 2 on AU915 per dropdown on selector. As advised above you need to check device is on same plan as GW’s…As advised there are also two AS923 options in AU - 920-923 and 923-925 with secondary channels.

If device and GW are not fully aligned they wont talk/hear on aligned channels and so join process will fail…

I see, thank you. I’ll try and find out

Is there an easy way of finding out a gateway’s config? i thought the TTN map might show some info but its just showing the gateway ID

Do you have the GW-ID’s? Post them…

And did you try the default FSB 2?

Gateway ID: eui-008000000000bcbf
Gateway ID: eui-00800000a00034d1

yes, i have tried all the australian plans from FSB 1 to 8, including FSB 2. none of them are connecting. I tried the AS ones too but they seem to give errors

Invalid value of field mac_settings.desired_rx2_data_rate_index.value

As I noted in the original question, I’m going through the documentation but they are very confusing and dont provide a clear a way of translating the parameters into AT commands.
Theres probably some configuration i need to do for the channels and stuff right?

lorawan regional parameters 1.0.2 doc

E5 AT commands specifications
image
image
image

Edit:
trying to read the documents and understand the commands. Ive double checked the commands and the DR command with the AU915 plan seems correct. ive checked ADR is on, class A should be correct. not sure if port is an issue.

i reread the section on ch command and made changes.
its now AT+CH=NUM,0-71 based on the Regional Parameters doc saying au915 has 72 channels from 0 to 71.
that still didn’t fix it though.
Am i supposed to create the channel data structure for each of the 72 channels and define the frequency and datarates? How does that work as the Regional Parameters document separately defines uplink and downlink but the AT command sheet doesnt differentiate between that

also tried

AT+JOIN=DR2
and
AT+JOIN=DR6

for the join request, also based on what the Regional Parameters doc said.
But that stopped messages from appearing in the live feed all together

both AS_923

Based on the reply above you need to use AT+DR=AS923 and optionally AT+CH=NUM,0-1 You should be able to join using those with a data rate of 0 to 5.

and the node on the ttn console needs to be AS_923

Do i select these ones?
image

I keep getting errors like
Specified version PHY_V1_0_2_REV_B of band AS_923_2 does not exist
Invalid value of field mac_settings.desired_rx2_data_rate_index.value
for all of the AS plans.

The top one.

Where is that error displayed? In the TTN console or the Lora e5 module?

the TTN console
image

(I’m using the specific lorawan version and regional parameters as per the manufacturer instructions)