mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 05:09:20 +02:00
hardware protocol v2: sessions, subscriptions and the transport worker
Adds the logical protocol layer on top of the frame codec: session handshake with negotiated frame size, session scoped symbol handles, state subscriptions with periodic, on change and deadband filtering, command events with duplicate protection and continuous controls with stale update rejection, plus per connection rate limits and diagnostics. A single worker thread owns all of the blocking transport work and exchanges finished frames with the simulation thread through short locked queues, so a stalled, flooding or unplugged controller cannot hold up the simulation.
This commit is contained in:
28
hardware/diagnostics/hardware_diagnostics.cpp
Normal file
28
hardware/diagnostics/hardware_diagnostics.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "hardware/diagnostics/hardware_diagnostics.h"
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
void rate_counter::update( double const Deltatime )
|
||||
{
|
||||
m_window += Deltatime;
|
||||
if( m_window < 1.0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_rate = static_cast<float>( m_accumulator / m_window );
|
||||
m_accumulator = 0;
|
||||
m_window = 0.0;
|
||||
}
|
||||
|
||||
} // namespace hardware
|
||||
114
hardware/diagnostics/hardware_diagnostics.h
Normal file
114
hardware/diagnostics/hardware_diagnostics.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "hardware/protocol/protocol_defs.h"
|
||||
|
||||
// diagnostics gathered per connection, presented in the simulator debug panel
|
||||
// transport and protocol problems are counted separately, as they usually have different causes
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
// sliding estimate of events per second
|
||||
class rate_counter
|
||||
{
|
||||
|
||||
public:
|
||||
// methods
|
||||
void add( std::uint64_t const Count = 1 ) { m_accumulator += Count; }
|
||||
void update( double const Deltatime );
|
||||
float rate() const { return m_rate; }
|
||||
|
||||
private:
|
||||
// members
|
||||
std::uint64_t m_accumulator = 0;
|
||||
double m_window = 0.0;
|
||||
float m_rate = 0.0f;
|
||||
};
|
||||
|
||||
struct transport_diagnostics
|
||||
{
|
||||
bool connected = false;
|
||||
std::string endpoint;
|
||||
std::string last_error;
|
||||
std::uint64_t rx_bytes = 0;
|
||||
std::uint64_t tx_bytes = 0;
|
||||
std::uint64_t transport_errors = 0;
|
||||
std::uint64_t reconnects = 0;
|
||||
};
|
||||
|
||||
struct protocol_diagnostics
|
||||
{
|
||||
std::uint64_t rx_frames = 0;
|
||||
std::uint64_t tx_frames = 0;
|
||||
std::uint64_t crc_errors = 0;
|
||||
std::uint64_t frame_errors = 0;
|
||||
std::uint64_t length_errors = 0;
|
||||
std::uint64_t version_errors = 0;
|
||||
std::uint64_t unknown_messages = 0;
|
||||
std::uint64_t unknown_handles = 0;
|
||||
std::uint64_t unknown_symbols = 0;
|
||||
std::uint64_t duplicate_commands = 0;
|
||||
std::uint64_t sequence_gaps = 0;
|
||||
std::uint64_t acks = 0;
|
||||
std::uint64_t nacks = 0;
|
||||
std::uint64_t rate_limited = 0;
|
||||
std::uint64_t state_updates = 0;
|
||||
std::uint64_t commands_executed = 0;
|
||||
std::uint64_t controls_applied = 0;
|
||||
// seconds since the last valid frame arrived
|
||||
double last_rx_age = 0.0;
|
||||
double round_trip_time_ms = 0.0;
|
||||
double connection_uptime = 0.0;
|
||||
float rx_rate = 0.0f;
|
||||
float tx_rate = 0.0f;
|
||||
int missed_heartbeats = 0;
|
||||
};
|
||||
|
||||
// optional information reported by the device itself
|
||||
struct device_diagnostics
|
||||
{
|
||||
bool available = false;
|
||||
std::uint32_t uptime_ms = 0;
|
||||
float supply_voltage = 0.0f;
|
||||
float temperature = 0.0f;
|
||||
std::uint32_t rx_overflows = 0;
|
||||
std::uint32_t tx_overflows = 0;
|
||||
std::uint32_t watchdog_resets = 0;
|
||||
std::string firmware_build;
|
||||
};
|
||||
|
||||
// everything the debug panel needs about one connection, copied out once per frame
|
||||
struct link_report
|
||||
{
|
||||
std::string transport_kind;
|
||||
std::string endpoint;
|
||||
std::string device_name;
|
||||
std::string device_role;
|
||||
std::string device_id;
|
||||
std::string firmware_version;
|
||||
link_state state = link_state::disconnected;
|
||||
session_state session = session_state::disconnected;
|
||||
std::uint32_t session_id = 0;
|
||||
std::uint8_t protocol_major = 0;
|
||||
std::uint8_t protocol_minor = 0;
|
||||
std::size_t handles = 0;
|
||||
std::size_t subscriptions = 0;
|
||||
std::size_t frame_size = 0;
|
||||
transport_diagnostics transport;
|
||||
protocol_diagnostics protocol;
|
||||
device_diagnostics device;
|
||||
};
|
||||
|
||||
} // namespace hardware
|
||||
56
hardware/hardware_config.h
Normal file
56
hardware/hardware_config.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "hardware/protocol/protocol_defs.h"
|
||||
|
||||
// configuration of the hardware protocol v2, filled from eu07.ini
|
||||
// the header deliberately doesn't pull in any transport library, so that it can be included by Globals
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
struct serial_link_config
|
||||
{
|
||||
std::string port;
|
||||
int baud = 115200;
|
||||
};
|
||||
|
||||
struct config
|
||||
{
|
||||
// v2 is active when at least one link is configured; uart v1 keeps working independently
|
||||
bool enable = false;
|
||||
std::vector<serial_link_config> serial_links;
|
||||
|
||||
std::size_t frame_size = protocol_frame_size_default;
|
||||
int heartbeat_interval_ms = 500;
|
||||
// link is dropped after this many missed heartbeats
|
||||
int heartbeat_timeout_count = 5;
|
||||
|
||||
// per connection safety limits, see the rate limiting chapter of the specification
|
||||
int max_frames_per_second = 500;
|
||||
int max_payload_bytes_per_second = 262144;
|
||||
int max_commands_per_second = 100;
|
||||
|
||||
// interval of the worker thread polling loop, in milliseconds
|
||||
int poll_interval_ms = 1;
|
||||
// delay between reconnection attempts, in seconds
|
||||
float reconnect_interval = 1.0f;
|
||||
|
||||
// verbose protocol logging
|
||||
bool debug = false;
|
||||
// additionally log every frame; very noisy
|
||||
bool debug_frames = false;
|
||||
};
|
||||
|
||||
} // namespace hardware
|
||||
278
hardware/hardware_manager.cpp
Normal file
278
hardware/hardware_manager.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "hardware/hardware_manager.h"
|
||||
|
||||
#include "utilities/Logs.h"
|
||||
|
||||
#ifdef WITH_UART
|
||||
#include "hardware/transport/serial_transport.h"
|
||||
#endif
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// received frames waiting to be picked up by the simulation thread
|
||||
constexpr std::size_t rx_queue_limit = 256;
|
||||
// frames waiting to be pushed out by the worker thread
|
||||
constexpr std::size_t tx_queue_limit = 512;
|
||||
constexpr std::size_t read_chunk_size = 4096;
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
hardware_manager *hardware_manager::s_instance = nullptr;
|
||||
|
||||
hardware_link::hardware_link( std::unique_ptr<hardware_transport> Transport, config const &Config ) : m_transport( std::move( Transport ) ), m_config( Config ), m_session( *this, Config )
|
||||
{
|
||||
m_readbuffer.resize( read_chunk_size );
|
||||
m_decoder.set_frame_size_limit( m_config.frame_size );
|
||||
m_lastconnectattempt = std::chrono::steady_clock::now() - std::chrono::seconds( 10 );
|
||||
|
||||
m_report.transport_kind = m_transport->kind();
|
||||
m_report.endpoint = m_transport->endpoint();
|
||||
m_transportstatus.endpoint = m_report.endpoint;
|
||||
}
|
||||
|
||||
hardware_link::~hardware_link()
|
||||
{
|
||||
m_transport->disconnect();
|
||||
}
|
||||
|
||||
void hardware_link::send_packet( packet_header const &Header, std::vector<std::uint8_t> const &Payload )
|
||||
{
|
||||
std::vector<std::uint8_t> frame;
|
||||
encode_frame( Header, Payload.data(), Payload.size(), frame );
|
||||
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
if( m_txqueue.size() >= tx_queue_limit )
|
||||
{
|
||||
// the device isn't keeping up; the oldest update is the least interesting one
|
||||
m_txqueue.pop_front();
|
||||
}
|
||||
m_txqueue.emplace_back( std::move( frame ) );
|
||||
}
|
||||
|
||||
void hardware_link::service()
|
||||
{
|
||||
if( false == m_transport->connected() )
|
||||
{
|
||||
auto const now = std::chrono::steady_clock::now();
|
||||
if( std::chrono::duration<float>( now - m_lastconnectattempt ).count() < m_config.reconnect_interval )
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_lastconnectattempt = now;
|
||||
|
||||
if( true == m_transport->connect() )
|
||||
{
|
||||
m_decoder.reset();
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
m_rxqueue.clear();
|
||||
m_txqueue.clear();
|
||||
m_transportstatus.connected = true;
|
||||
m_transportstatus.last_error.clear();
|
||||
++m_transportstatus.reconnects;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
if( true == m_transportstatus.last_error.empty() )
|
||||
{
|
||||
++m_transportstatus.transport_errors;
|
||||
}
|
||||
m_transportstatus.connected = false;
|
||||
m_transportstatus.last_error = m_transport->last_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool failed = false;
|
||||
|
||||
// inbound
|
||||
std::vector<decoded_packet> packets;
|
||||
int const received = m_transport->read( m_readbuffer.data(), m_readbuffer.size() );
|
||||
if( received < 0 )
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
else if( received > 0 )
|
||||
{
|
||||
m_decoder.feed( m_readbuffer.data(), static_cast<std::size_t>( received ), packets );
|
||||
}
|
||||
|
||||
// outbound; partially written frames are kept until the port accepts the rest
|
||||
if( false == failed )
|
||||
{
|
||||
if( true == m_writebuffer.empty() )
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
while( ( false == m_txqueue.empty() ) && ( m_writebuffer.size() < read_chunk_size ) )
|
||||
{
|
||||
auto const &frame = m_txqueue.front();
|
||||
m_writebuffer.insert( m_writebuffer.end(), frame.begin(), frame.end() );
|
||||
m_txqueue.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
if( false == m_writebuffer.empty() )
|
||||
{
|
||||
int const written = m_transport->write( m_writebuffer.data(), m_writebuffer.size() );
|
||||
if( written < 0 )
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
else if( written > 0 )
|
||||
{
|
||||
m_writebuffer.erase( m_writebuffer.begin(), m_writebuffer.begin() + written );
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
m_transportstatus.tx_bytes += written;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( true == failed )
|
||||
{
|
||||
m_transport->disconnect();
|
||||
m_writebuffer.clear();
|
||||
m_decoder.reset();
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
m_transportstatus.connected = false;
|
||||
++m_transportstatus.transport_errors;
|
||||
m_transportstatus.last_error = m_transport->last_error();
|
||||
m_transportdropped = true;
|
||||
m_rxqueue.clear();
|
||||
m_txqueue.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
if( received > 0 )
|
||||
{
|
||||
m_transportstatus.rx_bytes += received;
|
||||
}
|
||||
m_framecounters = m_decoder.stats();
|
||||
for( auto &packet : packets )
|
||||
{
|
||||
if( m_rxqueue.size() >= rx_queue_limit )
|
||||
{
|
||||
m_rxqueue.pop_front();
|
||||
}
|
||||
m_rxqueue.emplace_back( std::move( packet ) );
|
||||
}
|
||||
}
|
||||
|
||||
void hardware_link::update( double const Deltatime, state_snapshot const &Snapshot )
|
||||
{
|
||||
std::deque<decoded_packet> packets;
|
||||
transport_diagnostics status;
|
||||
frame_decoder::counters counters;
|
||||
bool dropped = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
packets.swap( m_rxqueue );
|
||||
status = m_transportstatus;
|
||||
counters = m_framecounters;
|
||||
dropped = m_transportdropped;
|
||||
m_transportdropped = false;
|
||||
}
|
||||
|
||||
if( true == dropped )
|
||||
{
|
||||
m_session.reset();
|
||||
}
|
||||
|
||||
frame_decoder::counters delta;
|
||||
delta.frames = counters.frames - m_lastframeerrors.frames;
|
||||
delta.crc_errors = counters.crc_errors - m_lastframeerrors.crc_errors;
|
||||
delta.frame_errors = counters.frame_errors - m_lastframeerrors.frame_errors;
|
||||
delta.length_errors = counters.length_errors - m_lastframeerrors.length_errors;
|
||||
m_lastframeerrors = counters;
|
||||
m_session.add_frame_errors( delta );
|
||||
|
||||
for( auto const &packet : packets )
|
||||
{
|
||||
m_session.handle_packet( packet );
|
||||
}
|
||||
|
||||
m_session.update( Deltatime, Snapshot );
|
||||
|
||||
m_report.transport = status;
|
||||
m_report.state = (
|
||||
false == status.connected ? link_state::disconnected :
|
||||
m_session.link_condition() );
|
||||
m_session.fill_report( m_report );
|
||||
}
|
||||
|
||||
hardware_manager::hardware_manager( config const &Config ) : m_config( Config )
|
||||
{
|
||||
#ifdef WITH_UART
|
||||
for( auto const &link : m_config.serial_links )
|
||||
{
|
||||
m_links.emplace_back( std::make_unique<hardware_link>( std::make_unique<serial_transport>( link.port, link.baud ), m_config ) );
|
||||
WriteLog( "hardware: protocol v2 link on " + link.port + " @ " + std::to_string( link.baud ) );
|
||||
}
|
||||
#endif
|
||||
|
||||
m_reports.resize( m_links.size() );
|
||||
|
||||
if( false == m_links.empty() )
|
||||
{
|
||||
m_thread = std::thread( &hardware_manager::worker, this );
|
||||
}
|
||||
|
||||
s_instance = this;
|
||||
}
|
||||
|
||||
hardware_manager::~hardware_manager()
|
||||
{
|
||||
s_instance = nullptr;
|
||||
m_quit = true;
|
||||
if( true == m_thread.joinable() )
|
||||
{
|
||||
m_thread.join();
|
||||
}
|
||||
m_links.clear();
|
||||
}
|
||||
|
||||
void hardware_manager::worker()
|
||||
{
|
||||
auto const interval = std::chrono::milliseconds( std::max( 1, m_config.poll_interval_ms ) );
|
||||
|
||||
while( false == m_quit )
|
||||
{
|
||||
for( auto &link : m_links )
|
||||
{
|
||||
link->service();
|
||||
}
|
||||
std::this_thread::sleep_for( interval );
|
||||
}
|
||||
}
|
||||
|
||||
void hardware_manager::update( double const Deltatime )
|
||||
{
|
||||
if( true == m_links.empty() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
state_registry::capture( m_snapshot );
|
||||
|
||||
for( std::size_t index = 0; index < m_links.size(); ++index )
|
||||
{
|
||||
m_links[ index ]->update( Deltatime, m_snapshot );
|
||||
m_reports[ index ] = m_links[ index ]->report();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hardware
|
||||
109
hardware/hardware_manager.h
Normal file
109
hardware/hardware_manager.h
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "hardware/diagnostics/hardware_diagnostics.h"
|
||||
#include "hardware/hardware_config.h"
|
||||
#include "hardware/protocol/protocol_session.h"
|
||||
#include "hardware/transport/hardware_transport.h"
|
||||
|
||||
// owner of every hardware protocol v2 connection, regardless of the transport behind it
|
||||
//
|
||||
// threading: a single worker thread does all of the blocking work, that is opening ports, reading
|
||||
// and writing bytes and turning them into frames. the simulation thread only exchanges ready made
|
||||
// frames with it through small locked queues, and is the only one which ever touches simulation
|
||||
// state. a device which stops responding, floods the link or disappears therefore cannot stall the
|
||||
// simulation
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
class hardware_link : public session_output
|
||||
{
|
||||
|
||||
public:
|
||||
// methods
|
||||
hardware_link( std::unique_ptr<hardware_transport> Transport, config const &Config );
|
||||
~hardware_link() override;
|
||||
|
||||
// worker thread: services the transport
|
||||
void service();
|
||||
// simulation thread: processes received frames and runs the session
|
||||
void update( double const Deltatime, state_snapshot const &Snapshot );
|
||||
// simulation thread: the report gathered during the last update
|
||||
link_report const &report() const { return m_report; }
|
||||
|
||||
void send_packet( packet_header const &Header, std::vector<std::uint8_t> const &Payload ) override;
|
||||
|
||||
private:
|
||||
// members
|
||||
std::unique_ptr<hardware_transport> m_transport;
|
||||
config m_config;
|
||||
protocol_session m_session;
|
||||
link_report m_report;
|
||||
|
||||
// worker thread only
|
||||
frame_decoder m_decoder;
|
||||
std::vector<std::uint8_t> m_readbuffer;
|
||||
std::vector<std::uint8_t> m_writebuffer;
|
||||
std::chrono::steady_clock::time_point m_lastconnectattempt;
|
||||
|
||||
// simulation thread only
|
||||
frame_decoder::counters m_lastframeerrors;
|
||||
|
||||
// shared
|
||||
std::mutex m_mutex;
|
||||
std::deque<decoded_packet> m_rxqueue;
|
||||
std::deque<std::vector<std::uint8_t>> m_txqueue;
|
||||
transport_diagnostics m_transportstatus;
|
||||
frame_decoder::counters m_framecounters;
|
||||
bool m_transportdropped = false;
|
||||
};
|
||||
|
||||
class hardware_manager
|
||||
{
|
||||
|
||||
public:
|
||||
// methods
|
||||
explicit hardware_manager( config const &Config );
|
||||
~hardware_manager();
|
||||
|
||||
// called once per simulation frame from the input polling code
|
||||
void update( double const Deltatime );
|
||||
|
||||
std::vector<link_report> const &reports() const { return m_reports; }
|
||||
bool active() const { return false == m_links.empty(); }
|
||||
|
||||
// the running instance, if any; used by the debug panel
|
||||
static hardware_manager *instance() { return s_instance; }
|
||||
|
||||
private:
|
||||
// methods
|
||||
void worker();
|
||||
|
||||
// members
|
||||
static hardware_manager *s_instance;
|
||||
|
||||
config m_config;
|
||||
std::vector<std::unique_ptr<hardware_link>> m_links;
|
||||
std::vector<link_report> m_reports;
|
||||
state_snapshot m_snapshot;
|
||||
std::thread m_thread;
|
||||
std::atomic<bool> m_quit { false };
|
||||
};
|
||||
|
||||
} // namespace hardware
|
||||
1057
hardware/protocol/protocol_session.cpp
Normal file
1057
hardware/protocol/protocol_session.cpp
Normal file
File diff suppressed because it is too large
Load Diff
155
hardware/protocol/protocol_session.h
Normal file
155
hardware/protocol/protocol_session.h
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "hardware/diagnostics/hardware_diagnostics.h"
|
||||
#include "hardware/hardware_config.h"
|
||||
#include "hardware/protocol/protocol_codec.h"
|
||||
#include "hardware/registry/state_registry.h"
|
||||
#include "hardware/subscriptions/subscription_manager.h"
|
||||
#include "input/command.h"
|
||||
|
||||
// logical protocol layer of one connection
|
||||
// runs on the simulation thread; the transport worker only hands over decoded frames, so nothing
|
||||
// here ever blocks on hardware, and nothing outside here touches simulation state
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
// sink for packets produced by the session
|
||||
class session_output
|
||||
{
|
||||
|
||||
public:
|
||||
// methods
|
||||
virtual ~session_output() = default;
|
||||
virtual void send_packet( packet_header const &Header, std::vector<std::uint8_t> const &Payload ) = 0;
|
||||
};
|
||||
|
||||
class protocol_session
|
||||
{
|
||||
|
||||
public:
|
||||
// methods
|
||||
protocol_session( session_output &Output, config const &Config );
|
||||
|
||||
// drops the session, called when the transport connection is lost
|
||||
void reset();
|
||||
// consumes one decoded frame
|
||||
void handle_packet( decoded_packet const &Packet );
|
||||
// periodic work: heartbeats, subscription updates, timeouts
|
||||
void update( double const Deltatime, state_snapshot const &Snapshot );
|
||||
// copies the current condition into the debug report
|
||||
void fill_report( link_report &Report ) const;
|
||||
|
||||
session_state state() const { return m_state; }
|
||||
link_state link_condition() const;
|
||||
protocol_diagnostics const &diagnostics() const { return m_diagnostics; }
|
||||
// adds the frame level error counts gathered by the transport worker since the last call
|
||||
void add_frame_errors( frame_decoder::counters const &Delta );
|
||||
|
||||
private:
|
||||
// types
|
||||
struct transaction_result
|
||||
{
|
||||
bool accepted = false;
|
||||
error_code error = error_code::none;
|
||||
};
|
||||
|
||||
// methods
|
||||
void send( message_type const Type, std::uint8_t const Flags, payload_writer const &Payload );
|
||||
void send_ack( std::uint32_t const Transaction, message_type const Type );
|
||||
void send_nack( std::uint32_t const Transaction, message_type const Type, error_code const Error );
|
||||
|
||||
void handle_hello( decoded_packet const &Packet );
|
||||
void handle_resolve_symbols( decoded_packet const &Packet );
|
||||
void handle_query_symbol( decoded_packet const &Packet );
|
||||
void handle_subscribe( decoded_packet const &Packet );
|
||||
void handle_unsubscribe( decoded_packet const &Packet );
|
||||
void handle_command_event( decoded_packet const &Packet );
|
||||
void handle_control_set( decoded_packet const &Packet );
|
||||
void handle_device_diagnostics( decoded_packet const &Packet );
|
||||
void handle_heartbeat( decoded_packet const &Packet );
|
||||
|
||||
void send_state_update( std::vector<state_item> const &Items, bool const Snapshot );
|
||||
void send_symbol_context_changed( std::uint32_t const Context );
|
||||
|
||||
// returns the registry index for a session handle, or symbol_registry::npos
|
||||
std::size_t symbol_of_handle( std::uint16_t const Handle ) const;
|
||||
std::uint16_t handle_of_symbol( std::size_t const SymbolIndex );
|
||||
|
||||
bool consume_frame_budget( std::size_t const PayloadSize );
|
||||
bool consume_command_budget();
|
||||
bool remember_transaction( std::uint32_t const Transaction, transaction_result const &Result );
|
||||
transaction_result const *recall_transaction( std::uint32_t const Transaction ) const;
|
||||
|
||||
// executes a command, returns the error which should be reported to the device
|
||||
error_code execute_command( std::size_t const SymbolIndex, command_action const Action, double const Parameter1, double const Parameter2, std::uint16_t const Recipient );
|
||||
|
||||
// members
|
||||
session_output &m_output;
|
||||
config m_config;
|
||||
command_relay m_relay;
|
||||
|
||||
session_state m_state = session_state::waiting_for_hello;
|
||||
std::uint32_t m_sessionid = 0;
|
||||
std::uint32_t m_sequence = 0;
|
||||
std::uint32_t m_lastreceivedsequence = 0;
|
||||
bool m_sequenceseen = false;
|
||||
std::size_t m_framesize = protocol_frame_size_default;
|
||||
std::uint32_t m_capabilities = 0;
|
||||
std::uint8_t m_peermajor = 0;
|
||||
std::uint8_t m_peerminor = 0;
|
||||
|
||||
std::string m_devicename;
|
||||
std::string m_devicerole;
|
||||
std::string m_deviceid;
|
||||
std::string m_firmwareversion;
|
||||
std::string m_hardwareversion;
|
||||
|
||||
std::vector<std::size_t> m_handles;
|
||||
std::unordered_map<std::size_t, std::uint16_t> m_handlelookup;
|
||||
subscription_manager m_subscriptions;
|
||||
// per control sequence counters, used to drop stale continuous updates
|
||||
std::unordered_map<std::uint16_t, std::uint32_t> m_controlsequence;
|
||||
|
||||
std::unordered_map<std::uint32_t, transaction_result> m_transactions;
|
||||
std::deque<std::uint32_t> m_transactionorder;
|
||||
|
||||
double m_uptime = 0.0;
|
||||
double m_lastreceived = 0.0;
|
||||
double m_heartbeattimer = 0.0;
|
||||
double m_pingtimer = 0.0;
|
||||
std::uint32_t m_pingtoken = 0;
|
||||
double m_pingsent = 0.0;
|
||||
bool m_pingpending = false;
|
||||
|
||||
double m_ratewindow = 0.0;
|
||||
int m_framebudget = 0;
|
||||
int m_payloadbudget = 0;
|
||||
int m_commandbudget = 0;
|
||||
|
||||
std::uint32_t m_context = 0;
|
||||
bool m_contextvalid = false;
|
||||
// most recent simulator state, kept so that a subscription can be answered right away
|
||||
state_snapshot m_snapshot;
|
||||
|
||||
protocol_diagnostics m_diagnostics;
|
||||
device_diagnostics m_devicediagnostics;
|
||||
rate_counter m_rxrate;
|
||||
rate_counter m_txrate;
|
||||
};
|
||||
|
||||
} // namespace hardware
|
||||
110
hardware/subscriptions/subscription_manager.cpp
Normal file
110
hardware/subscriptions/subscription_manager.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "hardware/subscriptions/subscription_manager.h"
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
error_code subscription_manager::subscribe( std::uint16_t const Handle, std::size_t const SymbolIndex, std::size_t const StateIndex, subscription_mode const Mode, std::uint16_t const PeriodMilliseconds, float const Deadband )
|
||||
{
|
||||
if( ( Mode != subscription_mode::periodic ) && ( Mode != subscription_mode::on_change ) && ( Mode != subscription_mode::periodic_or_change ) )
|
||||
{
|
||||
return error_code::invalid_field;
|
||||
}
|
||||
if( ( false == std::isfinite( Deadband ) ) || ( Deadband < 0.0f ) )
|
||||
{
|
||||
return error_code::invalid_field;
|
||||
}
|
||||
|
||||
auto const period = std::clamp<std::uint16_t>( ( PeriodMilliseconds > 0 ? PeriodMilliseconds : subscription_period_min_ms ), subscription_period_min_ms, subscription_period_max_ms ) * 0.001;
|
||||
|
||||
auto entry = std::find_if( m_entries.begin(), m_entries.end(), [ Handle ]( subscription_entry const &Entry ) { return Entry.handle == Handle; } );
|
||||
if( entry == m_entries.end() )
|
||||
{
|
||||
if( m_entries.size() >= subscriptions_max )
|
||||
{
|
||||
return error_code::busy;
|
||||
}
|
||||
m_entries.emplace_back();
|
||||
entry = std::prev( m_entries.end() );
|
||||
}
|
||||
|
||||
entry->handle = Handle;
|
||||
entry->symbol_index = SymbolIndex;
|
||||
entry->state_index = StateIndex;
|
||||
entry->mode = Mode;
|
||||
entry->period = period;
|
||||
entry->deadband = Deadband;
|
||||
entry->timer = period;
|
||||
entry->sampled = false;
|
||||
|
||||
return error_code::none;
|
||||
}
|
||||
|
||||
bool subscription_manager::unsubscribe( std::uint16_t const Handle )
|
||||
{
|
||||
auto const entry = std::find_if( m_entries.begin(), m_entries.end(), [ Handle ]( subscription_entry const &Entry ) { return Entry.handle == Handle; } );
|
||||
if( entry == m_entries.end() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_entries.erase( entry );
|
||||
return true;
|
||||
}
|
||||
|
||||
void subscription_manager::clear()
|
||||
{
|
||||
m_entries.clear();
|
||||
}
|
||||
|
||||
void subscription_manager::collect( double const Deltatime, state_snapshot const &Snapshot, bool const Everything, std::vector<state_item> &Output )
|
||||
{
|
||||
auto const &states = state_registry::instance().entries();
|
||||
|
||||
for( auto &entry : m_entries )
|
||||
{
|
||||
if( entry.state_index >= states.size() )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.timer += Deltatime;
|
||||
|
||||
auto const value = states[ entry.state_index ].getter( Snapshot );
|
||||
|
||||
bool const periodic = ( ( entry.mode != subscription_mode::on_change ) && ( entry.timer >= entry.period ) );
|
||||
bool changed = false;
|
||||
if( entry.mode != subscription_mode::periodic )
|
||||
{
|
||||
changed = (
|
||||
( false == entry.sampled )
|
||||
|| ( value.quality != entry.last_quality )
|
||||
|| ( std::abs( value.numeric - entry.last_numeric ) > entry.deadband ) );
|
||||
}
|
||||
|
||||
if( ( false == Everything ) && ( false == periodic ) && ( false == changed ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.timer = 0.0;
|
||||
entry.sampled = true;
|
||||
entry.last_numeric = value.numeric;
|
||||
entry.last_quality = value.quality;
|
||||
|
||||
state_item item;
|
||||
item.handle = entry.handle;
|
||||
item.value = value;
|
||||
Output.emplace_back( std::move( item ) );
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hardware
|
||||
71
hardware/subscriptions/subscription_manager.h
Normal file
71
hardware/subscriptions/subscription_manager.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "hardware/protocol/protocol_defs.h"
|
||||
#include "hardware/registry/state_registry.h"
|
||||
|
||||
// state subscriptions of a single session
|
||||
// a device receives only the values it asked for, at the rate it asked for; unchanged values within
|
||||
// the configured deadband generate no traffic at all
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
// upper bound on subscriptions per session, keeps a misbehaving device from exhausting memory
|
||||
constexpr std::size_t subscriptions_max = 128;
|
||||
constexpr std::uint16_t subscription_period_min_ms = 10;
|
||||
constexpr std::uint16_t subscription_period_max_ms = 60000;
|
||||
|
||||
struct subscription_entry
|
||||
{
|
||||
std::uint16_t handle = 0;
|
||||
std::size_t symbol_index = 0;
|
||||
std::size_t state_index = 0;
|
||||
subscription_mode mode = subscription_mode::periodic;
|
||||
double period = 0.05;
|
||||
double deadband = 0.0;
|
||||
double timer = 0.0;
|
||||
bool sampled = false;
|
||||
double last_numeric = 0.0;
|
||||
value_quality last_quality = value_quality::unavailable;
|
||||
};
|
||||
|
||||
struct state_item
|
||||
{
|
||||
std::uint16_t handle = 0;
|
||||
state_value value;
|
||||
};
|
||||
|
||||
class subscription_manager
|
||||
{
|
||||
|
||||
public:
|
||||
// methods
|
||||
// registers or updates a subscription, returns the error to be reported back to the device
|
||||
error_code subscribe( std::uint16_t const Handle, std::size_t const SymbolIndex, std::size_t const StateIndex, subscription_mode const Mode, std::uint16_t const PeriodMilliseconds, float const Deadband );
|
||||
bool unsubscribe( std::uint16_t const Handle );
|
||||
void clear();
|
||||
|
||||
std::size_t size() const { return m_entries.size(); }
|
||||
std::vector<subscription_entry> const &entries() const { return m_entries; }
|
||||
|
||||
// advances the timers and collects the values which are due for transmission
|
||||
void collect( double const Deltatime, state_snapshot const &Snapshot, bool const Everything, std::vector<state_item> &Output );
|
||||
|
||||
private:
|
||||
// members
|
||||
std::vector<subscription_entry> m_entries;
|
||||
};
|
||||
|
||||
} // namespace hardware
|
||||
47
hardware/transport/hardware_transport.h
Normal file
47
hardware/transport/hardware_transport.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// transport abstraction of the hardware protocol
|
||||
// the upper layers only ever see a byte stream, which is what lets the same protocol run over
|
||||
// serial, tcp or rs-485 without changing message semantics
|
||||
// every method is called from the hardware worker thread only
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
class hardware_transport
|
||||
{
|
||||
|
||||
public:
|
||||
// methods
|
||||
virtual ~hardware_transport() = default;
|
||||
|
||||
virtual bool connected() const = 0;
|
||||
// tries to establish the link, returns true on success
|
||||
virtual bool connect() = 0;
|
||||
virtual void disconnect() = 0;
|
||||
// non-blocking read, returns the number of bytes placed in the buffer or -1 on error
|
||||
virtual int read( std::uint8_t *Buffer, std::size_t const Size ) = 0;
|
||||
// non-blocking write, returns the number of bytes accepted or -1 on error
|
||||
virtual int write( std::uint8_t const *Data, std::size_t const Size ) = 0;
|
||||
|
||||
// human readable transport type, i.e. "Serial"
|
||||
virtual std::string kind() const = 0;
|
||||
// human readable endpoint, i.e. "COM4 @ 115200"
|
||||
virtual std::string endpoint() const = 0;
|
||||
// last error reported by the underlying device
|
||||
virtual std::string last_error() const = 0;
|
||||
};
|
||||
|
||||
} // namespace hardware
|
||||
151
hardware/transport/serial_transport.cpp
Normal file
151
hardware/transport/serial_transport.cpp
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "hardware/transport/serial_transport.h"
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
serial_transport::serial_transport( std::string Port, int const Baud ) : m_portname( std::move( Port ) ), m_baud( Baud )
|
||||
{
|
||||
}
|
||||
|
||||
serial_transport::~serial_transport()
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
std::string serial_transport::endpoint() const
|
||||
{
|
||||
return m_portname + " @ " + std::to_string( m_baud );
|
||||
}
|
||||
|
||||
bool serial_transport::connect()
|
||||
{
|
||||
disconnect();
|
||||
|
||||
sp_port *port = nullptr;
|
||||
if( sp_get_port_by_name( m_portname.c_str(), &port ) != SP_OK )
|
||||
{
|
||||
m_lasterror = "port not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
if( sp_open( port, static_cast<sp_mode>( SP_MODE_READ | SP_MODE_WRITE ) ) != SP_OK )
|
||||
{
|
||||
m_lasterror = "cannot open port";
|
||||
sp_free_port( port );
|
||||
return false;
|
||||
}
|
||||
|
||||
sp_port_config *config = nullptr;
|
||||
if( ( sp_new_config( &config ) != SP_OK )
|
||||
|| ( sp_set_config_baudrate( config, m_baud ) != SP_OK )
|
||||
|| ( sp_set_config_flowcontrol( config, SP_FLOWCONTROL_NONE ) != SP_OK )
|
||||
|| ( sp_set_config_bits( config, 8 ) != SP_OK )
|
||||
|| ( sp_set_config_stopbits( config, 1 ) != SP_OK )
|
||||
|| ( sp_set_config_parity( config, SP_PARITY_NONE ) != SP_OK )
|
||||
|| ( sp_set_config( port, config ) != SP_OK ) )
|
||||
{
|
||||
m_lasterror = "cannot configure port";
|
||||
if( config != nullptr )
|
||||
{
|
||||
sp_free_config( config );
|
||||
}
|
||||
sp_close( port );
|
||||
sp_free_port( port );
|
||||
return false;
|
||||
}
|
||||
sp_free_config( config );
|
||||
|
||||
if( sp_flush( port, SP_BUF_BOTH ) != SP_OK )
|
||||
{
|
||||
m_lasterror = "cannot flush port";
|
||||
sp_close( port );
|
||||
sp_free_port( port );
|
||||
return false;
|
||||
}
|
||||
|
||||
m_port = port;
|
||||
m_lasterror.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void serial_transport::disconnect()
|
||||
{
|
||||
if( m_port == nullptr )
|
||||
{
|
||||
return;
|
||||
}
|
||||
sp_close( m_port );
|
||||
sp_free_port( m_port );
|
||||
m_port = nullptr;
|
||||
}
|
||||
|
||||
int serial_transport::read( std::uint8_t *Buffer, std::size_t const Size )
|
||||
{
|
||||
if( m_port == nullptr )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
int const waiting = sp_input_waiting( m_port );
|
||||
if( waiting < 0 )
|
||||
{
|
||||
m_lasterror = "read failed";
|
||||
return -1;
|
||||
}
|
||||
if( waiting == 0 )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int const result = sp_nonblocking_read( m_port, Buffer, std::min( Size, static_cast<std::size_t>( waiting ) ) );
|
||||
if( result < 0 )
|
||||
{
|
||||
m_lasterror = "read failed";
|
||||
return -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int serial_transport::write( std::uint8_t const *Data, std::size_t const Size )
|
||||
{
|
||||
if( m_port == nullptr )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
int const result = sp_nonblocking_write( m_port, Data, Size );
|
||||
if( result < 0 )
|
||||
{
|
||||
m_lasterror = "write failed";
|
||||
return -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> list_serial_ports()
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
sp_port **ports = nullptr;
|
||||
if( sp_list_ports( &ports ) != SP_OK )
|
||||
{
|
||||
return result;
|
||||
}
|
||||
for( int index = 0; ports[ index ] != nullptr; ++index )
|
||||
{
|
||||
result.emplace_back( sp_get_port_name( ports[ index ] ) );
|
||||
}
|
||||
sp_free_port_list( ports );
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace hardware
|
||||
54
hardware/transport/serial_transport.h
Normal file
54
hardware/transport/serial_transport.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <libserialport.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "hardware/transport/hardware_transport.h"
|
||||
|
||||
// serial port transport, one connection per port, as described by the specification
|
||||
// the port is opened and serviced exclusively from the hardware worker thread
|
||||
|
||||
namespace hardware
|
||||
{
|
||||
|
||||
class serial_transport : public hardware_transport
|
||||
{
|
||||
|
||||
public:
|
||||
// methods
|
||||
serial_transport( std::string Port, int const Baud );
|
||||
~serial_transport() override;
|
||||
|
||||
bool connected() const override { return m_port != nullptr; }
|
||||
bool connect() override;
|
||||
void disconnect() override;
|
||||
int read( std::uint8_t *Buffer, std::size_t const Size ) override;
|
||||
int write( std::uint8_t const *Data, std::size_t const Size ) override;
|
||||
|
||||
std::string kind() const override { return "Serial"; }
|
||||
std::string endpoint() const override;
|
||||
std::string last_error() const override { return m_lasterror; }
|
||||
|
||||
private:
|
||||
// members
|
||||
std::string m_portname;
|
||||
int m_baud = 115200;
|
||||
sp_port *m_port = nullptr;
|
||||
std::string m_lasterror;
|
||||
};
|
||||
|
||||
// list of serial ports currently present in the system
|
||||
std::vector<std::string> list_serial_ports();
|
||||
|
||||
} // namespace hardware
|
||||
Reference in New Issue
Block a user