How to get LoRaWAN Network Time to RAK4631?

Please include the following information, in order for us to help you as effectively as possible.

  • What product do you wish to discuss? RAK4631

  • What firmware are you using? PlatformIO

  • What firmware version? 4.1.1

  • Computer OS? (MacOS, Linux, Windows) Windows

  • What Computer OS version? Windows 11

  • How often does the problem happen? It is not a problem

  • How can we replicate the problem? Yes

  • Provide source code if custom firmware is used or link to example if RAKwireless example code is used.

In IP Networks, we can work with Network Time Protocol to get from a time source, for example, in Brazil we have the NTP time source: a.ntp.br

I would like to know : How to get LoRaWAN Network Time to RAK4631?

Regards,

Claudio

At the moment the network time can only be requested with AT commands:
AT+TIMEREQ

API call for the time request will be added in the next version of RUI3.

I am working with RAK4631 with FreeRTOS instead RUI with RAK4631-R.

Do we have a solution without RUI?

Claudio

No, I don’t have a solution unless you send a downlink with the time-stamp or add the timerequest to the LoRaWAN library.

Thank you by your support.

Cláudio

@beegee , I found something: https://lora-alliance.org/wp-content/uploads/2020/11/LoRaWAN-1.0.4-Specification-Package_0.zip

It is already available in the LoRaWAN 1.0.3:

Yes, but it is not implemented in the open source LoRaWAN library (SX126x-Arduino). It is available in the RUI3 LoRaWAN stack.
not sure what LoRaWAN library you are using with FreeRTOS.

Example how to get network time with RUI3 AT commands:

1 Like

Hello Bernd,

As it’s related I’m getting lorawan time with calling service_lora_set_timereq(1) at join time plus setting a variable inflighttimereq to true but I’ve found that there’s no callback to get the time.
At the moment I’m implementing inside the receive callback like this:

void receiveCallback(SERVICE_LORA_RECEIVE_T *data)
{
    //MAC command
    if (data->Port == 0)
    {
        MYLOG("RX-CB", "MAC command received");
        if (inflighttimereq && !service_lora_get_timereq()) {
            MYLOG("RX-MAC", "Syncing internal RTC with LoRaWAN");
            syncRTCfromLoraWan();
        }
    }

I think it’s better here to have a callback for this function, but this does the trick for me, it’s possible to implement time related commands callbacks for next version maybe?

Regards

1 Like

The callbacks and a proper API call will come with the next RUI3 release V4.1.1

1 Like

I get the time via decoding the Cayenne LPP function (LoRaWAN uses LPP)
The transmitted package has a special entry for storing the date and time in Unix format (Unix Time Stamp).
In this record, you can both send and receive time and date. Data is written to LPP_UNIXTIME (see library code).
I also took Bernd Giesecke’s idea and wrote a small tick-based clock function RAK4631 for FreeRTOS. At these hours I transmit the exact time in the LPP packet. I transmit the exact time using GPS and RTC Wisblock clocks.
Also, on the advice of Bernd Giesecke, I rewrote the Melopero RV 3028 library and added the getUnixTime() and setUnixTime() functions.
The Cayenne LPP decoder is not too complicated, I have built the code into the library.
The code just writes LPP_UNIXTIME to the variable

  u_int32_t lppunixtime;

u_int8_t   CayenneLPP::decodeData (u_int8_t * buffer, u_int8_t len,
			  struct cell *data_array)
{

// This is where the rest of the code and the decoding loop goes...

  if (LPP_UNIXTIME == type)
	    {
	      lppunixtime = getValue32 (&buffer[index], size);
	    }

}

The entire function is large (all records are decoded), I will not post it here.

Here is the clock code (thanks to Bernd for the idea!)

#include "app.h"
#include <Arduino.h>
#include <TimeLib.h>
#define LPP_UNIXTIME 1357041600 // Jan 1 2013

unsigned long tick = DEFAULT_TIME; // example of getting time
// unsigned long tick = lppunixtime; 

volatile bool tick_flag = false;

TimerHandle_t timer_handle;

void second_call(TimerHandle_t  xTimer)
{
    tick = tick + 1;
    tick_flag = true;
}

bool init_time()
{
    // Start timer with time = 1 tick
    // NRF RTC tick enabled
    timer_handle = xTimerCreate("1_second", pdMS_TO_TICKS(1000), true, NULL, second_call);
    xTimerStart(timer_handle, 0);
return true;
}

void read_time()
{

// time synchronization
       setTime(tick); // Sync  clock to the time 

/* The “accuracy” of the clock can be increased, but this will result in increased power  consumption */

    if (tick_flag)
    {
	MYLOG("TIME", "%d.%02d.%02d %d:%02d:%02d", year(), month(), day(), hour(), minute(), second());
	tick_flag = false;
    }
}


Sorry, I don’t write well in English. I’m using Bernd Giesecke’s Wisblock API library

1 Like

Thank you for sharing @renice123
Can you share some links to the changed RTC library and maybe to your code.

Yes of course, I’m going to post the code as soon as I’ve fully tested it
(I will also post the code for working with the Wisblock RGB sensor for a smart home)

The decoder code is large, here are the main functions (they work, but there may be bugs, I haven’t tested everything yet!) Later I will post the Melopero code (my changes)

// Outputting data from an array of structures
void CayenneLPP::printLPP (struct cell *vals, int len)
{
  for (int idx = 0; idx < len; idx++)
    {
	if (vals[idx].channel != 0) {
          printf ("ch. [%d], ", vals[idx].channel);
	  printf ("%s: ", vals[idx].type);
          printf ("%.2f\n", vals[idx].data);
	}
    }
}

// https://stackoverflow.com/questions/50568238/c-char-array-to-uint8-t-array
int CayenneLPP::char2int (char val)
{
  return (val >= '0' && val <= '9') ? (val - '0') : (val - 'A' + 10);
}

int CayenneLPP::chars2int (unsigned char const vals[2])
{
  return (char2int (vals[0]) << 4) | (char2int (vals[1]));
}

u_int8_t   CayenneLPP::decodeData (u_int8_t * buffer, u_int8_t len,
			  struct cell *data_array)
{
  u_int32_t lppunixtime;

  u_int8_t count = 0;
  u_int8_t index = 0;

  while ((index + 2) < len)
    {

// Get channel #
      u_int8_t channel = buffer[index++];

// Get data type
      u_int8_t type = buffer[index++];
      if (!isType (type))
	{
	  _error = LPP_ERROR_UNKOWN_TYPE;
	  return 0;
	}

// Type definition
      u_int8_t size = getTypeSize (type);
      u_int32_t multiplier = getTypeMultiplier (type);
      bool is_signed = getTypeSigned (type);

// Check buffer size
      if (index + size > len)
	{
	  _error = LPP_ERROR_OVERFLOW;
	  return 0;
	}

// Parse types
      if (LPP_COLOUR == type)
	{

	  data_array[count].channel = channel;
	  data_array[count + 1].channel = channel;
	  data_array[count + 2].channel = channel;

	  data_array[count].data =
	    getValue (&buffer[index], 1, multiplier, is_signed);
	  data_array[count + 1].data =
	    getValue (&buffer[index + 1], 1, multiplier, is_signed);
	  data_array[count + 2].data =
	    getValue (&buffer[index + 2], 1, multiplier, is_signed);

	  sprintf (data_array[count].type, "%s r %f", getTypeName (type),
		   getValue (&buffer[index], 1, multiplier, is_signed));
	  sprintf (data_array[count + 1].type, "%s g %f", getTypeName (type),
		   getValue (&buffer[index + 1], 1, multiplier, is_signed));
	  sprintf (data_array[count + 2].type, "%s b %f", getTypeName (type),
		   getValue (&buffer[index + 2], 1, multiplier, is_signed));
	  count = count + 2;
	}
      else if (LPP_ACCELEROMETER == type || LPP_GYROMETER == type)
	{
	  data_array[count].channel = channel;
	  data_array[count + 1].channel = channel;
	  data_array[count + 2].channel = channel;

	  data_array[count].data =
	    getValue (&buffer[index], 2, multiplier, is_signed);
	  data_array[count + 1].data =
	    getValue (&buffer[index + 2], 2, multiplier, is_signed);
	  data_array[count + 2].data =
	    getValue (&buffer[index + 4], 2, multiplier, is_signed);
	  sprintf (data_array[count].type, "%s x %f", getTypeName (type),
		   getValue (&buffer[index], 2, multiplier, is_signed));
	  sprintf (data_array[count + 1].type, "%s y %f", getTypeName (type),
		   getValue (&buffer[index + 2], 2, multiplier, is_signed));
	  sprintf (data_array[count + 2].type, "%s z %f", getTypeName (type),
		   getValue (&buffer[index + 4], 2, multiplier, is_signed));
	  count = count + 2;
	}
      else if (LPP_GPS == type)
	{
	  data_array[count].channel = channel;
	  data_array[count + 1].channel = channel;
	  data_array[count + 2].channel = channel;
	  data_array[count].data =
	    getValue (&buffer[index], 3, 10000, is_signed);
	  data_array[count + 1].data =
	    getValue (&buffer[index + 3], 3, 10000, is_signed);
	  data_array[count + 2].data =
	    getValue (&buffer[index + 6], 3, 10000, is_signed);
	  sprintf (data_array[count].type, "%s latitude %f",
		   getTypeName (type), getValue (&buffer[index], 3, 10000,
						 is_signed));
	  sprintf (data_array[count + 1].type, "%s longitude %f",
		   getTypeName (type), getValue (&buffer[index + 3], 3, 10000,
						 is_signed));
	  sprintf (data_array[count + 2].type, "%s altitude %f",
		   getTypeName (type), getValue (&buffer[index + 6], 3, 10000,
						 is_signed));
	  count = count + 2;
	}
      else if (LPP_GENERIC_SENSOR == type || LPP_UNIXTIME == type)
	{
	  if (LPP_UNIXTIME == type)
	    {
	      lppunixtime = getValue32 (&buffer[index], size);
	    }
	  else
	    {
	      data_array[count].channel = channel;
	      data_array[count].data = getValue32 (&buffer[index], size);
	      sprintf (data_array[count].type, "%s", getTypeName (type));
	    }
	}
      else
	{
	  data_array[count].channel = channel;
	  data_array[count].data =
	    getValue (&buffer[index], size, multiplier, is_signed);
	  sprintf (data_array[count].type, "%s", getTypeName (type));
	}
      count++;
      index += size;
    }
  return count;
}


// This function for Wisblock Sensors is to receive LoRa data.
// The function decodes LPP data received via LoRa and prints it
// void  lora_data_handler(void), g_rx_lora_data

void decode_lpp ()
{
  CayenneLPP lpp (160);
// Decoding received data, example
  char tmp_buffer[] = "0802016C076880067325C5026700B404020C96";
// char tmp_buffer[] = "32B59633B59734B59835B59936B59A37B59B38B59C39B59D3CB4000026021FB43DB70000000000FE3EB8000036990D013FB5A340B5A441B5A542B5A643B5A744B5A845B5A9";

  assert ((sizeof (tmp_buffer) - 1) % 2 == 0);
  u_int8_t buff[(sizeof (tmp_buffer) - 1) / 2];

  for (unsigned idx = 0; idx < sizeof (tmp_buffer); idx += 2)
    {
      buff[idx / 2] = lpp.chars2int ((unsigned char *) tmp_buffer + idx);
    }

  u_int8_t cnt = lpp.countArray (buff, sizeof (buff));
  printf ("cnt = %d\n", cnt);
  struct cell *data_array;
  data_array = (struct cell *) malloc (cnt * sizeof (struct cell *));

  lpp.decodeData (buff, sizeof (buff), data_array);
// Output all sensor data
  lpp.printLPP (data_array, cnt);
  free (data_array);

}


/********************************/
// These new functions should be added to the Melopero_RV3028.h file
	bool setTime(uint8_t * time, uint8_t len);
	bool setSeconds(uint8_t value);
	bool setMinutes(uint8_t value);
	bool setHours(uint8_t value);
	bool setWeekday(uint8_t value);
	bool setDate(uint8_t value);
	bool setMonth(uint8_t value);
	bool setYear(uint16_t value);

	bool setUnixTime(uint32_t value);//Set the UNIX Time (Real Time and UNIX Time are INDEPENDENT!)

/********************************/

// These new functions should be added to the .cpp file

//ATTENTION: Real Time and UNIX Time are INDEPENDENT!

//Unix Time registers
#define RV3028_UNIX_TIME0				0x1B

bool Melopero_RV3028::setUnixTime(uint32_t value)
{
	uint8_t unix_reg[4];
	unix_reg[0] = value;
	unix_reg[1] = value >> 8;
	unix_reg[2] = value >> 16;
	unix_reg[3] = value >> 24;

	return writeMultipleRegisters(RV3028_UNIX_TIME0, unix_reg, 4);
}


// setTime -- Set time and date/day registers of RV3028 (using data array)

//REGISTERS
//Clock registers
#define RV3028_SECONDS      			0x00
#define RV3028_MINUTES      			0x01
#define RV3028_HOURS        			0x02
//Calendar registers
#define RV3028_WEEKDAY				0x03
#define RV3028_DATE         			0x04
#define RV3028_MONTHS        			0x05
#define RV3028_YEARS        			0x06


bool Melopero_RV3028::setTime(uint8_t * time, uint8_t len)
{
	if (len != TIME_ARRAY_LENGTH)
		return false;

	return writeMultipleRegisters(RV3028_SECONDS, time, len);
}

bool Melopero_RV3028::setSeconds(uint8_t value)
{
	_time[TIME_SECONDS] = DECtoBCD(value);
	return setTime(_time, TIME_ARRAY_LENGTH);
}

bool Melopero_RV3028::setMinutes(uint8_t value)
{
	_time[TIME_MINUTES] = DECtoBCD(value);
	return setTime(_time, TIME_ARRAY_LENGTH);
}

bool Melopero_RV3028::setHours(uint8_t value)
{
	_time[TIME_HOURS] = DECtoBCD(value);
	return setTime(_time, TIME_ARRAY_LENGTH);
}

bool Melopero_RV3028::setWeekday(uint8_t value)
{
	_time[TIME_WEEKDAY] = DECtoBCD(value);
	return setTime(_time, TIME_ARRAY_LENGTH);
}

bool Melopero_RV3028::setDate(uint8_t value)
{
	_time[TIME_DATE] = DECtoBCD(value);
	return setTime(_time, TIME_ARRAY_LENGTH);
}

bool Melopero_RV3028::setMonth(uint8_t value)
{
	_time[TIME_MONTH] = DECtoBCD(value);
	return setTime(_time, TIME_ARRAY_LENGTH);
}

bool Melopero_RV3028::setYear(uint16_t value)
{
	_time[TIME_YEAR] = DECtoBCD(value - 2000);
	return setTime(_time, TIME_ARRAY_LENGTH);
}


3 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.