Ottawa Bootcamp Publish

Dependencies:   SX127x lorawan1v1

sensorDemoVT100.cpp

Committer:
Wayne Roberts
Date:
2018-03-29
Revision:
4:72b8fdc9878e
Parent:
3:fc23bbc14475
Child:
5:53cd6c24a4ab

File content as of revision 4:72b8fdc9878e:

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

Description: LoRaMac classB device implementation

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

*/

#include "LoRaMac.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)
InterruptIn d8(D8);
DigitalOut extLed(D15);
#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);
#elif defined(TARGET_FF_ARDUINO)
PwmOut pwm(PB_11);
#endif /* TARGET_DISCO_L072CZ_LRWAN1 */
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
#define CAYENNE_CH_POT      1
#define CAYENNE_CH_DIN      4

AnalogIn a1(A1);
AnalogIn a3(A3);

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 = a3.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;
            case CAYENNE_CH_POT:
                AppData[gAppDataSize++] = LPP_ANALOG_INPUT;
                u16 = a1.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;
            case CAYENNE_CH_DOUT:
                AppData[gAppDataSize++] = LPP_DIGITAL_OUTPUT;
                AppData[gAppDataSize++] = extLed.read();
                break;
            case CAYENNE_CH_AOUT:
                AppData[gAppDataSize++] = LPP_ANALOG_OUTPUT;
                u16 = pwm.read() * 100;
                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;
                u16 = pwm.read() * 100;
                AppData[gAppDataSize++] = u16 >> 8;
                AppData[gAppDataSize++] = u16;
                break;
        }
        cayenne_ack_ch = -1;
    }

    while (d8.read() == 1) {
        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;
            u16 = a3.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;
            AppData[gAppDataSize++] = CAYENNE_CH_POT;
            AppData[gAppDataSize++] = LPP_ANALOG_INPUT;
            u16 = a1.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;

            /* 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:
                    pwm.write(val / 100.0);
                    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 %08x ", str, RCC->CSR);
    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 d8isr()
{
    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");
                }
                
                pwm.period(1.0 / 60);
                cayenne_ack_ch = -1;
                c_ch = 0xff;
                d8.mode(PullDown);                
                d8.rise(d8isr);

                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 */