view main/robots/odr/Scoreboard.cpp @ 225:c4009187a522 main

Checkpoint
author Bob Cook <bob@bobcookdev.com>
date Sat, 28 Jun 2014 16:12:28 -0700
parents 8e6ca2ead01a
children 03064d391e79
line wrap: on
line source

// ----------------------------------------------------------------------------------------
//
//  robots/odr/Scoreboard.cpp
//    
//  Bob Cook Development, Robotics Library
//  http://www.bobcookdev.com/rl/
// 
//  Global thread-safe state derived from incoming CAN messages and other internal data.
//
//  Copyright (c) 2010-2014 Bob Cook
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//
// ----------------------------------------------------------------------------------------

#include "Scoreboard.h"

#include <stdexcept>

#include <Poco/Exception.h>
#include <Poco/Logger.h>
#include <Poco/NumberFormatter.h>
#include <Poco/Thread.h>

#include "packages/common/can/can_helpers.h"
#include "packages/common/can/can_messages.h"
#include "packages/common/can/can_nodes.h"

#include "packages/linux/can/CANMessage.h"

// ----------------------------------------------------------------------------------------

Poco::RWLock    Scoreboard::sm_rwLock;
bool            Scoreboard::sm_isEmergencyActive;
bool            Scoreboard::sm_MotionEmergency;
bool            Scoreboard::sm_isMotionAlive;
bool            Scoreboard::sm_isControllerAlive;
bool            Scoreboard::sm_isEStopActive;
bool            Scoreboard::sm_isButtonOneDown;
bool            Scoreboard::sm_isButtonTwoDown;
std::string     Scoreboard::sm_msgLastText;
Poco::Timestamp Scoreboard::sm_msgLastUpdate;
bool            Scoreboard::sm_isSonarFrontAlive;
bool            Scoreboard::sm_isSonarFrontEnabled;
uint8_t         Scoreboard::sm_sonarFrontLeft;
uint8_t         Scoreboard::sm_sonarFrontCenterLeft;
uint8_t         Scoreboard::sm_sonarFrontCenterRight;
uint8_t         Scoreboard::sm_sonarFrontRight;
bool            Scoreboard::sm_isGpsAlive;
bool            Scoreboard::sm_gpsHasFix;
std::list< double > Scoreboard::sm_gpsLatitudes;
double                Scoreboard::sm_gpsLatitudeAvg;
std::list< double > Scoreboard::sm_gpsLongitudes;
double                Scoreboard::sm_gpsLongitudeAvg;
bool            Scoreboard::sm_isImuAlive;
Poco::Timestamp Scoreboard::sm_imuLastUpdate;
double          Scoreboard::sm_imuRoll;
double          Scoreboard::sm_imuPitch;
double          Scoreboard::sm_imuYaw;
double          Scoreboard::sm_navTargetHeading = 100.0;
int8_t          Scoreboard::sm_navMaximumSpeed = 30;

// ----------------------------------------------------------------------------------------

bool Scoreboard::isGpsSensorAlive()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_isGpsAlive;
}

// ----------------------------------------------------------------------------------------

bool Scoreboard::isControllerAlive()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return true; // fake it
    // return sm_isControllerAlive;
}

// ----------------------------------------------------------------------------------------

bool Scoreboard::isEStopActive()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_isEStopActive;
}

// ----------------------------------------------------------------------------------------

bool Scoreboard::isButtonOneDown()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_isButtonOneDown;
}

// ----------------------------------------------------------------------------------------

bool Scoreboard::isButtonTwoDown()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_isButtonTwoDown;
}
                 
// ----------------------------------------------------------------------------------------

void Scoreboard::sendManagerMessage( const char* msg )
{
    // don't repeat the same message more than once per five seconds

    static const Poco::Timestamp::TimeDiff FiveSeconds = Poco::Timestamp::resolution() * 5;

    if ( sm_msgLastText == msg )
    {
        if ( sm_msgLastUpdate.elapsed() <= FiveSeconds )
        {
            return;
        }
    }

    // either a new message, or time to send it again

    sm_msgLastText = msg;
    sm_msgLastUpdate.update();

    uint32_t updatemsgid = can_build_message_id(
                    can_node_odr_manager, can_node_broadcast, can_dataid_odrmgr_update );

    can_data_odrmgr_update update;

    uint8_t len = 0;
    for ( ; len < sizeof( update.msg ); ++len )
    {
        if ( *msg == '\0' ) break;
        update.msg[len] = *msg++;
    }

    CANMessage::QueueToSend(
            new CANMessage( updatemsgid, reinterpret_cast< uint8_t* >( &update ), len ) );
}

// ----------------------------------------------------------------------------------------

bool Scoreboard::isSonarFrontAlive()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_isSonarFrontAlive;
}

// ----------------------------------------------------------------------------------------

bool Scoreboard::isSonarFrontEnabled()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_isSonarFrontEnabled;
}

// ----------------------------------------------------------------------------------------

uint8_t Scoreboard::sonarFrontLeft()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_sonarFrontLeft;
}

// ----------------------------------------------------------------------------------------

uint8_t Scoreboard::sonarFrontCenterLeft()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_sonarFrontCenterLeft;
}

// ----------------------------------------------------------------------------------------

uint8_t Scoreboard::sonarFrontCenterRight()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_sonarFrontCenterRight;
}

// ----------------------------------------------------------------------------------------

uint8_t Scoreboard::sonarFrontRight()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_sonarFrontRight;
}

// ----------------------------------------------------------------------------------------

bool Scoreboard::isGpsAlive()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_isGpsAlive;
}

// ----------------------------------------------------------------------------------------

bool Scoreboard::gpsHasFix()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_gpsHasFix;
}

// ----------------------------------------------------------------------------------------

double Scoreboard::gpsLatitude()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_gpsLatitudeAvg;
}

// ----------------------------------------------------------------------------------------

double Scoreboard::gpsLongitude()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_gpsLongitudeAvg;
}

// ----------------------------------------------------------------------------------------

bool Scoreboard::isImuAlive()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_isImuAlive;
}

// ----------------------------------------------------------------------------------------

double Scoreboard::imuRoll()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_imuRoll;
}

// ----------------------------------------------------------------------------------------

double Scoreboard::imuPitch()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_imuPitch;
}

// ----------------------------------------------------------------------------------------

double Scoreboard::imuYaw()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_imuYaw;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::imuUpdate( double roll, double pitch, double yaw )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    sm_isImuAlive = true;
    sm_imuLastUpdate.update();

    sm_imuRoll  = roll;
    sm_imuPitch = pitch;
    sm_imuYaw   = yaw;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::imuIsOffline()
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );
    sm_isImuAlive = false;
}

// ----------------------------------------------------------------------------------------

int8_t Scoreboard::navMaximumSpeed()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_navMaximumSpeed;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::navSetMaximumSpeed( int8_t max )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );
    sm_navMaximumSpeed = max;
}

// ----------------------------------------------------------------------------------------

double Scoreboard::navTargetHeading()
{
    Poco::RWLock::ScopedReadLock lock( sm_rwLock );
    return sm_navTargetHeading;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::navSetTargetHeading( double heading )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );
    sm_navTargetHeading = heading;
}

// ----------------------------------------------------------------------------------------

Scoreboard::Scoreboard( const std::string& loggerName )
    : Poco::Runnable(),
      m_loggerName( loggerName ),
      m_quitEvent( false /* not auto-reset */ ),
      m_timerLogStatus(),
      m_timerSendManagerUpdate(),
      m_timerGpsUpdate(),
      m_timerMotionUpdate(),
      m_timerControllerUpdate(),
      m_timerSonarFrontUpdate(),
      m_timerSendImuDataUpdate()
{
    m_timerLogStatus.setPeriodicInterval( 5000 );
    m_timerLogStatus.start(
            Poco::TimerCallback< Scoreboard >( *this, &Scoreboard::logSystemStatus ) );

    m_timerSendManagerUpdate.setPeriodicInterval( 2500 );
    m_timerSendManagerUpdate.start(
            Poco::TimerCallback< Scoreboard >( *this, &Scoreboard::sendManagerUpdate ) );

    m_timerGpsUpdate.setPeriodicInterval( 5000 );
    m_timerGpsUpdate.start(
            Poco::TimerCallback< Scoreboard >(
                    *this, &Scoreboard::timeoutGpsSensorUpdate ) );

    m_timerMotionUpdate.setPeriodicInterval( 5000 );
    m_timerMotionUpdate.start(
            Poco::TimerCallback< Scoreboard >(
                    *this, &Scoreboard::timeoutMotionUpdate ) );

    m_timerControllerUpdate.setPeriodicInterval( 5000 );
    m_timerControllerUpdate.start(
            Poco::TimerCallback< Scoreboard >(
                    *this, &Scoreboard::timeoutControllerUpdate ) );

    m_timerSonarFrontUpdate.setPeriodicInterval( 5000 );
    m_timerSonarFrontUpdate.start(
            Poco::TimerCallback< Scoreboard >(
                    *this, &Scoreboard::timeoutSonarFrontUpdate ) );

    m_timerSendImuDataUpdate.setPeriodicInterval( 750 );
    m_timerSendImuDataUpdate.start(
            Poco::TimerCallback< Scoreboard >( *this, &Scoreboard::sendImuDataUpdate ) );
}

// ----------------------------------------------------------------------------------------

Scoreboard::~Scoreboard()
{
}

// ----------------------------------------------------------------------------------------

void Scoreboard::timeToQuit()
{
    m_quitEvent.set();
}

// ----------------------------------------------------------------------------------------

void Scoreboard::logSystemStatus( Poco::Timer& timer )
{
    Poco::Logger& log = Poco::Logger::get( m_loggerName );

    Poco::RWLock::ScopedReadLock lock( sm_rwLock );

    if ( sm_isGpsAlive )
    {
        if ( sm_gpsHasFix )
        {
            log.information(
                Poco::Logger::format(
                    "Scoreboard: GPS latitude: $0 longitude: $1",
                    Poco::NumberFormatter::format( sm_gpsLatitudeAvg, 5 ),
                    Poco::NumberFormatter::format( sm_gpsLongitudeAvg, 5 ) ) );
        }
        else
        {
            log.information( "Scoreboard: GPS has no position fix" );
        }
    }
    else
    {
        log.information( "Scoreboard: GPS is not alive" );
    }
}

// ----------------------------------------------------------------------------------------

void Scoreboard::sendManagerUpdate( Poco::Timer& timer )
{
    Poco::Logger& log = Poco::Logger::get( m_loggerName );

    // send a heartbeat as if we are odr-controller (which is deprecated)

    uint32_t hbeatmsgid = can_build_message_id(
                    can_node_odr_manager, can_node_broadcast, can_dataid_heartbeat );

    CANMessage::QueueToSend( new CANMessage( hbeatmsgid ) );

    // send the manager status message

    uint32_t updatemsgid = can_build_message_id(
                    can_node_odr_manager, can_node_broadcast, can_dataid_odrmgr_update );

    CANMessage::QueueToSend( new CANMessage( updatemsgid ) );

    // lock the scoreboard data for read-only access

    Poco::RWLock::ScopedReadLock lock( sm_rwLock );

    // update the emergency / all-clear status

    if ( sm_isEmergencyActive )
    {
        uint32_t msgid = can_build_message_id(
                    can_node_odr_manager, can_node_broadcast, can_dataid_emergency );

        CANMessage::QueueToSend( new CANMessage( msgid ) );
    }
    else
    {
        uint32_t msgid = can_build_message_id(
                    can_node_odr_manager, can_node_broadcast, can_dataid_all_clear );

        CANMessage::QueueToSend( new CANMessage( msgid ) );
    }

    // monitor the length of pending CAN messages, log when it looks too long

    unsigned send_q_len = CANMessage::SendQueueSize();
    unsigned recv_q_len = CANMessage::ReceiveQueueSize();

    if ( send_q_len > 5 || recv_q_len > 5 )
    {
        log.information(
            Poco::Logger::format(
                "Scoreboard: CAN queue length: send $0 / recv $1",
                Poco::NumberFormatter::format( send_q_len ),
                Poco::NumberFormatter::format( recv_q_len ) ) );
    }
}

// ----------------------------------------------------------------------------------------

void Scoreboard::timeoutGpsSensorUpdate( Poco::Timer& timer )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );
    sm_isGpsAlive = false;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::timeoutMotionUpdate( Poco::Timer& timer )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );
    sm_isMotionAlive = false;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::timeoutControllerUpdate( Poco::Timer& timer )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );
    sm_isControllerAlive = false;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::timeoutSonarFrontUpdate( Poco::Timer& timer )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );
    sm_isSonarFrontAlive = false;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::sendImuDataUpdate( Poco::Timer& timer )
{
    // check if the IMU is actually offline; more than 2 seconds silence means yes

    static const Poco::Timestamp::TimeDiff TwoSeconds = Poco::Timestamp::resolution() * 2;

    if ( sm_imuLastUpdate.elapsed() > TwoSeconds )
    {
        sm_isImuAlive = false;
        return;
    }

    // YAW

    uint32_t imudatamsgid = can_build_message_id(
                    can_node_odr_manager, can_node_broadcast, can_dataid_imu_yaw );

    can_data_imu_data data;
    data.data = static_cast< int32_t >( Scoreboard::imuYaw() * can_data_imu_multiplier );

    CANMessage::QueueToSend(
            new CANMessage( imudatamsgid,
                            reinterpret_cast< uint8_t* >( &data ),
                            sizeof( data ) ) );

    // PITCH

    imudatamsgid = can_build_message_id(
                    can_node_odr_manager, can_node_broadcast, can_dataid_imu_pitch );

    data.data = static_cast< int32_t >( Scoreboard::imuPitch() * can_data_imu_multiplier );

    CANMessage::QueueToSend(
            new CANMessage( imudatamsgid,
                            reinterpret_cast< uint8_t* >( &data ),
                            sizeof( data ) ) );

    // ROLL

    imudatamsgid = can_build_message_id(
                    can_node_odr_manager, can_node_broadcast, can_dataid_imu_roll );

    data.data = static_cast< int32_t >( Scoreboard::imuRoll() * can_data_imu_multiplier );

    CANMessage::QueueToSend(
            new CANMessage( imudatamsgid,
                            reinterpret_cast< uint8_t* >( &data ),
                            sizeof( data ) ) );
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvEmergency( uint8_t srcNode )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    sm_isEmergencyActive = true;

    switch ( srcNode )
    {
        case can_node_odr_motion:
            sm_MotionEmergency = true;
            break;
    }
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvAllClear( uint8_t srcNode )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    switch ( srcNode )
    {
        case can_node_odr_motion:
            sm_MotionEmergency = false;
            break;
    }

    if ( sm_isEmergencyActive )
    {
        sm_isEmergencyActive = sm_MotionEmergency; // | sm_xxxEmergency

    }
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvHeartbeat( uint8_t srcNode )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    switch ( srcNode )
    {
        case can_node_odr_controller:
            m_timerControllerUpdate.restart();
            sm_isControllerAlive = true;
            break;

        case can_node_odr_motion:
            m_timerMotionUpdate.restart();
            sm_isMotionAlive = true;
            break;

        case can_node_sensor_gps:
            m_timerGpsUpdate.restart();
            sm_isGpsAlive = true;
            break;
    }
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvControllerUpdate( CANMessage* msg )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    m_timerControllerUpdate.restart();
    sm_isControllerAlive = true;

    can_data_odrctl_update info;
    msg->msgData( reinterpret_cast< uint8_t* >( &info ), sizeof( info ) );

    sm_isEStopActive = info.estop;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvButtonUpdate( CANMessage* msg )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    can_data_odrctl_button info;
    msg->msgData( reinterpret_cast< uint8_t* >( &info ), sizeof( info ) );

    sm_isButtonOneDown = info.button_1;
    sm_isButtonTwoDown = info.button_2;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvSonarFrontStatus( bool enabled )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    m_timerSonarFrontUpdate.restart();
    sm_isSonarFrontAlive = true;

    sm_isSonarFrontEnabled = enabled;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvSonarFrontValues( CANMessage* msg )
{
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    can_data_sonar_front info;
    msg->msgData( reinterpret_cast< uint8_t* >( &info ), sizeof( info ) );

    sm_sonarFrontLeft        = info.left;
    sm_sonarFrontCenterLeft  = info.center_left;
    sm_sonarFrontCenterRight = info.center_right;
    sm_sonarFrontRight       = info.right;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvGpsFixInfo( CANMessage* msg )
{
    can_data_gps_fix info;
    msg->msgData( reinterpret_cast< uint8_t* >( &info ), sizeof( info ) );

    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );
    sm_gpsHasFix = info.satellites > 0;
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvGpsLatitude( CANMessage* msg )
{
    can_data_gps_data info;
    msg->msgData( reinterpret_cast< uint8_t* >( &info ), sizeof( info ) );

    double lat = static_cast< double >( info.degrees );
    double min = static_cast< double >( info.min_thousandths )
               / static_cast< double >( can_data_gps_min_multiplier );
    lat += ( min / 60.0L );
    
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    sm_gpsLatitudes.push_back( lat );
    while ( sm_gpsLatitudes.size() > 10 )
    {
        sm_gpsLatitudes.pop_front();
    }

    std::list< double >::iterator itr = sm_gpsLatitudes.begin();
    double latsTotal = 0.0;

    while ( itr != sm_gpsLatitudes.end() )
    {
        latsTotal += *itr++;
    }

    sm_gpsLatitudeAvg = latsTotal / sm_gpsLatitudes.size();
}

// ----------------------------------------------------------------------------------------

void Scoreboard::recvGpsLongitude( CANMessage* msg )
{
    can_data_gps_data info;
    msg->msgData( reinterpret_cast< uint8_t* >( &info ), sizeof( info ) );

    double lon = static_cast< double >( info.degrees );
    double min = static_cast< double >( info.min_thousandths )
               / static_cast< double >( can_data_gps_min_multiplier );
    lon += ( min / 60.0L );
    
    Poco::RWLock::ScopedWriteLock lock( sm_rwLock );

    sm_gpsLongitudes.push_back( lon );
    while ( sm_gpsLongitudes.size() > 10 )
    {
        sm_gpsLongitudes.pop_front();
    }

    std::list< double >::iterator itr = sm_gpsLongitudes.begin();
    double lonsTotal = 0.0;

    while ( itr != sm_gpsLongitudes.end() )
    {
        lonsTotal += *itr++;
    }

    sm_gpsLongitudeAvg = lonsTotal / sm_gpsLongitudes.size();
}

// ----------------------------------------------------------------------------------------

void Scoreboard::run()
{
    Poco::Logger& log = Poco::Logger::get( m_loggerName );

    for ( ;; )
    {
        log.information( std::string( "Scoreboard::run() starting" ) );

        try
        {
            for ( ;; )
            {
                CANMessage* msg = CANMessage::WaitDequeueReceived( 250 ); // 250 ms

                if ( msg == 0 && m_quitEvent.tryWait( 0 ) )
                {
                    log.information( std::string( "Scoreboard::run() stopping" ) );
                    return;
                }

                if ( msg == 0 )
                {
                    continue;
                }

                uint8_t  srcNode;
                uint8_t  dstNode;
                uint16_t dataId;

                can_parse_message_id( 
                        msg->msgIdentifier(), &srcNode, &dstNode, &dataId );

                switch ( dataId )
                {
                    case can_dataid_emergency:
                        recvEmergency( srcNode );
                        break;

                    case can_dataid_all_clear:
                        recvAllClear( srcNode );
                        break;

                    case can_dataid_heartbeat:
                        recvHeartbeat( srcNode );
                        break;

                    case can_dataid_odrctl_update:
                        recvControllerUpdate( msg );
                        break;

                    case can_dataid_odrctl_button:
                        recvButtonUpdate( msg );
                        break;

                    case can_dataid_sonar_front_state_enabled:
                        recvSonarFrontStatus( true /* enabled */ );
                        break;

                    case can_dataid_sonar_front_state_disabled:
                        recvSonarFrontStatus( false /* not enabled */ );
                        break;

                    case can_dataid_sonar_front:
                        recvSonarFrontStatus( true /* enabled */ );
                        recvSonarFrontValues( msg );
                        break;

                    case can_dataid_gps_fix:
                        recvGpsFixInfo( msg );
                        break;

                    case can_dataid_latitude:
                        recvGpsLatitude( msg );
                        break;

                    case can_dataid_longitude:
                        recvGpsLongitude( msg );
                        break;
                }
            }
        }
        catch ( const Poco::Exception& ex )
        {
            log.error(
                Poco::Logger::format(
                    "Scoreboard::run() caught Poco::Exception: $0", ex.displayText() ) );
        }
        catch ( const std::exception& ex )
        {
            log.error(
                Poco::Logger::format(
                    "Scoreboard::run() caught std::exception: $0", ex.what() ) );
        }
        catch ( ... )
        {
            log.error( "Scoreboard::run() caught unknown exception" );
        }

        // sleep for 1/2 second after exception processing, but don't exit
        Poco::Thread::sleep( 500 );
    }
}

// ----------------------------------------------------------------------------------------