/**************************************************************************************************
  Filename:       topo.c
  Description:    ZigBee network topology report + serial terminal (4-node teaching experiment).

                  Each node prints its own "network ID card" over the serial port:

                    Role      : Coordinator / Router / End Device
                    ShortAddr : own 16-bit network address
                    IEEEAddr  : own 64-bit IEEE address
                    PANID     : network PAN ID
                    Channel   : logical channel
                    Depth     : distance (in hops) from the coordinator
                    CoordAddr : coordinator short address
                    Parent    : the node we are attached to
                    Children  : the nodes attached to us (+ LQI of each link)
                    Neighbors : the whole neighbour table (+ LQI)

                  Students read the report of every node and draw the topology by hand.

                  The same UART accepts a few single-character commands, so the report can be
                  asked for at any moment from a plain serial terminal:

                    t   print the report now
                    p   automatic report on / off
                    +   report interval + 5 s
                    -   report interval - 5 s
                    h   command list

                  WHERE THE DATA COMES FROM (all synchronous, no ZDO request needed):
                    _NIB                  -> ShortAddr, PANID, Channel, Depth, CoordAddr
                    NLME_GetExtAddr()     -> IEEEAddr
                    AssociatedDevList[]   -> Parent and Children (nodeRelation field)
                    NLME_GetRequest()     -> Neighbor table (address + LQI)

                  HOW IT IS DRIVEN (context safety):
                    Topo_RxByte()  may run in interrupt context - it only stores one byte.
                    Topo_Tick()    runs in the application task context - it does the printing.
                  So no long UART transmission ever happens inside an interrupt.

                  Build hint: this file must be added to the CoordinatorEB, RouterEB and
                  EndDeviceEB configurations of SampleSwitch.eww.
**************************************************************************************************/

/**************************************************************************************************
 *                                            INCLUDES
 **************************************************************************************************/
#include "ZComDef.h"
#include "OSAL.h"
#include "ZGlobals.h"
#include "NLMEDE.h"
#include "nwk.h"
#include "nwk_globals.h"
#include "AssocList.h"
#include "nwk_util.h"
#include "ZDApp.h"
#include "hal_mcu.h"
#include "hal_uart.h"
#include "topo.h"

/**************************************************************************************************
 *                                       LOCAL CONSTANTS
 **************************************************************************************************/

/* Report interval expressed in application ticks. */
#define TOPO_REPORT_TICKS_DEF     ( TOPO_REPORT_MS_DEFAULT / TOPO_TICK_MS )
#define TOPO_REPORT_TICKS_MAX     ( TOPO_REPORT_MS_MAX     / TOPO_TICK_MS )
#define TOPO_REPORT_TICKS_STEP    ( TOPO_REPORT_MS_STEP    / TOPO_TICK_MS )

/* Ticks per second, used only to print the interval in seconds. */
#define TOPO_TICKS_PER_SEC        ( 1000 / TOPO_TICK_MS )

/* ---------------------------------------------------------------------------
 * DIAGNOSTIC BREADCRUMBS
 *
 * While bringing the module up it helps to see exactly how far Topo_Print()
 * gets. With TOPO_DEBUG_STEPS = 1 a short "[TOPO] n ..." line is printed at
 * every stage; set it to 0 when everything works.
 * ------------------------------------------------------------------------- */
#define TOPO_DEBUG_STEPS          1

#if TOPO_DEBUG_STEPS
  #define TOPO_STEP(tag)   do { Topo_BeginLine(); Topo_PutStr( "[TOPO] " ); \
                                 Topo_PutStr( tag ); Topo_EndLine(); } while ( 0 )
#else
  #define TOPO_STEP(tag)
#endif

/**************************************************************************************************
 *                                        LOCAL VARIABLES
 **************************************************************************************************/
static uint8  topoBuf[TOPO_LINE_BUF_LEN];
static uint8  topoLen;

/* RAM NOTE ---------------------------------------------------------------
 * The CC2530 has 8 KB of XDATA and this project uses almost all of it, so
 * every avoidable XDATA byte is avoided here on purpose:
 *   - no hexadecimal lookup table (it would cost 17 initialized bytes);
 *     the digits are computed by Topo_PutHexDigit() instead;
 *   - only ONE initialized variable (topoReportTicks);
 *   - the output buffer is only TOPO_LINE_BUF_LEN (16) bytes: it is flushed
 *     automatically whenever it is full, so long report lines cost no extra RAM.
 * This module therefore needs only about 22 bytes of XDATA in total.
 * ---------------------------------------------------------------------- */

/* Terminal state. topoCmd is written by the UART callback and cleared by Topo_Tick(). */
static volatile uint8 topoCmd = 0;
static uint16 topoTickCnt = 0;
static uint16 topoReportTicks = TOPO_REPORT_TICKS_DEF;

/* ---------------------------------------------------------------------------
 * REPORT EMISSION WINDOW
 *
 * A whole report is about 600 bytes, but the UART DMA transmit buffer holds only
 * about 256 bytes and HalUARTWrite() DROPS the data (it returns 0) as soon as
 * that buffer is full.  That buffer is only drained by HalUARTPoll(), which does
 * not run while we are inside the application task.
 *
 * Therefore the report is NOT emitted in one go: Topo_ReportStep() emits only
 * TOPO_LINES_PER_PASS lines per application tick, and the next tick continues
 * where the previous one stopped.  Every UART burst is then only a couple of
 * hundred bytes, so the DMA buffer always keeps up and nothing is lost.
 *
 *   topoWinFrom / topoWinTo : the line range emitted by the current pass
 *   topoLineNo              : index of the line currently being built
 *   topoSuppress            : TRUE while building a line outside the window
 *   topoNextFrom            : first line of the next pass
 *   topoBusy                : TRUE while a report is still being emitted
 * ------------------------------------------------------------------------- */
static uint8  topoWinFrom  = 0;
static uint8  topoWinTo    = 255;   /* 255 = no filtering outside a pass */
static uint8  topoLineNo   = 0;
static uint8  topoSuppress = FALSE;
static uint8  topoNextFrom = 0;
static uint8  topoBusy     = FALSE;

/**************************************************************************************************
 *                                   SMALL TEXT HELPERS
 *
 * The IAR 8051 printf library is intentionally NOT used: these helpers keep the
 * code small, keep the report readable and avoid any library configuration issue.
 **************************************************************************************************/

/*********************************************************************
 * @fn      Topo_Flush
 * @brief   Send the buffered bytes out of the UART.
 *
 *          IMPORTANT (this is a real Z-Stack trap):
 *          HalUARTWrite() is backed by HalUARTWriteDMA(), which RETURNS 0 and
 *          silently DROPS the data whenever its DMA TX buffer is full.  Look at
 *          _hal_uart_dma.c:  "if ((len + dmaCfg.txIdx[dmaCfg.txSel]) >
 *          HAL_UART_DMA_TX_MAX) return 0;"
 *          That buffer is only drained by HalUARTPoll(), which does NOT run while
 *          we are inside the application task.  So a report longer than one DMA
 *          buffer (about 256 bytes) would lose everything past that point.
 *
 *          Fix: check the return value, pump the HAL and retry until every byte
 *          has been accepted.  The guard prevents an endless loop if the UART is
 *          really stuck.
 */
static void Topo_Flush( void )
{
  uint16 written;
  uint16 guard = 0;

  if ( topoLen == 0 )
  {
    return;
  }

  do
  {
    written = HalUARTWrite( TOPO_UART_PORT, topoBuf, topoLen );

    if ( written >= topoLen )
    {
      break;                       /* everything accepted */
    }

    HalUARTPoll();                 /* let the DMA drain, then retry */
    guard++;
  }
  while ( guard < 20000 );

  topoLen = 0;
}

/*********************************************************************
 * @fn      Topo_BeginLine
 * @brief   Start a new text line.
 */
static void Topo_BeginLine( void )
{
  Topo_Flush();

  /* Decide whether this line belongs to the current emission window.
   * Outside a report pass topoWinTo is 255, so nothing is ever filtered. */
  topoSuppress = (uint8)( ( topoLineNo <  topoWinFrom ) ||
                          ( topoLineNo >= topoWinTo   ) );
  topoLineNo++;
}

/*********************************************************************
 * @fn      Topo_PutChar
 * @brief   Append one character.
 *
 *          The buffer is deliberately tiny (TOPO_LINE_BUF_LEN bytes): as soon as
 *          it is full its content is sent out, so a line that is longer than the
 *          buffer is simply transmitted in several UART writes.
 *          This is what keeps the XDATA cost of this module down to a few bytes,
 *          no matter how long the report lines are.
 */
static void Topo_PutChar( uint8 c )
{
  if ( topoSuppress != FALSE )
  {
    return;                    /* this line is outside the current window */
  }

  if ( topoLen >= (uint16)TOPO_LINE_BUF_LEN )
  {
    Topo_Flush();
  }

  topoBuf[topoLen++] = c;
}

/*********************************************************************
 * @fn      Topo_PutStr
 * @brief   Append a zero terminated string.
 */
static void Topo_PutStr( const char *s )
{
  while ( *s != '\0' )
  {
    Topo_PutChar( (uint8)(*s) );
    s++;
  }
}

/*********************************************************************
 * @fn      Topo_PutDec
 * @brief   Append an unsigned decimal number (no leading zeros).
 */
static void Topo_PutDec( uint16 v )
{
  uint8 tmp[5];
  uint8 n = 0;

  if ( v == 0 )
  {
    Topo_PutChar( '0' );
    return;
  }

  while ( (v != 0) && (n < 5) )
  {
    tmp[n++] = (uint8)('0' + (v % 10));
    v /= 10;
  }

  while ( n > 0 )
  {
    Topo_PutChar( tmp[--n] );
  }
}

/*********************************************************************
 * @fn      Topo_PutHexDigit
 * @brief   Append one hexadecimal digit (0-9, A-F).
 *
 *          Computed with arithmetic instead of a lookup table on purpose:
 *          a 17 byte initialized table does not fit in the remaining XDATA.
 */
static void Topo_PutHexDigit( uint8 v )
{
  v &= 0x0F;

  if ( v < 10 )
  {
    Topo_PutChar( (uint8)('0' + v) );
  }
  else
  {
    Topo_PutChar( (uint8)('A' + v - 10) );
  }
}

/*********************************************************************
 * @fn      Topo_PutHex16
 * @brief   Append a 16-bit value as "0xABCD".
 */
static void Topo_PutHex16( uint16 v )
{
  Topo_PutStr( "0x" );
  Topo_PutHexDigit( (uint8)(v >> 12) );
  Topo_PutHexDigit( (uint8)(v >>  8) );
  Topo_PutHexDigit( (uint8)(v >>  4) );
  Topo_PutHexDigit( (uint8)(v) );
}

/*********************************************************************
 * @fn      Topo_PutExtAddr
 * @brief   Append an 8 byte IEEE address as 16 hex digits (big endian order).
 */
static void Topo_PutExtAddr( uint8 *pExt )
{
  uint8 i;

  for ( i = 0; i < Z_EXTADDR_LEN; i++ )
  {
    Topo_PutHexDigit( (uint8)(pExt[i] >> 4) );
    Topo_PutHexDigit( pExt[i] );
  }
}

/*********************************************************************
 * @fn      Topo_EndLine
 * @brief   Terminate the line with CR LF and send it out of the UART.
 */
static void Topo_EndLine( void )
{
  if ( topoSuppress != FALSE )
  {
    return;                    /* this line is outside the current window */
  }

  Topo_PutChar( '\r' );
  Topo_PutChar( '\n' );
  Topo_Flush();
}

/*********************************************************************
 * @fn      Topo_PutInterval
 * @brief   Append the current report interval, e.g. "10 s" or "OFF".
 */
static void Topo_PutInterval( void )
{
  if ( topoReportTicks == 0 )
  {
    Topo_PutStr( "OFF" );
  }
  else
  {
    Topo_PutDec( (uint16)(topoReportTicks / TOPO_TICKS_PER_SEC) );
    Topo_PutStr( " s" );
  }
}

/*********************************************************************
 * @fn      Topo_RoleStr
 * @brief   Text of the compile time device role.
 *
 *          These macros come from the linker configuration files:
 *            f8wCoord.cfg -> ZDO_COORDINATOR + RTR_NWK
 *            f8wRouter.cfg-> RTR_NWK
 *            f8wEndev.cfg -> (none)
 */
static const char *Topo_RoleStr( void )
{
#if defined ( ZDO_COORDINATOR )
  return "Coordinator";
#elif defined ( RTR_NWK )
  return "Router";
#else
  return "End Device";
#endif
}

/*********************************************************************
 * @fn      Topo_RelationStr
 * @brief   Text of one nodeRelation value taken from AssocList.h.
 */
static const char *Topo_RelationStr( uint8 rel )
{
  switch ( rel )
  {
    case PARENT:            return "Parent";
    case CHILD_RFD:         return "Child/EndDev/rxOff";
    case CHILD_RFD_RX_IDLE: return "Child/EndDev/rxOn";
    case CHILD_FFD:         return "Child/Router/rxOff";
    case CHILD_FFD_RX_IDLE: return "Child/Router/rxOn";
    case NEIGHBOR:          return "Neighbor";
    default:                return "Other";
  }
}

/*********************************************************************
 * @fn      Topo_IsChild
 * @brief   TRUE when the association entry describes one of our children.
 */
static uint8 Topo_IsChild( uint8 rel )
{
  if ( (rel == CHILD_RFD)         ||
       (rel == CHILD_RFD_RX_IDLE) ||
       (rel == CHILD_FFD)         ||
       (rel == CHILD_FFD_RX_IDLE) )
  {
    return TRUE;
  }
  return FALSE;
}

/**************************************************************************************************
 *                                     TOPOLOGY REPORT
 **************************************************************************************************/

/*********************************************************************
 * @fn      Topo_PrintHelp
 * @brief   Print the terminal command list and the current settings.
 */
static void Topo_PrintHelp( void )
{
  Topo_BeginLine();
  Topo_PutStr( "+---------- TOPO terminal commands ----------+" );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "   t : print the report now" );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "   p : automatic report ON / OFF" );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "   + : report interval + 5 s" );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "   - : report interval - 5 s" );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "   h : this list" );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "   current interval : " );
  Topo_PutInterval();
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "+--------------------------------------------+" );
  Topo_EndLine();
}

/*********************************************************************
 * @fn      Topo_Print
 * @brief   Print one complete topology report.
 *
 * @return  none
 */
void Topo_Print( void )
{
  uint8                 i;
  uint8                 cnt;
  uint16                parentAddr;
  associated_devices_t *pDev;
  neighborEntry_t       nb;

  TOPO_STEP( "1 enter" );

  /* ---------------- make sure we are on a network ---------------- */
  if ( (devState != DEV_ROUTER)    &&
       (devState != DEV_END_DEVICE) &&
       (devState != DEV_ZB_COORD) )
  {
    /* Still not on a network. Print what the firmware thinks it is and the raw
     * devState value - this makes it possible to tell these three apart:
     *   Role=Router      -> the wrong firmware was flashed (a router cannot form a network)
     *   Role=Coordinator -> the firmware is right but network formation failed
     *   (nothing at all) -> Topo_Print() was never reached
     */
    Topo_BeginLine();
    Topo_PutStr( "TOPO: not joined yet. Role=" );
    Topo_PutStr( Topo_RoleStr() );
    Topo_PutStr( " devState=" );
    Topo_PutDec( (uint16)devState );
    Topo_EndLine();
    return;
  }

  TOPO_STEP( "2 joined" );

  /* ---------------- block header (with our own short address) ---------------- */
  Topo_BeginLine();
  Topo_PutStr( "+-------- TOPO START " );
  Topo_PutHex16( _NIB.nwkDevAddress );
  Topo_PutStr( " --------" );
  Topo_EndLine();

  /* ---------------- own identity ---------------- */
  Topo_BeginLine();
  Topo_PutStr( "Role      : " );
  Topo_PutStr( Topo_RoleStr() );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "ShortAddr : " );
  Topo_PutHex16( _NIB.nwkDevAddress );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "IEEEAddr  : " );
  Topo_PutExtAddr( NLME_GetExtAddr() );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "PANID     : " );
  Topo_PutHex16( _NIB.nwkPanId );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "Channel   : " );
  Topo_PutDec( (uint16)_NIB.nwkLogicalChannel );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "Depth     : " );
  Topo_PutDec( (uint16)_NIB.nodeDepth );
  Topo_EndLine();

  Topo_BeginLine();
  Topo_PutStr( "CoordAddr : " );
  Topo_PutHex16( _NIB.nwkCoordAddress );
  Topo_EndLine();

  TOPO_STEP( "3 identity ok" );

  /* ---------------- parent ---------------- */
  parentAddr = INVALID_NODE_ADDR;

  for ( i = 0; i < NWK_MAX_DEVICES; i++ )
  {
    pDev = AssocFindDevice( i );
    if ( pDev == NULL )
    {
      continue;
    }
    if ( pDev->nodeRelation == PARENT )
    {
      parentAddr = pDev->shortAddr;
      break;
    }
  }

  Topo_BeginLine();
  Topo_PutStr( "Parent    : " );
  if ( parentAddr == INVALID_NODE_ADDR )
  {
    Topo_PutStr( "(none)" );
  }
  else
  {
    Topo_PutHex16( parentAddr );
  }
  Topo_EndLine();

  /* ---------------- children ---------------- */
  cnt = 0;
  for ( i = 0; i < NWK_MAX_DEVICES; i++ )
  {
    pDev = AssocFindDevice( i );
    if ( (pDev != NULL) && Topo_IsChild( pDev->nodeRelation ) )
    {
      cnt++;
    }
  }

  Topo_BeginLine();
  Topo_PutStr( "Children  : " );
  Topo_PutDec( (uint16)cnt );
  Topo_EndLine();

  for ( i = 0; i < NWK_MAX_DEVICES; i++ )
  {
    pDev = AssocFindDevice( i );
    if ( pDev == NULL )
    {
      continue;
    }
    if ( Topo_IsChild( pDev->nodeRelation ) == FALSE )
    {
      continue;
    }

    Topo_BeginLine();
    Topo_PutStr( "   -> " );
    Topo_PutHex16( pDev->shortAddr );
    Topo_PutStr( "  " );
    Topo_PutStr( Topo_RelationStr( pDev->nodeRelation ) );
    Topo_PutStr( "  LQI=" );
    Topo_PutDec( (uint16)pDev->linkInfo.rxLqi );
    Topo_PutStr( " / 255" );
    Topo_EndLine();
  }

  TOPO_STEP( "4 assoc ok" );

  /* ---------------- neighbour table ---------------- */
  cnt = 0;
  NLME_GetRequest( nwkNumNeighborTableEntries, 0, &cnt );

  Topo_BeginLine();
  Topo_PutStr( "Neighbors : " );
  Topo_PutDec( (uint16)cnt );
  Topo_EndLine();

  for ( i = 0; i < cnt; i++ )
  {
    if ( NLME_GetRequest( nwkNeighborTable, i, &nb ) != ZSuccess )
    {
      continue;
    }

    Topo_BeginLine();
    Topo_PutStr( "   [" );
    Topo_PutDec( (uint16)i );
    Topo_PutStr( "] " );
    Topo_PutHex16( nb.neighborAddress );
    Topo_PutStr( "  IEEE=" );
    Topo_PutExtAddr( nb.neighborExtAddr );
    Topo_PutStr( "  LQI=" );
    Topo_PutDec( (uint16)nb.linkInfo.rxLqi );
    Topo_PutStr( " / 255" );
    Topo_EndLine();
  }

  TOPO_STEP( "5 done" );

  /* ---------------- block footer ---------------- */
  Topo_BeginLine();
  Topo_PutStr( "+--------- TOPO END " );
  Topo_PutHex16( _NIB.nwkDevAddress );
  Topo_PutStr( " ---------" );
  Topo_EndLine();
}

/**************************************************************************************************
 *                                  SERIAL TERMINAL (INPUT)
 **************************************************************************************************/

/*********************************************************************
 * @fn      Topo_ReportRequest
 * @brief   Ask for a full report.
 *
 *          The report is NOT printed here - it is emitted a few lines at a time
 *          by Topo_ReportStep(), which the application tick calls.  This keeps
 *          every UART burst short enough for the small DMA transmit buffer.
 */
static void Topo_ReportRequest( void )
{
  topoNextFrom = 0;
  topoBusy     = TRUE;
}

/*********************************************************************
 * @fn      Topo_ReportStep
 * @brief   Emit the next slice of the report.
 *
 *          Topo_Print() always runs from beginning to end, but only the lines
 *          inside [topoWinFrom, topoWinTo) are actually sent out; the rest are
 *          built and thrown away.  That costs a little CPU but no RAM, and it
 *          keeps this module free of any dynamic state machine.
 *
 * @return  TRUE while more of the report still has to be emitted.
 */
static uint8 Topo_ReportStep( void )
{
  topoWinFrom = topoNextFrom;
  topoWinTo   = (uint8)( topoNextFrom + TOPO_LINES_PER_PASS );
  topoLineNo  = 0;

  Topo_Print();

  if ( topoLineNo <= topoWinTo )
  {
    /* the report ended inside this window - we are done */
    topoBusy     = FALSE;
    topoNextFrom = 0;
    topoLineNo   = 0;
    topoWinFrom  = 0;
    topoWinTo    = 255;
  }
  else
  {
    topoNextFrom = topoWinTo;
  }

  return topoBusy;
}

/*********************************************************************
 * @fn      Topo_SetInterval
 * @brief   Store a new report interval and tell the user about it.
 */
static void Topo_SetInterval( uint16 ticks )
{
  topoReportTicks = ticks;
  topoTickCnt = 0;

  Topo_BeginLine();
  Topo_PutStr( "TOPO: automatic report interval = " );
  Topo_PutInterval();
  Topo_EndLine();
}

/*********************************************************************
 * @fn      Topo_HandleCmd
 * @brief   Execute one terminal command. Task context only.
 */
static void Topo_HandleCmd( uint8 ch )
{
  uint16 ticks;

  /* Any command cancels a report that is still being emitted, and the helpers
   * below must not be filtered by a leftover emission window. */
  topoBusy     = FALSE;
  topoNextFrom = 0;
  topoWinFrom  = 0;
  topoWinTo    = 255;
  topoLineNo   = 0;

  switch ( ch )
  {
    case 't':
    case 'T':
      topoTickCnt = 0;
      Topo_ReportRequest();      /* emitted over the next few ticks */
      break;

    case 'p':
    case 'P':
      if ( topoReportTicks != 0 )
      {
        Topo_SetInterval( 0 );                     /* automatic report OFF */
      }
      else
      {
        Topo_SetInterval( TOPO_REPORT_TICKS_DEF ); /* back to the default interval */
      }
      break;

    case '+':
    case '=':
      ticks = (uint16)(topoReportTicks + TOPO_REPORT_TICKS_STEP);
      if ( ticks > TOPO_REPORT_TICKS_MAX )
      {
        ticks = TOPO_REPORT_TICKS_MAX;
      }
      Topo_SetInterval( ticks );
      break;

    case '-':
      if ( topoReportTicks > TOPO_REPORT_TICKS_STEP )
      {
        ticks = (uint16)(topoReportTicks - TOPO_REPORT_TICKS_STEP);
      }
      else
      {
        ticks = 0;
      }
      Topo_SetInterval( ticks );
      break;

    case 'h':
    case 'H':
    case '?':
      Topo_PrintHelp();
      break;

    default:
      /* unknown character - ignore it silently */
      break;
  }
}

/*********************************************************************
 * @fn      Topo_RxByte
 * @brief   Feed one received serial byte.
 *
 *          This function may be called from the HAL UART callback, which can run in
 *          interrupt context. It therefore only records the byte; the printing is
 *          done later by Topo_Tick() in task context.
 */
void Topo_RxByte( uint8 ch )
{
  switch ( ch )
  {
    case 't': case 'T':
    case 'p': case 'P':
    case 'h': case 'H':
    case '?':
    case '+': case '=':
    case '-':
      topoCmd = ch;
      break;

    default:
      /* CR, LF, spaces and everything else: ignore */
      break;
  }
}

/*********************************************************************
 * @fn      Topo_Tick
 * @brief   Drive the module. Task context only.
 *
 *          Called by the application every TOPO_TICK_MS milliseconds.
 *          A pending terminal command always has priority over the automatic report.
 */
void Topo_Tick( void )
{
  uint8         cmd;
  halIntState_t intState;

  /* take the pending command atomically: the UART callback may run in between */
  HAL_ENTER_CRITICAL_SECTION( intState );
  cmd = topoCmd;
  topoCmd = 0;
  HAL_EXIT_CRITICAL_SECTION( intState );

  if ( cmd != 0 )
  {
    Topo_HandleCmd( cmd );
    return;
  }

  /* a report that is still being emitted has priority */
  if ( topoBusy != FALSE )
  {
    Topo_ReportStep();
    return;
  }

  /* no command: run the automatic report if it is enabled */
  if ( topoReportTicks == 0 )
  {
    return;
  }

  topoTickCnt++;
  if ( topoTickCnt >= topoReportTicks )
  {
    topoTickCnt = 0;
    Topo_ReportRequest();
  }
}

/**************************************************************************************************
**************************************************************************************************/
