Text menu driven ANSI/VT100 console test utility for LoRa transceivers

radio chip selection

Radio chip driver is not included, allowing choice of radio device.
If you're using SX1272 or SX1276, then import sx127x driver into your program.
if you're using SX1261 or SX1262, then import sx126x driver into your program.
if you're using SX1280, then import sx1280 driver into your program.
if you're using LR1110, then import LR1110 driver into your program.
If you're using NAmote72 or Murata discovery, then you must import only sx127x driver.
If you're using Type1SJ select target DISCO_L072CZ_LRWAN1 and import sx126x driver into your program.

This is VT100 text-based menu driven test program for SX12xx transceiver devices.
Serial console is divided into horizontally into top half and bottom half.
The bottom half serves as scrolling area to log activity.
The top half serves as menu, to configure the radio.
For all devices, the serial console operates at 115200 8N1, and requires terminal with ANSI-VT100 capability, such as putty/teraterm/minicom etc.
Use program only with keyboard up/down/left/right keys. Enter to change an item, or number for value item. Some items are single bit, requiring only enter key to toggle. Others with fixed choices give a drop-down menu.

main.cpp

Committer:
Wayne Roberts
Date:
2018-11-01
Revision:
3:56fc764dee0a
Parent:
2:ea9245bb1c53
Child:
4:fa31fdf4ec8d

File content as of revision 3:56fc764dee0a:

#include "mbed.h"
#include "radio.h"

//#define MENU_DEBUG

RawSerial pc(USBTX, USBRX);

menuState_t menuState;

typedef enum {
    MSG_TYPE_PACKET = 0,
    MSG_TYPE_PER,
    MSG_TYPE_PINGPONG
} msgType_e;

#define MAX_MENU_ROWS       16
// [row][column]
const menu_t* menu_table[MAX_MENU_ROWS][MAX_MENU_COLUMNS];
int8_t StopMenuCols[MAX_MENU_ROWS];

#define SCROLLING_ROWS      20

struct _cp_ {
    uint8_t row;
    uint8_t tableCol;
} curpos;

uint8_t entry_buf_idx;
char entry_buf[64];

msgType_e msg_type;

const uint8_t PingMsg[] = "PING";
const uint8_t PongMsg[] = "PONG";
const uint8_t PerMsg[]  = "PER";

volatile struct _flags_ {
    uint8_t ping_master     : 1; // 0
    uint8_t send_ping       : 1; // 1
    uint8_t send_pong       : 1; // 2
    uint8_t pingpongEnable  : 1; // 3
} flags;

uint8_t botRow;

unsigned lfsr;
#define LFSR_INIT       0x1ff

uint8_t get_pn9_byte()
{
    uint8_t ret = 0;
    int xor_out;

    xor_out = ((lfsr >> 5) & 0xf) ^ (lfsr & 0xf);   // four bits at a time
    lfsr = (lfsr >> 4) | (xor_out << 5);    // four bits at a time

    ret |= (lfsr >> 5) & 0x0f;

    xor_out = ((lfsr >> 5) & 0xf) ^ (lfsr & 0xf);   // four bits at a time
    lfsr = (lfsr >> 4) | (xor_out << 5);    // four bits at a time

    ret |= ((lfsr >> 1) & 0xf0);

    return ret;
}

void hw_reset_push()
{
    Radio::hw_reset();

    Radio::readChip();
    menuState.mode = MENUMODE_REINIT_MENU;
}

const button_item_t hw_reset_item = { _ITEM_BUTTON, "hw_reset", hw_reset_push };

void clearIrqs_push()
{
    Radio::clearIrqFlags();
}

const button_item_t clearirqs_item = { _ITEM_BUTTON, "clearIrqs", clearIrqs_push };

const dropdown_item_t pktType_item = { _ITEM_DROPDOWN, Radio::pktType_strs, Radio::pktType_strs, Radio::pktType_read, Radio::pktType_write};


unsigned msgType_read(bool fw)
{
    return msg_type;
}

menuMode_e msgType_write(unsigned widx)
{
    msg_type = (msgType_e)widx;

    if (msg_type == MSG_TYPE_PER) {
        flags.pingpongEnable = 0;
        if (Radio::get_payload_length() < 7)
            Radio::set_payload_length(7);
    } else if (msg_type == MSG_TYPE_PINGPONG) {
        if (Radio::get_payload_length() != 12)
            Radio::set_payload_length(12);
    } else if (msg_type == MSG_TYPE_PACKET) {
        flags.pingpongEnable = 0;
    }

    return MENUMODE_REINIT_MENU;
}

const char* const msgType_strs[] = {
    "PACKET ",
    "PER    ",
    "PINGPONG",
    NULL
};

const dropdown_item_t msgType_item = { _ITEM_DROPDOWN, msgType_strs, msgType_strs, msgType_read, msgType_write};

#define LAST_CHIP_MENU_ROW          (MAX_MENU_ROWS-5)

const value_item_t tx_payload_length_item = { _ITEM_VALUE, 5, Radio::tx_payload_length_print, Radio::tx_payload_length_write};

void pn9_push()
{
    uint8_t i, len = Radio::get_payload_length();
    for (i = 0; i < len; i++)
        Radio::radio.tx_buf[i] = get_pn9_byte();
}

const button_item_t tx_payload_pn9_item  = { _ITEM_BUTTON, "PN9", pn9_push };

void tx_payload_print()
{
    uint8_t i, len = Radio::get_payload_length();
    for (i = 0; i < len; i++)
        pc.printf("%02x ", Radio::radio.tx_buf[i]);
}

bool tx_payload_write(const char* txt)
{
    unsigned o, i = 0, pktidx = 0;
    while (i < entry_buf_idx) {
        sscanf(entry_buf+i, "%02x", &o);
        pc.printf("%02x ", o);
        i += 2;
        Radio::radio.tx_buf[pktidx++] = o;
        while (entry_buf[i] == ' ' && i < entry_buf_idx)
            i++;
    }
    log_printf("set payload len %u\r\n", pktidx);
    Radio::set_payload_length(pktidx);
    return true;
}

const value_item_t tx_payload_item = { _ITEM_VALUE, 160, tx_payload_print, tx_payload_write};

void txpkt_push()
{
    Radio::txPkt();
}

const button_item_t tx_pkt_item  = { _ITEM_BUTTON, "TXPKT", txpkt_push };

void rxpkt_push()
{
    Radio::Rx();
    menuState.mode = MENUMODE_REDRAW;
}
const button_item_t rx_pkt_item  = { _ITEM_BUTTON, "RXPKT", rxpkt_push };

const dropdown_item_t opmode_item = { _ITEM_DROPDOWN, Radio::opmode_status_strs, Radio::opmode_select_strs, Radio::opmode_read, Radio::opmode_write };

void tx_carrier_push()
{
    Radio::tx_carrier();
}
const button_item_t tx_carrier_item = { _ITEM_BUTTON, "TX_CARRIER", tx_carrier_push };

void tx_preamble_push()
{
    Radio::tx_preamble();
}
const button_item_t tx_preamble_item = { _ITEM_BUTTON, "TX_PREAMBLE", tx_preamble_push };


const menu_t msg_pkt_menu[] = {
    { {LAST_CHIP_MENU_ROW+1,  1}, "tx payload length:", &tx_payload_length_item, FLAG_MSGTYPE_PKT },
    { {LAST_CHIP_MENU_ROW+1, 25},                 NULL,    &tx_payload_pn9_item, FLAG_MSGTYPE_PKT, &tx_payload_item },
    { {LAST_CHIP_MENU_ROW+2,  1},                 NULL,        &tx_payload_item, FLAG_MSGTYPE_PKT },
    { {LAST_CHIP_MENU_ROW+3,  1},                 NULL,            &tx_pkt_item, FLAG_MSGTYPE_PKT, &opmode_item },
    { {LAST_CHIP_MENU_ROW+3, 10},                 NULL,            &rx_pkt_item, FLAG_MSGTYPE_PKT },
    { {LAST_CHIP_MENU_ROW+3, 20},                 NULL,        &tx_carrier_item, FLAG_MSGTYPE_PKT, &opmode_item },
    { {LAST_CHIP_MENU_ROW+3, 45},                 NULL,       &tx_preamble_item, FLAG_MSGTYPE_PKT, &opmode_item },

    { {0, 0}, NULL, NULL }
};

unsigned tx_ipd_ms;
Timeout mbedTimeout;
unsigned MaxNumPacket;
unsigned CntPacketTx;
unsigned PacketRxSequencePrev;
unsigned CntPacketRxKO;
unsigned CntPacketRxOK;
unsigned RxTimeOutCount;
unsigned receivedCntPacket;

void next_tx_callback()
{
    Radio::radio.tx_buf[0] = CntPacketTx >> 24;
    Radio::radio.tx_buf[1] = CntPacketTx >> 16;
    Radio::radio.tx_buf[2] = CntPacketTx >> 8;
    Radio::radio.tx_buf[3] = CntPacketTx;

    Radio::radio.tx_buf[4] = PerMsg[0];
    Radio::radio.tx_buf[5] = PerMsg[1];
    Radio::radio.tx_buf[6] = PerMsg[2];

    Radio::txPkt();
}


void pertx_push()
{
    CntPacketTx = 1;    // PacketRxSequencePrev initialized to 0 on receiver

    log_printf("do perTx\r\n");

    next_tx_callback();
}


const button_item_t pertx_item = { _ITEM_BUTTON, "PERTX", pertx_push };

void tx_ipd_print()
{
    pc.printf("%u", tx_ipd_ms);
}

bool tx_ipd_write(const char* valStr)
{
    sscanf(valStr, "%u", &tx_ipd_ms);
    return false;
}

value_item_t per_ipd_item = { _ITEM_VALUE, 4, tx_ipd_print, tx_ipd_write };

void numpkts_print()
{
    pc.printf("%u", MaxNumPacket);
}

bool numpkts_write(const char* valStr)
{
    sscanf(valStr, "%u", &MaxNumPacket);
    return false;
}

value_item_t per_numpkts_item = { _ITEM_VALUE, 6, numpkts_print, numpkts_write };

void perrx_push()
{
    PacketRxSequencePrev = 0;
    CntPacketRxKO = 0;
    CntPacketRxOK = 0;
    RxTimeOutCount = 0;

    Radio::Rx();
}

const button_item_t perrx_item = { _ITEM_BUTTON, "PERRX", perrx_push };

void per_reset_push()
{
    PacketRxSequencePrev = 0;
    CntPacketRxKO = 0;
    CntPacketRxOK = 0;
    RxTimeOutCount = 0;
}

const button_item_t per_clear_item = { _ITEM_BUTTON, "resetCount", per_reset_push };

const menu_t msg_per_menu[] = {
    { {LAST_CHIP_MENU_ROW+3,  1},          NULL,       &pertx_item, FLAG_MSGTYPE_PER },
    { {LAST_CHIP_MENU_ROW+3,  12}, "TX IPD ms:",     &per_ipd_item, FLAG_MSGTYPE_PER },
    { {LAST_CHIP_MENU_ROW+3,  26},   "numPkts:", &per_numpkts_item, FLAG_MSGTYPE_PER },
    { {LAST_CHIP_MENU_ROW+3,  40},         NULL,       &perrx_item, FLAG_MSGTYPE_PER, &opmode_item },
    { {LAST_CHIP_MENU_ROW+3,  50},         NULL,   &per_clear_item, FLAG_MSGTYPE_PER, &opmode_item },
    { {0, 0}, NULL, NULL }
};

void SendPong()
{
    unsigned failCnt = CntPacketRxKO + RxTimeOutCount;

    /* ping slave tx */
    log_printf("ACK PKT%u\r\n", receivedCntPacket);

    Radio::radio.tx_buf[ 0] = receivedCntPacket >> 24;
    Radio::radio.tx_buf[ 1] = receivedCntPacket >> 16;
    Radio::radio.tx_buf[ 2] = receivedCntPacket >> 8;
    Radio::radio.tx_buf[ 3] = receivedCntPacket;
    Radio::radio.tx_buf[ 4] = failCnt >> 24;
    Radio::radio.tx_buf[ 5] = failCnt >> 16;
    Radio::radio.tx_buf[ 6] = failCnt >> 8;
    Radio::radio.tx_buf[ 7] = failCnt;
    Radio::radio.tx_buf[ 8] = PongMsg[0];
    Radio::radio.tx_buf[ 9] = PongMsg[1];
    Radio::radio.tx_buf[10] = PongMsg[2];
    Radio::radio.tx_buf[11] = PongMsg[3];

    Radio::txPkt();
}

void SendPing()
{
    /* ping master tx */

    log_printf("MASTER > PING PKT%u\r\n", CntPacketTx);
    Radio::radio.tx_buf[0] = CntPacketTx >> 24;
    Radio::radio.tx_buf[1] = CntPacketTx >> 16;
    Radio::radio.tx_buf[2] = CntPacketTx >> 8;
    Radio::radio.tx_buf[3] = CntPacketTx;
    Radio::radio.tx_buf[4] = PingMsg[0];
    Radio::radio.tx_buf[5] = PingMsg[1];
    Radio::radio.tx_buf[6] = PingMsg[2];
    Radio::radio.tx_buf[7] = PingMsg[3];

    Radio::txPkt();
}

void
pingpong_start_push()
{
    CntPacketTx = 1;
    CntPacketRxKO = 0;
    CntPacketRxOK = 0;
    RxTimeOutCount = 0;
    receivedCntPacket = 0;

    flags.send_ping = 1;

    flags.ping_master = 1;

    flags.pingpongEnable = 1;
    log_printf("ping start\r\n");
}

const button_item_t pingpong_start_item = { _ITEM_BUTTON, "START", pingpong_start_push };

void
pingpong_stop_push ()
{
    flags.pingpongEnable = 0;
}

const button_item_t pingpong_stop_item = { _ITEM_BUTTON, "STOP", pingpong_stop_push };

const menu_t msg_pingpong_menu[] = {
    { {LAST_CHIP_MENU_ROW+3,  1}, NULL, &pingpong_start_item, FLAG_MSGTYPE_PING },
    { {LAST_CHIP_MENU_ROW+3, 15}, NULL,  &pingpong_stop_item, FLAG_MSGTYPE_PING },
    { {0, 0}, NULL, NULL }
};

const menu_t* get_msg_menu()
{
    switch (msg_type) {
        case MSG_TYPE_PACKET:
            return msg_pkt_menu;
        case MSG_TYPE_PER:
            return msg_per_menu;
        case MSG_TYPE_PINGPONG:
            return msg_pingpong_menu;
    }
    return NULL;
}

void frf_print()
{
    float MHz;
#ifdef SX127x_H
    MHz = Radio::radio.get_frf_MHz();
#else
    MHz = Radio::radio.getMHz();
#endif
    pc.printf("%.3fMHz", MHz);
}

bool frf_write(const char* valStr)
{
    float MHz;
    sscanf(valStr, "%f", &MHz);
#ifdef SX127x_H
    Radio::radio.set_frf_MHz(MHz);
#else
    Radio::radio.setMHz(MHz);
#endif
    return false;
}

const value_item_t frf_item = { _ITEM_VALUE, 14, frf_print, frf_write };

const value_item_t Radio::tx_dbm_item = { _ITEM_VALUE, 7, Radio::tx_dbm_print, Radio::tx_dbm_write };

const dropdown_item_t tx_ramp_item = { _ITEM_DROPDOWN, Radio::tx_ramp_strs, Radio::tx_ramp_strs, Radio::tx_ramp_read, Radio::tx_ramp_write };

const button_item_t chipNum_item = { _ITEM_BUTTON, Radio::chipNum_str, NULL};

void botrow_print()
{
    pc.printf("%u", botRow);
}

bool botrow_write(const char* str)
{
    unsigned n;
    sscanf(str, "%u", &n);
    botRow = n;

    pc.printf("\e[%u;%ur", MAX_MENU_ROWS, botRow); // set scrolling region

    return false;
}

const value_item_t bottomRow_item = { _ITEM_VALUE, 4, botrow_print, botrow_write };

const menu_t common_menu[] = {
    {  {1, 1},          NULL,  &hw_reset_item, FLAG_MSGTYPE_ALL },
    { {1, 15}, "packetType:",   &pktType_item, FLAG_MSGTYPE_ALL },
    { {1, 37},          NULL,   &msgType_item, FLAG_MSGTYPE_ALL },
    { {1, 50},          NULL,   &chipNum_item, FLAG_MSGTYPE_ALL },
    { {1, 60},  "bottomRow:", &bottomRow_item, FLAG_MSGTYPE_ALL },
    { {2, 1},           NULL,       &frf_item, FLAG_MSGTYPE_ALL },
    { {2, 15},     "TX dBm:",    &Radio::tx_dbm_item, FLAG_MSGTYPE_ALL },
    { {2, 30},    "ramp us:",   &tx_ramp_item, FLAG_MSGTYPE_ALL },
    { {2, 45},          NULL, &clearirqs_item, FLAG_MSGTYPE_ALL },
    { {2, 55},     "opmode:",    &opmode_item, FLAG_MSGTYPE_ALL },


    { {0, 0}, NULL, NULL }
};

bool
is_menu_item_changable(uint8_t table_row, uint8_t table_col)
{
    const menu_t* m = menu_table[table_row][table_col];
    const dropdown_item_t* di = (const dropdown_item_t*)m->itemPtr;

    switch (msg_type) {
        case MSG_TYPE_PACKET:
            if (m->flags & FLAG_MSGTYPE_PKT)
                break;
            else
                return false;
        case MSG_TYPE_PER:
            if (m->flags & FLAG_MSGTYPE_PER)
                break;
            else
                return false;
        case MSG_TYPE_PINGPONG:
            if (m->flags & FLAG_MSGTYPE_PING)
                break;
            else
                return false;
    }

    if (di->itemType == _ITEM_DROPDOWN) {
        if (di->selectable_strs == NULL || di->selectable_strs[0] == NULL) {
            log_printf("NULLstrs%u,%u\r\n", table_row, table_col);
            return false;
        }
    } else if (di->itemType == _ITEM_VALUE) {
        const value_item_t* vi = (const value_item_t*)m->itemPtr;
        if (vi->write == NULL)
            return false;
    } else if (di->itemType == _ITEM_BUTTON) {
        const button_item_t* bi = (const button_item_t*)m->itemPtr;
        if (bi->push == NULL)
            return false;
    } else if (di->itemType == _ITEM_TOGGLE) {
        const toggle_item_t * ti = (const toggle_item_t *)m->itemPtr;
        if (ti->push == NULL)
            return false;
    }

    return true;
} // ..is_menu_item_changable()

void read_menu_item(const menu_t* m, bool selected)
{
    uint8_t valCol;
    const dropdown_item_t* di = (const dropdown_item_t*)m->itemPtr;

    switch (msg_type) {
        case MSG_TYPE_PACKET:
            if (m->flags & FLAG_MSGTYPE_PKT)
                break;
            else
                return;
        case MSG_TYPE_PER:
            if (m->flags & FLAG_MSGTYPE_PER)
                break;
            else
                return;
        case MSG_TYPE_PINGPONG:
            if (m->flags & FLAG_MSGTYPE_PING)
                break;
            else
                return;
    }

    pc.printf("\e[%u;%uf", m->pos.row, m->pos.col);  // set (force) cursor to row;column
    valCol = m->pos.col;
    if (m->label) {
        pc.printf(m->label);
        valCol += strlen(m->label);
    }
    if (di->itemType == _ITEM_DROPDOWN) {
        if (di->read && di->printed_strs) {
            uint8_t ridx = di->read(false);
            if (selected)
                pc.printf("\e[7m");
            pc.printf(di->printed_strs[ridx]);
            if (selected)
                pc.printf("\e[0m");
        } else if (di->printed_strs) {
            pc.printf(di->printed_strs[0]);
        }
    } else if (di->itemType == _ITEM_VALUE) {
        const value_item_t* vi = (const value_item_t*)m->itemPtr;
        if (vi->print) {
            for (unsigned n = 0; n < vi->width; n++)
                pc.putc(' ');

            pc.printf("\e[%u;%uf", m->pos.row, valCol);  // set (force) cursor to row;column
            if (selected)
                pc.printf("\e[7m");
            vi->print();
            if (selected)
                pc.printf("\e[0m");
        }
    } else if (di->itemType == _ITEM_BUTTON) {
        const button_item_t* bi = (const button_item_t*)m->itemPtr;
        if (bi->label) {
            if (selected)
                pc.printf("\e[7m%s\e[0m", bi->label);
            else
                pc.printf("%s", bi->label);
        }
    } else if (di->itemType == _ITEM_TOGGLE) {
        const toggle_item_t* ti = (const toggle_item_t *)m->itemPtr;
        bool on = ti->read();
        if (ti->label1) {
            const char* const cptr = on ? ti->label1 : ti->label0;
            if (selected)
                pc.printf("\e[7m%s\e[0m", cptr);
            else
                pc.printf("%s", cptr);
        } else {
            if (on)
                pc.printf("\e[1m");
            if (selected)
                pc.printf("\e[7m");

            pc.printf("%s", ti->label0);

            if (selected || on)
                pc.printf("\e[0m");
        }
    }
} // ..read_menu_item()

void draw_meni()
{
    unsigned table_row;

    for (table_row = 0; table_row < MAX_MENU_ROWS; table_row++) {
        int table_col;
        for (table_col = 0; table_col < StopMenuCols[table_row]; table_col++) {
            read_menu_item(menu_table[table_row][table_col], false);
        } // ..table column iterator
    } // ..table row iterator

    read_menu_item(menu_table[curpos.row][curpos.tableCol], true);

} // ..draw_menu()

typedef struct {
    int row;
    int col;
} tablexy_t;

void
menu_init_(const menu_t* in, tablexy_t* tc)
{
    unsigned n;

    for (n = 0; in[n].pos.row > 0; n++) {
        const menu_t* m = &in[n];
        if (tc->row != m->pos.row - 1) {
            tc->row = m->pos.row - 1;
            tc->col = 0;
        } else
            tc->col++;

        menu_table[tc->row][tc->col] = m;
#ifdef MENU_DEBUG
        pc.printf("table:%u,%u ", tc->row, tc->col);
        pc.printf(" %d<%d? ", StopMenuCols[tc->row], tc->col);
#endif /* MENU_DEBUG */
        if (StopMenuCols[tc->row] < tc->col)
            StopMenuCols[tc->row] = tc->col;
#ifdef MENU_DEBUG
        pc.printf("{%u %u}", tc->row, tc->col);
        pc.printf("in:%p[%u] screen:%u,%u ", in, n, m->pos.row, m->pos.col);
        //pc.printf(" loc:%p ", &in[n].itemPtr);
        if (in[n].itemPtr) {
            const dropdown_item_t* di = (const dropdown_item_t*)m->itemPtr;
            pc.printf(" itemPtr:%p type:%02x ", di, di->itemType);
        }
        pc.printf("stopMenuCols[%u]: %d ", tc->row, StopMenuCols[tc->row]);
        if (m->label)
            pc.printf("label:%s", m->label);
        else
            pc.printf("noLabel");
        pc.printf("\r\n");
#endif /* MENU_DEBUG */
    }
#ifdef MENU_DEBUG
    pc.printf("hit key:");
    pc.getc();
#endif /* MENU_DEBUG */

}

void navigate_dropdown(uint8_t ch)
{
    unsigned n;
    const menu_t* m = menuState.sm;
    const dropdown_item_t* di = (const dropdown_item_t*)m->itemPtr;

    switch (ch) {
        case 'A':   // cursor UP
            if (menuState.sel_idx > 0) {
                menuState.sel_idx--;
            }
            break;
        case 'B':   // cursor DOWN
            if (di->selectable_strs[menuState.sel_idx+1] != NULL)
                menuState.sel_idx++;
            break;
    } // ..switch (ch)

    for (n = 0; di->selectable_strs[n] != NULL; n++) {
        pc.printf("\e[%u;%uf", m->pos.row+n, menuState.dropdown_col);
        if (n == menuState.sel_idx)
            pc.printf("\e[7m");
        pc.printf(di->selectable_strs[n]);
        if (n == menuState.sel_idx)
            pc.printf("\e[0m");
    }
    pc.printf("\e[%u;%uf", m->pos.row + menuState.sel_idx, menuState.dropdown_col + strlen(di->selectable_strs[menuState.sel_idx]));
}

bool is_item_selectable(const menu_t* m)
{
    const dropdown_item_t* di = (const dropdown_item_t*)m->itemPtr;

    if (di->itemType == _ITEM_BUTTON) {
        const button_item_t* bi = (const button_item_t*)m->itemPtr;
        if (bi->push == NULL)
            return false;
    } else if (di->itemType == _ITEM_TOGGLE) {
        const toggle_item_t* ti = (const toggle_item_t*)m->itemPtr;
        if (ti->push == NULL)
            return false;
    }

    return true;
}

void navigate_menu(uint8_t ch)
{
    read_menu_item(menu_table[curpos.row][curpos.tableCol], false);

    switch (ch) {
        case 'A':   // cursor UP
            if (curpos.row == 0)
                break;

            {   // find previous row up with column
                int8_t row;
                for (row = curpos.row - 1; row >= 0; row--) {
                    if (StopMenuCols[row] > -1) {
                        curpos.row = row;
                        break;
                    }
                }
                if (row == 0 && StopMenuCols[0] < 0)
                    break;  // nothing found
            }

            if (curpos.tableCol >= StopMenuCols[curpos.row]) {
                curpos.tableCol = StopMenuCols[curpos.row]-1;
            }

            break;
        case 'B':   // cursor DOWN
            if (curpos.row >= MAX_MENU_ROWS)
                break;

            {   // find next row down with column
                uint8_t row;
                for (row = curpos.row + 1; row < MAX_MENU_ROWS; row++) {
                    if (StopMenuCols[row] != -1) {
                        curpos.row = row;
                        break;
                    }
                }
                if (row == MAX_MENU_ROWS-1 && StopMenuCols[row] == -1)
                    break;  // nothing found
            }

            if (curpos.tableCol >= StopMenuCols[curpos.row]) {
                curpos.tableCol = StopMenuCols[curpos.row]-1;
            }


            break;
        case 'C':    // cursor LEFT
            if (curpos.tableCol >= StopMenuCols[curpos.row]-1)
                break;

            { // find next row left with editable
                uint8_t tcol;
                for (tcol = curpos.tableCol + 1; tcol < StopMenuCols[curpos.row]; tcol++) {
                    if (is_menu_item_changable(curpos.row, tcol)) {
                        curpos.tableCol = tcol;
                        break;
                    }
                }
            }

            break;
        case 'D':   // cursor RIGHT
            if (curpos.tableCol == 0)
                break;

            {
                int8_t tcol;
                for (tcol = curpos.tableCol - 1;  tcol >= 0; tcol--) {
                    if (is_menu_item_changable(curpos.row, tcol)) {
                        curpos.tableCol = tcol;
                        break;
                    }
                }
            }

            break;
        default:
            //pc.printf("unhancled-csi:%02x\eE", ch);
            break;
    } // ..switch (ch)

    if (!is_item_selectable(menu_table[curpos.row][curpos.tableCol])) {
        int c;
        for (c = 0; c < StopMenuCols[curpos.row]; c++) {
            if (is_item_selectable(menu_table[curpos.row][c])) {
                curpos.tableCol = c;
                break;
            }
        }
        if (c == StopMenuCols[curpos.row])
            return;
    }

#ifdef MENU_DEBUG
    log_printf("table:%u,%u screen:%u,%u      \r\n", curpos.row, curpos.tableCol, 
        menu_table[curpos.row][curpos.tableCol]->pos.row,
        menu_table[curpos.row][curpos.tableCol]->pos.col
    );
#endif /* MENU_DEBUG */

    read_menu_item(menu_table[curpos.row][curpos.tableCol], true);
} // ..navigate_menu

void commit_menu_item_change()
{
    const menu_t* m = menu_table[curpos.row][curpos.tableCol];
    const dropdown_item_t* di = (const dropdown_item_t*)m->itemPtr;

    if (di->itemType == _ITEM_DROPDOWN) {
        menuState.mode = di->write(menuState.sel_idx);

        pc.printf("\e[%u;%uf", m->pos.row, m->pos.col-2);
    } else if (di->itemType == _ITEM_VALUE) {
        const value_item_t* vi = (const value_item_t*)m->itemPtr;
        /* commit value entry */
        if (vi->write) {
            if (vi->write(entry_buf))
                menuState.mode = MENUMODE_REDRAW;
            else
                menuState.mode = MENUMODE_NONE;
        } else
            menuState.mode = MENUMODE_NONE;

        if (menuState.mode == MENUMODE_NONE) {
            read_menu_item(menu_table[curpos.row][curpos.tableCol], true);
        }
    }
} // ..commit_menu_item_change()

void refresh_item_in_table(const void* item)
{
    unsigned table_row;

    if (item == NULL)
        return;

    for (table_row = 0; table_row < MAX_MENU_ROWS; table_row++) {
        int table_col;
        for (table_col = 0; table_col < StopMenuCols[table_row]; table_col++) {
            //log_printf("%u %u %p\r\n", table_row, table_col, menu_table[table_row][table_col]->itemPtr);
            if (item == menu_table[table_row][table_col]->itemPtr) {
                read_menu_item(menu_table[table_row][table_col], false);
                return;
            }
        }
    }
}

void
start_value_entry(const menu_t* m)
{
    const value_item_t* vi = (const value_item_t*)m->itemPtr;
    uint8_t col = m->pos.col;

    if (m->label)
        col += strlen(m->label);

    pc.printf("\e[%u;%uf", m->pos.row, col);
    for (unsigned i = 0; i < vi->width; i++)
        pc.putc(' ');   // clear displayed value for user entry

    pc.printf("\e[%u;%uf", m->pos.row, col);
    menuState.mode = MENUMODE_ENTRY;
    entry_buf_idx = 0;
}

void start_menu_item_change()
{
    const menu_t* m = menu_table[curpos.row][curpos.tableCol];
    const dropdown_item_t* di = (const dropdown_item_t*)m->itemPtr;
    bool checkRefresh = false;

    if (di->itemType == _ITEM_DROPDOWN && di->selectable_strs) {
        menuState.dropdown_col = m->pos.col;
        unsigned n, sidx = 0;
        /* start dropdown */
        if (di->read)
            sidx = di->read(true);

        if (m->label)
            menuState.dropdown_col += strlen(m->label);

        for (n = 0; di->selectable_strs[n] != NULL; n++) {
            uint8_t col = menuState.dropdown_col;
            bool leftPad = false;
            if (col > 3 && n > 0) { // dropdown left side padding
                col -= 2;
                leftPad = true;
            }
            pc.printf("\e[%u;%uf", m->pos.row+n, col);
            if (leftPad ) {
                pc.putc(' ');
                pc.putc(' ');
            }
            if (n == sidx)
                pc.printf("\e[7m");
            pc.printf(di->selectable_strs[n]);
            if (n == sidx)
                pc.printf("\e[0m");
            pc.putc(' ');   // right side padding
            pc.putc(' ');
        }
        pc.printf("\e[%u;%uf", m->pos.row, menuState.dropdown_col-2);

        menuState.mode = MENUMODE_DROPDOWN;
        menuState.sel_idx = sidx;
        menuState.sm = m;
    } else if (di->itemType == _ITEM_VALUE) {
        /* start value entry */
        start_value_entry(m);
    } else if (di->itemType == _ITEM_BUTTON) {
        const button_item_t* bi = (const button_item_t*)m->itemPtr;
        if (bi->push) {
            bi->push();
            checkRefresh = true;
        }
    } else if (di->itemType == _ITEM_TOGGLE) {
        const toggle_item_t* ti = (const toggle_item_t*)m->itemPtr;
        if (ti->push) {
            bool on = ti->push();
            uint8_t col = m->pos.col;

            if (m->label)
                col += strlen(m->label);

            pc.printf("\e[%u;%uf", m->pos.row, col);
            if (ti->label1) {
                pc.printf("\e[7m%s\e[0m", on ? ti->label1 : ti->label0);
            } else {
                if (on)
                    pc.printf("\e[1;7m%s\e[0m", ti->label0);
                else
                    pc.printf("\e[7m%s\e[0m", ti->label0);
            }
            checkRefresh = true;
        }
    }

    if (checkRefresh) {
        if (m->refreshReadItem) {
            refresh_item_in_table(m->refreshReadItem);  // read associated
            read_menu_item(m, true);   // restore cursor
        }
    }
} // ..start_menu_item_change()

void full_menu_init()
{
    unsigned n;
    const menu_t *m;
    tablexy_t txy;

    txy.row = INT_MAX;
    txy.col = 0;
    
    for (n = 0; n < MAX_MENU_ROWS; n++) {
        StopMenuCols[n] = -1;
    }

    menu_init_(common_menu, &txy);

    menu_init_(Radio::common_menu, &txy);

    m = Radio::get_modem_menu();
    if (m == NULL) {
        log_printf("NULL-modemMenu\r\n");
        for (;;) asm("nop");
    }
#ifdef MENU_DEBUG
    pc.printf("modemmenuInit\r\n");
#endif
    menu_init_(m, &txy);

    m = Radio::get_modem_sub_menu();
    if (m) {
#ifdef MENU_DEBUG
        pc.printf("modemsubmenuInit\r\n");
#endif
        menu_init_(m, &txy);
    }
#ifdef MENU_DEBUG
    else
        pc.printf("no-modemsubmenu\r\n");
#endif

    m = get_msg_menu();
    if (m == NULL) {
        log_printf("NULL-msgMenu\r\n");
        for (;;) asm("nop");
    }
    menu_init_(m, &txy);

    for (n = 0; n < MAX_MENU_ROWS; n++) {
        if (StopMenuCols[n] != -1)
            StopMenuCols[n]++;
    }
}

bool ishexchar(char ch)
{
    if (((ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) || (ch >= '0' && ch <= '9'))
        return true;
    else
        return false;
}

enum _urx_ {
    URX_STATE_NONE = 0,
    URX_STATE_ESCAPE,
    URX_STATE_CSI,
} uart_rx_state;

Timeout uartRxTimeout;

void uart_rx_timeout()
{
    /* escape by itself: abort change on item */
    menuState.mode = MENUMODE_REDRAW;

    uart_rx_state = URX_STATE_NONE;
}

void serial_callback()
{
    char ch = pc.getc();

    switch (uart_rx_state) {
        case URX_STATE_NONE:
            if (ch == 0x1b) {
                if (menuState.mode == MENUMODE_ENTRY) {
                    /* abort entry mode */
                    menuState.mode = MENUMODE_NONE;
                    read_menu_item(menu_table[curpos.row][curpos.tableCol], true);
                } else {
                    uart_rx_state = URX_STATE_ESCAPE;
                    if (menuState.mode != MENUMODE_NONE) {
                        /* is this escape by itself, user wants to abort? */
                        uartRxTimeout.attach(uart_rx_timeout, 0.03);
                    }
                }
            } else if (ch == 2) { // ctrl-B
                log_printf("--------------\r\n");
            } else if (ch == '\r') {
                if (menuState.mode == MENUMODE_NONE) {
                    start_menu_item_change();
                } else {
                    entry_buf[entry_buf_idx] = 0;
                    commit_menu_item_change();
                }
            } else if (menuState.mode == MENUMODE_ENTRY) {
                if (ch == 8) {
                    if (entry_buf_idx > 0) {
                        pc.putc(8);
                        pc.putc(' ');
                        pc.putc(8);
                        entry_buf_idx--;
                    }
                } else if (ch == 3) {    // ctrl-C
                    menuState.mode = MENUMODE_NONE;
                } else if (entry_buf_idx < sizeof(entry_buf)) {
                    entry_buf[entry_buf_idx++] = ch;
                    pc.putc(ch);
                }
            } else if (menuState.mode == MENUMODE_NONE) {
                if (ishexchar(ch)) {
                    const value_item_t* vi = (const value_item_t*)menu_table[curpos.row][curpos.tableCol]->itemPtr;
                    if (vi->itemType == _ITEM_VALUE) {
                        start_value_entry(menu_table[curpos.row][curpos.tableCol]);
                        entry_buf[entry_buf_idx++] = ch;
                        pc.putc(ch);
                    }
                } else if (ch == 'r') {
                    menuState.mode = MENUMODE_REDRAW;
                } else if (ch == '.') {
                    Radio::test();
                }

            }
            break;
        case URX_STATE_ESCAPE:
            uartRxTimeout.detach();
            if (ch == '[')
                uart_rx_state = URX_STATE_CSI;
            else {
#ifdef MENU_DEBUG
                log_printf("unhancled-esc:%02x\r\n", ch);
#endif /* MENU_DEBUG */
                uart_rx_state = URX_STATE_NONE;
            }
            break;
        case URX_STATE_CSI:
            if (menuState.mode == MENUMODE_NONE)
                navigate_menu(ch);
            else if (menuState.mode == MENUMODE_DROPDOWN)
                navigate_dropdown(ch);

            uart_rx_state = URX_STATE_NONE;
            //pc.printf("\e[18;1f");  // set (force) cursor to row;column
            break;
    } // ..switch (uart_rx_state)
}

void txDone()
{

    if (msg_type == MSG_TYPE_PER) {
        log_printf("CntPacketTx%u, max:%u  ipd%u\r\n", CntPacketTx, MaxNumPacket, tx_ipd_ms);
        if (++CntPacketTx <= MaxNumPacket)
            mbedTimeout.attach_us(next_tx_callback, tx_ipd_ms * 1000);
    } else if (msg_type == MSG_TYPE_PINGPONG) {
        if (flags.ping_master) {
            ++CntPacketTx;
        }

        Radio::Rx();
    }
}

static void
printRxPkt(uint8_t size)
{
    char str[80];
    char *ptr, *endPtr;
    unsigned n = 0;
    endPtr = str + sizeof(str);
    ptr = str;
    while (ptr < endPtr) {
        sprintf(ptr, "%02x ", Radio::radio.rx_buf[n]);
        ptr += 3;
        if (++n >= size)
            break;
    }
    log_printf("%s\r\n", str);
}

void rxDone(uint8_t size, float rssi, float snr)
{
    log_printf("rxDone %u, %.1fdBm  %.1fdB\r\n", size, rssi, snr);
    if (msg_type == MSG_TYPE_PACKET) {
        printRxPkt(size);
    } else if (msg_type == MSG_TYPE_PER) {
        if (memcmp(Radio::radio.rx_buf+4, PerMsg, 3) == 0) {
            unsigned i, PacketRxSequence = Radio::radio.rx_buf[0];
            PacketRxSequence <<= 8;
            PacketRxSequence += Radio::radio.rx_buf[1];
            PacketRxSequence <<= 8;
            PacketRxSequence += Radio::radio.rx_buf[2];
            PacketRxSequence <<= 8;
            PacketRxSequence += Radio::radio.rx_buf[3];

            CntPacketRxOK++;

            if (PacketRxSequence <= PacketRxSequencePrev || PacketRxSequencePrev == 0)
                i = 0;  // sequence reset to resync, dont count missed packets this time
            else
                i = PacketRxSequence - PacketRxSequencePrev - 1;


            CntPacketRxKO += i;
            RxTimeOutCount = 0;
            log_printf("PER rx%u  ok%u   ko%u\r\n", PacketRxSequence , CntPacketRxOK, CntPacketRxKO);

            PacketRxSequencePrev = PacketRxSequence;
        } // ..if PerMsg
        else {
            log_printf("per?\r\n");
            printRxPkt(size);
        }
    } else if (msg_type == MSG_TYPE_PINGPONG) {
        if (memcmp(Radio::radio.rx_buf+4, PingMsg, 4) == 0) {
            /* ping slave rx */
            Radio::setFS();
            receivedCntPacket = Radio::radio.rx_buf[0];
            receivedCntPacket <<= 8;
            receivedCntPacket += Radio::radio.rx_buf[1];
            receivedCntPacket <<= 8;
            receivedCntPacket += Radio::radio.rx_buf[2];
            receivedCntPacket <<= 8;
            receivedCntPacket += Radio::radio.rx_buf[3];
            log_printf("%u rxPing->txPong\r\n", receivedCntPacket);

            flags.ping_master = 0;
            flags.send_pong = 1;

        } else if (memcmp(Radio::radio.rx_buf+8, PongMsg, 4) == 0) {
            unsigned cnt;
            /* ping master rx */
            Radio::setFS();
            cnt = Radio::radio.rx_buf[0];
            cnt <<= 8;
            cnt += Radio::radio.rx_buf[1];
            cnt <<= 8;
            cnt += Radio::radio.rx_buf[2];
            cnt <<= 8;
            cnt += Radio::radio.rx_buf[3];
            log_printf("%u rxPong->txPing\r\n", cnt);
            flags.send_ping = 1;
        } else {
            log_printf("pingpong?\r\n");
            printRxPkt(size);
        }
    } else {
        /*for (unsigned n = 0; n < size; n++)
            log_printf("%02x\r\n", Radio::radio.rx_buf[n]);*/
        log_printf("msg_type %u\r\n", msg_type);
    }

}

const RadioEvents_t rev = {
    txDone,
    rxDone
};

int main()
{
    //pos_t pos;

    lfsr = LFSR_INIT;
    msg_type = MSG_TYPE_PACKET;

    uart_rx_state = URX_STATE_NONE;
    pc.baud(115200);

    Radio::boardInit(&rev);

    {
        unsigned n;
        for (n = 0; n < MAX_MENU_ROWS; n++)
            StopMenuCols[n] = -1;
    }

    botRow = MAX_MENU_ROWS + SCROLLING_ROWS;

    pc.printf("\e[7h"); // enable line wrapping
    pc.printf("\e[%u;%ur", MAX_MENU_ROWS, botRow); // set scrolling region
    pc.printf("\e[2J"); // erase entire screen

    full_menu_init();

    pc.printf("\e[2J"); // erase entire screen

    menuState.mode = MENUMODE_NONE;

    draw_meni();

    curpos.row = 0;
    curpos.tableCol = 0;

    tx_ipd_ms = 100;

    for (;;) {
        if (pc.readable()) {
            serial_callback();
        }

        if (flags.send_ping) {
            if (flags.pingpongEnable)
                SendPing();
            flags.send_ping = 0;
        }

        if (flags.send_pong) {
            if (flags.pingpongEnable)
                SendPong();
            flags.send_pong = 0;
        }

        if (menuState.mode == MENUMODE_REINIT_MENU) {
            full_menu_init();
            menuState.mode = MENUMODE_REDRAW;
        }

        if (menuState.mode == MENUMODE_REDRAW) {
            // erase entire screen, some dropdowns extend to scrolling area
            pc.printf("\e[%u;%ur", MAX_MENU_ROWS, botRow); // set scrolling region, if terminal started after
            pc.printf("\e[2J");
            //pc.printf("\e[%u;1f\e[1J", MAX_MENU_ROWS);  // erase menu area

            menuState.mode = MENUMODE_NONE;
            draw_meni();
        }

        if (Radio::service(menuState.mode == MENUMODE_NONE ? LAST_CHIP_MENU_ROW : -1)) {
            read_menu_item(menu_table[curpos.row][curpos.tableCol], true);
        }
    } // ..for (;;)
}

char strbuf[255];

void log_printf(const char* format, ...)
{
    va_list arglist;

    // put cursor at last scrolling-area line
    pc.printf("\e[%u;1f", botRow);
    va_start(arglist, format);
    vsprintf(strbuf, format, arglist);
    va_end(arglist);

    pc.printf(strbuf);
}