CubeCell HTCC-AB02S
HTCC-AB02S is a Dev-Board. Already integrated AIR530Z GPS module, friendly designed for developers, easy to verify communication solutions.
I found this board by browsing around for an integrated solution that has GPS, Radio, and display capabilities. Getting the libraries working took some effort, but it has been worth it.
The CubeCell HTCC-AB02S is LoRa capable out-of-the-box, and eventually I will invest more time in understanding the proper usage of the protocol. In my project I took a very naive approach, please be advised this is probably not an optimal solution.
I purchased two of these boards, one to act as a beacon/transmitter that chirps out it’s current GPS coordinates every few seconds. The second board I programmed as a receiver and it computes the distance and bearing to the beacon and displays information on the LCD screen.

Product Link

ezradio.h
#include "Arduino.h"
#include "LoRaWan_APP.h"
#define LORA_PREAMBLE_LENGTH 8 // Same for Tx and Rx
#define LORA_FIXED_PAYLOAD false
#define LORA_CRC true
#define TX_OUTPUT_POWER 20 // 14 // dBm (max = 22).
#define LORA_SYMBOL_TIMEOUT 0 // Symbols
#define LORA_USE_FIXED_PAYLOAD false
#define LORA_IQ_INVERSION false
#define LORA_FREQ_HOP false
// radio transmission data
struct RadioPacket {
int16_t id;
uint8_t from;
uint8_t type;
int16_t rssi;
int8_t snr;
uint8_t signalStrength;
uint32_t at;
double lat;
double lon;
uint32_t age;
int length;
char data[64];
};
struct EzRadioCfg {
RadioModems_t modem = MODEM_LORA;
int8_t power = 18; // max 22
uint8_t fdev = 0; // FSK Modem Only. Sets the frequency deviation (FSK only)
// LORA_BANDWIDTH
// 0: 125 kHz,
// 1: 250 kHz,
// 2: 500 kHz,
// 3: Reserved
byte bandwidth = 0;
// LORA_SPREADING_FACTOR
// min/default 7, max 12 [SF7..SF12]
byte datarate = 7;
// LORA_CODINGRATE
// 1: 4/5,
// 2: 4/6,
// 3: 4/7,
// 4: 4/8
uint8_t coderate = 1;
uint16_t preambleLen = LORA_PREAMBLE_LENGTH;
bool fixLen = LORA_FIXED_PAYLOAD;
bool crcOn = LORA_CRC;
// Frequency Hop
// Enables disables the intra-packet frequency hopping
bool freqHop = LORA_FREQ_HOP;
// Number of symbols between each hop
uint8_t hopPeriod = 0;
bool iqInverted = false;
uint32_t timeout = 3000; // milliseconds
};
class EzRadio {
public:
RadioEvents_t* handler;
EzRadioCfg cfg;
EzRadio(uint8_t uuid, RadioEvents_t* handler, EzRadioCfg cfg);
void reconfigure();
void transmit(uint16_t id, uint8_t type, char* data);
void ack(uint16_t id, uint8_t from, uint8_t sig);
bool waitForAck(uint16_t id);
void ping(uint16_t id, uint8_t client);
bool read(RadioPacket* message);
bool read(RadioPacket* message, uint16_t timeout);
private:
uint8_t uuid;
void OnTxDone( void );
void OnTxTimeout( void );
};
EzRadio* eZRadio_configure(uint8_t uuid, RadioEvents_t* events, EzRadioCfg cfg);
void eZRadioReceiveSignal(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr );
ezradio.cpp
#include "ezradio.h"
static RadioPacket RECEIVED;
static bool eZRadioMessageReady = false;
static bool eZRadioReadingMessage = false;
static uint32_t eZRadio_lastTx = 0;
static uint32_t eZRadio_lastRx = 0;
const uint8_t SYM_5 = 0x7b;
const uint8_t HDR_SIZE = 8; // + sizeof(double) * 2;
void eZRadioReceiveSignal( uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr ) {
// message structure
//
// first two bytes = 18 bits of uint16_t message sequence id
// third byte = 8 bits of client identifier
//
eZRadioMessageReady = false;
if (size < HDR_SIZE || payload[5] != SYM_5 || payload[6] != 0x8c || payload[7] != ':') {
// invalid message, throw it away
for (int i = 0; i < 3; i++) {
// flash the LED red on error reading messages
turnOnRGB(0x550000, 120);
turnOnRGB(0, 100);
}
return;
}
eZRadioReadingMessage = true;
turnOnRGB(0x000500, 0);
RECEIVED.at = millis();
RECEIVED.id = (uint16_t)payload[0] | ((uint16_t)payload[1] << 8); // (uint16_t)((uint16_t*)payload)[0];
RECEIVED.from = (short) payload[2] & 0x0F;
RECEIVED.type = payload[3];
RECEIVED.rssi = rssi;
RECEIVED.snr = snr;
RECEIVED.length = size - HDR_SIZE;
RECEIVED.signalStrength = 100 + (min(-30, max(-130, rssi)) + 30);
memcpy(RECEIVED.data, &payload[HDR_SIZE], RECEIVED.length);
switch (payload[3]) {
case '1': // ack
break;
case '2': // ping
case 'P': // position
char readBuffer[16];
int i = HDR_SIZE;
for (; i + 1 < size && payload[i] != ' '; i++) {
readBuffer[i - HDR_SIZE] = payload[i];
}
readBuffer[i] = 0;
RECEIVED.lat = String(readBuffer).toDouble();
int k = i + 1;
for (i = k; i + 1 < size && payload[i] != 0; i++) {
readBuffer[i - k] = payload[i];
}
readBuffer[i] = 0;
RECEIVED.lon = String(readBuffer).toDouble();
break;
}
Serial.printf("receiving size=%d msg='%s'\n", size, &payload[HDR_SIZE]);
eZRadio_lastRx = millis();
eZRadioReadingMessage = false;
eZRadioMessageReady = true;
Radio.Rx(0);
turnOnRGB(0, 0);
}
EzRadio::EzRadio(uint8_t uuid, RadioEvents_t* handler, EzRadioCfg cfg) {
this->uuid = uuid;
this->handler = handler;
this->cfg = cfg;
handler->RxDone = eZRadioReceiveSignal;
Radio.Init(handler);
}
void EzRadio::reconfigure() {
Radio.SetTxConfig( cfg.modem,
cfg.power,
0,
cfg.bandwidth,
cfg.datarate,
cfg.coderate,
cfg.preambleLen,
cfg.fixLen,
cfg.crcOn,
cfg.freqHop,
cfg.hopPeriod,
LORA_IQ_INVERSION, 3000
);
Radio.SetRxConfig( cfg.modem,
cfg.bandwidth,
cfg.datarate,
cfg.coderate,
0, // bandwidthAfc
cfg.preambleLen,
LORA_SYMBOL_TIMEOUT,
cfg.fixLen,
64, // uint8_t payloadLen (if fixLen == true)
cfg.crcOn,
cfg.freqHop,
cfg.hopPeriod,
LORA_IQ_INVERSION,
true // contiunous
);
}
void EzRadio::OnTxDone( void ) {
}
void EzRadio::OnTxTimeout( void ) {
}
void EzRadio::ack(uint16_t id, uint8_t from, uint8_t sig) {
byte channel = cfg.bandwidth;
transmit(id, '1', "ACK");
}
void EzRadio::ping(uint16_t id, uint8_t client) {
transmit(id, '2', "PING");
}
void EzRadio::transmit(uint16_t id, uint8_t type, char* data) {
turnOnRGB(0x070000, 10);
int size = strlen(data);
char* txpacket = new char[size + HDR_SIZE];
txpacket[0] = (uint8_t) (id & 0xFF); // low bits
txpacket[1] = (uint8_t) (id >> 8); // high bits
txpacket[2] = uuid;
txpacket[3] = type;
txpacket[4] = 0x2a; // reserved
txpacket[5] = SYM_5; // reserved
txpacket[6] = 0x8c; // reserved
txpacket[7] = ':';
sprintf(&txpacket[HDR_SIZE], data, size);
txpacket[size + HDR_SIZE] = 0;
Serial.printf("\r\nsending packet \"%s\" , length %d\r\n", &txpacket[HDR_SIZE], HDR_SIZE + size);
Radio.Send( (uint8_t *)txpacket, size + HDR_SIZE);
eZRadio_lastTx = millis();
turnOnRGB(0, 0);
free(txpacket);
}
bool EzRadio::read(RadioPacket* message) {
return read(message, 0);
}
bool EzRadio::waitForAck(uint16_t msgId) {
Radio.Rx(100);
RadioPacket ack;
if (read(&ack, 100)) {
return ack.type == '1' && msgId == ack.id;
}
return false;
}
bool EzRadio::read(RadioPacket* message, uint16_t timeout) {
timeout += millis();
while ((!eZRadioMessageReady || eZRadioReadingMessage) && timeout > millis()) {
delay(20); // reading a message now
}
if (!eZRadioMessageReady) {
return false;
}
message->at = RECEIVED.at;
message->id = RECEIVED.id;
message->from = RECEIVED.from;
message->rssi = RECEIVED.rssi;
message->snr = RECEIVED.snr;
message->signalStrength = RECEIVED.signalStrength;
message->length = RECEIVED.length;
memcpy(message->data, RECEIVED.data, RECEIVED.length);
message->lat = RECEIVED.lat;
message->lon = RECEIVED.lon;
message->age = millis() - RECEIVED.at;
message->type = RECEIVED.type;
eZRadioMessageReady = false;
return true;
;
}
EzRadio* eZRadio_configure(uint8_t uuid, RadioEvents_t* events, EzRadioCfg cfg) {
EzRadio* ez = new EzRadio(uuid, events, cfg);
ez->reconfigure();
return ez;
}