LoRaWAN demo application using grove peripherals generating Cayenne LPP

Dependencies:   lorawan1v1

radio chip selection

Radio chip driver is not included, because two options are available.
If you're using SX1272 or SX1276, then import sx127x driver into your program.
if you're using SX1261 or SX1262, then import sx126x driver into your program.
If you're using NAmote72 or Murata discovery, then you must import only sx127x driver.

Grove peripherals -> Cayenne demo

Read LoRaWAN-1.1 page for configuration instructions.

This project adds support for Murata discovery board, in addition to LoRa shields on NUCLEO boards.

Use with sx1272 shield with grove peripherals connected:

D8 D9: ButtonRX TXA3 A4: TempSense
D6 D7:SCL SDA : LEDA1 A2: Pot

Button

Sends two different payload types: short press (under 1 sec)
long press: held down > 1 sec.

serial console keys

115200bps, 8N1
Enter key not used
Keys '0' to '3': cayenne channel number
'0': pot (rotary sensor)
'1': temperature
'2': digital out
'3': analog out

DevEUI configuration

For use on networks which force you to use DevEUI defined by network, comment out HardwareIDtoDevEUI().
HardwareIDtoDevEUI() obtains DevEUI from the CPU unique hardware serial number. However, some networks may force you to use their DevEUI value.

sensorDemoVT100.cpp

Committer:
Wayne Roberts
Date:
2018-07-23
Revision:
8:efe6002910df
Parent:
6:795461780e10

File content as of revision 8:efe6002910df:

/*
 / _____)             _              | |
( (____  _____ ____ _| |_ _____  ____| |__
 \____ \| ___ |    (_   _) ___ |/ ___)  _ \
 _____) ) ____| | | || |_| ____( (___| | | |
(______/|_____)_|_|_| \__)_____)\____)_| |_|
    (C)2018 Semtech

Description: LoRaMac classB device implementation

License: Revised BSD License, see LICENSE.TXT file include in the project

*/

#include "LoRaMac1v1.h"
#include "SerialDisplay.h"
#include "LoRaMacString.h"
#ifdef ENABLE_VT100

/*!
 * Defines the application data transmission duty cycle. 5s, value in [ms].
 */
#define APP_TX_DUTYCYCLE_us                            8000000

/*!
 * Defines a random delay for application data transmission duty cycle. 1s,
 * value in [ms].
 */
#define APP_TX_DUTYCYCLE_RND_us                        2000000

/*!
 * Default datarate
 */

#if defined( USE_BAND_ARIB_8CH )
    #define LORAWAN_DEFAULT_DATARATE                    DR_3
#else
    #define LORAWAN_DEFAULT_DATARATE                    DR_0
#endif

/*!
 * LoRaWAN confirmed messages
 */
#define LORAWAN_CONFIRMED_MSG_ON                    false

/*!
 * LoRaWAN Adaptive Data Rate
 *
 * \remark Please note that when ADR is enabled the end-device should be static
 */
#define LORAWAN_ADR_ON                              1

#if defined( USE_BAND_868 )

/*!
 * LoRaWAN ETSI duty cycle control enable/disable
 *
 * \remark Please note that ETSI mandates duty cycled transmissions. Use only for test purposes
 */
#define LORAWAN_DUTYCYCLE_ON                        false

#define USE_SEMTECH_DEFAULT_CHANNEL_LINEUP          1

#if( USE_SEMTECH_DEFAULT_CHANNEL_LINEUP == 1 ) 

#define LC4                { 867100000, { ( ( DR_5 << 4 ) | DR_0 ) }, 0 }
#define LC5                { 867300000, { ( ( DR_5 << 4 ) | DR_0 ) }, 0 }
#define LC6                { 867500000, { ( ( DR_5 << 4 ) | DR_0 ) }, 0 }
#define LC7                { 867700000, { ( ( DR_5 << 4 ) | DR_0 ) }, 0 }
#define LC8                { 867900000, { ( ( DR_5 << 4 ) | DR_0 ) }, 0 }
#define LC9                { 868800000, { ( ( DR_7 << 4 ) | DR_7 ) }, 2 }
#define LC10               { 868300000, { ( ( DR_6 << 4 ) | DR_6 ) }, 1 }

#endif

#endif

/*!
 * LoRaWAN application port
 */
#define LORAWAN_APP_PORT                            2

/*!
 * User application data buffer size
 */
#define LORAWAN_APP_DATA_SIZE                       3


#ifdef LORAWAN_JOIN_EUI
    static uint8_t DevEui[] = LORAWAN_DEVICE_EUI;
    static const uint8_t JoinEui[] = LORAWAN_JOIN_EUI;
    static const uint8_t NwkKey[] = LORAWAN_ROOT_NWKKEY;
    #ifdef LORAWAN_ROOT_APPKEY
        static const uint8_t AppKey[] = LORAWAN_ROOT_APPKEY;
    #endif
#else
    static const uint8_t FNwkSIntKey[] = LORAWAN_FNwkSIntKey;
    static const uint8_t AppSKey[] = LORAWAN_APPSKEY;
    static const uint32_t DevAddr = LORAWAN_DEVICE_ADDRESS;
    #if defined(LORAWAN_SNwkSIntKey) && defined(LORAWAN_NwkSEncKey)
        static const uint8_t SNwkSIntKey[] = LORAWAN_SNwkSIntKey;
        static const uint8_t NwkSEncKey[] = LORAWAN_NwkSEncKey;
    #endif
#endif

/*!
 * Application port
 */
static uint8_t AppPort = LORAWAN_APP_PORT;

/*!
 * User application data size
 */
static uint8_t gAppDataSize = LORAWAN_APP_DATA_SIZE;

/*!
 * User application data buffer size
 */
#define LORAWAN_APP_DATA_MAX_SIZE                           64

/*!
 * User application data
 */
static uint8_t AppData[LORAWAN_APP_DATA_MAX_SIZE];

/*!
 * Indicates if the node is sending confirmed or unconfirmed messages
 */
static uint8_t gIsTxConfirmed = LORAWAN_CONFIRMED_MSG_ON;

/*!
 * Timer to handle the application data transmission duty cycle
 *
 */
LowPowerTimeout tx_timeout;

/*!
 * Indicates if a new packet can be sent
 */
static volatile struct {
    uint8_t gmi : 1;
    uint8_t gmc : 1;
} flags;

/*!
 * Device states
 */
volatile enum eDevicState
{
    /* 0 */ DEVICE_STATE_INIT = 0,
    /* 1 */ DEVICE_STATE_SEND, 
    /* 2 */ DEVICE_STATE_TRIGGER,
    /* 3 */ DEVICE_STATE_SLEEP,
#ifdef LORAWAN_JOIN_EUI
    /* 4 */ DEVICE_STATE_JOIN,
    /* 5 */ DEVICE_STATE_JOIN_OK
#endif /* LORAWAN_JOIN_EUI */
} DeviceState, WakeUpState;

#if defined(TARGET_MOTE_L152RC) && !defined(TARGET_FF_ARDUINO)
    #define TARGET_FF_ARDUINO
#endif

#if defined(TARGET_FF_ARDUINO)
    #ifdef TARGET_DISCO_L072CZ_LRWAN1
        #define LED_GREEN       LED1
        #define LED_RED2        LED2    // next to LD7
        #define LED_BLUE        LED3
        #define LED_RED4        LED4
        InterruptIn button_pin(USER_BUTTON);
        #define BUTTON_PRESSED      0
        DigitalOut extLed(LED_RED4);
        AnalogIn ain_temp(PA_0);
    #else
        InterruptIn button_pin(D8);
        #define BUTTON_PRESSED      1
        DigitalOut extLed(D15);
        AnalogIn ain_pot(A1);
        AnalogIn ain_temp(A3);
    #endif
#endif /* TARGET_FF_ARDUINO */

#if defined(TARGET_FF_MORPHO) && !defined(TARGET_DISCO_L072CZ_LRWAN1)
    #define JUMPER_ENABLE
#endif /* */

#ifdef JUMPER_ENABLE
    #define TX_INTERVAL_US      15000000 
    DigitalOut jumper_out(PC_10);
    InterruptIn jumper_in(PC_12);
#endif /* JUMPER_ENABLE */

uint8_t c_ch;
us_timestamp_t buttonStartAt;
#ifdef TARGET_DISCO_L072CZ_LRWAN1
    PwmOut pwm(PA_0);
    #define _PWM_
#elif defined(TARGET_FF_ARDUINO) && defined(TARGET_FAMILY_STM32)
    PwmOut pwm(PB_11);
    #define _PWM_
#elif defined(TARGET_NRF52_DK)
    //PwmOut pwm(P0_0);
    #undef _PWM_
#else
    #error define_PWM_pin_for_this_target
#endif /* */
volatile int cayenne_ack_ch;

/*!
 * LoRaWAN compliance tests support data
 */
struct ComplianceTest_s
{
    bool Running;
    uint8_t State;
    bool IsTxConfirmed;
    uint8_t AppPort;
    uint8_t AppDataSize;
    uint8_t *AppDataBuffer;
    uint16_t DownLinkCounter;
    bool LinkCheck;
    uint8_t DemodMargin;
    uint8_t NbGateways;
}ComplianceTest;

McpsIndication_t gmi;

McpsConfirm_t gmc;

#ifdef JUMPER_ENABLE
void autoUplink()
{
    if (jumper_in.read() == 1) {
        tx_timeout.attach_us(autoUplink, TX_INTERVAL_US);
    }

    c_ch = 0xff;
    DeviceState = DEVICE_STATE_SEND;
}

void jumper_callback()
{
    tx_timeout.attach_us(autoUplink, TX_INTERVAL_US);
}
#endif /* JUMPER_ENABLE */

static void
clearIndications()
{
    vt.SetCursorPos(ROW_MCPS_CONF, 1);
    vt.printf("\e[K");
    vt.SetCursorPos(ROW_MCPS_IND, 1);
    vt.printf("\e[K");
    vt.SetCursorPos(ROW_MLME_IND, 1);
    vt.printf("\e[K");
    vt.SetCursorPos(ROW_MLME_CONF, 1);
    vt.printf("\e[K");

    vt.SetCursorPos(ROW_MIC+3, 1);
    vt.printf("\e[K");
}

#define LPP_DIGITAL_INPUT       0       // 1 byte
#define LPP_DIGITAL_OUTPUT      1       // 1 byte
#define LPP_ANALOG_INPUT        2       // 2 bytes, 0.01 signed
#define LPP_ANALOG_OUTPUT       3       // 2 bytes, 0.01 signed
#define LPP_LUMINOSITY          101     // 2 bytes, 1 lux unsigned
#define LPP_PRESENCE            102     // 1 byte, 1
#define LPP_TEMPERATURE         103     // 2 bytes, 0.1°C signed
#define LPP_RELATIVE_HUMIDITY   104     // 1 byte, 0.5% unsigned
#define LPP_ACCELEROMETER       113     // 2 bytes per axis, 0.001G
#define LPP_BAROMETRIC_PRESSURE 115     // 2 bytes 0.1 hPa Unsigned
#define LPP_GYROMETER           134     // 2 bytes per axis, 0.01 °/s
#define LPP_GPS                 136     // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01m


// Data ID + Data Type + Data Size
#define LPP_DIGITAL_INPUT_SIZE       3
#define LPP_DIGITAL_OUTPUT_SIZE      3
#define LPP_ANALOG_INPUT_SIZE        4
#define LPP_ANALOG_OUTPUT_SIZE       4
#define LPP_LUMINOSITY_SIZE          4
#define LPP_PRESENCE_SIZE            3
#define LPP_TEMPERATURE_SIZE         4
#define LPP_RELATIVE_HUMIDITY_SIZE   3
#define LPP_ACCELEROMETER_SIZE       8
#define LPP_BAROMETRIC_PRESSURE_SIZE 4
#define LPP_GYROMETER_SIZE           8
#define LPP_GPS_SIZE                 11

#define CAYENNE_CH_DOUT     2
#define CAYENNE_CH_AOUT     3
#define CAYENNE_CH_TEMP     0
#ifndef TARGET_DISCO_L072CZ_LRWAN1
    #define CAYENNE_CH_POT      1
#endif /* !TARGET_DISCO_L072CZ_LRWAN1 */
#define CAYENNE_CH_DIN      4


const unsigned R0 = 100000;
const unsigned B = 4275;
volatile bool dout_downlink;

/*!
 * \brief   Prepares the payload of the frame
 */
static void PrepareTxFrame( uint8_t port )
{
    uint16_t u16, rot;
    float t, f, R;
    static uint8_t seq;

    if (c_ch != 0xff) {
        gAppDataSize = 0;
        AppData[gAppDataSize++] = c_ch;
        switch (c_ch) {
            case CAYENNE_CH_TEMP:
                AppData[gAppDataSize++] = LPP_TEMPERATURE;
                u16 = ain_temp.read_u16() >> 4;
                R = 4096.0 / u16 - 1.0;
                R = R0 * R;
                t = 1.0/(log(R/R0)/B+1/298.15)-273.15;
                u16 = t * 10;  // 0.1C per bit
                AppData[gAppDataSize++] = u16 >> 8;
                AppData[gAppDataSize++] = u16;
                break;
#ifndef TARGET_DISCO_L072CZ_LRWAN1
            case CAYENNE_CH_POT:
                AppData[gAppDataSize++] = LPP_ANALOG_INPUT;
                u16 = ain_pot.read_u16();    // pot (rotary angle)
                f = u16 / 198.6;    // scale 65535/3.3 to 0.01v per bit
                rot = (uint16_t) f;
                AppData[gAppDataSize++] = rot >> 8;
                AppData[gAppDataSize++] = rot;
                break;
#endif /* !TARGET_DISCO_L072CZ_LRWAN1 */
            case CAYENNE_CH_DOUT:
                AppData[gAppDataSize++] = LPP_DIGITAL_OUTPUT;
                AppData[gAppDataSize++] = extLed.read();
                break;
            case CAYENNE_CH_AOUT:
                AppData[gAppDataSize++] = LPP_ANALOG_OUTPUT;
#ifdef _PWM_
                u16 = pwm.read() * 100;
#endif /* _PWM_ */
                AppData[gAppDataSize++] = u16 >> 8;
                AppData[gAppDataSize++] = u16;
                break;
        }
        return;
    } else if (cayenne_ack_ch != -1) {
        switch (cayenne_ack_ch) {
            case CAYENNE_CH_DOUT:
                AppData[gAppDataSize++] = LPP_DIGITAL_OUTPUT;
                AppData[gAppDataSize++] = extLed.read();
                break;
            case CAYENNE_CH_AOUT:
                AppData[gAppDataSize++] = LPP_ANALOG_OUTPUT;
#ifdef _PWM_
                u16 = pwm.read() * 100;
#endif /* _PWM_ */
                AppData[gAppDataSize++] = u16 >> 8;
                AppData[gAppDataSize++] = u16;
                break;
        }
        cayenne_ack_ch = -1;
    }

    while (button_pin.read() == BUTTON_PRESSED) {
        us_timestamp_t duration = LoRaMacReadTimer() - buttonStartAt;
        if (duration > 1000000) {
            gAppDataSize = 0;
            AppData[gAppDataSize++] = CAYENNE_CH_DOUT;
            AppData[gAppDataSize++] = LPP_DIGITAL_OUTPUT;
            AppData[gAppDataSize++] = extLed.read();
            return;
        }
    }

    switch( port ) {
        case LORAWAN_APP_PORT:
            gAppDataSize = 0;
            AppData[gAppDataSize++] = CAYENNE_CH_TEMP;
            AppData[gAppDataSize++] = LPP_TEMPERATURE;

            //vt.SetCursorPos( ROW_END-1, 1 );

            u16 = ain_temp.read_u16() >> 4;
            //vt.printf("0x%03x ", u16);
            R = 4096.0 / u16 - 1.0;
            R = R0 * R;
            t = 1.0/(log(R/R0)/B+1/298.15)-273.15;
            //vt.printf("%.1fC\e[K", t);
            u16 = t * 10;  // 0.1C per bit
            AppData[gAppDataSize++] = u16 >> 8;
            AppData[gAppDataSize++] = u16;
    #ifndef TARGET_DISCO_L072CZ_LRWAN1
            AppData[gAppDataSize++] = CAYENNE_CH_POT;
            AppData[gAppDataSize++] = LPP_ANALOG_INPUT;
            u16 = ain_pot.read_u16();    // pot (rotary angle)
            f = u16 / 198.6;    // scale 65535/3.3 to 0.01v per bit
            rot = (uint16_t) f;
            AppData[gAppDataSize++] = rot >> 8;
            AppData[gAppDataSize++] = rot;
    #endif /* !TARGET_DISCO_L072CZ_LRWAN1 */

            /* limited packet size: either ack downlink, or send sequence number */
            if (dout_downlink) {
                AppData[gAppDataSize++] = CAYENNE_CH_DOUT;
                AppData[gAppDataSize++] = LPP_DIGITAL_OUTPUT;
                AppData[gAppDataSize++] = extLed.read();
                dout_downlink = false;
            } else {
                AppData[gAppDataSize++] = CAYENNE_CH_DIN;
                AppData[gAppDataSize++] = LPP_DIGITAL_INPUT;
                AppData[gAppDataSize++] = seq++;
            }
            break;
        case 224:
            if( ComplianceTest.LinkCheck == true ) {
                ComplianceTest.LinkCheck = false;
                gAppDataSize = 3;
                AppData[0] = 5;
                AppData[1] = ComplianceTest.DemodMargin;
                AppData[2] = ComplianceTest.NbGateways;
                ComplianceTest.State = 1;
            } else {
                switch( ComplianceTest.State ) {
                    case 4:
                        ComplianceTest.State = 1;
                        break;
                    case 1:
                        gAppDataSize = 2;
                        AppData[0] = ComplianceTest.DownLinkCounter >> 8;
                        AppData[1] = ComplianceTest.DownLinkCounter;
                        break;
                }
            }
            break;
        default:
            break;
    }
}


/*!
 * \brief   Prepares the payload of the frame
 *
 * \retval  [0: frame could be send, 1: error]
 */
static LoRaMacStatus_t SendFrame(bool IsTxConfirmed, uint8_t AppDataSize)
{
    LoRaMacStatus_t status;
    char str[64];
    McpsReq_t mcpsReq;
    LoRaMacTxInfo_t txInfo;
    
    if( LoRaMacQueryTxPossible( AppDataSize, &txInfo ) != LORAMAC_STATUS_OK )
    {
        // Send empty frame in order to flush MAC commands
        mcpsReq.Type = MCPS_UNCONFIRMED;
        mcpsReq.Req.fBuffer = NULL;
        mcpsReq.Req.fBufferSize = 0;
        mcpsReq.Req.Datarate = LORAWAN_DEFAULT_DATARATE;
    }
    else
    {
        SerialDisplayUpdateFrameType(IsTxConfirmed);
        if( IsTxConfirmed == false )
        {
            mcpsReq.Type = MCPS_UNCONFIRMED;
            mcpsReq.Req.fPort = AppPort;
            mcpsReq.Req.fBuffer = AppData;
            mcpsReq.Req.fBufferSize = AppDataSize;
            mcpsReq.Req.Datarate = LORAWAN_DEFAULT_DATARATE;
        }
        else
        {
            mcpsReq.Type = MCPS_CONFIRMED;
            mcpsReq.Req.fPort = AppPort;
            mcpsReq.Req.fBuffer = AppData;
            mcpsReq.Req.fBufferSize = AppDataSize;
            mcpsReq.Req.Datarate = LORAWAN_DEFAULT_DATARATE;
        }
    }

    clearIndications();
    status = LoRaMacMcpsRequest( &mcpsReq );
    if (status == LORAMAC_STATUS_OK) {
        SerialDisplayUplink(mcpsReq.Req.fPort, AppData, mcpsReq.Req.fBufferSize);
        vt.SetCursorPos( ROW_END, 1 );
        vt.printf("sendFrame() OK %u\e[K", AppDataSize);
    } else {
        LoRaMacStatus_to_string(status, str);
        vt.SetCursorPos( ROW_END, 1 );
        vt.printf("sendFrame() %s rx%d\e[K", str, LoRaMacGetRxSlot());
    }

    return status;
} // ..SendFrame()

/*!
 * \brief   MCPS-Confirm event function
 *
 * \param   [IN] mcpsConfirm - Pointer to the confirm structure,
 *               containing confirm attributes.
 */
static void McpsConfirm( const McpsConfirm_t *mcpsConfirm )
{
    char str[64];

    vt.SetCursorPos( ROW_MCPS_CONF, 1);
    vt.printf("McpsConfirm up:%uhz ", mcpsConfirm->UpLinkFreqHz);
    LoRaMacEventInfoStatus_to_string(mcpsConfirm->Status, str);
    if (mcpsConfirm->Status == LORAMAC_EVENT_INFO_STATUS_OK)
        vt.printf("%s \e[K", str);
    else
        vt.printf("\e[31m%s\e[0m \e[K", str);

    if (mcpsConfirm->Status == LORAMAC_EVENT_INFO_STATUS_OK)
    {
        switch( mcpsConfirm->McpsRequest )
        {
            case MCPS_UNCONFIRMED:
            {
                // Check Datarate
                // Check TxPower
                break;
            }
            case MCPS_CONFIRMED:
            {
                // Check Datarate
                // Check TxPower
                // Check AckReceived
                // Check NbTrials
                //LoRaMacUplinkStatus.Acked = mcpsConfirm->AckReceived;
                break;
            }
            case MCPS_PROPRIETARY:
            {
                break;
            }
            default:
                break;
        }
    } else {
        /* fail */
    }
    memcpy(&gmc, mcpsConfirm, sizeof(McpsConfirm_t));
    flags.gmc = true;

    DeviceState = DEVICE_STATE_TRIGGER;
} // ..McpsConfirm()


/*!
 * \brief   MCPS-Indication event function
 *
 * \param   [IN] mcpsIndication - Pointer to the indication structure,
 *               containing indication attributes.
 */
static void McpsIndication( const McpsIndication_t *mcpsIndication )
{
    char str[64];

    memcpy(&gmi, mcpsIndication, sizeof(McpsIndication_t));
    flags.gmi = true;

    vt.SetCursorPos(ROW_MCPS_CONF, 1);
    vt.printf("\e[K");  // clear stale mcpsconf if retrying

    vt.SetCursorPos( ROW_MCPS_IND, 0);
    vt.printf("McpsIndication rx%d ADR_ACK_CNT:%u ", mcpsIndication->RxSlot, mcpsIndication->ADR_ACK_CNT);
    if (mcpsIndication->Status != LORAMAC_EVENT_INFO_STATUS_OK)
    {
        LoRaMacEventInfoStatus_to_string(mcpsIndication->Status, str);
        vt.printf("\e[31m%s\e[0m\e[K", str);
        return;
    }
    vt.printf("OK\e[K");

    switch( mcpsIndication->McpsIndication )
    {
        case MCPS_UNCONFIRMED:
        {
            break;
        }
        case MCPS_CONFIRMED:
        {
            /* ack sent by mac layer */
            break;
        }
        case MCPS_PROPRIETARY:
        {
            break;
        }
        case MCPS_MULTICAST:
        {
            break;
        }
        default:
            break;
    }

    // Check Multicast
    // Check Port
    // Check Datarate
    // Check FramePending
    // Check Buffer
    // Check BufferSize
    // Check Rssi
    // Check Snr
    // Check RxSlot


    if( ComplianceTest.Running == true )
    {
        ComplianceTest.DownLinkCounter++;
    }

    if( mcpsIndication->RxData == true )
    {
        unsigned n;
        for (n = 0; n < mcpsIndication->BufferSize; n += 4) {
            uint16_t val = mcpsIndication->Buffer[n+1] << 8;
            val += mcpsIndication->Buffer[n+2];
            cayenne_ack_ch = mcpsIndication->Buffer[n];
            switch (mcpsIndication->Buffer[n]) {
                case CAYENNE_CH_DOUT:
                    extLed.write(val);
                    dout_downlink = true;
                    break;
                case CAYENNE_CH_AOUT:
#ifdef _PWM_
                    pwm.write(val / 100.0);
#endif /* _PWM_ */
                    break;
                default:
                    break;
            }
        }

        switch( mcpsIndication->Port )
        {
        case 1: // The application LED can be controlled on port 1 or 2
        case 2:
            break;
        case 224:
            if( ComplianceTest.Running == false )
            {
                // Check compliance test enable command (i)
                if( ( mcpsIndication->BufferSize == 4 ) &&
                    ( mcpsIndication->Buffer[0] == 0x01 ) &&
                    ( mcpsIndication->Buffer[1] == 0x01 ) &&
                    ( mcpsIndication->Buffer[2] == 0x01 ) &&
                    ( mcpsIndication->Buffer[3] == 0x01 ) )
                {
                    gIsTxConfirmed = false;
                    AppPort = 224;
                    gAppDataSize = 2;
                    ComplianceTest.DownLinkCounter = 0;
                    ComplianceTest.LinkCheck = false;
                    ComplianceTest.DemodMargin = 0;
                    ComplianceTest.NbGateways = 0;
                    ComplianceTest.Running = true;
                    ComplianceTest.State = 1;
                    
                    MibRequestConfirm_t mibReq;
                    mibReq.Type = MIB_ADR;
                    mibReq.Param.AdrEnable = true;
                    LoRaMacMibSetRequestConfirm( &mibReq );

#if defined( USE_BAND_868 )
                    DutyCycleOn = false;
#endif
                }
            }
            else
            {
                ComplianceTest.State = mcpsIndication->Buffer[0];
                switch( ComplianceTest.State )
                {
                case 0: // Check compliance test disable command (ii)
                    gIsTxConfirmed = LORAWAN_CONFIRMED_MSG_ON;
                    AppPort = LORAWAN_APP_PORT;
                    gAppDataSize = LORAWAN_APP_DATA_SIZE;
                    ComplianceTest.DownLinkCounter = 0;
                    ComplianceTest.Running = false;

                    MibRequestConfirm_t mibReq;
                    mibReq.Type = MIB_ADR;
                    mibReq.Param.AdrEnable = LORAWAN_ADR_ON;
                    LoRaMacMibSetRequestConfirm( &mibReq );
#if defined( USE_BAND_868 )
                    DutyCycleOn = LORAWAN_DUTYCYCLE_ON;
#endif
                    break;
                case 1: // (iii, iv)
                    gAppDataSize = 2;
                    break;
                case 2: // Enable confirmed messages (v)
                    gIsTxConfirmed = true;
                    ComplianceTest.State = 1;
                    break;
                case 3:  // Disable confirmed messages (vi)
                    gIsTxConfirmed = false;
                    ComplianceTest.State = 1;
                    break;
                case 4: // (vii)
                    gAppDataSize = mcpsIndication->BufferSize;

                    AppData[0] = 4;
                    for( uint8_t i = 1; i < gAppDataSize; i++ )
                    {
                        AppData[i] = mcpsIndication->Buffer[i] + 1;
                    }
                    break;
                case 5: // (viii)
                    {
                        MlmeReq_t mlmeReq;
                        mlmeReq.Type = MLME_LINK_CHECK;
                        LoRaMacMlmeRequest( &mlmeReq );
                    }
                    break;
                case 6: // (ix)
                    {
#ifdef LORAWAN_JOIN_EUI
                        MlmeReq_t mlmeReq = {};

                        // Disable TestMode and revert back to normal operation
                        gIsTxConfirmed = LORAWAN_CONFIRMED_MSG_ON;
                        AppPort = LORAWAN_APP_PORT;
                        gAppDataSize = LORAWAN_APP_DATA_SIZE;
                        ComplianceTest.DownLinkCounter = 0;
                        ComplianceTest.Running = false;

                        MibRequestConfirm_t mibReq;
                        mibReq.Type = MIB_ADR;
                        mibReq.Param.AdrEnable = LORAWAN_ADR_ON;
                        LoRaMacMibSetRequestConfirm( &mibReq );
#if defined( USE_BAND_868 )
                        DutyCycleOn = LORAWAN_DUTYCYCLE_ON;
#endif

                        mlmeReq.Type = MLME_JOIN;

                        mlmeReq.Req.Join.DevEui = DevEui;
                        mlmeReq.Req.Join.JoinEui = JoinEui;
                        mlmeReq.Req.Join.NwkKey = NwkKey;
    #ifdef LORAWAN_ROOT_APPKEY
                        mlmeReq.Req.Join.AppKey = AppKey;
    #endif /* LORAWAN_ROOT_APPKEY */

                        LoRaMacMlmeRequest( &mlmeReq );
#endif /* LORAWAN_JOIN_EUI */
                        DeviceState = DEVICE_STATE_SLEEP;
                    }
                    break;
                case 7: // Switch end device Class
                    {
                        MlmeReq_t mlmeReq;

                        mlmeReq.Type = MLME_SWITCH_CLASS;

                        // CLASS_A = 0, CLASS_B = 1, CLASS_C = 2
                        mlmeReq.Req.SwitchClass.Class = ( DeviceClass_t )mcpsIndication->Buffer[1];

                        LoRaMacMlmeRequest( &mlmeReq );

                        PrepareTxFrame( AppPort );
                        /*status =*/ SendFrame(gIsTxConfirmed, gAppDataSize);
                    }
                    break;
                case 8: // Send PingSlotInfoReq
                    {
                        MlmeReq_t mlmeReq;

                        mlmeReq.Type = MLME_PING_SLOT_INFO;

                        mlmeReq.Req.PingSlotInfo.Value = mcpsIndication->Buffer[1];

                        LoRaMacMlmeRequest( &mlmeReq );
                        PrepareTxFrame( AppPort );
                        /*status =*/ SendFrame(gIsTxConfirmed, gAppDataSize);
                    }
                    break;
                case 9: // Send BeaconTimingReq
                    {
                        MlmeReq_t mlmeReq;

                        mlmeReq.Type = MLME_BEACON_TIMING;

                        LoRaMacMlmeRequest( &mlmeReq );
                        PrepareTxFrame( AppPort );
                        /*status =*/ SendFrame(gIsTxConfirmed, gAppDataSize);
                    }
                    break;
                default:
                    break;
                }
            }
            break;
        default:
            break;
        }

    }

} // ..McpsIndication()

#ifdef LORAWAN_JOIN_EUI 
void
join(uint8_t tries)
{
    char str[64];
    LoRaMacStatus_t status;
    MlmeReq_t mlmeReq = { };

    mlmeReq.Type = MLME_JOIN;

    clearIndications();
#ifdef LORAWAN_ROOT_APPKEY
    mlmeReq.Req.Join.AppKey = AppKey;
#endif
    mlmeReq.Req.Join.DevEui = DevEui;
    mlmeReq.Req.Join.JoinEui = JoinEui;
    mlmeReq.Req.Join.NwkKey = NwkKey;
    mlmeReq.Req.Join.NbTrials = tries;
    status = LoRaMacMlmeRequest( &mlmeReq );
    if (status != LORAMAC_STATUS_OK) {
        LoRaMacStatus_to_string(status, str);
    } else
        extLed = 1;
}
#endif /* LORAWAN_JOIN_EUI */

/*!
 * \brief   MLME-Confirm event function
 *
 * \param   [IN] mlmeConfirm - Pointer to the confirm structure,
 *               containing confirm attributes.
 */
static void MlmeConfirm( const MlmeConfirm_t *mlmeConfirm )
{
    char str[64];
    static uint8_t failCnt = 0;

    vt.SetCursorPos(ROW_MLME_CONF, 1);
    Mlme_to_string(mlmeConfirm->MlmeRequest, str);
    vt.printf("MlmeConfirm %s ", str);
    LoRaMacEventInfoStatus_to_string(mlmeConfirm->Status, str);
    if (mlmeConfirm->Status != LORAMAC_EVENT_INFO_STATUS_OK)
        vt.printf("\e[31m%s \e[0m \e[K", str);
    else
        vt.printf("%s \e[K", str);

#if defined(LORAWAN_ROOT_APPKEY) && defined(LORAWAN_JOIN_EUI)
    /* 1v1 joinNonce is incrementing non-volatile value */
    if (mlmeConfirm->MlmeRequest == MLME_JOIN) {
        vt.printf(" rxJoinNonce:%u vs %u", 
            mlmeConfirm->fields.join.rxJoinNonce,
            mlmeConfirm->fields.join.myJoinNonce
        );
    }
#endif /* LORAWAN_ROOT_APPKEY */

    if (mlmeConfirm->Status == LORAMAC_EVENT_INFO_STATUS_OK)
    {
        failCnt = 0;
        switch (mlmeConfirm->MlmeRequest)
        {
#ifdef LORAWAN_JOIN_EUI
            case MLME_JOIN:
            {
                // Status is OK, node has joined the network
                /* collect any mac cmds from server until expected channel mask */
                extLed = 0;
                DeviceState = DEVICE_STATE_JOIN_OK;
                break;
            }
#endif /* LORAWAN_JOIN_EUI*/
            case MLME_LINK_CHECK:
            {
                // Check DemodMargin
                // Check NbGateways
                if( ComplianceTest.Running == true )
                {
                    ComplianceTest.LinkCheck = true;
                    ComplianceTest.DemodMargin = mlmeConfirm->fields.link.DemodMargin;
                    ComplianceTest.NbGateways = mlmeConfirm->fields.link.NbGateways;
                }
                break;
            }
            case MLME_TIME_REQ:
                break;
            default:
                /* TODO: handle unknown MLME request */
                DeviceState = DEVICE_STATE_SLEEP;
                break;
        }
    }
    else    // not ok...
    {
        failCnt++;

#ifdef LORAWAN_JOIN_EUI
        if (failCnt > 5) {
            join(1);
            return;
        }
#endif

        switch( mlmeConfirm->MlmeRequest )
        {
#ifdef LORAWAN_JOIN_EUI
            case MLME_JOIN:
            {
                // Join failed, restart join procedure
                break;
            }
#endif /* LORAWAN_JOIN_EUI */
            case MLME_LINK_CHECK:
                DeviceState = DEVICE_STATE_SLEEP;
                break;
#ifdef LORAWAN_JOIN_EUI
            case MLME_REJOIN_0:
                break;
            case MLME_REJOIN_2:
                break;
            case MLME_TIME_REQ:
                break;
#endif /* LORAWAN_JOIN_EUI */
            default:
                DeviceState = DEVICE_STATE_SLEEP;
                break;
        }
    }
} // ..MlmeConfirm

static void MlmeIndication( const MlmeIndication_t *MlmeIndication )
{
    char str[48];
    MibRequestConfirm_t mibReq;

    vt.SetCursorPos(ROW_MLME_IND, 1);
    Mlme_to_string(MlmeIndication->MlmeIndication, str);
    vt.printf("MlmeIndication %s ", str);
#ifdef TARGET_STM32
    vt.printf(" %08x ", RCC->CSR);
#endif /* TARGET_STM32 */
    LoRaMacEventInfoStatus_to_string(MlmeIndication->Status, str);
    if (MlmeIndication->Status != LORAMAC_EVENT_INFO_STATUS_OK)
        vt.printf("\e[31m%s \e[0m \e[K", str);
    else
        vt.printf("%s \e[K", str);

    switch( MlmeIndication->MlmeIndication )
    {
        case MLME_SWITCH_CLASS:
        {
            /* mac gave up on beacon */
            mibReq.Type = MIB_DEVICE_CLASS;
            mibReq.Param.Class = CLASS_A;
            LoRaMacMibSetRequestConfirm( &mibReq );

            // Switch to class A again
            DeviceState = DEVICE_STATE_SLEEP;    // class-B manual switch
            break;
        }
        case MLME_BEACON:
        {
            LoRaMacEventInfoStatus_to_string(MlmeIndication->Status, str);
            break;

        }
#ifdef LORAWAN_JOIN_EUI
        case MLME_JOIN:
            vt.printf("%uhz try%u", MlmeIndication->freqHz, MlmeIndication->JoinRequestTrials);
            break;
#endif /* !LORAWAN_JOIN_EUI  */
        default:
            break;
    }

} // ..MlmeIndication()

uint8_t periodicity;

void SerialDisplayRefresh( void )
{
#ifdef LORAWAN_JOIN_EUI
    MibRequestConfirm_t mibReq;
#endif

    SerialDisplayInit( );
#ifdef LORAWAN_JOIN_EUI
    SerialDisplayUpdateActivationMode(true);
    SerialDisplayUpdateEui( ROW_DEVEUI, DevEui);
    SerialDisplayUpdateEui( ROW_JOINEUI, JoinEui);
    SerialDisplayUpdateKey( ROW_NWKKEY, NwkKey);

    #ifdef LORAWAN_ROOT_APPKEY
    SerialDisplayUpdateKey(ROW_APPKEY, AppKey);
    #endif

    mibReq.Type = MIB_NETWORK_JOINED;
    LoRaMacMibGetRequestConfirm( &mibReq );
    SerialDisplayUpdateNetworkIsJoined( mibReq.Param.IsNetworkJoined );
#else
    //SerialDisplayUpdateNwkId( LORAWAN_NETWORK_ID );
    SerialDisplayUpdateDevAddr( DevAddr );
    SerialDisplayUpdateKey( ROW_FNwkSIntKey, FNwkSIntKey);
    SerialDisplayUpdateKey( ROW_AppSKey, AppSKey );
    #if defined(LORAWAN_SNwkSIntKey) && defined(LORAWAN_NwkSEncKey)
    SerialDisplayUpdateKey(ROW_NwkSEncKey, NwkSEncKey);
    SerialDisplayUpdateKey(ROW_SNwkSIntKey, SNwkSIntKey);
    #endif /* 1v1 ABP */

    vt.SetCursorPos( ROW_END, 1 );
    vt.printf("FCntUp:%08x", eeprom_read(EEPROM_FCNTUP));
    vt.printf(" AFCntDown:%08x", get_fcntdwn(true));
    vt.printf(" NFCntDown:%08x", get_fcntdwn(false));
#endif


    SerialDisplayUpdateAdr( LORAWAN_ADR_ON );
#if defined( USE_BAND_868 )
    SerialDisplayUpdateDutyCycle( LORAWAN_DUTYCYCLE_ON );
#else
    SerialDisplayUpdateDutyCycle( false );
#endif
    SerialDisplayUpdatePublicNetwork( LORAWAN_PUBLIC_NETWORK );

    //SerialDisplayUpdateLedState( 3, AppLedStateOn );
}

void SerialRxProcess( void )
{
    LoRaMacStatus_t status;
    MlmeReq_t mlmeReq;
#ifndef LORAWAN_JOIN_EUI
    static uint8_t icnt = 0;
#endif
    
    if( SerialDisplayReadable( ) == true ) {
        char ch = SerialDisplayGetChar();
#ifndef LORAWAN_JOIN_EUI
        if (ch == 'I') {
            if (++icnt == 3) {
                vt.SetCursorPos( ROW_END, 1 );
                vt.printf("reset-fcnts\e[K");
                eeprom_clear(EEPROM_AFCNTDWN);
                eeprom_clear(EEPROM_NFCNTDWN);
                eeprom_clear(EEPROM_FCNTUP);
            }
        } else
            icnt = 0;
#endif /* !LORAWAN_JOIN_EUI */

        if ( ch >= '0' && ch <= '9') {
            c_ch = ch - '0';
            DeviceState = DEVICE_STATE_SEND;
            return;
        }
        switch( ch ) {
            case 'R':
            case 'r':
                // Refresh Serial screen
                SerialDisplayRefresh( );
                break;
            case 'L':
                clearIndications();
                mlmeReq.Type = MLME_LINK_CHECK;
                status = LoRaMacMlmeRequest( &mlmeReq );
                if (status == LORAMAC_STATUS_OK)
                    SendFrame(0, false);
                break;
#ifdef LORAWAN_JOIN_EUI
            case 'j':
                DeviceState = DEVICE_STATE_JOIN;
                break;
#endif
            default:
                break;
        }
    }
}

static void button_isr()
{
    c_ch = 0xff;
    DeviceState = DEVICE_STATE_SEND;
    buttonStartAt = LoRaMacReadTimer();
}

static const LoRaMacPrimitives_t LoRaMacPrimitives = {
    McpsConfirm,
    McpsIndication,
    MlmeConfirm,
    MlmeIndication
};

static const LoRaMacCallback_t LoRaMacCallbacks = {
    BoardGetBatteryLevel,
    NULL
};

/**
 * Main application entry point.
 */
int main()
{
    LoRaMacStatus_t status;
    MibRequestConfirm_t mibReq;

    DeviceState = DEVICE_STATE_INIT;

    if (sleep_manager_can_deep_sleep())
        sleep_manager_lock_deep_sleep();    // prevent deep sleep

#ifdef JUMPER_ENABLE
    jumper_out = 1;
    jumper_in.mode(PullDown);
    jumper_in.rise(jumper_callback);
    // Q: does InterruptIn.rise() call immediately if already high?
    if (jumper_in.read())
        jumper_callback(); // A: probably not
#endif /* JUMPER_ENABLE */

    while( 1 )
    {
        SerialRxProcess( );

        if (flags.gmi) {
            flags.gmi = false;
            SerialDisplayMcpsIndication(&gmi);
        }

        if (flags.gmc) {
            flags.gmc = false;
            SerialDisplayMcpsConfirm(&gmc);
        }

        switch( DeviceState )
        {
            case DEVICE_STATE_INIT:
            {
                status = LoRaMacInitialization( &LoRaMacPrimitives, &LoRaMacCallbacks );
                if (LORAMAC_STATUS_OK != status) {
                    char str[48];
                    LoRaMacStatus_to_string(status, str);
                    vt.SetCursorPos(1, 1);
                    vt.printf("MacInit: %s\e[K", str);
                    for (;;) asm("nop");
                }
                
#ifdef _PWM_
                pwm.period(1.0 / 60);
#endif /* _PWM_ */
                cayenne_ack_ch = -1;
                c_ch = 0xff;
                button_pin.mode(PullDown);                
#ifdef TARGET_DISCO_L072CZ_LRWAN1
                button_pin.fall(button_isr);
#else
                button_pin.rise(button_isr);
#endif

                mibReq.Type = MIB_ADR;
                mibReq.Param.AdrEnable = LORAWAN_ADR_ON;
                LoRaMacMibSetRequestConfirm( &mibReq );

                mibReq.Type = MIB_PUBLIC_NETWORK;
                mibReq.Param.EnablePublicNetwork = LORAWAN_PUBLIC_NETWORK;
                LoRaMacMibSetRequestConfirm( &mibReq );

#if defined( USE_BAND_868 )
                DutyCycleOn = LORAWAN_DUTYCYCLE_ON ;
#if( USE_SEMTECH_DEFAULT_CHANNEL_LINEUP == 1 ) 
                LoRaMacChannelAdd( 3, ( ChannelParams_t )LC4 );
                LoRaMacChannelAdd( 4, ( ChannelParams_t )LC5 );
                LoRaMacChannelAdd( 5, ( ChannelParams_t )LC6 );
                LoRaMacChannelAdd( 6, ( ChannelParams_t )LC7 );
                LoRaMacChannelAdd( 7, ( ChannelParams_t )LC8 );
                LoRaMacChannelAdd( 8, ( ChannelParams_t )LC9 );
                LoRaMacChannelAdd( 9, ( ChannelParams_t )LC10 );

                mibReq.Type = MIB_RX2_CHANNEL;
                mibReq.Param.Rx2Channel = ( Rx2ChannelParams_t ){ 869525000, DR_3 };
                LoRaMacMibSetRequestConfirm( &mibReq );
#endif

#endif

                SerialDisplayRefresh();
#ifdef LORAWAN_JOIN_EUI

    #ifndef SENETCO /* for senet, use network provided DevEUI */
                // Initialize LoRaMac device unique ID
                HardwareIDtoDevEUI(DevEui);
    #ifdef LORAWAN_ROOT_APPKEY
                // inverted DevEui provisioned as v1.1 on server (non-inv = lorawan1v0)
                for (int i = 0; i < 8; i++)
                    DevEui[i] ^= 0xff;
    #endif /* LORAWAN_ROOT_APPKEY */
    #endif /* !SENETCO */
                SerialDisplayUpdateEui( 5, DevEui );
                DeviceState = DEVICE_STATE_JOIN;
#else   /* ABP... */

                mibReq.Type = MIB_DEV_ADDR;
                mibReq.Param.DevAddr = DevAddr;
                LoRaMacMibSetRequestConfirm( &mibReq );
                SerialDisplayUpdateDevAddr(DevAddr);

                mibReq.Type = MIB_APP_SKEY;
                mibReq.Param.key = AppSKey;
                LoRaMacMibSetRequestConfirm( &mibReq );
                SerialDisplayUpdateKey(ROW_AppSKey, AppSKey);

    #if defined(LORAWAN_SNwkSIntKey) && defined(LORAWAN_NwkSEncKey)
                /* lorawan 1v1 ABP */
                mibReq.Type = MIB_NwkSEncKey;
                mibReq.Param.key = NwkSEncKey;
                LoRaMacMibSetRequestConfirm( &mibReq );
                SerialDisplayUpdateKey(ROW_NwkSEncKey, NwkSEncKey);

                mibReq.Type = MIB_SNwkSIntKey;
                mibReq.Param.key = SNwkSIntKey;
                LoRaMacMibSetRequestConfirm( &mibReq );
                SerialDisplayUpdateKey(ROW_SNwkSIntKey, SNwkSIntKey);

                mibReq.Type = MIB_FNwkSIntKey;
                mibReq.Param.key = FNwkSIntKey;
                LoRaMacMibSetRequestConfirm( &mibReq );
                SerialDisplayUpdateKey(ROW_FNwkSIntKey, mibReq.Param.key);
    #else
                /* lorawan 1v0 ABP */
                mibReq.Type = MIB_NwkSKey;
                mibReq.Param.key = FNwkSIntKey;
                LoRaMacMibSetRequestConfirm( &mibReq );
                SerialDisplayUpdateKey(ROW_FNwkSIntKey, mibReq.Param.key);
    #endif

                DeviceState = DEVICE_STATE_TRIGGER;
#endif /* !LORAWAN_JOIN_EUI */
                break;
            }
#ifdef LORAWAN_JOIN_EUI
            case DEVICE_STATE_JOIN:
            {
                join(8);
                DeviceState = DEVICE_STATE_SLEEP;
                break;
            }
            case DEVICE_STATE_JOIN_OK:
                MibRequestConfirm_t mibReq;
                mibReq.Type = MIB_NETWORK_JOINED;
                LoRaMacMibGetRequestConfirm( &mibReq );
                SerialDisplayUpdateNetworkIsJoined( mibReq.Param.IsNetworkJoined );
                mibReq.Type = MIB_DEV_ADDR;
                LoRaMacMibGetRequestConfirm( &mibReq );
                SerialDisplayUpdateDevAddr(mibReq.Param.DevAddr);
                mibReq.Type = MIB_FNwkSIntKey;
                LoRaMacMibGetRequestConfirm( &mibReq );
                SerialDisplayUpdateKey( ROW_FNwkSIntKey, mibReq.Param.key );
                mibReq.Type = MIB_APP_SKEY;
                LoRaMacMibGetRequestConfirm( &mibReq );
                SerialDisplayUpdateKey( ROW_AppSKey,  mibReq.Param.key );

    #ifdef LORAWAN_ROOT_APPKEY
                mibReq.Type = MIB_SNwkSIntKey;
                LoRaMacMibGetRequestConfirm( &mibReq );
                SerialDisplayUpdateKey(ROW_SNwkSIntKey,  mibReq.Param.key);
                mibReq.Type = MIB_NwkSEncKey;
                LoRaMacMibGetRequestConfirm( &mibReq );
                SerialDisplayUpdateKey(ROW_NwkSEncKey,  mibReq.Param.key);
    #endif /* LORAWAN_ROOT_APPKEY */
                DeviceState = DEVICE_STATE_TRIGGER;
                break;
#endif /* LORAWAN_JOIN_EUI */
            case DEVICE_STATE_SEND:
                SerialDisplayUpdateUplinkAcked( false );
                SerialDisplayUpdateDonwlinkRxData( false );
                PrepareTxFrame( AppPort );
                status = SendFrame(gIsTxConfirmed, gAppDataSize);
                if (status == LORAMAC_STATUS_OK) {
                    /* McpsConfirm or McpsIndication callback will continue */
                    DeviceState = DEVICE_STATE_SLEEP;
                } else
                    DeviceState = DEVICE_STATE_TRIGGER;
                break;
            case DEVICE_STATE_SLEEP:
            {
                // Wake up through events
                sleep_manager_sleep_auto();
                break;
            }
            case DEVICE_STATE_TRIGGER:
                /* wait button ISR */
                sleep_manager_sleep_auto();
                break;
            default:
                DeviceState = DEVICE_STATE_INIT;
                break;
        } // ..switch( DeviceState )

        LoRaMacUserContext();
    } // ..while( 1 )
}
#endif /* ENABLE_VT100 */