IBL LoRaWAN implementation

Dependents:   Simple-LoRaWAN

Fork of LMiC by Semtech

lmic.cpp

Committer:
mluis
Date:
2015-01-22
Revision:
0:62d1edcc13d1
Child:
1:d3b7bde3995c

File content as of revision 0:62d1edcc13d1:

/*******************************************************************************
 * Copyright (c) 2014 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    IBM Zurich Research Lab - initial API, implementation and documentation
 *******************************************************************************/

#include "lmic.h"

#define PAMBL_SYMS 8
#define MINRX_SYMS 5
#define PAMBL_FSK  5
#define PRERX_FSK  1
#define RXLEN_FSK  (1+5+2)

#define BCN_INTV_osticks       sec2osticks(BCN_INTV_sec)
#define TXRX_GUARD_osticks     ms2osticks(TXRX_GUARD_ms)
#define JOIN_GUARD_osticks     ms2osticks(JOIN_GUARD_ms)
#define DELAY_DNW1_osticks     sec2osticks(DELAY_DNW1)
#define DELAY_DNW2_osticks     sec2osticks(DELAY_DNW2)
#define DELAY_JACC1_osticks    sec2osticks(DELAY_JACC1)
#define DELAY_JACC2_osticks    sec2osticks(DELAY_JACC2)
#define DELAY_EXTDNW2_osticks  sec2osticks(DELAY_EXTDNW2)
#define BCN_RESERVE_osticks    ms2osticks(BCN_RESERVE_ms)
#define BCN_GUARD_osticks      ms2osticks(BCN_GUARD_ms)
#define BCN_WINDOW_osticks     ms2osticks(BCN_WINDOW_ms)
#define AIRTIME_BCN_osticks    us2osticks(AIRTIME_BCN)

DEFINE_LMIC;
DECL_ON_LMIC_EVENT;


// Fwd decls.
static void engineUpdate(void);
static void startScan (void);


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

#if !HAS_os_calls

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

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


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


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

#ifndef 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

#ifndef 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

#ifndef os_getBattLevel
u1_t os_getBattLevel (void) {
    return MCMD_DEVS_BATT_NOINFO;
}
#endif

#ifndef os_crc16
static const u2_t CRC_TABLE[] = {
    0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
    0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
    0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
    0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
    0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
    0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
    0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
    0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
    0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
    0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
    0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
    0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
    0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
    0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
    0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
    0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
    0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
    0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
    0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
    0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
    0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
    0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
    0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
    0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
    0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
    0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
    0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
    0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
    0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
    0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
    0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
    0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78
};
u2_t os_crc16 (xref2u1_t data, uint len) {
    u2_t fcs = 0;
    for( uint u=0; u<len; u++ ) {
    u1_t b = data[u];
    fcs = (CRC_TABLE[(fcs ^ b) & 0xFF] ^ ((fcs >> 8) & 0xFF));
    }
    return fcs;
}
#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

#if CFG_eu868 // ========================================

#define maxFrameLen(dr) ((dr)<=DR_SF9 ? maxFrameLens[(dr)] : 0xFF)
const u1_t maxFrameLens [] = { 64,64,64,123 };

const u1_t _DR2RPS_CRC[] = {
    ILLEGAL_RPS,
    (u1_t)MAKERPS(SF12, BW125, CR_4_5, 0, 0),
    (u1_t)MAKERPS(SF11, BW125, CR_4_5, 0, 0),
    (u1_t)MAKERPS(SF10, BW125, CR_4_5, 0, 0),
    (u1_t)MAKERPS(SF9,  BW125, CR_4_5, 0, 0),
    (u1_t)MAKERPS(SF8,  BW125, CR_4_5, 0, 0),
    (u1_t)MAKERPS(SF7,  BW125, CR_4_5, 0, 0),
    (u1_t)MAKERPS(SF7,  BW250, CR_4_5, 0, 0),
    (u1_t)MAKERPS(FSK,  BW125, CR_4_5, 0, 0),
    ILLEGAL_RPS
};

static const s1_t TXPOWLEVELS[] = {
    20, 14, 11, 8, 5, 2, 0,0, 0,0,0,0, 0,0,0,0
};
#define pow2dBm(mcmd_ladr_p1) (TXPOWLEVELS[(mcmd_ladr_p1&MCMD_LADR_POW_MASK)>>MCMD_LADR_POW_SHIFT])

#elif CFG_us915 // ========================================

#define maxFrameLen(dr) ((dr)<=DR_SF11CR ? maxFrameLens[(dr)] : 0xFF)
const u1_t maxFrameLens [] = { 24,66,142,255,255,255,255,255,  66,142 };

const u1_t _DR2RPS_CRC[] = {
    ILLEGAL_RPS,
    MAKERPS(SF10, BW125, CR_4_5, 0, 0),
    MAKERPS(SF9 , BW125, CR_4_5, 0, 0),
    MAKERPS(SF8 , BW125, CR_4_5, 0, 0),
    MAKERPS(SF7 , BW125, CR_4_5, 0, 0),
    MAKERPS(SF8 , BW500, CR_4_5, 0, 0),
    ILLEGAL_RPS ,
    ILLEGAL_RPS ,
    ILLEGAL_RPS ,
    MAKERPS(SF12, BW500, CR_4_5, 0, 0),
    MAKERPS(SF11, BW500, CR_4_5, 0, 0),
    MAKERPS(SF10, BW500, CR_4_5, 0, 0),
    MAKERPS(SF9 , BW500, CR_4_5, 0, 0),
    MAKERPS(SF8 , BW500, CR_4_5, 0, 0),
    MAKERPS(SF7 , BW500, CR_4_5, 0, 0),
    ILLEGAL_RPS
};

#define pow2dBm(mcmd_ladr_p1) ((s1_t)(30 - (((mcmd_ladr_p1)&MCMD_LADR_POW_MASK)<<1)))

#endif // ================================================

static const 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 + 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 ) {
    // MLu - 12012015
    return (plen+/*preamble*/5+/*syncword*/3+/*len*/1+/*crc*/2) * /*bits/byte*/8
        * (s4_t)OSTICKS_PER_SEC / /*kbit/s*/50000;
    //return (plen+/*preamble*/5+/*syncword*/2+/*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;
}

extern inline s1_t  rssi2s1 (int v);
extern inline int   s12rssi (s1_t v);
extern inline float  s12snr (s1_t v);
extern inline s1_t   snr2s1 (double v);
extern inline int   getRssi (rxqu_t*rxq);
extern inline void  setRssi (rxqu_t*rxq, int v);

extern inline rps_t updr2rps (dr_t dr);
extern inline rps_t dndr2rps (dr_t dr);
extern inline int isFasterDR (dr_t dr1, dr_t dr2);
extern inline int isSlowerDR (dr_t dr1, dr_t dr2);
extern inline dr_t  incDR    (dr_t dr);
extern inline dr_t  decDR    (dr_t dr);
extern inline dr_t  assertDR (dr_t dr);
extern inline dr_t  validDR  (dr_t dr);
extern inline dr_t  lowerDR  (dr_t dr, u1_t n);

extern inline sf_t  getSf    (rps_t params);
extern inline rps_t setSf    (rps_t params, sf_t sf);
extern inline bw_t  getBw    (rps_t params);
extern inline rps_t setBw    (rps_t params, bw_t cr);
extern inline cr_t  getCr    (rps_t params);
extern inline rps_t setCr    (rps_t params, cr_t cr);
extern inline int   getNocrc (rps_t params);
extern inline rps_t setNocrc (rps_t params, int nocrc);
extern inline int   getIh    (rps_t params);
extern inline rps_t setIh    (rps_t params, int ih);
extern inline rps_t makeRps  (sf_t sf, bw_t bw, cr_t cr, int ih, int nocrc);
extern inline int   sameSfBw (rps_t r1, rps_t r2);

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


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


// 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
//  
// Times for half symbol per DR
// Per DR table to minimize rounding errors
static const ostime_t DR2HSYM_osticks[] = {
#if CFG_eu868
#define dr2hsym(dr) (DR2HSYM_osticks[(dr)])
    us2osticksRound(128<<7),  // DR_SF12
    us2osticksRound(128<<6),  // DR_SF11
    us2osticksRound(128<<5),  // DR_SF10
    us2osticksRound(128<<4),  // DR_SF9
    us2osticksRound(128<<3),  // DR_SF8
    us2osticksRound(128<<2),  // DR_SF7
    us2osticksRound(128<<1),  // DR_SF7B
    us2osticksRound(80)       // FSK -- not used (time for 1/2 byte)
#elif CFG_us915
#define dr2hsym(dr) (DR2HSYM_osticks[(dr)&7])  // map DR_SFnCR -> 0-6
    us2osticksRound(128<<5),  // DR_SF10   DR_SF12CR
    us2osticksRound(128<<4),  // DR_SF9    DR_SF11CR
    us2osticksRound(128<<3),  // DR_SF8    DR_SF10CR
    us2osticksRound(128<<2),  // DR_SF7    DR_SF9CR
    us2osticksRound(128<<1),  // DR_SF8C   DR_SF8CR
    us2osticksRound(128<<0)   // ------    DR_SF7CR
#endif
};


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;
}


// 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;
}


static ostime_t 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 += rndDelay(secSpan);
    if( LMIC.globalDutyRate == 0  ||  (reftime - LMIC.globalDutyAvail) > 0 ) {
    LMIC.globalDutyAvail = reftime;
    LMIC.opmode |= OP_RNDTX;
    }
}


static void setDrJoin (u1_t reason, u1_t dr) {
    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(updateDatarate, dr);
}


static void setDrTxpow (u1_t reason, u1_t dr, s1_t pow) {
    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 )
    LMIC.adrTxPow = pow;
    LMIC.datarate = dr;
    DO_DEVDB(updateDatarate, dr);
    LMIC.opmode |= OP_NEXTCHNL;
}


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);
}


#if CFG_eu868
// ================================================================================
//
// BEG: EU868 related stuff
//
enum { BAND_MILLI=0, BAND_CENTI=1, BAND_DECI=2 };
static const u4_t iniChannelFreq[12] = {
    // Join frequencies and duty cycle limit (0.1%)
    EU868_F1|BAND_MILLI, EU868_F2|BAND_MILLI, EU868_F3|BAND_MILLI,
    EU868_J4|BAND_MILLI, EU868_J5|BAND_MILLI, EU868_J6|BAND_MILLI,
    // Default operational frequencies
    EU868_F1|BAND_CENTI, EU868_F2|BAND_CENTI, EU868_F3|BAND_CENTI,
    EU868_F4|BAND_MILLI, EU868_F5|BAND_MILLI, EU868_F6|BAND_DECI
};

static void initDefaultChannels (bit_t join) {
    LMIC.channelMap = 0x3F;
    u1_t su = join ? 0 : 6;
    for( u1_t fu=0; fu<6; fu++,su++ ) {
    LMIC.channelFreq[fu] = iniChannelFreq[su];
    LMIC.channelDrs[fu]  = DR_SF12 | (DR_SF7<<4);
    }
    if( !join )
    LMIC.channelDrs[1] = DR_SF12 | (DR_FSK<<4);

    LMIC.bands[BAND_MILLI].txcap = 1000;  // 0.1%
    LMIC.bands[BAND_MILLI].txpow = 14;
    LMIC.bands[BAND_CENTI].txcap = 100;   // 1%
    LMIC.bands[BAND_CENTI].txpow = 14;
    LMIC.bands[BAND_DECI ].txcap = 10;    // 10%
    LMIC.bands[BAND_DECI ].txpow = 27;
    LMIC.bands[BAND_MILLI].avail = 
    LMIC.bands[BAND_CENTI].avail =
    LMIC.bands[BAND_DECI ].avail = os_getTime();
}

static u4_t convFreq (xref2u1_t ptr) {
    u4_t freq = (os_rlsbf4(ptr-1) >> 8) * 100;
    if( freq >= EU868_FREQ_MIN && freq <= EU868_FREQ_MAX )
    freq = 0;
    return freq;
}

static bit_t setupChannel (u1_t chidx, u4_t freq, int drs) {
    if( chidx >= MAX_CHANNELS )
    return 0;
    if( freq >= 869400000 && freq <= 869650000 )
    freq |= BAND_DECI;  // 10% 27dBm
    else if( (freq >= 868000000 && freq <= 868600000) ||
         (freq >= 869700000 && freq <= 870000000)  )
    freq |= BAND_CENTI;  // 1% 14dBm
    //else 
    //  freq |= BAND_MILLI;  // 0.1% 14dBm
    LMIC.channelFreq[chidx] = freq;
    LMIC.channelDrs [chidx] = drs < 0 ? (DR_SF12|(DR_SF7<<4)) : drs;
    return 1;
}

static u1_t mapChannels (u1_t chpage, u2_t chmap) {
    if( chpage != 0 || (chmap & ~((u2_t)(1<<MAX_CHANNELS)-1)) != 0 )
    return 0;  // illegal input
    for( u1_t chnl=0; chnl<MAX_CHANNELS; chnl++ ) {
    if( (chmap & (1<<chnl)) != 0 && LMIC.channelFreq[chnl] == 0 )
        chmap &= ~(1<<chnl); // ignore - channel is not defined
    }
    LMIC.channelMap = chmap;
    return 1;
}


static void updateTx (ostime_t txbeg) {
    u4_t freq = LMIC.channelFreq[LMIC.txChnl];
    // Update global/band specific duty cycle stats
    ostime_t airtime = calcAirTime(LMIC.rps, LMIC.dataLen);
    // Update channel/global duty cycle stats
    xref2band_t band = &LMIC.bands[freq & 0x3];
    LMIC.freq  = freq & ~(u4_t)3;
    LMIC.txpow = band->txpow;
    band->avail = txbeg + airtime * band->txcap;
    // Update obsolete avail's to prevent rollover
    // (needed for devices that send rarely - e.g. once/twice a day)
    for( u1_t b=0; b<MAX_BANDS; b++ ) {
    if( LMIC.bands[b].avail - txbeg < 0 )
        LMIC.bands[b].avail = txbeg;
    }
    if( LMIC.globalDutyRate != 0 )
    LMIC.globalDutyAvail = txbeg + (airtime<<LMIC.globalDutyRate);
}

static ostime_t nextTx (ostime_t now) {
    // If we do not find a suitable channel stop sending (misconfigured device)
    ostime_t mintime = now + /*10h*/36000*OSTICKS_PER_SEC;
    s1_t     bmask   = 0;
    for( u1_t b=0; b<MAX_BANDS; b++ ) {
    xref2band_t band = &LMIC.bands[b];
    if( band->txcap == 0 ) // band not setup
        continue;
    if( now - band->avail >= 0 ) {
        bmask = (bmask < 0 ? 0 : bmask) | (1<<b);
    }
    else if( mintime - band->avail > 0 ) {
        mintime = band->avail;
    }
    }
    if( bmask == 0 )
    return mintime;
    u1_t chnl, ccnt;
    u2_t cset, mask;
    chnl = cset = ccnt = 0;
    mask = 1;
    while( mask && mask <= LMIC.channelMap ) {
    // Channel is enabled AND and in a ready band and can handle the datarate
    if( (mask & LMIC.channelMap) != 0  &&  (bmask & (1 << (LMIC.channelFreq[chnl] & 0x3))) != 0) {
        u1_t drs = LMIC.channelDrs[chnl];
        if( !(isSlowerDR((dr_t)LMIC.datarate, (dr_t)(drs & 0xF)) ||   // lowest datarate
          isFasterDR((dr_t)LMIC.datarate, (dr_t)(drs >>  4))) ) { // fastest datarate
        cset |= mask;
        ccnt++;
        }
    }
    mask <<= 1;
    chnl++;
    }
    if( ccnt == 0 ) // No eligible channel - misconfigured device?
    return mintime;
    ccnt = os_getRndU2() % ccnt;
    mask = 1;
    chnl = 0;
    do {
    if( (cset & mask) != 0 ) {
        if( ccnt==0 ) {
        LMIC.txChnl = chnl;
        return now;
        }
        ccnt--;
    }
    mask <<= 1;
    chnl++;
    } while(1);
}

static void setBcnRxParams (void) {
    LMIC.dataLen = 0;
    LMIC.freq = LMIC.channelFreq[LMIC.bcnChnl] & ~(u4_t)3;
    LMIC.rps  = setIh(setNocrc(dndr2rps((dr_t)DR_BCN),1),LEN_BCN);
}

#define setRx1Params() /*LMIC.freq/rps remain unchanged*/

static void initJoinLoop (void) {
    LMIC.txChnl = os_getRndU1() % 3;
    LMIC.adrTxPow = 14;
    setDrJoin(DRCHG_SET, DR_SF7);
    initDefaultChannels(1);
    ASSERT((LMIC.opmode & OP_NEXTCHNL)==0);
    LMIC.txend = LMIC.bands[BAND_MILLI].avail;
}


static ostime_t nextJoinState (void) {
    // Try 869.x and then 864.x with same DR
    // If both fail try next lower datarate
    LMIC.opmode &= ~OP_NEXTCHNL;
    LMIC.txend = LMIC.bands[BAND_MILLI].avail;
    if( LMIC.txChnl < 3 ) {
    LMIC.txChnl += 3;
    } else {
    if( LMIC.datarate == DR_SF12 ) {
        setDrJoin(DRCHG_NOJACC, DR_SF7);
        // We tried one round of FSK..SF12
        // Now pick new random channel and insert a random delay and try another round.
        LMIC.txChnl = os_getRndU1() % 3;
        if( LMIC.txCnt < 5 )
        LMIC.txCnt += 1;
        // Delay by 7,15,31,63,127,255 secs
        return rndDelay((4<<LMIC.txCnt)-1) | 1;   // |1 - trigger EV_JOIN_FAILED event
    }
    LMIC.txChnl = LMIC.txChnl==5 ? 0 : LMIC.txChnl-2;
    setDrJoin(DRCHG_NOJACC, decDR((dr_t)LMIC.datarate));
    }
    // Avoid collision with JOIN ACCEPT being sent by GW (but we missed it)
    return 3*OSTICKS_PER_SEC;
}

//
// END: EU868 related stuff
//
// ================================================================================
#elif CFG_us915
// ================================================================================
//
// BEG: US915 related stuff
//

static void initDefaultChannels (void) {
    for( u1_t i=0; i<4; i++ )
    LMIC.channelMap[i] = 0xFFFF;
    LMIC.channelMap[4] = 0x00FF;
}

static u4_t convFreq (xref2u1_t ptr) {
    u4_t freq = (os_rlsbf4(ptr-1) >> 8) * 100;
    if( freq >= US915_FREQ_MIN && freq <= US915_FREQ_MAX )
    freq = 0;
    return freq;
}

static bit_t setupChannel (u1_t chidx, u4_t freq, int drs) {
    if( chidx < 72 || chidx >= 72+MAX_XCHANNELS )
    return 0; // channels 0..71 are hardwired
    chidx -= 72;
    LMIC.xchFreq[chidx] = freq;
    LMIC.xchDrs[chidx] = drs < 0 ? (DR_SF10|(DR_SF8C<<4)) : drs;
    LMIC.channelMap[chidx>>4] |= (1<<(chidx&0xF));
    return 1;
}


static u1_t mapChannels (u1_t chpage, u2_t chmap) {
    if( chpage == MCMD_LADR_CHP_125ON || chpage == MCMD_LADR_CHP_125OFF ) {
    u2_t en125 = chpage == MCMD_LADR_CHP_125ON ? 0xFFFF : 0x0000;
    for( u1_t u=0; u<4; u++ )
        LMIC.channelMap[u] = en125;
    LMIC.channelMap[64/16] = chmap;
    } else {
    if( chpage >= (72+MAX_XCHANNELS+15)/16 )
        return 0;
    LMIC.channelMap[chpage] = chmap;
    }
    return 1;
}

static void updateTx (ostime_t txbeg) {
    u1_t chnl = LMIC.txChnl;
    if( chnl < 64 ) {
    LMIC.freq = US915_125kHz_UPFBASE + chnl*US915_125kHz_UPFSTEP;
    LMIC.txpow = 30;
    return;
    }
    LMIC.txpow = 26;
    if( chnl < 64+8 ) {
    LMIC.freq = US915_500kHz_UPFBASE + (chnl-64)*US915_500kHz_UPFSTEP;
    } else {
    ASSERT(chnl < 64+8+MAX_XCHANNELS);
    LMIC.freq = LMIC.xchFreq[chnl-72];
    }

    // Update global duty cycle stats
    if( LMIC.globalDutyRate != 0 ) {
    ostime_t airtime = calcAirTime(LMIC.rps, LMIC.dataLen);
    LMIC.globalDutyAvail = txbeg + (airtime<<LMIC.globalDutyRate);
    }
}

// US does not have duty cycling - return now as earliest TX time
#define nextTx(now) (_nextTx(),(now))
static void _nextTx (void) {
    u1_t  chnl  = LMIC.txChnl;
    u1_t  cnt   = 0;
    bit_t bw500 = (LMIC.datarate >= DR_SF8C);

    if( (LMIC.chRnd & 0xF) == 0 ) {
    chnl += LMIC.chRnd>>4;
    LMIC.chRnd = os_getRndU1();
    }
    LMIC.chRnd--;
  again:
    chnl += 1;
    if( bw500 ) {
    // Only channels 64..71
    chnl = 64+(chnl&0x7);
    // At least one 500kHz channel must be enabled
    // - otherwise MAC should not have used such a datarate
    ASSERT((LMIC.channelMap[64/16]&0xFF)!=0x00);
    if( ++cnt == 8 )
        return; // no appropriate channel enabled stay where we are
    } else {
    // At least one 125kHz channel must be enabled
    // - otherwise MAC should not have used DR_SF10-7
    ASSERT((LMIC.channelMap[0]|LMIC.channelMap[1]|LMIC.channelMap[2]|LMIC.channelMap[3])!=0x0000);
    chnl = chnl&0x3F;
    if( ++cnt == 64 )
        return; // no appropriate channel enabled stay where we are
    }
    if( (LMIC.channelMap[(chnl >> 4)] & (1<<(chnl&0xF))) == 0 )
    goto again;  // not enabled
    LMIC.txChnl = chnl;
}

static void setBcnRxParams (void) {
    LMIC.dataLen = 0;
    LMIC.freq = US915_500kHz_DNFBASE + LMIC.bcnChnl*US915_500kHz_DNFSTEP;
    LMIC.rps  = setIh(setNocrc(dndr2rps((dr_t)DR_BCN),1),LEN_BCN);
}

#define setRx1Params(void) {                         \
    LMIC.freq = US915_500kHz_DNFBASE + (LMIC.txChnl & 0x7) * US915_500kHz_DNFSTEP; \
    if( /* TX datarate */LMIC.rxsyms < DR_SF8C )                \
    LMIC.rxsyms += DR_SF10CR - DR_SF10;                \
    else if( LMIC.rxsyms == DR_SF8C )                    \
    LMIC.rxsyms = DR_SF7CR;                        \
    LMIC.rps = dndr2rps(LMIC.rxsyms);                    \
}

static void initJoinLoop (void) {
    LMIC.txChnl = os_getRndU1() & 0x3F;
    LMIC.adrTxPow = 20;
    ASSERT((LMIC.opmode & OP_NEXTCHNL)==0);
    LMIC.txend = os_getTime();
    setDrJoin(DRCHG_SET, DR_SF7);
}

static ostime_t nextJoinState (void) {
    // Try the following:
    //   SF7/8/9/10  on a random channel 0..63
    //   SF8C        on a random channel 64..71
    //
    LMIC.opmode &= ~OP_NEXTCHNL;
    LMIC.txend = os_getTime();
    if( LMIC.datarate != DR_SF8C ) {
    LMIC.txChnl = 64+(LMIC.txChnl&7);
    LMIC.datarate = DR_SF8C;
    } else {
    LMIC.txChnl = os_getRndU1() & 0x3F;
    LMIC.datarate = DR_SF7 - (LMIC.txCnt++ & 3);
    if( (LMIC.txCnt & 3) == 0 ) {
        // Tried SF10/SF8C
        // Delay by 7,15,31,63,127,255 secs
        return rndDelay((4<<(LMIC.txCnt>>2))-1) | 1;   // |1 - trigger EV_JOIN_FAILED event
    }
    }
    // Always wait 500ms in case there was a TX and we just missed it.
    // Sending immediately would not be picked up because GW is still busy txing missed frame.
    return (OSTICKS_PER_SEC/2) & ~1;
}

//
// END: US915 related stuff
//
// ================================================================================
#else
#error Unsupported frequency band!
#endif


static void runEngineUpdate (xref2osjob_t osjob) {
    engineUpdate();
}


static void reportEvent (ev_t ev) {
    EV(devCond, INFO, (e_.reason = EV::devCond_t::LMIC_EV,
               e_.eui    = MAIN::CDEV->getEui(),
               e_.info   = ev));
    ON_LMIC_EVENT(ev);
    engineUpdate();
}


static void stateJustJoined (void) {
    LMIC.seqnoDn     = LMIC.seqnoUp = 0;
    LMIC.rejoinCnt   = 0;
    LMIC.dnConf      = LMIC.adrChanged = LMIC.ladrAns = LMIC.devsAns = 0;
    LMIC.moreData    = LMIC.dn2Ans = LMIC.snchAns = LMIC.dutyCapAns = 0;
    LMIC.pingSetAns  = 0;
    LMIC.upRepeat    = 0;
    LMIC.adrAckReq   = LINK_CHECK_INIT;
    LMIC.dn2Dr       = DR_DNW2;
    LMIC.dn2Freq     = FREQ_DNW2;
    LMIC.bcnChnl     = CHNL_BCN;
    LMIC.ping.freq   = FREQ_PING;
    LMIC.ping.dr     = DR_PING;
}


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


// 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( os_rlsbf2(&d[OFF_BCN_CRC1]) != os_crc16(d, OFF_BCN_CRC1) )
    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
    os_copyMem((xref2u1_t)&LMIC.bcninfo.rxq, (xref2u1_t)&LMIC.rxq, SIZEOFEXPR(LMIC.rxq));
    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;
}


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( 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:
    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(specCond, ERR, (e_.reason = EV::specCond_t::CORRUPTED_MIC,
               e_.eui    = MAIN::CDEV->getEui(),
               e_.info   = Base::lsbf4(&d[pend]),
               e_.info2  = seqno));
    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(updateSeqnoDn, LMIC.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 = 0;
    LMIC.adrAckReq = LINK_CHECK_INIT;

    // Process OPTS
    int m = LMIC.rxq.rssi - RSSI_OFF - getSensitivity(LMIC.rps);
    LMIC.margin = m < 0 ? 0 : m > 254 ? 254 : m;

    xref2u1_t opts = &d[OFF_DAT_OPTS];
    int oidx = 0;
    while( oidx < olen ) {
    switch( opts[oidx] ) {
    case MCMD_LCHK_ANS: {
        //int gwmargin = opts[oidx+1];
        //int ngws = opts[oidx+2];
        oidx += 3;
        continue;
    }
    case MCMD_LADR_REQ: {
        u1_t p1     = opts[oidx+1];            // txpow + DR
        u2_t chmap  = os_rlsbf2(&opts[oidx+2]);// list of enabled channel
        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
        oidx += 5;

        LMIC.ladrAns = 0x80 |     // Include an answer into next frame up
        MCMD_LADR_ANS_POWACK | MCMD_LADR_ANS_CHACK | MCMD_LADR_ANS_DRACK;
        if( !mapChannels(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;
        dr = (dr_t)LMIC.datarate;
        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])));
        }
        LMIC.upRepeat = uprpt;
        setDrTxpow(DRCHG_NWKCMD, dr, pow2dBm(p1));
        LMIC.adrChanged = 1;  // Trigger an ACK to NWK
        continue;
    }
    case MCMD_DEVS_REQ: {
        LMIC.devsAns = 1;
        oidx += 1;
        continue;
    }
    case MCMD_DN2P_SET: {
        dr_t dr = (dr_t)(opts[oidx+1] & 0x0F);
        u4_t freq = convFreq(&opts[oidx+2]);
        oidx += 5;
        LMIC.dn2Ans = 0x80;   // answer pending
        if( validDR(dr) )
        LMIC.dn2Ans |= MCMD_DN2P_ANS_DRACK;
        if( freq != 0 )
        LMIC.dn2Ans |= MCMD_DN2P_ANS_CHACK;
        if( LMIC.dn2Ans == (0x80|MCMD_DN2P_ANS_DRACK|MCMD_DN2P_ANS_CHACK) ) {
        LMIC.dn2Dr = dr;
        LMIC.dn2Freq = freq;
        DO_DEVDB(updateDn2,LMIC.dn2Dr,LMIC.dn2Freq);
        }
        continue;
    }
    case MCMD_DCAP_REQ: {
        u1_t cap = opts[oidx+1];
        oidx += 2;
        // A value cap=0xFF means device is OFF unless enabled again manually
        // We just set duty cap to 0xF which is 0.003% -- pretty much off.
        // We don't check 0xF0 bits if cap!=0xFF
        LMIC.globalDutyRate  = cap & 0xF;
        LMIC.globalDutyAvail = os_getTime();
        DO_DEVDB(updateDutyCap,cap);
        LMIC.dutyCapAns = 1;
        continue;
    }
    case MCMD_SNCH_REQ: {
        u1_t chidx = opts[oidx+1];  // channel
        u4_t freq  = convFreq(&opts[oidx+2]); // freq
        u1_t drs   = opts[oidx+5];  // datarate span
        LMIC.snchAns = 0x80;
        if( freq != 0 && setupChannel(chidx,freq,drs) )
        LMIC.snchAns |= MCMD_SNCH_ANS_DRACK|MCMD_SNCH_ANS_FQACK;
        oidx += 6;
    }
    case MCMD_PING_SET: {
        u4_t freq = convFreq(&opts[oidx+1]);
        oidx += 4;
        u1_t flags = 0x80;
        if( freq != 0 ) {
        flags |= MCMD_PING_ANS_FQACK;
        LMIC.ping.freq = freq;
        DO_DEVDB(updateClassB,LMIC.ping.intvExp,freq,LMIC.ping.dr);
        }
        LMIC.pingSetAns = flags;
        continue;
    }
    case MCMD_BCNI_ANS: {
        // 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
                       - calcAirTime(LMIC.rps, LMIC.dataLen)
                       + ms2osticks(os_rlsbf2(&opts[oidx+1]) * 10)
                       + ms2osticksCeil(5)
                       - BCN_INTV_osticks);
        LMIC.bcninfo.flags = 0;  // txtime above cannot be used as reference (BCN_PARTIAL|BCN_FULL cleared)
        calcBcnRxWindowFromMillis(10,1);  // error of +/-5 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)));
        }
        oidx += 4;
        continue;
    }
    }
    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])));
    break;
    }
    if( oidx != olen ) {
    EV(specCond, ERR, (e_.reason = EV::specCond_t::CORRUPTED_FRAME,
               e_.eui    = MAIN::CDEV->getEui(),
               e_.info   = 0x1000000 + (oidx) + (olen<<8)));
    }

    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);

    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_.olen    = olen,
               memcpy(e_.opts, 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.txCnt != 0 ) // we requested an ACK
    LMIC.txrxFlags |= ackup ? TXRX_ACK : TXRX_NACK;

    if( port < 0 ) {
    LMIC.txrxFlags |= TXRX_NOPORT;
    LMIC.dataBeg = LMIC.dataLen = 0;
    } else {
    LMIC.dataBeg = poff;
    LMIC.dataLen = pend-poff;
    }
    return 1;
}


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


static void setupRx2 (void) {
    LMIC.txrxFlags = TXRX_DNW2;
    LMIC.rps = dndr2rps(LMIC.dn2Dr);
    LMIC.freq = LMIC.dn2Freq;
    LMIC.dataLen = 0;
    os_radio(RADIO_RX);
}


static void schedRx2 (ostime_t delay, osjobcb_t func) {
    // Add 1.5 symbols we need 5 out of 8. Try to sync 1.5 symbols into the preamble.
    LMIC.rxtime = LMIC.txend + delay + (PAMBL_SYMS-MINRX_SYMS)*dr2hsym(LMIC.dn2Dr);
    os_setTimedCallback(&LMIC.osjob, LMIC.rxtime - RX_RAMPUP, func);
}

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


// 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) {
    // NOTE: LMIC.rxsyms carries the modified DR for TX (LMIC.datarate is the base DR - [CONFIRM mode])
    u1_t dr = LMIC.rxsyms;    // rxschedInit - would overwrite rxsyms
    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;
    }
    // Change RX frequency / rps (US only) before we increment txChnl
    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 CFG_eu868
    if( /* TX datarate */LMIC.rxsyms == DR_FSK ) {
    LMIC.rxtime = LMIC.txend + delay - PRERX_FSK*us2osticksRound(160);
    LMIC.rxsyms = RXLEN_FSK;
    }
    else
#endif
    {
    LMIC.rxtime = LMIC.txend + delay + (PAMBL_SYMS-MINRX_SYMS)*dr2hsym(dr);
    LMIC.rxsyms = MINRX_SYMS;
    }
    os_setTimedCallback(&LMIC.osjob, LMIC.rxtime - RX_RAMPUP, func);
}


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


static void onJoinFailed (xref2osjob_t osjob) {
    // Notify app - must call LMIC_reset() to stop joining
    // otherwise join procedure continues.
    reportEvent(EV_JOIN_FAILED);
}


static bit_t processJoinAccept (void) {
    ASSERT(LMIC.txrxFlags != TXRX_DNW1 || LMIC.dataLen != 0);
    ASSERT((LMIC.opmode & OP_TXRXPEND)!=0);

    if( LMIC.dataLen == 0 ) {
      nojoinframe:
    if( (LMIC.opmode & OP_OTA) != 0 ) {
    if( (LMIC.opmode & OP_JOINING) == 0 ) {
        ASSERT((LMIC.opmode & OP_REJOIN) != 0);
        // REJOIN attempt for roaming
        LMIC.opmode &= ~(OP_REJOIN|OP_TXRXPEND);
        if( LMIC.rejoinCnt < 10 )
        LMIC.rejoinCnt++;
        reportEvent(EV_REJOIN_FAILED);
        return 1;
    }
    }
    LMIC.opmode &= ~OP_TXRXPEND;
    ostime_t delay = nextJoinState();
    EV(devCond, DEBUG, (e_.reason = EV::devCond_t::NO_JACC,
                e_.eui    = MAIN::CDEV->getEui(),
                e_.info   = LMIC.datarate|DR_PAGE,
                e_.info2  = osticks2ms(delay)));
    // Build next JOIN REQUEST with next engineUpdate call
    // Optionally, report join failed.
    // Both after a random/chosen amount of ticks.
    os_setTimedCallback(&LMIC.osjob, os_getTime()+delay,
                (delay&1) != 0
                ? FUNC_ADDR(onJoinFailed)      // one JOIN iteration done and failed
                : FUNC_ADDR(runEngineUpdate)); // next step to be delayed
    return 1;
    }
    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!
    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)));
      badframe:
    goto nojoinframe;
    }
    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));
    goto badframe;
    }

    u4_t addr = os_rlsbf4(LMIC.frame+OFF_JA_DEVADDR);
    LMIC.devaddr = addr;
    LMIC.netid = os_rlsbf4(&LMIC.frame[OFF_BCN_NETID-1]) >> 8;

#if CFG_eu868
    initDefaultChannels(0);
#endif
    if( dlen > LEN_JA ) {
    dlen = OFF_CFLIST;
#if CFG_eu868
    u1_t chidx=3;
#elif CFG_us915
    u1_t chidx=72;
#endif
    for( ; chidx<8; chidx++, dlen+=3 )
        setupChannel(chidx, os_rlsbf4(&LMIC.frame[dlen-1]) >> 8, -1);
    }

    // already incremented when JOIN REQ got sent off
    aes_sessKeys(LMIC.devNonce-1, &LMIC.frame[OFF_JA_ARTNONCE], LMIC.nwkKey, LMIC.artKey);
    DO_DEVDB(updateJoinAcc, LMIC.devaddr, LMIC.nwkKey, LMIC.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_.flags   = ((LMIC.opmode & OP_REJOIN) != 0
                      ? EV::joininfo_t::REJOIN_ACCEPT
                      : EV::joininfo_t::ACCEPT)));
    
    ASSERT((LMIC.opmode & (OP_JOINING|OP_REJOIN))!=0);
    if( (LMIC.opmode & OP_REJOIN) != 0 ) {
    LMIC.datarate = lowerDR(LMIC.datarate, LMIC.rejoinCnt);
    }
    LMIC.opmode &= ~(OP_JOINING|OP_TRACK|OP_REJOIN|OP_TXRXPEND|OP_PINGINI) | OP_NEXTCHNL;
    stateJustJoined();
    reportEvent(EV_JOINED);
    return 1;
}

static void processRx1Jacc (xref2osjob_t osjob) {
    if( LMIC.dataLen == 0 )
    LMIC.txrxFlags = 0;  // nothing in 1st/2nd DN slot
    processJoinAccept();
}

static void setupRx1Jacc (xref2osjob_t osjob) {
    setupRx1(FUNC_ADDR(processRx1Jacc));
}


static void jreqDone (xref2osjob_t osjob) {
    txDone(DELAY_JACC1_osticks, FUNC_ADDR(setupRx1Jacc));
}

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

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

static void processRx2DnData (xref2osjob_t osjob) {
    if( LMIC.dataLen == 0 )
    LMIC.txrxFlags = 0;  // nothing in 1st/2nd DN slot
    processDnData();
}


static void setupRx2DnData (xref2osjob_t osjob) {
    LMIC.osjob.func = FUNC_ADDR(processRx2DnData);
    setupRx2();
}


static void processRx1DnData (xref2osjob_t osjob) {
    if( LMIC.dataLen == 0 || !processDnData() )
    schedRx2(DELAY_DNW2_osticks, FUNC_ADDR(setupRx2DnData));
}


static void setupRx1DnData (xref2osjob_t osjob) {
    setupRx1(FUNC_ADDR(processRx1DnData));
}


static void updataDone (xref2osjob_t osjob) {
    txDone(DELAY_DNW1_osticks, 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( (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;
    }
    if( LMIC.dutyCapAns ) {
    LMIC.frame[end] = MCMD_DCAP_ANS;
    end += 1;
    LMIC.dutyCapAns = 0;
    }
    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;
    }
    if( LMIC.devsAns ) {  // answer to device status
    LMIC.frame[end+0] = MCMD_DEVS_ANS;
    LMIC.frame[end+1] = LMIC.margin;
    LMIC.frame[end+2] = os_getBattLevel();
    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( LMIC.bcninfoTries > 0 ) {
    LMIC.frame[end] = MCMD_BCNI_REQ;
    end += 1;
    }
    if( LMIC.adrChanged ) {
    LMIC.adrAckReq = LMIC.adrAckReq < 0 ? 0 : LMIC.adrAckReq;
    LMIC.adrChanged = 0;
    }
    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;
    }
    if( LMIC.snchAns ) {
    LMIC.frame[end+0] = MCMD_SNCH_ANS;
    LMIC.frame[end+1] = LMIC.snchAns;
    end += 2;
    LMIC.snchAns = 0;
    }
    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 >= 0 ? 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(updateSeqnoUp, LMIC.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) | (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;
        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_.olen    = end-LORA::OFF_DAT_OPTS,
               memcpy(e_.opts, LMIC.frame+LORA::OFF_DAT_OPTS, end-LORA::OFF_DAT_OPTS)));
    LMIC.dataLen = flen;
}


// Callback from HAL during scan mode or when job timer expires.
static void onBcnRx (xref2osjob_t job) {
    // 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);
    reportEvent(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
    reportEvent(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);
    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();
}


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

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_.flags   = ((LMIC.opmode & OP_REJOIN) != 0
                    ? EV::joininfo_t::REJOIN_REQUEST
                    : EV::joininfo_t::REQUEST)));
    LMIC.dataLen = LEN_JR;
    LMIC.devNonce++;
    DO_DEVDB(updateDevNonce, LMIC.devNonce);
}

static void startJoining (xref2osjob_t osjob) {
    reportEvent(EV_JOINING);
}

// 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);
    // Cancel scanning
    LMIC.opmode &= ~(OP_SCAN|OP_REJOIN|OP_LINKDEAD|OP_NEXTCHNL);
    // Setup state
    LMIC.rejoinCnt = LMIC.txCnt = 0;
    initJoinLoop();
    LMIC.opmode |= OP_OTA|OP_JOINING;
    // reportEvent will call engineUpdate which then starts sending JOIN REQUESTS
    os_setCallback(&LMIC.osjob, FUNC_ADDR(startJoining));
    return 1;
    }
    return 0;
}


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

static void processPingRx (xref2osjob_t osjob) {
    if( LMIC.dataLen != 0 ) {
    LMIC.txrxFlags = TXRX_PING;
    if( decodeFrame() ) {
        reportEvent(EV_RXCOMPLETE);
        return;
    }
    }
    // Pick next ping slot
    engineUpdate();
}


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 ) {
        // Schedule another retransmission
        txDelay(LMIC.rxtime, RETRY_PERIOD_secs);
        LMIC.opmode &= ~OP_TXRXPEND;
        engineUpdate();
        return 1;
        }
        LMIC.txrxFlags = TXRX_NACK | TXRX_NOPORT;
    } else {
        // Nothing received - implies no port
        LMIC.txrxFlags = TXRX_NOPORT;
    }
    LMIC.adrAckReq += 1;
    LMIC.dataBeg = LMIC.dataLen = 0;
      txcomplete:
    LMIC.opmode &= ~(OP_TXDATA|OP_TXRXPEND);
    if( (LMIC.txrxFlags & (TXRX_DNW1|TXRX_DNW2|TXRX_PING)) != 0  &&  (LMIC.opmode & OP_LINKDEAD) != 0 ) {
        LMIC.opmode &= ~OP_LINKDEAD;
        reportEvent(EV_LINK_ALIVE);
    }
    reportEvent(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));
        setDrTxpow(DRCHG_NOADRACK, decDR((dr_t)LMIC.datarate), KEEP_TXPOW);
        LMIC.adrAckReq = LINK_CHECK_CONT;
        if( (LMIC.opmode & OP_OTA) != 0 ) {
            LMIC.opmode |= OP_REJOIN|OP_LINKDEAD;
        }
        else {
            LMIC.opmode |= OP_LINKDEAD;
        }
        reportEvent(EV_LINK_DEAD);
    }
    // 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 ) {
        reportEvent(EV_BEACON_FOUND);
        LMIC.bcninfoTries = 0;
        }
        else if( --LMIC.bcninfoTries == 0 ) {
        startScan();   // NWK did not answer - try scan
        }
    }
    return 1;
    }
    if( !decodeFrame() ) {
    if( (LMIC.txrxFlags & TXRX_DNW1) != 0 )
        return 0;
    goto norx;
    }
    // If we received a frame reset counter
    LMIC.adrAckReq = LINK_CHECK_INIT;
    LMIC.rejoinCnt = 0;
    goto txcomplete;
}


static void processBeacon (xref2osjob_t 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_.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.opmode & OP_OTA) != 0 ) {
    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);
        reportEvent(EV_LOST_TSYNC);
        return;
    }
    }
    LMIC.bcnRxtime = LMIC.bcninfo.txtime + BCN_INTV_osticks - calcRxWindow(0,DR_BCN);
    LMIC.bcnRxsyms = LMIC.rxsyms;    
  rev:
    if( (LMIC.opmode & OP_PINGINI) != 0 )
    rxschedInit(&LMIC.ping);  // note: reuses LMIC.frame buffer!
    reportEvent(ev);
}


static void startRxBcn (xref2osjob_t osjob) {
    LMIC.osjob.func = FUNC_ADDR(processBeacon);
    os_radio(RADIO_RX);
}


static void startRxPing (xref2osjob_t osjob) {
    LMIC.osjob.func = FUNC_ADDR(processPingRx);
    os_radio(RADIO_RX);
}


// Decide what to do next for the MAC layer of a device
static void engineUpdate (void) {
    // Check for ongoing state: scan or TX/RX transaction
    if( (LMIC.opmode & (OP_SCAN|OP_TXRXPEND|OP_SHUTDOWN)) != 0 ) 
    return;

    if( LMIC.devaddr == 0 && (LMIC.opmode & OP_JOINING) == 0 ) {
    LMIC_startJoining();
    return;
    }

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

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

    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 = 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 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;
    }
    // Earliest possible time vs overhead to setup radio
    if( txbeg - (now + TX_RAMPUP) < 0 ) {
        // We could send right now!
        dr_t txdr = (dr_t)LMIC.datarate;
        if( jacc ) {
        u1_t ftype;
        if( (LMIC.opmode & OP_REJOIN) != 0 ) {
            txdr = lowerDR(txdr, LMIC.rejoinCnt);
            ftype = HDR_FTYPE_REJOIN;
        } else {
            ftype = HDR_FTYPE_JREQ;
        }
        buildJoinRequest(ftype);
        LMIC.osjob.func = FUNC_ADDR(jreqDone);
        } else {
        if( 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()));
            // Rerun join procedure - start with current datarate
            LMIC.devaddr = 0;
            LMIC_startJoining();
            reportEvent(EV_RESET);
            return;
        }
        buildDataFrame();
        // NOTE: a channel decision for LMIC.txChnl above will never be rendered invalid
        // by chosing a slower datarate (it could be invalided ONLY by a faster datarate).
        txdr = lowerDR(txdr, DRADJUST[LMIC.txCnt]);
        LMIC.osjob.func = FUNC_ADDR(updataDone);
        }
        LMIC.rps    = setCr(updr2rps(txdr), (cr_t)LMIC.errcr);
        LMIC.rxsyms = txdr;  // carry TX datarate (can be != LMIC.datarate) over to txDone/setupRx1
        LMIC.opmode = (LMIC.opmode & ~(OP_POLL|OP_RNDTX)) | OP_TXRXPEND | OP_NEXTCHNL;
        updateTx(txbeg);
        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;
    }

    // Are we pingable?
  checkrx:
    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
    }

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

    setBcnRxParams();
    LMIC.rxsyms = LMIC.bcnRxsyms;
    LMIC.rxtime = LMIC.bcnRxtime;
    if( now - rxtime >= 0 ) {
    LMIC.osjob.func = FUNC_ADDR(processBeacon);
    os_radio(RADIO_RX);
    return;
    }
    os_setTimedCallback(&LMIC.osjob, rxtime, FUNC_ADDR(startRxBcn));
    return;

  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));
    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;
}


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);

    os_clearMem((xref2u1_t)&LMIC,SIZEOFEXPR(LMIC));
    LMIC.devaddr     = 0;
    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 2ns DN window of join accept
    LMIC.dn2Freq     = FREQ_DNW2; // ditto
#if CFG_us915
    initDefaultChannels();
#endif
}


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


void LMIC_clrTxData (void) {
    LMIC.opmode &= ~(OP_TXDATA|OP_TXRXPEND|OP_POLL);
    LMIC.pendTxLen = 0;
    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();
}


//
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 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();
}

void LMIC_startABP(u4_t netid, devaddr_t devaddr, u1_t* nwkKey, u1_t* artKey)
{
    LMIC.netid = netid;
    LMIC.devaddr = devaddr;
    memcpy(LMIC.nwkKey, nwkKey, 16);
    memcpy(LMIC.artKey, artKey, 16);
    
#if CFG_eu868
    initDefaultChannels(0);
#endif

    LMIC.opmode &= ~(OP_JOINING|OP_TRACK|OP_REJOIN|OP_TXRXPEND|OP_PINGINI) | OP_NEXTCHNL;
    stateJustJoined();
    //engineUpdate();
    reportEvent(EV_JOINED);
}