/*
 * Copyright (c) 2014-2016 IBM Corporation.
 * All rights reserved.
 *
 * Copyright (c) 2016-2019 MCCI Corporation.
 * All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of the <organization> nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

//! \file
#define LMIC_DR_LEGACY 0
#include "lmic_bandplan.h"

#if defined(DISABLE_BEACONS) && !defined(DISABLE_PING)
#error Ping needs beacon tracking
#endif

DEFINE_LMIC;


// Fwd decls.
static void reportEventNoUpdate(ev_t);
static void reportEventAndUpdate(ev_t);
static void engineUpdate(void);
static bit_t processJoinAccept_badframe(void);
static bit_t processJoinAccept_nojoinframe(void);


#if !defined(DISABLE_BEACONS)
static void startScan (void);
#endif

static inline void initTxrxFlags(const char *func, u1_t mask) {
	LMIC_DEBUG2_PARAMETER(func);

#if LMIC_DEBUG_LEVEL > 1
	LMIC_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": %s txrxFlags %#02x --> %02x\n", os_getTime(), func, LMIC.txrxFlags, mask);
#endif
	LMIC.txrxFlags = mask;
}

static inline void orTxrxFlags(const char *func, u1_t mask) {
	initTxrxFlags(func, LMIC.txrxFlags | mask);
}



// ================================================================================
// BEG OS - default implementations for certain OS suport functions

#if !defined(HAS_os_calls)

#if !defined(os_rlsbf2)
u2_t os_rlsbf2 (xref2cu1_t buf) {
    return (u2_t)((u2_t)buf[0] | ((u2_t)buf[1]<<8));
}
#endif

#if !defined(os_rlsbf4)
u4_t os_rlsbf4 (xref2cu1_t buf) {
    return (u4_t)((u4_t)buf[0] | ((u4_t)buf[1]<<8) | ((u4_t)buf[2]<<16) | ((u4_t)buf[3]<<24));
}
#endif


#if !defined(os_rmsbf4)
u4_t os_rmsbf4 (xref2cu1_t buf) {
    return (u4_t)((u4_t)buf[3] | ((u4_t)buf[2]<<8) | ((u4_t)buf[1]<<16) | ((u4_t)buf[0]<<24));
}
#endif


#if !defined(os_wlsbf2)
void os_wlsbf2 (xref2u1_t buf, u2_t v) {
    buf[0] = v;
    buf[1] = v>>8;
}
#endif

#if !defined(os_wlsbf4)
void os_wlsbf4 (xref2u1_t buf, u4_t v) {
    buf[0] = v;
    buf[1] = v>>8;
    buf[2] = v>>16;
    buf[3] = v>>24;
}
#endif

#if !defined(os_wmsbf4)
void os_wmsbf4 (xref2u1_t buf, u4_t v) {
    buf[3] = v;
    buf[2] = v>>8;
    buf[1] = v>>16;
    buf[0] = v>>24;
}
#endif

#if !defined(os_getBattLevel)
u1_t os_getBattLevel (void) {
    return MCMD_DEVS_BATT_NOINFO;
}
#endif

#if !defined(os_crc16)
// New CRC-16 CCITT(XMODEM) checksum for beacons:
u2_t os_crc16 (xref2cu1_t data, uint len) {
    u2_t remainder = 0;
    u2_t polynomial = 0x1021;
    for( uint i = 0; i < len; i++ ) {
        remainder ^= data[i] << 8;
        for( u1_t bit = 8; bit > 0; bit--) {
            if( (remainder & 0x8000) )
                remainder = (remainder << 1) ^ polynomial;
            else
                remainder <<= 1;
        }
    }
    return remainder;
}
#endif

#endif // !HAS_os_calls

// END OS - default implementations for certain OS suport functions
// ================================================================================

// ================================================================================
// BEG AES

static void micB0 (u4_t devaddr, u4_t seqno, int dndir, int len) {
    os_clearMem(AESaux,16);
    AESaux[0]  = 0x49;
    AESaux[5]  = dndir?1:0;
    AESaux[15] = len;
    os_wlsbf4(AESaux+ 6,devaddr);
    os_wlsbf4(AESaux+10,seqno);
}


static int aes_verifyMic (xref2cu1_t key, u4_t devaddr, u4_t seqno, int dndir, xref2u1_t pdu, int len) {
    micB0(devaddr, seqno, dndir, len);
    os_copyMem(AESkey,key,16);
    return os_aes(AES_MIC, pdu, len) == os_rmsbf4(pdu+len);
}


static void aes_appendMic (xref2cu1_t key, u4_t devaddr, u4_t seqno, int dndir, xref2u1_t pdu, int len) {
    micB0(devaddr, seqno, dndir, len);
    os_copyMem(AESkey,key,16);
    // MSB because of internal structure of AES
    os_wmsbf4(pdu+len, os_aes(AES_MIC, pdu, len));
}


static void aes_appendMic0 (xref2u1_t pdu, int len) {
    os_getDevKey(AESkey);
    os_wmsbf4(pdu+len, os_aes(AES_MIC|AES_MICNOAUX, pdu, len));  // MSB because of internal structure of AES
}


static int aes_verifyMic0 (xref2u1_t pdu, int len) {
    os_getDevKey(AESkey);
    return os_aes(AES_MIC|AES_MICNOAUX, pdu, len) == os_rmsbf4(pdu+len);
}


static void aes_encrypt (xref2u1_t pdu, int len) {
    os_getDevKey(AESkey);
    os_aes(AES_ENC, pdu, len);
}


static void aes_cipher (xref2cu1_t key, u4_t devaddr, u4_t seqno, int dndir, xref2u1_t payload, int len) {
    if( len <= 0 )
        return;
    os_clearMem(AESaux, 16);
    AESaux[0] = AESaux[15] = 1; // mode=cipher / dir=down / block counter=1
    AESaux[5] = dndir?1:0;
    os_wlsbf4(AESaux+ 6,devaddr);
    os_wlsbf4(AESaux+10,seqno);
    os_copyMem(AESkey,key,16);
    os_aes(AES_CTR, payload, len);
}


static void aes_sessKeys (u2_t devnonce, xref2cu1_t artnonce, xref2u1_t nwkkey, xref2u1_t artkey) {
    os_clearMem(nwkkey, 16);
    nwkkey[0] = 0x01;
    os_copyMem(nwkkey+1, artnonce, LEN_ARTNONCE+LEN_NETID);
    os_wlsbf2(nwkkey+1+LEN_ARTNONCE+LEN_NETID, devnonce);
    os_copyMem(artkey, nwkkey, 16);
    artkey[0] = 0x02;

    os_getDevKey(AESkey);
    os_aes(AES_ENC, nwkkey, 16);
    os_getDevKey(AESkey);
    os_aes(AES_ENC, artkey, 16);
}

// END AES
// ================================================================================


// ================================================================================
// BEG LORA

static CONST_TABLE(u1_t, SENSITIVITY)[7][3] = {
    // ------------bw----------
    // 125kHz    250kHz    500kHz
    { 141-109,  141-109, 141-109 },  // FSK
    { 141-127,  141-124, 141-121 },  // SF7
    { 141-129,  141-126, 141-123 },  // SF8
    { 141-132,  141-129, 141-126 },  // SF9
    { 141-135,  141-132, 141-129 },  // SF10
    { 141-138,  141-135, 141-132 },  // SF11
    { 141-141,  141-138, 141-135 }   // SF12
};

int getSensitivity (rps_t rps) {
    return -141 + TABLE_GET_U1_TWODIM(SENSITIVITY, getSf(rps), getBw(rps));
}

ostime_t calcAirTime (rps_t rps, u1_t plen) {
    u1_t bw = getBw(rps);  // 0,1,2 = 125,250,500kHz
    u1_t sf = getSf(rps);  // 0=FSK, 1..6 = SF7..12
    if( sf == FSK ) {
        return (plen+/*preamble*/5+/*syncword*/3+/*len*/1+/*crc*/2) * /*bits/byte*/8
            * (s4_t)OSTICKS_PER_SEC / /*kbit/s*/50000;
    }
    u1_t sfx = 4*(sf+(7-SF7));
    u1_t q = sfx - (sf >= SF11 ? 8 : 0);
    int tmp = 8*plen - sfx + 28 + (getNocrc(rps)?0:16) - (getIh(rps)?20:0);
    if( tmp > 0 ) {
        tmp = (tmp + q - 1) / q;
        tmp *= getCr(rps)+5;
        tmp += 8;
    } else {
        tmp = 8;
    }
    tmp = (tmp<<2) + /*preamble*/49 /* 4 * (8 + 4.25) */;
    // bw = 125000 = 15625 * 2^3
    //      250000 = 15625 * 2^4
    //      500000 = 15625 * 2^5
    // sf = 7..12
    //
    // osticks =  tmp * OSTICKS_PER_SEC * 1<<sf / bw
    //
    // 3 => counter reduced divisor 125000/8 => 15625
    // 2 => counter 2 shift on tmp
    sfx = sf+(7-SF7) - (3+2) - bw;
    int div = 15625;
    if( sfx > 4 ) {
        // prevent 32bit signed int overflow in last step
        div >>= sfx-4;
        sfx = 4;
    }
    // Need 32bit arithmetic for this last step
    return (((ostime_t)tmp << sfx) * OSTICKS_PER_SEC + div/2) / div;
}

// END LORA
// ================================================================================


// Adjust DR for TX retries
//  - indexed by retry count
//  - return steps to lower DR
static CONST_TABLE(u1_t, DRADJUST)[2+TXCONF_ATTEMPTS] = {
    // normal frames - 1st try / no retry
    0,
    // confirmed frames
    0,0,1,0,1,0,1,0,0
};


// Table below defines the size of one symbol as
//   symtime = 256us * 2^T(sf,bw)
// 256us is called one symunit.
//                 SF:
//      BW:      |__7___8___9__10__11__12
//      125kHz   |  2   3   4   5   6   7
//      250kHz   |  1   2   3   4   5   6
//      500kHz   |  0   1   2   3   4   5
//

#if !defined(DISABLE_BEACONS)
static ostime_t calcRxWindow (u1_t secs, dr_t dr) {
    ostime_t rxoff, err;
    if( secs==0 ) {
        // aka 128 secs (next becaon)
        rxoff = LMIC.drift;
        err = LMIC.lastDriftDiff;
    } else {
        // scheduled RX window within secs into current beacon period
        rxoff = (LMIC.drift * (ostime_t)secs) >> BCN_INTV_exp;
        err = (LMIC.lastDriftDiff * (ostime_t)secs) >> BCN_INTV_exp;
    }
    u1_t rxsyms = MINRX_SYMS;
    err += (ostime_t)LMIC.maxDriftDiff * LMIC.missedBcns;
    LMIC.rxsyms = MINRX_SYMS + (err / dr2hsym(dr));

    return (rxsyms-PAMBL_SYMS) * dr2hsym(dr) + rxoff;
}


// Setup beacon RX parameters assuming we have an error of ms (aka +/-(ms/2))
static void calcBcnRxWindowFromMillis (u1_t ms, bit_t ini) {
    if( ini ) {
        LMIC.drift = 0;
        LMIC.maxDriftDiff = 0;
        LMIC.missedBcns = 0;
        LMIC.bcninfo.flags |= BCN_NODRIFT|BCN_NODDIFF;
    }
    ostime_t hsym = dr2hsym(DR_BCN);
    LMIC.bcnRxsyms = MINRX_SYMS + ms2osticksCeil(ms) / hsym;
    LMIC.bcnRxtime = LMIC.bcninfo.txtime + BCN_INTV_osticks - (LMIC.bcnRxsyms-PAMBL_SYMS) * hsym;
}
#endif // !DISABLE_BEACONS


#if !defined(DISABLE_PING)
// Setup scheduled RX window (ping/multicast slot)
static void rxschedInit (xref2rxsched_t rxsched) {
    os_clearMem(AESkey,16);
    os_clearMem(LMIC.frame+8,8);
    os_wlsbf4(LMIC.frame, LMIC.bcninfo.time);
    os_wlsbf4(LMIC.frame+4, LMIC.devaddr);
    os_aes(AES_ENC,LMIC.frame,16);
    u1_t intvExp = rxsched->intvExp;
    ostime_t off = os_rlsbf2(LMIC.frame) & (0x0FFF >> (7 - intvExp)); // random offset (slot units)
    rxsched->rxbase = (LMIC.bcninfo.txtime +
                       BCN_RESERVE_osticks +
                       ms2osticks(BCN_SLOT_SPAN_ms * off)); // random offset osticks
    rxsched->slot   = 0;
    rxsched->rxtime = rxsched->rxbase - calcRxWindow(/*secs BCN_RESERVE*/2+(1<<intvExp),rxsched->dr);
    rxsched->rxsyms = LMIC.rxsyms;
}


static bit_t rxschedNext (xref2rxsched_t rxsched, ostime_t cando) {
  again:
    if( rxsched->rxtime - cando >= 0 )
        return 1;
    u1_t slot;
    if( (slot=rxsched->slot) >= 128 )
        return 0;
    u1_t intv = 1<<rxsched->intvExp;
    if( (rxsched->slot = (slot += (intv))) >= 128 )
        return 0;
    rxsched->rxtime = rxsched->rxbase
        + ((BCN_WINDOW_osticks * (ostime_t)slot) >> BCN_INTV_exp)
        - calcRxWindow(/*secs BCN_RESERVE*/2+slot+intv,rxsched->dr);
    rxsched->rxsyms = LMIC.rxsyms;
    goto again;
}
#endif // !DISABLE_PING)


ostime_t LMICcore_rndDelay (u1_t secSpan) {
    u2_t r = os_getRndU2();
    ostime_t delay = r;
    if( delay > OSTICKS_PER_SEC )
        delay = r % (u2_t)OSTICKS_PER_SEC;
    if( secSpan > 0 )
        delay += ((u1_t)r % secSpan) * OSTICKS_PER_SEC;
    return delay;
}


static void txDelay (ostime_t reftime, u1_t secSpan) {
    reftime += LMICcore_rndDelay(secSpan);
    if( LMIC.globalDutyRate == 0  ||  (reftime - LMIC.globalDutyAvail) > 0 ) {
        LMIC.globalDutyAvail = reftime;
        LMIC.opmode |= OP_RNDTX;
    }
}


void LMICcore_setDrJoin (u1_t reason, u1_t dr) {
    LMIC_EV_PARAMETER(reason);

    EV(drChange, INFO, (e_.reason    = reason,
                        e_.deveui    = MAIN::CDEV->getEui(),
                        e_.dr        = dr|DR_PAGE,
                        e_.txpow     = LMIC.adrTxPow,
                        e_.prevdr    = LMIC.datarate|DR_PAGE,
                        e_.prevtxpow = LMIC.adrTxPow));
    LMIC.datarate = dr;
    DO_DEVDB(LMIC.datarate,datarate);
}


static bit_t setDrTxpow (u1_t reason, u1_t dr, s1_t pow) {
    bit_t result = 0;

    LMIC_EV_PARAMETER(reason);

    EV(drChange, INFO, (e_.reason    = reason,
                        e_.deveui    = MAIN::CDEV->getEui(),
                        e_.dr        = dr|DR_PAGE,
                        e_.txpow     = pow,
                        e_.prevdr    = LMIC.datarate|DR_PAGE,
                        e_.prevtxpow = LMIC.adrTxPow));

    if( pow != KEEP_TXPOW && pow != LMIC.adrTxPow ) {
        LMIC.adrTxPow = pow;
        result = 1;
    }
    if( LMIC.datarate != dr ) {
        LMIC.datarate = dr;
        DO_DEVDB(LMIC.datarate,datarate);
        LMIC.opmode |= OP_NEXTCHNL;
        result = 1;
    }
    return result;
}


#if !defined(DISABLE_PING)
void LMIC_stopPingable (void) {
    LMIC.opmode &= ~(OP_PINGABLE|OP_PINGINI);
}


void LMIC_setPingable (u1_t intvExp) {
    // Change setting
    LMIC.ping.intvExp = (intvExp & 0x7);
    LMIC.opmode |= OP_PINGABLE;
    // App may call LMIC_enableTracking() explicitely before
    // Otherwise tracking is implicitly enabled here
    if( (LMIC.opmode & (OP_TRACK|OP_SCAN)) == 0  &&  LMIC.bcninfoTries == 0 )
        LMIC_enableTracking(0);
}

#endif // !DISABLE_PING

static void runEngineUpdate (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    engineUpdate();
}

static void reportEventAndUpdate(ev_t ev) {
    reportEventNoUpdate(ev);
    engineUpdate();
}

static void reportEventNoUpdate (ev_t ev) {
    uint32_t const evSet = UINT32_C(1) << ev;
    EV(devCond, INFO, (e_.reason = EV::devCond_t::LMIC_EV,
                       e_.eui    = MAIN::CDEV->getEui(),
                       e_.info   = ev));
#if LMIC_ENABLE_onEvent
    void (*pOnEvent)(ev_t) = onEvent;

    // rxstart is critical timing; legacy onEvent handlers
    // don't comprehend this; so don't report.
    if (pOnEvent != NULL && (evSet & (UINT32_C(1)<<EV_RXSTART)) == 0)
        pOnEvent(ev);
#endif // LMIC_ENABLE_onEvent

    // we want people who need tiny RAM footprints to be able
    // to use onEvent and overide the dynamic mechanism.
#if LMIC_ENABLE_user_events
    // create a mask to test against sets of events.

    // if a message was received, notify the user.
    if ((evSet & ((UINT32_C(1)<<EV_TXCOMPLETE) | (UINT32_C(1)<<EV_RXCOMPLETE))) != 0 &&
        LMIC.client.rxMessageCb != NULL &&
        (LMIC.dataLen  != 0 || LMIC.dataBeg != 0)) {
        uint8_t port;

        // assume no port.
        port = 0;

        // correct assumption if a port was provided.
        if (LMIC.txrxFlags & TXRX_PORT)
            port = LMIC.frame[LMIC.dataBeg - 1];

        // notify the user.
        LMIC.client.rxMessageCb(
                LMIC.client.rxMessageUserData,
                port,
                LMIC.frame + LMIC.dataBeg,
                LMIC.dataLen
                );
    }

    // tell the client about completed transmits -- the buffer
    // is now available again.  We use set notation again in case
    // we later discover another event completes messages
    if ((evSet & ((UINT32_C(1)<<EV_TXCOMPLETE) | (UINT32_C(1) <<EV_TXCANCELED))) != 0) {
        lmic_txmessage_cb_t * const pTxMessageCb = LMIC.client.txMessageCb;

        if (pTxMessageCb != NULL) {
            int fSuccess;
            // reset before notifying user. If we reset after
            // notifying, then if user does a recursive call
            // in their message processing
            // function, we would clobber the value
            LMIC.client.txMessageCb = NULL;

            // compute exit status
            if (ev == EV_TXCANCELED) {
                // canceled: unsuccessful.
                fSuccess = 0;
            } else if (/* ev == EV_TXCOMPLETE  && */ LMIC.pendTxConf) {
                fSuccess = (LMIC.txrxFlags & TXRX_ACK) != 0;
            } else {
                // unconfirmed uplinks are successful if they were sent.
                fSuccess = 1;
            }

            // notify the user.
            pTxMessageCb(LMIC.client.txMessageUserData, fSuccess);
        }
    }

    // tell the client about events in general
    if (LMIC.client.eventCb != NULL)
        LMIC.client.eventCb(LMIC.client.eventUserData, ev);
#endif // LMIC_ENABLE_user_events
}

int LMIC_registerRxMessageCb(lmic_rxmessage_cb_t *pRxMessageCb, void *pUserData) {
#if LMIC_ENABLE_user_events
    LMIC.client.rxMessageCb = pRxMessageCb;
    LMIC.client.rxMessageUserData = pUserData;
    return 1;
#else // !LMIC_ENABLE_user_events
    return 0;
#endif // !LMIC_ENABLE_user_events
}

int LMIC_registerEventCb(lmic_event_cb_t *pEventCb, void *pUserData) {
#if LMIC_ENABLE_user_events
    LMIC.client.eventCb = pEventCb;
    LMIC.client.eventUserData = pUserData;
    return 1;
#else // ! LMIC_ENABLE_user_events
    return 0;
#endif // ! LMIC_ENABLE_user_events
}

static void runReset (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    // clear pending TX.
    LMIC_clrTxData();

    // Disable session
    LMIC_reset();

    // report event before the join event.
    reportEventNoUpdate(EV_RESET);

#if !defined(DISABLE_JOIN)
    LMIC_startJoining();
#else
    os_setCallback(&LMIC.osjob, FUNC_ADDR(runEngineUpdate));
#endif // !DISABLE_JOIN
}

static void resetJoinParams(void) {
    LMIC.rx1DrOffset = 0;
    LMIC.dn2Dr       = DR_DNW2;
    LMIC.dn2Freq     = FREQ_DNW2;
}

static void stateJustJoined (void) {
    LMIC.seqnoDn     = LMIC.seqnoUp = 0;
    LMIC.rejoinCnt   = 0;
    LMIC.dnConf      = LMIC.adrChanged = LMIC.ladrAns = LMIC.devsAns = 0;
#if !defined(DISABLE_MCMD_SNCH_REQ)
    LMIC.snchAns     = 0;
#endif
#if !defined(DISABLE_MCMD_DN2P_SET)
    LMIC.dn2Ans      = 0;
#endif
    LMIC.moreData    = 0;
#if !defined(DISABLE_MCMD_DCAP_REQ)
    LMIC.dutyCapAns  = 0;
#endif
#if !defined(DISABLE_MCMD_PING_SET) && !defined(DISABLE_PING)
    LMIC.pingSetAns  = 0;
#endif
    LMIC.upRepeat    = 0;
    resetJoinParams();
#if !defined(DISABLE_BEACONS)
    LMIC.bcnChnl     = CHNL_BCN;
#endif
#if !defined(DISABLE_PING)
    LMIC.ping.freq   = FREQ_PING;
    LMIC.ping.dr     = DR_PING;
#endif
}


// ================================================================================
// Decoding frames


#if !defined(DISABLE_BEACONS)
// Decode beacon  - do not overwrite bcninfo unless we have a match!
static int decodeBeacon (void) {
    ASSERT(LMIC.dataLen == LEN_BCN); // implicit header RX guarantees this
    xref2u1_t d = LMIC.frame;
    if(! LMICbandplan_isValidBeacon1(d))
        return 0;   // first (common) part fails CRC check
    // First set of fields is ok
    u4_t bcnnetid = os_rlsbf4(&d[OFF_BCN_NETID]) & 0xFFFFFF;
    if( bcnnetid != LMIC.netid )
        return -1;  // not the beacon we're looking for

    LMIC.bcninfo.flags &= ~(BCN_PARTIAL|BCN_FULL);
    // Match - update bcninfo structure
    LMIC.bcninfo.snr    = LMIC.snr;
    LMIC.bcninfo.rssi   = LMIC.rssi;
    LMIC.bcninfo.txtime = LMIC.rxtime - AIRTIME_BCN_osticks;
    LMIC.bcninfo.time   = os_rlsbf4(&d[OFF_BCN_TIME]);
    LMIC.bcninfo.flags |= BCN_PARTIAL;

    // Check 2nd set
    if( os_rlsbf2(&d[OFF_BCN_CRC2]) != os_crc16(d,OFF_BCN_CRC2) )
        return 1;
    // Second set of fields is ok
    LMIC.bcninfo.lat    = (s4_t)os_rlsbf4(&d[OFF_BCN_LAT-1]) >> 8; // read as signed 24-bit
    LMIC.bcninfo.lon    = (s4_t)os_rlsbf4(&d[OFF_BCN_LON-1]) >> 8; // ditto
    LMIC.bcninfo.info   = d[OFF_BCN_INFO];
    LMIC.bcninfo.flags |= BCN_FULL;
    return 2;
}
#endif // !DISABLE_BEACONS

static CONST_TABLE(u1_t, macCmdSize)[] = {
    /* 2: LinkCheckAns */ 3,
    /* 3: LinkADRReq */ 5,
    /* 4: DutyCycleReq */ 2,
    /* 5: RXParamSetupReq */ 5,
    /* 6: DevStatusReq */ 1,
    /* 7: NewChannel Req */ 6,
    /* 8: RXTiminSetupReq */ 2,
    /* 9: TxParamSetupReq */ 2,
    /* 0x0A: DlChannelReq */ 5,
    /* B, C: RFU */ 0, 0,
    /* 0x0D: DeviceTimeAns */ 6,
    /* 0x0E, 0x0F */ 0, 0,
    /* 0x10: PingSlotInfoAns */ 1,
    /* 0x11: PingSlotChannelReq */ 4,
    /* 0x12: BeaconTimingAns */ 4,
    /* 0x13: BeaconFreqReq */ 4
};

static u1_t getMacCmdSize(u1_t macCmd) {
    if (macCmd < 2)
        return 0;
    if (macCmd >= LENOF_TABLE(macCmdSize) - 2)
        return 0;
    return TABLE_GET_U1(macCmdSize, macCmd - 2);
}

static void
applyAdrRequests(
    const uint8_t *opts,
    int olen
) {
    if( (LMIC.ladrAns & 0x7F) == (MCMD_LADR_ANS_POWACK | MCMD_LADR_ANS_CHACK | MCMD_LADR_ANS_DRACK) ) {
        lmic_saved_adr_state_t initialState;
        int oidx;
        u1_t p1 = 0;
        u1_t p4 = 0;

        LMICbandplan_saveAdrState(&initialState);

        for (oidx = 0; oidx < olen; ) {
            u1_t const cmd = opts[oidx];

            if (cmd == MCMD_LADR_REQ) {
                u2_t chmap  = os_rlsbf2(&opts[oidx+2]);// list of enabled channels

                p1     = opts[oidx+1];            // txpow + DR, in case last
                p4     = opts[oidx+4];
                u1_t chpage = p4 & MCMD_LADR_CHPAGE_MASK;     // channel page

                LMICbandplan_mapChannels(chpage, chmap);
            }

            int cmdlen = getMacCmdSize(cmd);

            // this really is an assert, we should never here
            // unless all the commands are valid.
            ASSERT(cmdlen != 0);

            oidx += cmdlen;
        }

        // all done scanning options
        bit_t changes = LMICbandplan_compareAdrState(&initialState);

        // handle uplink repeat count
        u1_t uprpt  = p4 & MCMD_LADR_REPEAT_MASK;     // up repeat count
        if (LMIC.upRepeat != uprpt) {
            LMIC.upRepeat = uprpt;
            changes = 1;
        }

        // handle power changes
        dr_t dr = (dr_t)(p1>>MCMD_LADR_DR_SHIFT);
        changes |= setDrTxpow(DRCHG_NWKCMD, dr, pow2dBm(p1));

        LMIC.adrChanged = changes;  // move the ADR FSM up to "time to request"
    }
}

// scan mac commands starting at opts[] for olen, return count of bytes consumed.
static int
scan_mac_cmds(
    const uint8_t *opts,
    int olen
    ) {
    int oidx = 0;
    // this parser is *really* fragile, especially for LinkADR requests.
    // it won't crash, but acks will be wrong if all ADR requests are
    // not contiguous.
    bit_t fSawAdrReq = 0;
    uint8_t cmd;

    while( oidx < olen ) {
        cmd = opts[oidx];
        switch( cmd ) {
        case MCMD_LCHK_ANS: {
            //int gwmargin = opts[oidx+1];
            //int ngws = opts[oidx+2];
            break;
        }
        case MCMD_LADR_REQ: {
            u1_t p1     = opts[oidx+1];            // txpow + DR
            u2_t chmap  = os_rlsbf2(&opts[oidx+2]);// list of enabled channels
            u1_t chpage = opts[oidx+4] & MCMD_LADR_CHPAGE_MASK;     // channel page
            u1_t uprpt  = opts[oidx+4] & MCMD_LADR_REPEAT_MASK;     // up repeat count

            // TODO(tmm@mcci.com): LoRaWAN 1.1 & 1.0.3 requires us to send one ack
            // for each LinkADRReq in a given MAC message. This code only sends
            // ack for all the LinkADRReqs. Fixing this is a lot of work, and TTN
            // behaves correctly with the current LMIC, so we'll leave this for
            // the fix of issue #87.
            if (! fSawAdrReq) {
                fSawAdrReq = 1;
                LMIC.ladrAns = 0x80 |     // Include an answer into next frame up
                    MCMD_LADR_ANS_POWACK | MCMD_LADR_ANS_CHACK | MCMD_LADR_ANS_DRACK;
            }
            if( !LMICbandplan_canMapChannels(chpage, chmap) )
                LMIC.ladrAns &= ~MCMD_LADR_ANS_CHACK;
            dr_t dr = (dr_t)(p1>>MCMD_LADR_DR_SHIFT);
            if( !validDR(dr) ) {
                LMIC.ladrAns &= ~MCMD_LADR_ANS_DRACK;
                EV(specCond, ERR, (e_.reason = EV::specCond_t::BAD_MAC_CMD,
                                   e_.eui    = MAIN::CDEV->getEui(),
                                   e_.info   = Base::lsbf4(&d[pend]),
                                   e_.info2  = Base::msbf4(&opts[oidx-4])));
            }
            break;
        }
        case MCMD_DEVS_REQ: {
            LMIC.devsAns = 1;
            // LMIC.snr is SNR time 4, convert to real SNR; rounding towards zero.
            const int snr = (LMIC.snr + 2) / 4;
            // per [1.02] 5.5. the margin is the SNR.
            LMIC.devAnsMargin = (u1_t)(0b00111111 & (snr <= -32 ? -32 : snr >= 31 ? 31 : snr));
            break;
        }
        case MCMD_DN2P_SET: {
#if !defined(DISABLE_MCMD_DN2P_SET)
            dr_t dr = (dr_t)(opts[oidx+1] & 0x0F);
            u1_t rx1DrOffset = (u1_t)((opts[oidx+1] & 0x70) >> 4);
            u4_t freq = LMICbandplan_convFreq(&opts[oidx+2]);
            LMIC.dn2Ans = 0x80;   // answer pending
            if( validDR(dr) )
                LMIC.dn2Ans |= MCMD_DN2P_ANS_DRACK;
            if( freq != 0 )
                LMIC.dn2Ans |= MCMD_DN2P_ANS_CHACK;
            if (rx1DrOffset <= 3)
                LMIC.dn2Ans |= MCMD_DN2P_ANS_RX1DrOffsetAck;

            if( LMIC.dn2Ans == (0x80|MCMD_DN2P_ANS_DRACK|MCMD_DN2P_ANS_CHACK| MCMD_DN2P_ANS_RX1DrOffsetAck) ) {
                LMIC.dn2Dr = dr;
                LMIC.dn2Freq = freq;
                LMIC.rx1DrOffset = rx1DrOffset;
                DO_DEVDB(LMIC.dn2Dr,dn2Dr);
                DO_DEVDB(LMIC.dn2Freq,dn2Freq);
            }
#endif // !DISABLE_MCMD_DN2P_SET
            break;
        }
        case MCMD_DCAP_REQ: {
#if !defined(DISABLE_MCMD_DCAP_REQ)
            u1_t cap = opts[oidx+1];
            // A value cap=0xFF means device is OFF unless enabled again manually.
            if( cap==0xFF )
                LMIC.opmode |= OP_SHUTDOWN;  // stop any sending
            LMIC.globalDutyRate  = cap & 0xF;
            LMIC.globalDutyAvail = os_getTime();
            DO_DEVDB(cap,dutyCap);
            LMIC.dutyCapAns = 1;
#endif // !DISABLE_MCMD_DCAP_REQ
            break;
        }
        case MCMD_SNCH_REQ: {
#if !defined(DISABLE_MCMD_SNCH_REQ)
            u1_t chidx = opts[oidx+1];  // channel
            u4_t freq  = LMICbandplan_convFreq(&opts[oidx+2]); // freq
            u1_t drs   = opts[oidx+5];  // datarate span
            LMIC.snchAns = 0x80;
            if( freq != 0 && LMIC_setupChannel(chidx, freq, DR_RANGE_MAP(drs&0xF,drs>>4), -1) )
                LMIC.snchAns |= MCMD_SNCH_ANS_DRACK|MCMD_SNCH_ANS_FQACK;
#endif // !DISABLE_MCMD_SNCH_REQ
            break;
        }
        case MCMD_PING_SET: {
#if !defined(DISABLE_MCMD_PING_SET) && !defined(DISABLE_PING)
            u4_t freq = LMICbandplan_convFreq(&opts[oidx+1]);
            u1_t flags = 0x80;
            if( freq != 0 ) {
                flags |= MCMD_PING_ANS_FQACK;
                LMIC.ping.freq = freq;
                DO_DEVDB(LMIC.ping.intvExp, pingIntvExp);
                DO_DEVDB(LMIC.ping.freq, pingFreq);
                DO_DEVDB(LMIC.ping.dr, pingDr);
            }
            LMIC.pingSetAns = flags;
#endif // !DISABLE_MCMD_PING_SET && !DISABLE_PING
            break;
        }
        case MCMD_BCNI_ANS: {
#if !defined(DISABLE_MCMD_BCNI_ANS) && !defined(DISABLE_BEACONS)
            // Ignore if tracking already enabled
            if( (LMIC.opmode & OP_TRACK) == 0 ) {
                LMIC.bcnChnl = opts[oidx+3];
                // Enable tracking - bcninfoTries
                LMIC.opmode |= OP_TRACK;
                // Cleared later in txComplete handling - triggers EV_BEACON_FOUND
                ASSERT(LMIC.bcninfoTries!=0);
                // Setup RX parameters
                LMIC.bcninfo.txtime = (LMIC.rxtime
                                       + ms2osticks(os_rlsbf2(&opts[oidx+1]) * MCMD_BCNI_TUNIT)
                                       + ms2osticksCeil(MCMD_BCNI_TUNIT/2)
                                       - BCN_INTV_osticks);
                LMIC.bcninfo.flags = 0;  // txtime above cannot be used as reference (BCN_PARTIAL|BCN_FULL cleared)
                calcBcnRxWindowFromMillis(MCMD_BCNI_TUNIT,1);  // error of +/-N ms

                EV(lostFrame, INFO, (e_.reason  = EV::lostFrame_t::MCMD_BCNI_ANS,
                                     e_.eui     = MAIN::CDEV->getEui(),
                                     e_.lostmic = Base::lsbf4(&d[pend]),
                                     e_.info    = (LMIC.missedBcns |
                                                   (osticks2us(LMIC.bcninfo.txtime + BCN_INTV_osticks
                                                               - LMIC.bcnRxtime) << 8)),
                                     e_.time    = MAIN::CDEV->ostime2ustime(LMIC.bcninfo.txtime + BCN_INTV_osticks)));
            }
#endif // !DISABLE_MCMD_BCNI_ANS && !DISABLE_BEACONS
            break;
        } /* end case */
        case MCMD_TxParamSetupReq: {
#if LMIC_ENABLE_TxParamSetupReq
            uint8_t txParam;
            txParam = opts[oidx+1];

            // we don't allow unrecognized bits to come through
            txParam &= (MCMD_TxParam_RxDWELL_MASK|
                        MCMD_TxParam_TxDWELL_MASK|
                        MCMD_TxParam_MaxEIRP_MASK);
            LMIC.txParam = txParam;
            LMIC.txParamSetupAns = 1;
#endif // LMIC_ENABLE_TxParamSetupReq
            break;
        } /* end case */
        case MCMD_DeviceTimeAns: {
#if LMIC_ENABLE_DeviceTimeReq
            // don't process a spurious downlink.
            if ( LMIC.txDeviceTimeReqState == lmic_RequestTimeState_rx ) {
                // remember that it's time to notify the client.
                LMIC.txDeviceTimeReqState = lmic_RequestTimeState_success;

                // the network time is linked to the time of the last TX.
                LMIC.localDeviceTime = LMIC.txend;

                // save the network time.
                // The first 4 bytes contain the seconds since the GPS epoch
                // (i.e January the 6th 1980 at 00:00:00 UTC).
                // Note: as per the LoRaWAN specs, the octet order for all
                //       multi-octet fields is little endian
                // Note: the casts are necessary, because opts is an array of
                //       single byte values, and they might overflow when shifted
                LMIC.netDeviceTime = ( (lmic_gpstime_t) opts[oidx + 1]       ) |
                                     (((lmic_gpstime_t) opts[oidx + 2]) <<  8) |
                                     (((lmic_gpstime_t) opts[oidx + 3]) << 16) |
                                     (((lmic_gpstime_t) opts[oidx + 4]) << 24);

                // The 5th byte contains the fractional seconds in 2^-8 second steps
                LMIC.netDeviceTimeFrac = opts[oidx + 5];
#if LMIC_DEBUG_LEVEL > 0
                LMIC_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": MAC command DeviceTimeAns received: seconds_since_gps_epoch=%"PRIu32", fractional_seconds=%d\n", os_getTime(), LMIC.netDeviceTime, LMIC.netDeviceTimeFrac);
#endif
            }
#endif // LMIC_ENABLE_DeviceTimeReq
            break;
        } /* end case */

        default: {
            // force olen to current oidx so we'll exit the while()
            olen = oidx;
            break;
        } /* end case */
        } /* end switch */

    /* compute length, and exit for illegal commands */
    int const cmdlen = getMacCmdSize(cmd);
    if (cmdlen == 0) {
        // "the first unknown command terminates processing"
        // force olen to current oidx so we'll exit the while().
        olen = oidx;
    }

    oidx += cmdlen;
    } /* end while */

    // go back and apply the ADR changes, if any -- use the effective length
    if (fSawAdrReq)
        applyAdrRequests(opts, olen);

    return oidx;
}

// change the ADR ack request count, unless adr ack is diabled.
static void setAdrAckCount (s1_t count) {
    if (LMIC.adrAckReq != LINK_CHECK_OFF) {
        LMIC.adrAckReq = count;
    }
}

static bit_t decodeFrame (void) {
    xref2u1_t d = LMIC.frame;
    u1_t hdr    = d[0];
    u1_t ftype  = hdr & HDR_FTYPE;
    int  dlen   = LMIC.dataLen;
#if LMIC_DEBUG_LEVEL > 0
    const char *window = (LMIC.txrxFlags & TXRX_DNW1) ? "RX1" : ((LMIC.txrxFlags & TXRX_DNW2) ? "RX2" : "Other");
#endif

    if( dlen < OFF_DAT_OPTS+4 ||
        (hdr & HDR_MAJOR) != HDR_MAJOR_V1 ||
        (ftype != HDR_FTYPE_DADN  &&  ftype != HDR_FTYPE_DCDN) ) {
        // Basic sanity checks failed
        EV(specCond, WARN, (e_.reason = EV::specCond_t::UNEXPECTED_FRAME,
                            e_.eui    = MAIN::CDEV->getEui(),
                            e_.info   = dlen < 4 ? 0 : os_rlsbf4(&d[dlen-4]),
                            e_.info2  = hdr + (dlen<<8)));
      norx:
#if LMIC_DEBUG_LEVEL > 0
        LMIC_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": Invalid downlink, window=%s\n", os_getTime(), window);
#endif
        LMIC.dataLen = 0;
        return 0;
    }
    // Validate exact frame length
    // Note: device address was already read+evaluated in order to arrive here.
    int  fct   = d[OFF_DAT_FCT];
    u4_t addr  = os_rlsbf4(&d[OFF_DAT_ADDR]);
    u4_t seqno = os_rlsbf2(&d[OFF_DAT_SEQNO]);
    int  olen  = fct & FCT_OPTLEN;
    int  ackup = (fct & FCT_ACK) != 0 ? 1 : 0;   // ACK last up frame
    int  poff  = OFF_DAT_OPTS+olen;
    int  pend  = dlen-4;  // MIC

    if( addr != LMIC.devaddr ) {
        EV(specCond, WARN, (e_.reason = EV::specCond_t::ALIEN_ADDRESS,
                            e_.eui    = MAIN::CDEV->getEui(),
                            e_.info   = addr,
                            e_.info2  = LMIC.devaddr));
        goto norx;
    }
    if( poff > pend ) {
        EV(specCond, ERR, (e_.reason = EV::specCond_t::CORRUPTED_FRAME,
                           e_.eui    = MAIN::CDEV->getEui(),
                           e_.info   = 0x1000000 + (poff-pend) + (fct<<8) + (dlen<<16)));
        goto norx;
    }

    int port = -1;
    int replayConf = 0;

    if( pend > poff )
        port = d[poff++];

    seqno = LMIC.seqnoDn + (u2_t)(seqno - LMIC.seqnoDn);

    if( !aes_verifyMic(LMIC.nwkKey, LMIC.devaddr, seqno, /*dn*/1, d, pend) ) {
        EV(spe3Cond, ERR, (e_.reason = EV::spe3Cond_t::CORRUPTED_MIC,
                           e_.eui1   = MAIN::CDEV->getEui(),
                           e_.info1  = Base::lsbf4(&d[pend]),
                           e_.info2  = seqno,
                           e_.info3  = LMIC.devaddr));
        goto norx;
    }
    if( seqno < LMIC.seqnoDn ) {
        if( (s4_t)seqno > (s4_t)LMIC.seqnoDn ) {
            EV(specCond, INFO, (e_.reason = EV::specCond_t::DNSEQNO_ROLL_OVER,
                                e_.eui    = MAIN::CDEV->getEui(),
                                e_.info   = LMIC.seqnoDn,
                                e_.info2  = seqno));
            goto norx;
        }
        if( seqno != LMIC.seqnoDn-1 || !LMIC.dnConf || ftype != HDR_FTYPE_DCDN ) {
            EV(specCond, INFO, (e_.reason = EV::specCond_t::DNSEQNO_OBSOLETE,
                                e_.eui    = MAIN::CDEV->getEui(),
                                e_.info   = LMIC.seqnoDn,
                                e_.info2  = seqno));
            goto norx;
        }
        // Replay of previous sequence number allowed only if
        // previous frame and repeated both requested confirmation
        replayConf = 1;
    }
    else {
        if( seqno > LMIC.seqnoDn ) {
            EV(specCond, INFO, (e_.reason = EV::specCond_t::DNSEQNO_SKIP,
                                e_.eui    = MAIN::CDEV->getEui(),
                                e_.info   = LMIC.seqnoDn,
                                e_.info2  = seqno));
        }
        LMIC.seqnoDn = seqno+1;  // next number to be expected
        DO_DEVDB(LMIC.seqnoDn,seqnoDn);
        // DN frame requested confirmation - provide ACK once with next UP frame
        LMIC.dnConf = (ftype == HDR_FTYPE_DCDN ? FCT_ACK : 0);
    }

    if( LMIC.dnConf || (fct & FCT_MORE) )
        LMIC.opmode |= OP_POLL;

    // We heard from network
    LMIC.adrChanged = LMIC.rejoinCnt = 0;
    setAdrAckCount(LINK_CHECK_INIT);

    int m = LMIC.rssi - RSSI_OFF - getSensitivity(LMIC.rps);
    // for legacy reasons, LMIC.margin is set to the unsigned sensitivity. It can never be negative.
    // it's only computed for legacy clients
    LMIC.margin = m < 0 ? 0 : m > 254 ? 254 : m;

#if LMIC_DEBUG_LEVEL > 0
    // Process OPTS
    LMIC_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": process options (olen=%#x)\n", os_getTime(), olen);
#endif

    xref2u1_t opts = &d[OFF_DAT_OPTS];
    int oidx = scan_mac_cmds(opts, olen);
    if( oidx != olen ) {
        EV(specCond, ERR, (e_.reason = EV::specCond_t::CORRUPTED_FRAME,
                           e_.eui    = MAIN::CDEV->getEui(),
                           e_.info   = 0x1000000 + (oidx) + (olen<<8)));
        oidx = olen;
    }

    if( !replayConf ) {
        // Handle payload only if not a replay
        // Decrypt payload - if any
        if( port >= 0  &&  pend-poff > 0 ) {
            aes_cipher(port <= 0 ? LMIC.nwkKey : LMIC.artKey, LMIC.devaddr, seqno, /*dn*/1, d+poff, pend-poff);
            if (port == 0) {
                // this is a mac command. scan the options.
#if LMIC_DEBUG_LEVEL > 0
                LMIC_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": process mac commands for port 0 (olen=%#x)\n", os_getTime(), pend-poff);
#endif
                int optendindex = scan_mac_cmds(d+poff, pend-poff);
                if (optendindex != pend-poff) {
#if LMIC_DEBUG_LEVEL > 0
                    LMIC_DEBUG_PRINTF(
                        "%"LMIC_PRId_ostime_t": error processing mac commands for port 0 "
                        "(len=%#x, optendindex=%#x)\n",
                        os_getTime(), pend-poff, optendindex
                        );
#endif
                }
            }
        } // end decrypt payload
        EV(dfinfo, DEBUG, (e_.deveui  = MAIN::CDEV->getEui(),
                           e_.devaddr = LMIC.devaddr,
                           e_.seqno   = seqno,
                           e_.flags   = (port < 0 ? EV::dfinfo_t::NOPORT : 0) | EV::dfinfo_t::DN,
                           e_.mic     = Base::lsbf4(&d[pend]),
                           e_.hdr     = d[LORA::OFF_DAT_HDR],
                           e_.fct     = d[LORA::OFF_DAT_FCT],
                           e_.port    = port,
                           e_.plen    = dlen,
                           e_.opts.length = olen,
                           memcpy(&e_.opts[0], opts, olen)));
    } else {
        EV(specCond, INFO, (e_.reason = EV::specCond_t::DNSEQNO_REPLAY,
                            e_.eui    = MAIN::CDEV->getEui(),
                            e_.info   = Base::lsbf4(&d[pend]),
                            e_.info2  = seqno));
    }

    if( // NWK acks but we don't have a frame pending
        (ackup && LMIC.txCnt == 0) ||
        // We sent up confirmed and we got a response in DNW1/DNW2
        // BUT it did not carry an ACK - this should never happen
        // Do not resend and assume frame was not ACKed.
        (!ackup && LMIC.txCnt != 0) ) {
        EV(specCond, ERR, (e_.reason = EV::specCond_t::SPURIOUS_ACK,
                           e_.eui    = MAIN::CDEV->getEui(),
                           e_.info   = seqno,
                           e_.info2  = ackup));
#if LMIC_DEBUG_LEVEL > 1
	LMIC_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": ??ack error ack=%d txCnt=%d\n", os_getTime(), ackup, LMIC.txCnt);
#endif
    }

    if( LMIC.txCnt != 0 ) // we requested an ACK
        orTxrxFlags(__func__, ackup ? TXRX_ACK : TXRX_NACK);

    if( port <= 0 ) {
        orTxrxFlags(__func__, TXRX_NOPORT);
        LMIC.dataBeg = poff;
        LMIC.dataLen = 0;
    } else {
        orTxrxFlags(__func__, TXRX_PORT);
        LMIC.dataBeg = poff;
        LMIC.dataLen = pend-poff;
    }
#if LMIC_DEBUG_LEVEL > 0
    LMIC_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": Received downlink, window=%s, port=%d, ack=%d, txrxFlags=%#x\n", os_getTime(), window, port, ackup, LMIC.txrxFlags);
#endif
    return 1;
}


// ================================================================================
// TX/RX transaction support

// start reception and log.
static void radioRx (void) {
    reportEventNoUpdate(EV_RXSTART);
    os_radio(RADIO_RX);
}

// start RX in window 2.
static void setupRx2 (void) {
    initTxrxFlags(__func__, TXRX_DNW2);
    LMIC.rps = dndr2rps(LMIC.dn2Dr);
    LMIC.freq = LMIC.dn2Freq;
    LMIC.dataLen = 0;
    radioRx();
}


static void schedRx12 (ostime_t delay, osjobcb_t func, u1_t dr) {
    ostime_t hsym = dr2hsym(dr);

    LMIC.rxsyms = MINRX_SYMS;

    // If a clock error is specified, compensate for it by extending the
    // receive window
    if (LMIC.client.clockError != 0) {
        // Calculate how much the clock will drift maximally after delay has
        // passed. This indicates the amount of time we can be early
        // _or_ late.
        ostime_t drift = (int64_t)delay * LMIC.client.clockError / MAX_CLOCK_ERROR;

        // Increase the receive window by twice the maximum drift (to
        // compensate for a slow or a fast clock).
        // decrease the rxtime to compensate for. Note that hsym is a
        // *half* symbol time, so the factor 2 is hidden. First check if
        // this would overflow (which can happen if the drift is very
        // high, or the symbol time is low at high datarates).
        if ((255 - LMIC.rxsyms) * hsym < drift)
            LMIC.rxsyms = 255;
        else
            LMIC.rxsyms += drift / hsym;

    }

    // Center the receive window on the center of the expected preamble
    // (again note that hsym is half a sumbol time, so no /2 needed)
    LMIC.rxtime = LMIC.txend + delay + PAMBL_SYMS * hsym - LMIC.rxsyms * hsym;

    LMIC_X_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": sched Rx12 %"LMIC_PRId_ostime_t"\n", os_getTime(), LMIC.rxtime - RX_RAMPUP);
    os_setTimedCallback(&LMIC.osjob, LMIC.rxtime - RX_RAMPUP, func);
}

static void setupRx1 (osjobcb_t func) {
    initTxrxFlags(__func__, TXRX_DNW1);
    // Turn LMIC.rps from TX over to RX
    LMIC.rps = setNocrc(LMIC.rps,1);
    LMIC.dataLen = 0;
    LMIC.osjob.func = func;
    radioRx();
}


// Called by HAL once TX complete and delivers exact end of TX time stamp in LMIC.rxtime
static void txDone (ostime_t delay, osjobcb_t func) {
#if !defined(DISABLE_PING)
    if( (LMIC.opmode & (OP_TRACK|OP_PINGABLE|OP_PINGINI)) == (OP_TRACK|OP_PINGABLE) ) {
        rxschedInit(&LMIC.ping);    // note: reuses LMIC.frame buffer!
        LMIC.opmode |= OP_PINGINI;
    }
#endif // !DISABLE_PING

    // Change RX frequency / rps (US only) before we increment txChnl
    LMICbandplan_setRx1Params();
    // LMIC.rxsyms carries the TX datarate (can be != LMIC.datarate [confirm retries etc.])
    // Setup receive - LMIC.rxtime is preloaded with 1.5 symbols offset to tune
    // into the middle of the 8 symbols preamble.
    if( LMICbandplan_isFSK() ) {
        LMICbandplan_txDoneFSK(delay, func);
    }
    else
    {
        schedRx12(delay, func, LMIC.dndr);
    }
}

// ======================================== Join frames


#if !defined(DISABLE_JOIN)
static void onJoinFailed (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    // Notify app - must call LMIC_reset() to stop joining
    // otherwise join procedure continues.
    reportEventAndUpdate(EV_JOIN_FAILED);
}

// process join-accept message or deal with no join-accept in slot 2.
static bit_t processJoinAccept (void) {
    if ((LMIC.txrxFlags & TXRX_DNW1) != 0 && LMIC.dataLen == 0)
        return 0;

    ASSERT((LMIC.opmode & OP_TXRXPEND)!=0);

    if( LMIC.dataLen == 0 ) {
        // we didn't get any data and we're in slot 2. So... there's no join frame.
        return processJoinAccept_nojoinframe();
    }
    u1_t hdr  = LMIC.frame[0];
    u1_t dlen = LMIC.dataLen;
    u4_t mic  = os_rlsbf4(&LMIC.frame[dlen-4]); // safe before modified by encrypt!
    LMIC_EV_VARIABLE(mic);                      // only used by EV().

    if( (dlen != LEN_JA && dlen != LEN_JAEXT)
        || (hdr & (HDR_FTYPE|HDR_MAJOR)) != (HDR_FTYPE_JACC|HDR_MAJOR_V1) ) {
        EV(specCond, ERR, (e_.reason = EV::specCond_t::UNEXPECTED_FRAME,
                           e_.eui    = MAIN::CDEV->getEui(),
                           e_.info   = dlen < 4 ? 0 : mic,
                           e_.info2  = hdr + (dlen<<8)));
        return processJoinAccept_badframe();
    }
    aes_encrypt(LMIC.frame+1, dlen-1);
    if( !aes_verifyMic0(LMIC.frame, dlen-4) ) {
        EV(specCond, ERR, (e_.reason = EV::specCond_t::JOIN_BAD_MIC,
                           e_.info   = mic));
        return processJoinAccept_badframe();
    }

    u4_t addr = os_rlsbf4(LMIC.frame+OFF_JA_DEVADDR);
    LMIC.devaddr = addr;
    LMIC.netid = os_rlsbf4(&LMIC.frame[OFF_JA_NETID]) & 0xFFFFFF;

    // initDefaultChannels(0) for EU-like, nothing otherwise
    LMICbandplan_joinAcceptChannelClear();

    if (!LMICbandplan_hasJoinCFlist() && dlen > LEN_JA) {
            // if no JoinCFList, we're supposed to continue
            // the join per 2.2.5 of LoRaWAN regional 2.2.4
            // https://github.com/mcci-catena/arduino-lmic/issues/19
    } else if ( LMICbandplan_hasJoinCFlist() && dlen > LEN_JA ) {
        dlen = OFF_CFLIST;
        for( u1_t chidx=3; chidx<8; chidx++, dlen+=3 ) {
            u4_t freq = LMICbandplan_convFreq(&LMIC.frame[dlen]);
            if( freq ) {
                LMIC_setupChannel(chidx, freq, 0, -1);
#if LMIC_DEBUG_LEVEL > 1
                LMIC_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": Setup channel, idx=%d, freq=%"PRIu32"\n", os_getTime(), chidx, freq);
#endif
            }
        }
    }

    // already incremented when JOIN REQ got sent off
    aes_sessKeys(LMIC.devNonce-1, &LMIC.frame[OFF_JA_ARTNONCE], LMIC.nwkKey, LMIC.artKey);
    DO_DEVDB(LMIC.netid,   netid);
    DO_DEVDB(LMIC.devaddr, devaddr);
    DO_DEVDB(LMIC.nwkKey,  nwkkey);
    DO_DEVDB(LMIC.artKey,  artkey);

    EV(joininfo, INFO, (e_.arteui  = MAIN::CDEV->getArtEui(),
                        e_.deveui  = MAIN::CDEV->getEui(),
                        e_.devaddr = LMIC.devaddr,
                        e_.oldaddr = oldaddr,
                        e_.nonce   = LMIC.devNonce-1,
                        e_.mic     = mic,
                        e_.reason  = ((LMIC.opmode & OP_REJOIN) != 0
                                      ? EV::joininfo_t::REJOIN_ACCEPT
                                      : EV::joininfo_t::ACCEPT)));

    ASSERT((LMIC.opmode & (OP_JOINING|OP_REJOIN))!=0);
    //
    // XXX(tmm@mcci.com) OP_REJOIN confuses me, and I'm not sure why we're
    // adjusting DRs here. We've just recevied a join accept, and the
    // datarate therefore shouldn't be in play.  In effect, we set the
    // initial data rate based on the number of times we tried to rejoin.
    //
    if( (LMIC.opmode & OP_REJOIN) != 0 ) {
#if CFG_region != LMIC_REGION_as923
	    // TODO(tmm@mcci.com) regionalize
        // Lower DR every try below current UP DR
        LMIC.datarate = lowerDR(LMIC.datarate, LMIC.rejoinCnt);
#else
        // in the join of AS923 v1.1 or older, only DR2 (SF10) is used.
        // TODO(tmm@mcci.com) if the rejoin logic is at all correct, we
        // should be setting the uplink datarate based on the number of
        // tries; this doesn't set the AS923 join data rate.
        LMIC.datarate = AS923_DR_SF10;
#endif
    }
    LMIC.opmode &= ~(OP_JOINING|OP_TRACK|OP_REJOIN|OP_TXRXPEND|OP_PINGINI);
    LMIC.opmode |= OP_NEXTCHNL;
    LMIC.txCnt = 0;
    stateJustJoined();
    // transition to the ADR_ACK initial state.
    setAdrAckCount(LINK_CHECK_INIT);

    LMIC.dn2Dr = LMIC.frame[OFF_JA_DLSET] & 0x0F;
    LMIC.rx1DrOffset = (LMIC.frame[OFF_JA_DLSET] >> 4) & 0x7;
    LMIC.rxDelay = LMIC.frame[OFF_JA_RXDLY];
    if (LMIC.rxDelay == 0) LMIC.rxDelay = 1;
    reportEventAndUpdate(EV_JOINED);
    return 1;
}

static bit_t processJoinAccept_badframe(void) {
        if( (LMIC.txrxFlags & TXRX_DNW1) != 0 )
            // continue the join process: there's another window.
            return 0;
        else
            // stop the join process
            return processJoinAccept_nojoinframe();
}

static bit_t processJoinAccept_nojoinframe(void) {
        // Valid states are JOINING (in which caise REJOIN is ignored)
        // or ~JOINING and REJOIN. If it's a REJOIN,
        // we need to turn off rejoin, signal an event, and increment
        // the rejoin-sent count. Internal callers will turn on rejoin
        // occasionally.
        if( (LMIC.opmode & OP_JOINING) == 0) {
            ASSERT((LMIC.opmode & OP_REJOIN) != 0);
            LMIC.opmode &= ~(OP_REJOIN|OP_TXRXPEND);
            if( LMIC.rejoinCnt < 10 )
                LMIC.rejoinCnt++;
            reportEventAndUpdate(EV_REJOIN_FAILED);
            // stop the join process.
            return 1;
        }
        // otherwise it's a normal join. At end of rx2, so we
        // need to schedule something.
        LMIC.opmode &= ~OP_TXRXPEND;
        reportEventNoUpdate(EV_JOIN_TXCOMPLETE);
        int failed = LMICbandplan_nextJoinState();
        EV(devCond, DEBUG, (e_.reason = EV::devCond_t::NO_JACC,
                            e_.eui    = MAIN::CDEV->getEui(),
                            e_.info   = LMIC.datarate|DR_PAGE,
                            e_.info2  = failed));
        // Build next JOIN REQUEST with next engineUpdate call
        // Optionally, report join failed.
        // Both after a random/chosen amount of ticks. That time
        // is in LMIC.txend. The delay here is either zero or 1
        // tick; onJoinFailed()/runEngineUpdate() are responsible
        // for honoring that. XXX(tmm@mcci.com) The IBM 1.6 code
        // claimed to return a delay but really returns 0 or 1.
        // Once we update as923 to return failed after dr2, we
        // can take out this #if.
#if CFG_region != LMIC_REGION_as923
        os_setTimedCallback(&LMIC.osjob, os_getTime()+failed,
                            failed
                            ? FUNC_ADDR(onJoinFailed)      // one JOIN iteration done and failed
                            : FUNC_ADDR(runEngineUpdate)); // next step to be delayed
#else
       // in the join of AS923 v1.1 older, only DR2 is used. Therefore,
       // not much improvement when it handles two different behavior;
       // onJoinFailed or runEngineUpdate.
        os_setTimedCallback(&LMIC.osjob, os_getTime()+failed,
                            FUNC_ADDR(onJoinFailed));
#endif
        // stop this join process.
        return 1;
}

static void processRx2Jacc (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    if( LMIC.dataLen == 0 ) {
        initTxrxFlags(__func__, 0);  // nothing in 1st/2nd DN slot
    }
    // we're done with this join cycle anyway, so ignore the
    // result of processJoinAccept()
    (void) processJoinAccept();
}


static void setupRx2Jacc (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    LMIC.osjob.func = FUNC_ADDR(processRx2Jacc);
    setupRx2();
}


static void processRx1Jacc (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    if( LMIC.dataLen == 0 || !processJoinAccept() )
        schedRx12(DELAY_JACC2_osticks, FUNC_ADDR(setupRx2Jacc), LMIC.dn2Dr);
}


static void setupRx1Jacc (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    setupRx1(FUNC_ADDR(processRx1Jacc));
}


static void jreqDone (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    txDone(DELAY_JACC1_osticks, FUNC_ADDR(setupRx1Jacc));
}

#endif // !DISABLE_JOIN

// ======================================== Data frames

// Fwd decl.
static bit_t processDnData(void);

static void processRx2DnDataDelay (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    processDnData();
}

static void processRx2DnData (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    if( LMIC.dataLen == 0 ) {
        initTxrxFlags(__func__, 0);  // nothing in 1st/2nd DN slot
        // Delay callback processing to avoid up TX while gateway is txing our missed frame!
        // Since DNW2 uses SF12 by default we wait 3 secs.
        os_setTimedCallback(&LMIC.osjob,
                            (os_getTime() + DNW2_SAFETY_ZONE + LMICcore_rndDelay(2)),
                            FUNC_ADDR(processRx2DnDataDelay));
        return;
    }
    processDnData();
}


static void setupRx2DnData (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    LMIC.osjob.func = FUNC_ADDR(processRx2DnData);
    setupRx2();
}


static void processRx1DnData (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    if( LMIC.dataLen == 0 || !processDnData() )
        schedRx12(sec2osticks(LMIC.rxDelay +(int)DELAY_EXTDNW2), FUNC_ADDR(setupRx2DnData), LMIC.dn2Dr);
}


static void setupRx1DnData (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    setupRx1(FUNC_ADDR(processRx1DnData));
}


static void updataDone (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    txDone(sec2osticks(LMIC.rxDelay), FUNC_ADDR(setupRx1DnData));
}

// ========================================


static void buildDataFrame (void) {
    bit_t txdata = ((LMIC.opmode & (OP_TXDATA|OP_POLL)) != OP_POLL);
    u1_t dlen = txdata ? LMIC.pendTxLen : 0;

    // Piggyback MAC options
    // Prioritize by importance
    int  end = OFF_DAT_OPTS;
#if !defined(DISABLE_PING)
    if( (LMIC.opmode & (OP_TRACK|OP_PINGABLE)) == (OP_TRACK|OP_PINGABLE) ) {
        // Indicate pingability in every UP frame
        LMIC.frame[end] = MCMD_PING_IND;
        LMIC.frame[end+1] = LMIC.ping.dr | (LMIC.ping.intvExp<<4);
        end += 2;
    }
#endif // !DISABLE_PING
#if !defined(DISABLE_MCMD_DCAP_REQ)
    if( LMIC.dutyCapAns ) {
        LMIC.frame[end] = MCMD_DCAP_ANS;
        end += 1;
        LMIC.dutyCapAns = 0;
    }
#endif // !DISABLE_MCMD_DCAP_REQ
    if( LMIC.devsAns ) {  // answer to device status
        LMIC.frame[end+0] = MCMD_DEVS_ANS;
        LMIC.frame[end+1] = os_getBattLevel();
        LMIC.frame[end+2] = LMIC.devAnsMargin;
        end += 3;
        LMIC.devsAns = 0;
    }
    if( LMIC.ladrAns ) {  // answer to ADR change
        LMIC.frame[end+0] = MCMD_LADR_ANS;
        LMIC.frame[end+1] = LMIC.ladrAns & ~MCMD_LADR_ANS_RFU;
        end += 2;
        LMIC.ladrAns = 0;
    }
#if !defined(DISABLE_BEACONS)
    if( LMIC.bcninfoTries > 0 ) {
        LMIC.frame[end] = MCMD_BCNI_REQ;
        end += 1;
    }
#endif // !DISABLE_BEACONS
    if( LMIC.adrChanged ) {
        // if ADR is enabled, and we were just counting down the
        // transmits before starting an ADR, advance the timer so
        // we'll do an ADR now.
        if (LMIC.adrAckReq < LINK_CHECK_CONT)
            setAdrAckCount(LINK_CHECK_CONT);
        LMIC.adrChanged = 0;
    }
#if !defined(DISABLE_MCMD_DN2P_SET)
    if (LMIC.dn2Ans) {
            LMIC.frame[end + 0] = MCMD_DN2P_ANS;
            LMIC.frame[end + 1] = LMIC.dn2Ans & ~MCMD_DN2P_ANS_RFU;
            end += 2;
            LMIC.dn2Ans = 0;
    }
#endif // !DISABLE_MCMD_DN2P_SET
#if !defined(DISABLE_MCMD_PING_SET) && !defined(DISABLE_PING)
    if( LMIC.pingSetAns != 0 ) {
        LMIC.frame[end+0] = MCMD_PING_ANS;
        LMIC.frame[end+1] = LMIC.pingSetAns & ~MCMD_PING_ANS_RFU;
        end += 2;
        LMIC.pingSetAns = 0;
    }
#endif // !DISABLE_MCMD_PING_SET && !DISABLE_PING
#if !defined(DISABLE_MCMD_SNCH_REQ)
    if( LMIC.snchAns ) {
        LMIC.frame[end+0] = MCMD_SNCH_ANS;
        LMIC.frame[end+1] = LMIC.snchAns & ~MCMD_SNCH_ANS_RFU;
        end += 2;
        LMIC.snchAns = 0;
    }
#endif // !DISABLE_MCMD_SNCH_REQ
#if LMIC_ENABLE_TxParamSetupReq
    if ( LMIC.txParamSetupAns ) {
        LMIC.frame[end+0] = MCMD_TxParamSetupAns;
        end += 1;
        LMIC.txParamSetupAns = 0;
    }
#endif
#if LMIC_ENABLE_DeviceTimeReq
    if ( LMIC.txDeviceTimeReqState == lmic_RequestTimeState_tx ) {
        LMIC.frame[end+0] = MCMD_DeviceTimeReq;
        end += 1;
        LMIC.txDeviceTimeReqState = lmic_RequestTimeState_rx;
    }
#endif // LMIC_ENABLE_DeviceTimeReq
    ASSERT(end <= OFF_DAT_OPTS+16);

    u1_t flen = end + (txdata ? 5+dlen : 4);
    if( flen > MAX_LEN_FRAME ) {
        // Options and payload too big - delay payload
        txdata = 0;
        flen = end+4;
    }
    LMIC.frame[OFF_DAT_HDR] = HDR_FTYPE_DAUP | HDR_MAJOR_V1;
    LMIC.frame[OFF_DAT_FCT] = (LMIC.dnConf | LMIC.adrEnabled
                              | (LMIC.adrAckReq >= LINK_CHECK_CONT ? FCT_ADRARQ : 0)
                              | (end-OFF_DAT_OPTS));
    os_wlsbf4(LMIC.frame+OFF_DAT_ADDR,  LMIC.devaddr);

    if( LMIC.txCnt == 0 ) {
        LMIC.seqnoUp += 1;
        DO_DEVDB(LMIC.seqnoUp,seqnoUp);
    } else {
        EV(devCond, INFO, (e_.reason = EV::devCond_t::RE_TX,
                           e_.eui    = MAIN::CDEV->getEui(),
                           e_.info   = LMIC.seqnoUp-1,
                           e_.info2  = ((LMIC.txCnt+1) |
                                        (TABLE_GET_U1(DRADJUST, LMIC.txCnt+1) << 8) |
                                        ((LMIC.datarate|DR_PAGE)<<16))));
    }
    os_wlsbf2(LMIC.frame+OFF_DAT_SEQNO, LMIC.seqnoUp-1);

    // Clear pending DN confirmation
    LMIC.dnConf = 0;

    if( txdata ) {
        if( LMIC.pendTxConf ) {
            // Confirmed only makes sense if we have a payload (or at least a port)
            LMIC.frame[OFF_DAT_HDR] = HDR_FTYPE_DCUP | HDR_MAJOR_V1;
            if( LMIC.txCnt == 0 ) LMIC.txCnt = 1;
        }
        LMIC.frame[end] = LMIC.pendTxPort;
        os_copyMem(LMIC.frame+end+1, LMIC.pendTxData, dlen);
        aes_cipher(LMIC.pendTxPort==0 ? LMIC.nwkKey : LMIC.artKey,
                   LMIC.devaddr, LMIC.seqnoUp-1,
                   /*up*/0, LMIC.frame+end+1, dlen);
    }
    aes_appendMic(LMIC.nwkKey, LMIC.devaddr, LMIC.seqnoUp-1, /*up*/0, LMIC.frame, flen-4);

    EV(dfinfo, DEBUG, (e_.deveui  = MAIN::CDEV->getEui(),
                       e_.devaddr = LMIC.devaddr,
                       e_.seqno   = LMIC.seqnoUp-1,
                       e_.flags   = (LMIC.pendTxPort < 0 ? EV::dfinfo_t::NOPORT : EV::dfinfo_t::NOP),
                       e_.mic     = Base::lsbf4(&LMIC.frame[flen-4]),
                       e_.hdr     = LMIC.frame[LORA::OFF_DAT_HDR],
                       e_.fct     = LMIC.frame[LORA::OFF_DAT_FCT],
                       e_.port    = LMIC.pendTxPort,
                       e_.plen    = txdata ? dlen : 0,
                       e_.opts.length = end-LORA::OFF_DAT_OPTS,
                       memcpy(&e_.opts[0], LMIC.frame+LORA::OFF_DAT_OPTS, end-LORA::OFF_DAT_OPTS)));
    LMIC.dataLen = flen;
}


#if !defined(DISABLE_BEACONS)
// Callback from HAL during scan mode or when job timer expires.
static void onBcnRx (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    // If we arrive via job timer make sure to put radio to rest.
    os_radio(RADIO_RST);
    os_clearCallback(&LMIC.osjob);
    if( LMIC.dataLen == 0 ) {
        // Nothing received - timeout
        LMIC.opmode &= ~(OP_SCAN | OP_TRACK);
        reportEventAndUpdate(EV_SCAN_TIMEOUT);
        return;
    }
    if( decodeBeacon() <= 0 ) {
        // Something is wrong with the beacon - continue scan
        LMIC.dataLen = 0;
        os_radio(RADIO_RXON);
        os_setTimedCallback(&LMIC.osjob, LMIC.bcninfo.txtime, FUNC_ADDR(onBcnRx));
        return;
    }
    // Found our 1st beacon
    // We don't have a previous beacon to calc some drift - assume
    // an max error of 13ms = 128sec*100ppm which is roughly +/-100ppm
    calcBcnRxWindowFromMillis(13,1);
    LMIC.opmode &= ~OP_SCAN;          // turn SCAN off
    LMIC.opmode |=  OP_TRACK;         // auto enable tracking
    reportEventAndUpdate(EV_BEACON_FOUND);    // can be disabled in callback
}


// Enable receiver to listen to incoming beacons
// netid defines when scan stops (any or specific beacon)
// This mode ends with events: EV_SCAN_TIMEOUT/EV_SCAN_BEACON
// Implicitely cancels any pending TX/RX transaction.
// Also cancels an onpoing joining procedure.
static void startScan (void) {
    ASSERT(LMIC.devaddr!=0 && (LMIC.opmode & OP_JOINING)==0);
    if( (LMIC.opmode & OP_SHUTDOWN) != 0 )
        return;
    // Cancel onging TX/RX transaction
    LMIC.txCnt = LMIC.dnConf = LMIC.bcninfo.flags = 0;
    LMIC.opmode = (LMIC.opmode | OP_SCAN) & ~(OP_TXRXPEND);
    LMICbandplan_setBcnRxParams();
    LMIC.rxtime = LMIC.bcninfo.txtime = os_getTime() + sec2osticks(BCN_INTV_sec+1);
    os_setTimedCallback(&LMIC.osjob, LMIC.rxtime, FUNC_ADDR(onBcnRx));
    os_radio(RADIO_RXON);
}


bit_t LMIC_enableTracking (u1_t tryBcnInfo) {
    if( (LMIC.opmode & (OP_SCAN|OP_TRACK|OP_SHUTDOWN)) != 0 )
        return 0;  // already in progress or failed to enable
    // If BCN info requested from NWK then app has to take are
    // of sending data up so that MCMD_BCNI_REQ can be attached.
    if( (LMIC.bcninfoTries = tryBcnInfo) == 0 )
        startScan();
    return 1;  // enabled
}


void LMIC_disableTracking (void) {
    LMIC.opmode &= ~(OP_SCAN|OP_TRACK);
    LMIC.bcninfoTries = 0;
    engineUpdate();
}
#endif // !DISABLE_BEACONS



























// ================================================================================
//
// Join stuff
//
// ================================================================================

#if !defined(DISABLE_JOIN)
static void buildJoinRequest (u1_t ftype) {
    // Do not use pendTxData since we might have a pending
    // user level frame in there. Use RX holding area instead.
    xref2u1_t d = LMIC.frame;
    d[OFF_JR_HDR] = ftype;
    os_getArtEui(d + OFF_JR_ARTEUI);
    os_getDevEui(d + OFF_JR_DEVEUI);
    os_wlsbf2(d + OFF_JR_DEVNONCE, LMIC.devNonce);
    aes_appendMic0(d, OFF_JR_MIC);

    EV(joininfo,INFO,(e_.deveui  = MAIN::CDEV->getEui(),
                      e_.arteui  = MAIN::CDEV->getArtEui(),
                      e_.nonce   = LMIC.devNonce,
                      e_.oldaddr = LMIC.devaddr,
                      e_.mic     = Base::lsbf4(&d[LORA::OFF_JR_MIC]),
                      e_.reason  = ((LMIC.opmode & OP_REJOIN) != 0
                                    ? EV::joininfo_t::REJOIN_REQUEST
                                    : EV::joininfo_t::REQUEST)));
    LMIC.dataLen = LEN_JR;
    LMIC.devNonce++;
    DO_DEVDB(LMIC.devNonce,devNonce);
}

static void startJoining (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    // see issue #244: for backwards compatibility
    // don't override what the user does after os_init().
    if (LMIC.initBandplanAfterReset)
        LMICbandplan_resetDefaultChannels();
    else
        LMIC.initBandplanAfterReset = 1;

    // let the client know that now's the time to update
    // network settings.
    reportEventAndUpdate(EV_JOINING);
}

// reset the joined-to-network state (and clean up)
void LMIC_unjoin(void) {
    // reset any joining flags
    LMIC.opmode &= ~(OP_SCAN|OP_REJOIN);

    // put us in unjoined state:
    LMIC.devaddr = 0;

    // clear transmit.
    LMIC_clrTxData();
}

// Start join procedure if not already joined.
bit_t LMIC_startJoining (void) {
    if( LMIC.devaddr == 0 ) {
        // There should be no TX/RX going on
        ASSERT((LMIC.opmode & (OP_POLL|OP_TXRXPEND)) == 0);
        // Lift any previous duty limitation
        LMIC.globalDutyRate = 0;
        // Cancel scanning
        LMIC.opmode &= ~(OP_SCAN|OP_REJOIN|OP_LINKDEAD|OP_NEXTCHNL);
        // Setup state
        LMIC.rejoinCnt = LMIC.txCnt = 0;
        resetJoinParams();
        LMICbandplan_initJoinLoop();
        LMIC.opmode |= OP_JOINING;
        // reportEventAndUpdate will call engineUpdate which then starts sending JOIN REQUESTS
        os_setCallback(&LMIC.osjob, FUNC_ADDR(startJoining));
        return 1;
    }
    return 0; // already joined
}
#endif // !DISABLE_JOIN


// ================================================================================
//
//
//
// ================================================================================

#if !defined(DISABLE_PING)
static void processPingRx (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    if( LMIC.dataLen != 0 ) {
        initTxrxFlags(__func__, TXRX_PING);
        if( decodeFrame() ) {
            reportEventNoUpdate(EV_RXCOMPLETE);
        }
    }
    // Pick next ping slot
    engineUpdate();
}
#endif // !DISABLE_PING


static bit_t processDnData (void) {
    ASSERT((LMIC.opmode & OP_TXRXPEND)!=0);

    if( LMIC.dataLen == 0 ) {
      norx:
        if( LMIC.txCnt != 0 ) {
            if( LMIC.txCnt < TXCONF_ATTEMPTS ) {
                LMIC.txCnt += 1;
                setDrTxpow(DRCHG_NOACK, lowerDR(LMIC.datarate, TABLE_GET_U1(DRADJUST, LMIC.txCnt)), KEEP_TXPOW);
                // Schedule another retransmission
                txDelay(LMIC.rxtime, RETRY_PERIOD_secs);
                LMIC.opmode &= ~OP_TXRXPEND;
                engineUpdate();
                return 1;
            }
            initTxrxFlags(__func__, TXRX_NACK | TXRX_NOPORT);
        } else {
            // Nothing received - implies no port
            initTxrxFlags(__func__, TXRX_NOPORT);
        }
        setAdrAckCount(LMIC.adrAckReq + 1);
        LMIC.dataBeg = LMIC.dataLen = 0;
      txcomplete:
        LMIC.opmode &= ~(OP_TXDATA|OP_TXRXPEND);

#if LMIC_ENABLE_DeviceTimeReq
        lmic_request_time_state_t const requestTimeState = LMIC.txDeviceTimeReqState;
        if ( requestTimeState != lmic_RequestTimeState_idle ) {
            lmic_request_network_time_cb_t * const pNetworkTimeCb = LMIC.client.pNetworkTimeCb;
            int flagSuccess = (LMIC.txDeviceTimeReqState == lmic_RequestTimeState_success);
            LMIC.txDeviceTimeReqState = lmic_RequestTimeState_idle;
            if (pNetworkTimeCb != NULL) {
                // reset the callback, so that the user's routine
                // can post another request if desired.
                LMIC.client.pNetworkTimeCb = NULL;

                // call the user's notification routine.
                (*pNetworkTimeCb)(LMIC.client.pNetworkTimeUserData, flagSuccess);
            }
        }
#endif // LMIC_ENABLE_DeviceTimeReq

        if( (LMIC.txrxFlags & (TXRX_DNW1|TXRX_DNW2|TXRX_PING)) != 0  &&  (LMIC.opmode & OP_LINKDEAD) != 0 ) {
            LMIC.opmode &= ~OP_LINKDEAD;
            reportEventNoUpdate(EV_LINK_ALIVE);
        }
        reportEventAndUpdate(EV_TXCOMPLETE);
        // If we haven't heard from NWK in a while although we asked for a sign
        // assume link is dead - notify application and keep going
        if( LMIC.adrAckReq > LINK_CHECK_DEAD ) {
            // We haven't heard from NWK for some time although we
            // asked for a response for some time - assume we're disconnected. Lower DR one notch.
            EV(devCond, ERR, (e_.reason = EV::devCond_t::LINK_DEAD,
                              e_.eui    = MAIN::CDEV->getEui(),
                              e_.info   = LMIC.adrAckReq));
            dr_t newDr = decDR((dr_t)LMIC.datarate);
            if( newDr == (dr_t)LMIC.datarate) {
                // We are already at the minimum datarate
                // if the link is already marked dead, we need to join.
                // REJOIN sends a single join packet then brings back
                // up the normal tx loop. If we miss the downlink for the
                // join-accept, all the TXs until the next window will
                // fail... so we leave in the OP_REJOIN, but set things
                // to trigger a REJOIN after each uplink from here on.
                LMIC.adrAckReq = LINK_CHECK_DEAD;
#if !defined(DISABLE_JOIN)
                LMIC.opmode |= OP_REJOIN;
#endif // !defined(DISABLE_JOIN)
            } else {
                // not in the dead state... let's wait another 32
                // uplinks before panicking.
                setAdrAckCount(LINK_CHECK_CONT);
            }
            // Decrease DataRate and restore fullpower.
            setDrTxpow(DRCHG_NOADRACK, newDr, pow2dBm(0));

            // be careful only to report EV_LINK_DEAD once.
            u2_t old_opmode = LMIC.opmode;
            LMIC.opmode = old_opmode | OP_LINKDEAD;
            if (LMIC.opmode != old_opmode)
                reportEventNoUpdate(EV_LINK_DEAD); // update?
        }
#if !defined(DISABLE_BEACONS)
        // If this falls to zero the NWK did not answer our MCMD_BCNI_REQ commands - try full scan
        if( LMIC.bcninfoTries > 0 ) {
            if( (LMIC.opmode & OP_TRACK) != 0 ) {
                reportEventNoUpdate(EV_BEACON_FOUND); // update?
                LMIC.bcninfoTries = 0;
            }
            else if( --LMIC.bcninfoTries == 0 ) {
                startScan();   // NWK did not answer - try scan
            }
        }
#endif // !DISABLE_BEACONS
        return 1;
    }
    if( !decodeFrame() ) {
        if( (LMIC.txrxFlags & TXRX_DNW1) != 0 )
            return 0;
        goto norx;
    }
    goto txcomplete;
}


#if !defined(DISABLE_BEACONS)
static void processBeacon (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    ostime_t lasttx = LMIC.bcninfo.txtime;   // save here - decodeBeacon might overwrite
    u1_t flags = LMIC.bcninfo.flags;
    ev_t ev;

    if( LMIC.dataLen != 0 && decodeBeacon() >= 1 ) {
        ev = EV_BEACON_TRACKED;
        if( (flags & (BCN_PARTIAL|BCN_FULL)) == 0 ) {
            // We don't have a previous beacon to calc some drift - assume
            // an max error of 13ms = 128sec*100ppm which is roughly +/-100ppm
            calcBcnRxWindowFromMillis(13,0);
            goto rev;
        }
        // We have a previous BEACON to calculate some drift
        s2_t drift = BCN_INTV_osticks - (LMIC.bcninfo.txtime - lasttx);
        if( LMIC.missedBcns > 0 ) {
            drift = LMIC.drift + (drift - LMIC.drift) / (LMIC.missedBcns+1);
        }
        if( (LMIC.bcninfo.flags & BCN_NODRIFT) == 0 ) {
            s2_t diff = LMIC.drift - drift;
            if( diff < 0 ) diff = -diff;
            LMIC.lastDriftDiff = diff;
            if( LMIC.maxDriftDiff < diff )
                LMIC.maxDriftDiff = diff;
            LMIC.bcninfo.flags &= ~BCN_NODDIFF;
        }
        LMIC.drift = drift;
        LMIC.missedBcns = LMIC.rejoinCnt = 0;
        LMIC.bcninfo.flags &= ~BCN_NODRIFT;
        EV(devCond,INFO,(e_.reason = EV::devCond_t::CLOCK_DRIFT,
                         e_.eui    = MAIN::CDEV->getEui(),
                         e_.info   = drift,
                         e_.info2  = /*occasion BEACON*/0));
        ASSERT((LMIC.bcninfo.flags & (BCN_PARTIAL|BCN_FULL)) != 0);
    } else {
        ev = EV_BEACON_MISSED;
        LMIC.bcninfo.txtime += BCN_INTV_osticks - LMIC.drift;
        LMIC.bcninfo.time   += BCN_INTV_sec;
        LMIC.missedBcns++;
        // Delay any possible TX after surmised beacon - it's there although we missed it
        txDelay(LMIC.bcninfo.txtime + BCN_RESERVE_osticks, 4);
        if( LMIC.missedBcns > MAX_MISSED_BCNS )
            LMIC.opmode |= OP_REJOIN;  // try if we can roam to another network
        if( LMIC.bcnRxsyms > MAX_RXSYMS ) {
            LMIC.opmode &= ~(OP_TRACK|OP_PINGABLE|OP_PINGINI|OP_REJOIN);
            reportEventAndUpdate(EV_LOST_TSYNC);
            return;
        }
    }
    LMIC.bcnRxtime = LMIC.bcninfo.txtime + BCN_INTV_osticks - calcRxWindow(0,DR_BCN);
    LMIC.bcnRxsyms = LMIC.rxsyms;
  rev:
    LMICbandplan_advanceBeaconChannel();
#if !defined(DISABLE_PING)
    if( (LMIC.opmode & OP_PINGINI) != 0 )
        rxschedInit(&LMIC.ping);  // note: reuses LMIC.frame buffer!
#endif // !DISABLE_PING
    reportEventAndUpdate(ev);
}

// job entry: time to start receiving a beacon.
static void startRxBcn (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    LMIC.osjob.func = FUNC_ADDR(processBeacon);
    radioRx();
}
#endif // !DISABLE_BEACONS


#if !defined(DISABLE_PING)
// job entry: time to start receiving in our scheduled downlink slot.
static void startRxPing (xref2osjob_t osjob) {
    LMIC_API_PARAMETER(osjob);

    LMIC.osjob.func = FUNC_ADDR(processPingRx);
    radioRx();
}
#endif // !DISABLE_PING


// Decide what to do next for the MAC layer of a device
static void engineUpdate (void) {
#if LMIC_DEBUG_LEVEL > 0
    LMIC_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": engineUpdate, opmode=0x%x\n", os_getTime(), LMIC.opmode);
#endif
    // Check for ongoing state: scan or TX/RX transaction
    if( (LMIC.opmode & (OP_SCAN|OP_TXRXPEND|OP_SHUTDOWN)) != 0 )
        return;

#if !defined(DISABLE_JOIN)
    if( LMIC.devaddr == 0 && (LMIC.opmode & OP_JOINING) == 0 ) {
        LMIC_startJoining();
        return;
    }
#endif // !DISABLE_JOIN

    ostime_t now    = os_getTime();
    ostime_t txbeg  = 0;

#if !defined(DISABLE_BEACONS)
    ostime_t rxtime = 0;

    if( (LMIC.opmode & OP_TRACK) != 0 ) {
        // We are tracking a beacon
        ASSERT( now + RX_RAMPUP - LMIC.bcnRxtime <= 0 );
        rxtime = LMIC.bcnRxtime - RX_RAMPUP;
    }
#endif // !DISABLE_BEACONS

    if( (LMIC.opmode & (OP_JOINING|OP_REJOIN|OP_TXDATA|OP_POLL)) != 0 ) {
        // Need to TX some data...
        // Assuming txChnl points to channel which first becomes available again.
        bit_t jacc = ((LMIC.opmode & (OP_JOINING|OP_REJOIN)) != 0 ? 1 : 0);
        // Find next suitable channel and return availability time
        if( (LMIC.opmode & OP_NEXTCHNL) != 0 ) {
            txbeg = LMIC.txend = LMICbandplan_nextTx(now);
            LMIC.opmode &= ~OP_NEXTCHNL;
        } else {
            txbeg = LMIC.txend;
        }
        // Delayed TX or waiting for duty cycle?
        if( (LMIC.globalDutyRate != 0 || (LMIC.opmode & OP_RNDTX) != 0)  &&  (txbeg - LMIC.globalDutyAvail) < 0 )
            txbeg = LMIC.globalDutyAvail;
#if !defined(DISABLE_BEACONS)
        // If we're tracking a beacon...
        // then make sure TX-RX transaction is complete before beacon
        if( (LMIC.opmode & OP_TRACK) != 0 &&
            txbeg + (jacc ? JOIN_GUARD_osticks : TXRX_GUARD_osticks) - rxtime > 0 ) {
            // Not enough time to complete TX-RX before beacon - postpone after beacon.
            // In order to avoid clustering of postponed TX right after beacon randomize start!
            txDelay(rxtime + BCN_RESERVE_osticks, 16);
            txbeg = 0;
            goto checkrx;
        }
#endif // !DISABLE_BEACONS
        // Earliest possible time vs overhead to setup radio
        if( txbeg - (now + TX_RAMPUP) < 0 ) {
            // We could send right now!
            txbeg = now;
            dr_t txdr = (dr_t)LMIC.datarate;
#if !defined(DISABLE_JOIN)
            if( jacc ) {
                u1_t ftype;
                if( (LMIC.opmode & OP_REJOIN) != 0 ) {
#if CFG_region != LMIC_REGION_as923
                    // in AS923 v1.1 or older, no need to change the datarate.
                    txdr = lowerDR(txdr, LMIC.rejoinCnt);
#endif
                }
                ftype = HDR_FTYPE_JREQ;
                buildJoinRequest(ftype);
                LMIC.osjob.func = FUNC_ADDR(jreqDone);
            } else
#endif // !DISABLE_JOIN
            {
                if( LMIC.seqnoDn >= 0xFFFFFF80 ) {
                    // Imminent roll over - proactively reset MAC
                    EV(specCond, INFO, (e_.reason = EV::specCond_t::DNSEQNO_ROLL_OVER,
                                        e_.eui    = MAIN::CDEV->getEui(),
                                        e_.info   = LMIC.seqnoDn,
                                        e_.info2  = 0));
                    // Device has to react! NWK will not roll over and just stop sending.
                    // Thus, we have N frames to detect a possible lock up.
                  reset:
                    os_setCallback(&LMIC.osjob, FUNC_ADDR(runReset));
                    return;
                }
                if( (LMIC.txCnt==0 && LMIC.seqnoUp == 0xFFFFFFFF) ) {
                    // Roll over of up seq counter
                    EV(specCond, ERR, (e_.reason = EV::specCond_t::UPSEQNO_ROLL_OVER,
                                       e_.eui    = MAIN::CDEV->getEui(),
                                       e_.info2  = LMIC.seqnoUp));
                    // Do not run RESET event callback from here!
                    // App code might do some stuff after send unaware of RESET.
                    goto reset;
                }
                buildDataFrame();
                LMIC.osjob.func = FUNC_ADDR(updataDone);
            }
            LMIC.rps    = setCr(updr2rps(txdr), (cr_t)LMIC.errcr);
            LMIC.dndr   = txdr;  // carry TX datarate (can be != LMIC.datarate) over to txDone/setupRx1
            LMIC.opmode = (LMIC.opmode & ~(OP_POLL|OP_RNDTX)) | OP_TXRXPEND | OP_NEXTCHNL;
            LMICbandplan_updateTx(txbeg);
            // limit power to value asked in adr
            LMIC.radio_txpow = LMIC.txpow > LMIC.adrTxPow ? LMIC.adrTxPow : LMIC.txpow;
            reportEventNoUpdate(EV_TXSTART);
            os_radio(RADIO_TX);
            return;
        }
        // Cannot yet TX
        if( (LMIC.opmode & OP_TRACK) == 0 )
            goto txdelay; // We don't track the beacon - nothing else to do - so wait for the time to TX
        // Consider RX tasks
        if( txbeg == 0 ) // zero indicates no TX pending
            txbeg += 1;  // TX delayed by one tick (insignificant amount of time)
    } else {
        // No TX pending - no scheduled RX
        if( (LMIC.opmode & OP_TRACK) == 0 )
            return;
    }

#if !defined(DISABLE_BEACONS)
    // Are we pingable?
  checkrx:
#if !defined(DISABLE_PING)
    if( (LMIC.opmode & OP_PINGINI) != 0 ) {
        // One more RX slot in this beacon period?
        if( rxschedNext(&LMIC.ping, now+RX_RAMPUP) ) {
            if( txbeg != 0  &&  (txbeg - LMIC.ping.rxtime) < 0 )
                goto txdelay;
            LMIC.rxsyms  = LMIC.ping.rxsyms;
            LMIC.rxtime  = LMIC.ping.rxtime;
            LMIC.freq    = LMIC.ping.freq;
            LMIC.rps     = dndr2rps(LMIC.ping.dr);
            LMIC.dataLen = 0;
            ASSERT(LMIC.rxtime - now+RX_RAMPUP >= 0 );
            os_setTimedCallback(&LMIC.osjob, LMIC.rxtime - RX_RAMPUP, FUNC_ADDR(startRxPing));
            return;
        }
        // no - just wait for the beacon
    }
#endif // !DISABLE_PING

    if( txbeg != 0  &&  (txbeg - rxtime) < 0 )
        goto txdelay;

    LMICbandplan_setBcnRxParams();
    LMIC.rxsyms = LMIC.bcnRxsyms;
    LMIC.rxtime = LMIC.bcnRxtime;
    if( now - rxtime >= 0 ) {
        LMIC.osjob.func = FUNC_ADDR(processBeacon);

        radioRx();
        return;
    }
    os_setTimedCallback(&LMIC.osjob, rxtime, FUNC_ADDR(startRxBcn));
    return;
#endif // !DISABLE_BEACONS

  txdelay:
    EV(devCond, INFO, (e_.reason = EV::devCond_t::TX_DELAY,
                       e_.eui    = MAIN::CDEV->getEui(),
                       e_.info   = osticks2ms(txbeg-now),
                       e_.info2  = LMIC.seqnoUp-1));
    LMIC_X_DEBUG_PRINTF("%"LMIC_PRId_ostime_t": next engine update in %"LMIC_PRId_ostime_t"\n", now, txbeg-TX_RAMPUP);
    os_setTimedCallback(&LMIC.osjob, txbeg-TX_RAMPUP, FUNC_ADDR(runEngineUpdate));
}


void LMIC_setAdrMode (bit_t enabled) {
    LMIC.adrEnabled = enabled ? FCT_ADREN : 0;
}


//  Should we have/need an ext. API like this?
void LMIC_setDrTxpow (dr_t dr, s1_t txpow) {
    setDrTxpow(DRCHG_SET, dr, txpow);
}


void LMIC_shutdown (void) {
    os_clearCallback(&LMIC.osjob);
    os_radio(RADIO_RST);
    LMIC.opmode |= OP_SHUTDOWN;
}

// reset the LMIC. This is called at startup; the clear of LMIC.osjob
// only works because the LMIC is guaranteed to be zero in that case.
// But it's also called at frame-count rollover; in that case we have
// to ensure that the user callback pointers are not clobbered.
void LMIC_reset (void) {
    EV(devCond, INFO, (e_.reason = EV::devCond_t::LMIC_EV,
                       e_.eui    = MAIN::CDEV->getEui(),
                       e_.info   = EV_RESET));
    os_radio(RADIO_RST);
    os_clearCallback(&LMIC.osjob);

    // save callback info, clear LMIC, restore.
    do {
        lmic_client_data_t  client = LMIC.client;

        os_clearMem((xref2u1_t)&LMIC,SIZEOFEXPR(LMIC));

        LMIC.client = client;
    } while (0);

    // LMIC.devaddr      =  0;      // true from os_clearMem().
    LMIC.devNonce     =  os_getRndU2();
    LMIC.opmode       =  OP_NONE;
    LMIC.errcr        =  CR_4_5;
    LMIC.adrEnabled   =  FCT_ADREN;
    LMIC.dn2Dr        =  DR_DNW2;   // we need this for 2nd DN window of join accept
    LMIC.dn2Freq      =  FREQ_DNW2; // ditto
    LMIC.rxDelay      =  DELAY_DNW1;
#if !defined(DISABLE_PING)
    LMIC.ping.freq    =  FREQ_PING; // defaults for ping
    LMIC.ping.dr      =  DR_PING;   // ditto
    LMIC.ping.intvExp =  0xFF;
#endif // !DISABLE_PING
    LMICbandplan_resetDefaultChannels();
    DO_DEVDB(LMIC.devaddr,      devaddr);
    DO_DEVDB(LMIC.devNonce,     devNonce);
    DO_DEVDB(LMIC.dn2Dr,        dn2Dr);
    DO_DEVDB(LMIC.dn2Freq,      dn2Freq);
#if !defined(DISABLE_PING)
    DO_DEVDB(LMIC.ping.freq,    pingFreq);
    DO_DEVDB(LMIC.ping.dr,      pingDr);
    DO_DEVDB(LMIC.ping.intvExp, pingIntvExp);
#endif // !DISABLE_PING
#if LMIC_ENABLE_DeviceTimeReq
    LMIC.txDeviceTimeReqState = lmic_RequestTimeState_idle;
    LMIC.netDeviceTime = 0;     // the "invalid" time.
    LMIC.netDeviceTimeFrac = 0;
#endif // LMIC_ENABLE_DeviceTimeReq
}


void LMIC_init (void) {
    LMIC.opmode = OP_SHUTDOWN;
    LMICbandplan_init();
}


void LMIC_clrTxData (void) {
    bit_t const txActive = LMIC.opmode & OP_TXDATA;
    LMIC.opmode &= ~(OP_TXDATA|OP_TXRXPEND|OP_POLL);
    LMIC.pendTxLen = 0;

    if (txActive)
        reportEventNoUpdate(EV_TXCANCELED);

    if( (LMIC.opmode & (OP_JOINING|OP_SCAN)) != 0 ) // do not interfere with JOINING
        return;
    os_clearCallback(&LMIC.osjob);
    os_radio(RADIO_RST);
    engineUpdate();
}


void LMIC_setTxData (void) {
    LMIC.opmode |= OP_TXDATA;
    if( (LMIC.opmode & OP_JOINING) == 0 )
        LMIC.txCnt = 0;             // cancel any ongoing TX/RX retries
    engineUpdate();
}


// send a message w/o callback
int LMIC_setTxData2 (u1_t port, xref2u1_t data, u1_t dlen, u1_t confirmed) {
    if( dlen > SIZEOFEXPR(LMIC.pendTxData) )
        return -2;
    if( data != (xref2u1_t)0 )
        os_copyMem(LMIC.pendTxData, data, dlen);
    LMIC.pendTxConf = confirmed;
    LMIC.pendTxPort = port;
    LMIC.pendTxLen  = dlen;
    LMIC_setTxData();
    return 0;
}

// send a message with callback
int LMIC_sendWithCallback(
    u1_t port, xref2u1_t data, u1_t dlen, u1_t confirmed,
    lmic_txmessage_cb_t *pCb, void *pUserData
) {
    int const result = LMIC_setTxData2(port, data, dlen, confirmed);
    if (result == 0) {
        LMIC.client.txMessageCb = pCb;
        LMIC.client.txMessageUserData = pUserData;
    }
    return result;
}


// Send a payload-less message to signal device is alive
void LMIC_sendAlive (void) {
    LMIC.opmode |= OP_POLL;
    engineUpdate();
}


// Check if other networks are around.
void LMIC_tryRejoin (void) {
    LMIC.opmode |= OP_REJOIN;
    engineUpdate();
}

//! \brief Setup given session keys
//! and put the MAC in a state as if
//! a join request/accept would have negotiated just these keys.
//! It is crucial that the combinations `devaddr/nwkkey` and `devaddr/artkey`
//! are unique within the network identified by `netid`.
//! NOTE: on Harvard architectures when session keys are in flash:
//!  Caller has to fill in LMIC.{nwk,art}Key  before and pass {nwk,art}Key are NULL
//! \param netid a 24 bit number describing the network id this device is using
//! \param devaddr the 32 bit session address of the device. It is strongly recommended
//!    to ensure that different devices use different numbers with high probability.
//! \param nwkKey  the 16 byte network session key used for message integrity.
//!     If NULL the caller has copied the key into `LMIC.nwkKey` before.
//! \param artKey  the 16 byte application router session key used for message confidentiality.
//!     If NULL the caller has copied the key into `LMIC.artKey` before.

// TODO(tmm@mcci.com) we ought to also save the channels that were returned by the
// join accept; right now this has to be done by the caller (or it doesn't get done).
void LMIC_setSession (u4_t netid, devaddr_t devaddr, xref2u1_t nwkKey, xref2u1_t artKey) {
    LMIC.netid = netid;
    LMIC.devaddr = devaddr;
    if( nwkKey != (xref2u1_t)0 )
        os_copyMem(LMIC.nwkKey, nwkKey, 16);
    if( artKey != (xref2u1_t)0 )
        os_copyMem(LMIC.artKey, artKey, 16);

    LMICbandplan_setSessionInitDefaultChannels();

    LMIC.opmode &= ~(OP_JOINING|OP_TRACK|OP_REJOIN|OP_TXRXPEND|OP_PINGINI);
    LMIC.opmode |= OP_NEXTCHNL;
    stateJustJoined();
    // transition to the ADR_ACK_DELAY state.
    setAdrAckCount(LINK_CHECK_CONT);

    DO_DEVDB(LMIC.netid,   netid);
    DO_DEVDB(LMIC.devaddr, devaddr);
    DO_DEVDB(LMIC.nwkKey,  nwkkey);
    DO_DEVDB(LMIC.artKey,  artkey);
    DO_DEVDB(LMIC.seqnoUp, seqnoUp);
    DO_DEVDB(LMIC.seqnoDn, seqnoDn);
}

// Enable/disable link check validation.
// LMIC sets the ADRACKREQ bit in UP frames if there were no DN frames
// for a while. It expects the network to provide a DN message to prove
// connectivity with a span of UP frames. If this no such prove is coming
// then the datarate is lowered and a LINK_DEAD event is generated.
// This mode can be disabled and no connectivity prove (ADRACKREQ) is requested
// nor is the datarate changed.
// This must be called only if a session is established (e.g. after EV_JOINED)
void LMIC_setLinkCheckMode (bit_t enabled) {
    LMIC.adrChanged = 0;
    LMIC.adrAckReq = enabled ? LINK_CHECK_INIT : LINK_CHECK_OFF;
}

// Sets the max clock error to compensate for (defaults to 0, which
// allows for +/- 640 at SF7BW250). MAX_CLOCK_ERROR represents +/-100%,
// so e.g. for a +/-1% error you would pass MAX_CLOCK_ERROR * 1 / 100.
void LMIC_setClockError(u2_t error) {
    LMIC.client.clockError = error;
}

// \brief return the uplink sequence number for the next transmission.
// This simple getter returns the uplink sequence number maintained by the LMIC engine.
// The caller should store the value and restore it (see LMIC_setSeqnoUp) on
// LMIC initialization to ensure monotonically increasing sequence numbers.
// It's also useful in debugging, as it allows you to correlate a debug trace event with
// a specific packet sent over the air.
u4_t LMIC_getSeqnoUp(void) {
    return LMIC.seqnoUp;
}

// \brief set the uplink sequence number for the next transmission.
// Use the function on startup to ensure that the next transmission uses
// a sequence number higher than the last transmission.
u4_t LMIC_setSeqnoUp(u4_t seq_no) {
    u4_t last = LMIC.seqnoUp;
    LMIC.seqnoUp = seq_no;
    return last;
}

// \brief return the current session keys returned from join.
void LMIC_getSessionKeys (u4_t *netid, devaddr_t *devaddr, xref2u1_t nwkKey, xref2u1_t artKey) {
    *netid = LMIC.netid;
    *devaddr = LMIC.devaddr;
    memcpy(artKey, LMIC.artKey, sizeof(LMIC.artKey));
    memcpy(nwkKey, LMIC.nwkKey, sizeof(LMIC.nwkKey));
}

// \brief post an asynchronous request for the network time.
void LMIC_requestNetworkTime(lmic_request_network_time_cb_t *pCallbackfn, void *pUserData) {
#if LMIC_ENABLE_DeviceTimeReq
    if (LMIC.txDeviceTimeReqState == lmic_RequestTimeState_idle) {
        LMIC.txDeviceTimeReqState = lmic_RequestTimeState_tx;
        LMIC.client.pNetworkTimeCb = pCallbackfn;
        LMIC.client.pNetworkTimeUserData = pUserData;
        return;
    }
#endif // LMIC_ENABLE_DeviceTimeReq
    // if no device time support, or if not in proper state,
    // report a failure.
    if (pCallbackfn != NULL)
        (*pCallbackfn)(pUserData, /* false */ 0);
}

// \brief return local/remote time pair (if valid, and DeviceTimeReq enabled),
// return true for success, false for error. We adjust the sampled OS time
// back in time to the nearest second boundary.
int LMIC_getNetworkTimeReference(lmic_time_reference_t *pReference) {
#if LMIC_ENABLE_DeviceTimeReq
    if (pReference != NULL &&       // valid parameter, and
        LMIC.netDeviceTime != 0) {  // ... we have a reasonable answer.
        const ostime_t tAdjust = LMIC.netDeviceTimeFrac * ms2osticks(1000) / 256;

        pReference->tLocal = LMIC.localDeviceTime - tAdjust;
        pReference->tNetwork = LMIC.netDeviceTime;
        return 1;
    }
#endif // LMIC_ENABLE_DeviceTimeReq
    return 0;
}
