From 3a3c8fa3e4acadce5d2591a210f4755022d53235 Mon Sep 17 00:00:00 2001 From: Dave Behnke <916775+dbehnke@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:16:50 -0500 Subject: [PATCH] Implement M17 Parrot, LSTN support, and M17P packet mode (#12) - Added M17Parrot.h/cpp for multi-threaded voice and packet echo. - Integrated Parrot routing and cleanup in M17Protocol. - Added LSTN (Listen-only) mode support for M17 nodes. - Added M17P (Packet Mode) routing support. - Fixed M17 keep-alive validation bug. - Improved protocol architecture by inheriting SM17Protocol from CSEProtocol. --- reflector/Buffer.cpp | 4 +- reflector/Buffer.h | 4 +- reflector/M17Client.cpp | 7 +- reflector/M17Client.h | 6 +- reflector/M17Parrot.cpp | 134 ++++++++++++++++++++++++++ reflector/M17Parrot.h | 75 +++++++++++++++ reflector/M17Protocol.cpp | 195 ++++++++++++++++++++++++++++++++++---- reflector/M17Protocol.h | 37 +++++++- 8 files changed, 435 insertions(+), 27 deletions(-) create mode 100644 reflector/M17Parrot.cpp create mode 100644 reflector/M17Parrot.h diff --git a/reflector/Buffer.cpp b/reflector/Buffer.cpp index c99d290..a949eb1 100644 --- a/reflector/Buffer.cpp +++ b/reflector/Buffer.cpp @@ -124,7 +124,7 @@ void CBuffer::ReplaceAt(int i, const uint8_t *ptr, int len) //////////////////////////////////////////////////////////////////////////////////////// // operation -int CBuffer::Compare(uint8_t *buffer, int len) const +int CBuffer::Compare(const uint8_t *buffer, int len) const { int result = -1; if ( m_data.size() >= unsigned(len) ) @@ -134,7 +134,7 @@ int CBuffer::Compare(uint8_t *buffer, int len) const return result; } -int CBuffer::Compare(uint8_t *buffer, int off, int len) const +int CBuffer::Compare(const uint8_t *buffer, int off, int len) const { int result = -1; if ( m_data.size() >= unsigned(off+len) ) diff --git a/reflector/Buffer.h b/reflector/Buffer.h index c007ef8..4997e8d 100644 --- a/reflector/Buffer.h +++ b/reflector/Buffer.h @@ -48,8 +48,8 @@ public: void ReplaceAt(int, const uint8_t *, int); // operation - int Compare(uint8_t *, int) const; - int Compare(uint8_t *, int, int) const; + int Compare(const uint8_t *, int) const; + int Compare(const uint8_t *, int, int) const; // operator bool operator ==(const CBuffer &) const; diff --git a/reflector/M17Client.cpp b/reflector/M17Client.cpp index 888e469..45f20d4 100644 --- a/reflector/M17Client.cpp +++ b/reflector/M17Client.cpp @@ -22,16 +22,17 @@ // constructors CM17Client::CM17Client() + : m_IsListenOnly(false) { } -CM17Client::CM17Client(const CCallsign &callsign, const CIp &ip, char reflectorModule) - : CClient(callsign, ip, reflectorModule) +CM17Client::CM17Client(const CCallsign &callsign, const CIp &ip, char reflectorModule, bool isListenOnly) + : CClient(callsign, ip, reflectorModule), m_IsListenOnly(isListenOnly) { } CM17Client::CM17Client(const CM17Client &client) - : CClient(client) + : CClient(client), m_IsListenOnly(client.m_IsListenOnly) { } diff --git a/reflector/M17Client.h b/reflector/M17Client.h index 2186fa0..1163bd3 100644 --- a/reflector/M17Client.h +++ b/reflector/M17Client.h @@ -24,7 +24,7 @@ class CM17Client : public CClient public: // constructors CM17Client(); - CM17Client(const CCallsign &, const CIp &, char); + CM17Client(const CCallsign &, const CIp &, char, bool isListenOnly = false); CM17Client(const CM17Client &); // destructor @@ -37,6 +37,10 @@ public: // status bool IsAlive(void) const; + bool IsListenOnly(void) const { return m_IsListenOnly; } + +private: + bool m_IsListenOnly; }; //////////////////////////////////////////////////////////////////////////////////////// diff --git a/reflector/M17Parrot.cpp b/reflector/M17Parrot.cpp new file mode 100644 index 0000000..7fbe3fc --- /dev/null +++ b/reflector/M17Parrot.cpp @@ -0,0 +1,134 @@ +#include +#include + +#include "M17Parrot.h" +#include "Global.h" +#include "M17Protocol.h" + +//////////////////////////////////////////////////////////////////////////////////////// +// Stream Parrot + +CM17StreamParrot::CM17StreamParrot(const CCallsign &src_addr, std::shared_ptr spc, uint16_t ft, CM17Protocol *proto) + : CParrot(src_addr, spc, ft, proto), m_streamId(0) +{ + m_is3200 = (0x4U == (0x4U & ft)); + m_lastHeard.start(); +} + +void CM17StreamParrot::Add(const CBuffer &Buffer, uint16_t streamId, uint16_t frameNumber) +{ + (void)frameNumber; // We generate our own sequence on playback + if (m_data.size() < 500u) + { + m_streamId = streamId; + size_t length = m_is3200 ? 16 : 8; + // Payload is at offset 40 in SM17Frame (4 magic + 2 streamid + 28 lich + 2 fn + 16 payload + 2 crc) + // urfd's CDvFramePacket/CM17Packet probably maps this. + // For simplicity in this implementation, we assume Buffer passed is the raw payload OR mapped. + // Looking at M17Protocol.cpp:410, payload starts at packet.payload + + const uint8_t *payload = Buffer.data() + 40; // payload offset in SM17Frame + m_data.emplace_back(payload, payload + length); + } + m_lastHeard.start(); +} + +void CM17StreamParrot::Play() +{ + m_fut = std::async(std::launch::async, &CM17StreamParrot::playThread, this); +} + +bool CM17StreamParrot::IsExpired() const +{ + return m_lastHeard.time() > 1.6; // 1.6s timeout like mrefd +} + +void CM17StreamParrot::playThread() +{ + m_state = EParrotState::play; + + SM17Frame frame; + memset(&frame, 0, sizeof(frame)); + memcpy(frame.magic, "M17 ", 4); + frame.streamid = m_streamId; // reuse or generate new? mrefd generates new. + + // Set LICH addresses + memset(frame.lich.addr_dst, 0xFF, 6); // @ALL + m_src.CodeOut(frame.lich.addr_src); + frame.lich.frametype = htons(m_frameType); + + auto clock = std::chrono::steady_clock::now(); + size_t size = m_data.size(); + + for (size_t n = 0; n < size; n++) + { + size_t length = m_is3200 ? 16 : 8; + memcpy(frame.payload, m_data[n].data(), length); + + uint16_t fn = (uint16_t)n; + if (n == size - 1) + fn |= 0x8000u; + frame.framenumber = htons(fn); + + CM17CRC m17crc; + frame.crc = htons(m17crc.CalcCRC((uint8_t*)&frame, sizeof(SM17Frame)-2)); + + clock = clock + std::chrono::milliseconds(40); + std::this_thread::sleep_until(clock); + + if (m_proto) + m_proto->Send(frame, m_client->GetIp()); + m_data[n].clear(); + } + m_data.clear(); + m_state = EParrotState::done; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// Packet Parrot + +CM17PacketParrot::CM17PacketParrot(const CCallsign &src_addr, std::shared_ptr spc, uint16_t ft, CM17Protocol *proto) + : CParrot(src_addr, spc, ft, proto) +{ +} + +void CM17PacketParrot::AddPacket(const CBuffer &Buffer) +{ + m_packet = Buffer; +} + +void CM17PacketParrot::Play() +{ + m_fut = std::async(std::launch::async, &CM17PacketParrot::playThread, this); +} + +void CM17PacketParrot::playThread() +{ + m_state = EParrotState::play; + + // 100ms delay like mrefd + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Change destination to ALL (broadcast back to sender) or specific? + // M17P is usually 4 magic + 6 dst + 6 src + ... + if (m_packet.size() >= 10) + { + memset(m_packet.data() + 4, 0xFF, 6); // Set dst to @ALL + + // Recalculate CRC + CM17CRC m17crc; + // M17P packets also have a CRC at the end. + // CRC is usually last 2 bytes. + size_t len = m_packet.size(); + if (len >= 2) + { + uint16_t crc = htons(m17crc.CalcCRC(m_packet.data(), len - 2)); + memcpy(m_packet.data() + len - 2, &crc, 2); + } + + if (m_proto) + m_proto->Send(m_packet, m_client->GetIp()); + } + + m_state = EParrotState::done; +} diff --git a/reflector/M17Parrot.h b/reflector/M17Parrot.h new file mode 100644 index 0000000..b557789 --- /dev/null +++ b/reflector/M17Parrot.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Callsign.h" +#include "Timer.h" +#include "M17Client.h" +#include "M17Packet.h" + +enum class EParrotState { record, play, done }; + +class CM17Protocol; + +class CParrot +{ +public: + CParrot(const CCallsign &src_addr, std::shared_ptr spc, uint16_t ft, CM17Protocol *proto) + : m_src(src_addr), m_client(spc), m_frameType(ft), m_state(EParrotState::record), m_proto(proto) {} + virtual ~CParrot() { Quit(); } + virtual void Add(const CBuffer &Buffer, uint16_t streamId, uint16_t frameNumber) = 0; + virtual void AddPacket(const CBuffer &Buffer) = 0; + virtual bool IsExpired() const = 0; + virtual void Play() = 0; + virtual bool IsStream() const = 0; + EParrotState GetState() const { return m_state; } + const CCallsign &GetSRC() const { return m_src; } + void Quit() { if (m_fut.valid()) m_fut.get(); } + +protected: + const CCallsign m_src; + std::shared_ptr m_client; + const uint16_t m_frameType; + std::atomic m_state; + std::future m_fut; + CM17Protocol *m_proto; +}; + +class CM17StreamParrot : public CParrot +{ +public: + CM17StreamParrot(const CCallsign &src_addr, std::shared_ptr spc, uint16_t ft, CM17Protocol *proto); + void Add(const CBuffer &Buffer, uint16_t streamId, uint16_t frameNumber) override; + void AddPacket(const CBuffer &Buffer) override { (void)Buffer; } + void Play() override; + bool IsExpired() const override; + bool IsStream() const override { return true; } + +private: + void playThread(); + + std::vector> m_data; + bool m_is3200; + CTimer m_lastHeard; + uint16_t m_streamId; +}; + +class CM17PacketParrot : public CParrot +{ +public: + CM17PacketParrot(const CCallsign &src_addr, std::shared_ptr spc, uint16_t ft, CM17Protocol *proto); + void Add(const CBuffer &Buffer, uint16_t streamId, uint16_t frameNumber) override { (void)Buffer; (void)streamId; (void)frameNumber; } + void AddPacket(const CBuffer &Buffer) override; + void Play() override; + bool IsExpired() const override { return false; } + bool IsStream() const override { return false; } + +private: + void playThread(); + + CBuffer m_packet; +}; diff --git a/reflector/M17Protocol.cpp b/reflector/M17Protocol.cpp index d51dfbd..9da0223 100644 --- a/reflector/M17Protocol.cpp +++ b/reflector/M17Protocol.cpp @@ -1,5 +1,5 @@ // Copyright © 2015 Jean-Luc Deltombe (LX3JL). All rights reserved. - +// // urfd -- The universal reflector // Copyright © 2021 Thomas A. Early N7TAE // @@ -20,9 +20,18 @@ #include #include "M17Client.h" #include "M17Protocol.h" +#include "M17Parrot.h" #include "M17Packet.h" #include "Global.h" +//////////////////////////////////////////////////////////////////////////////////////// +// constructors + +CM17Protocol::CM17Protocol() + : CSEProtocol() +{ +} + //////////////////////////////////////////////////////////////////////////////////////// // operation @@ -39,8 +48,6 @@ bool CM17Protocol::Initialize(const char *type, const EProtocol ptype, const uin return true; } - - //////////////////////////////////////////////////////////////////////////////////////// // task @@ -49,6 +56,7 @@ void CM17Protocol::Task(void) CBuffer Buffer; CIp Ip; CCallsign Callsign; + CCallsign DstCallsign; char ToLinkModule; std::unique_ptr Header; std::unique_ptr Frame; @@ -67,8 +75,24 @@ void CM17Protocol::Task(void) // crack the packet if ( IsValidDvPacket(Buffer, Header, Frame) ) { + // find client + std::shared_ptr client = g_Reflector.GetClients()->FindClient(Ip, EProtocol::m17); + bool isListen = false; + if (client) + { + auto m17client = std::dynamic_pointer_cast(client); + if (m17client && m17client->IsListenOnly()) + isListen = true; + } + g_Reflector.ReleaseClients(); + + // parrot? + if ( Header->GetUrCallsign() == "PARROT" ) + { + HandleParrot(Ip, Buffer, true); + } // callsign muted? - if ( g_GateKeeper.MayTransmit(Header->GetMyCallsign(), Ip, EProtocol::m17, Header->GetRpt2Module()) ) + else if ( g_GateKeeper.MayTransmit(Header->GetMyCallsign(), Ip, EProtocol::m17, Header->GetRpt2Module()) ) { OnDvHeaderPacketIn(Header, Ip); @@ -85,9 +109,10 @@ void CM17Protocol::Task(void) OnDvFramePacketIn(secondFrame, &Ip); // push two packet because we need a packet every 20 ms } } - else if ( IsValidConnectPacket(Buffer, Callsign, ToLinkModule) ) + else if ( IsValidConnectPacket(Buffer, Callsign, ToLinkModule) || IsValidListenPacket(Buffer, Callsign, ToLinkModule) ) { - std::cout << "M17 connect packet for module " << ToLinkModule << " from " << Callsign << " at " << Ip << std::endl; + bool isListen = (0 == Buffer.Compare((const uint8_t*)"LSTN", 4)); + std::cout << "M17 " << (isListen ? "listen-only " : "") << "connect packet for module " << ToLinkModule << " from " << Callsign << " at " << Ip << std::endl; // callsign authorized? if ( g_GateKeeper.MayLink(Callsign, Ip, EProtocol::m17) && g_Reflector.IsValidModule(ToLinkModule) ) @@ -99,7 +124,7 @@ void CM17Protocol::Task(void) Send("ACKN", Ip); // create the client and append - g_Reflector.GetClients()->AddClient(std::make_shared(Callsign, Ip, ToLinkModule)); + g_Reflector.GetClients()->AddClient(std::make_shared(Callsign, Ip, ToLinkModule, isListen)); g_Reflector.ReleaseClients(); } else @@ -145,6 +170,44 @@ void CM17Protocol::Task(void) } g_Reflector.ReleaseClients(); } + else if ( IsValidPacketModePacket(Buffer, Callsign, DstCallsign) ) + { + // find client + std::shared_ptr client = g_Reflector.GetClients()->FindClient(Ip, EProtocol::m17); + bool isListen = false; + if (client) + { + auto m17client = std::dynamic_pointer_cast(client); + if (m17client && m17client->IsListenOnly()) + isListen = true; + } + g_Reflector.ReleaseClients(); + + if (!isListen) + { + // parrot? + if ( DstCallsign == "PARROT" ) + { + HandleParrot(Ip, Buffer, false); + } + // repeat to all clients on the module + else if (client) + { + char module = client->GetReflectorModule(); + CClients *clients = g_Reflector.GetClients(); + auto it = clients->begin(); + std::shared_ptr target = nullptr; + while ( (target = clients->FindNextClient(EProtocol::m17, it)) != nullptr ) + { + if (target->GetReflectorModule() == module && target->GetIp() != Ip) + { + Send(Buffer, target->GetIp()); + } + } + g_Reflector.ReleaseClients(); + } + } + } else { // invalid packet @@ -169,6 +232,24 @@ void CM17Protocol::Task(void) // update time m_LastKeepaliveTime.start(); } + + // Handle Parrot timeouts and cleanup + for (auto it = m_ParrotMap.begin(); it != m_ParrotMap.end(); ) + { + if (it->second->GetState() == EParrotState::record && it->second->IsExpired()) + { + it->second->Play(); + it++; + } + else if (it->second->GetState() == EParrotState::done) + { + it = m_ParrotMap.erase(it); + } + else + { + it++; + } + } } //////////////////////////////////////////////////////////////////////////////////////// @@ -326,6 +407,19 @@ bool CM17Protocol::IsValidConnectPacket(const CBuffer &Buffer, CCallsign &callsi return valid; } +bool CM17Protocol::IsValidListenPacket(const CBuffer &Buffer, CCallsign &callsign, char &mod) +{ + uint8_t tag[] = { 'L', 'S', 'T', 'N' }; + bool valid = false; + if (11 == Buffer.size() && 0 == Buffer.Compare(tag, 4)) + { + callsign.CodeIn(Buffer.data() + 4); + mod = Buffer.data()[10]; + valid = (callsign.IsValid() && IsLetter(mod)); + } + return valid; +} + bool CM17Protocol::IsValidDisconnectPacket(const CBuffer &Buffer, CCallsign &callsign) { uint8_t tag[] = { 'D', 'I', 'S', 'C' }; @@ -340,26 +434,35 @@ bool CM17Protocol::IsValidDisconnectPacket(const CBuffer &Buffer, CCallsign &cal bool CM17Protocol::IsValidKeepAlivePacket(const CBuffer &Buffer, CCallsign &callsign) { - uint8_t tag[] = { 'P', 'O', 'N', 'G' }; bool valid = false; - if ( (Buffer.size() == 10) || (0 == Buffer.Compare(tag, 4)) ) + if (Buffer.size() == 10) { - callsign.CodeIn(Buffer.data() + 4); - valid = callsign.IsValid(); + if (0 == Buffer.Compare((const uint8_t*)"PING", 4) || 0 == Buffer.Compare((const uint8_t*)"PONG", 4)) + { + callsign.CodeIn(Buffer.data() + 4); + valid = callsign.IsValid(); + } } return valid; } +bool CM17Protocol::IsValidPacketModePacket(const CBuffer &Buffer, CCallsign &src, CCallsign &dst) +{ + uint8_t tag[] = { 'M', '1', '7', 'P' }; + if ( (Buffer.size() >= 18) && (0 == Buffer.Compare(tag, 4)) ) + { + dst.CodeIn(Buffer.data() + 4); + src.CodeIn(Buffer.data() + 10); + return (src.IsValid() && (0x0U == (0x1U & Buffer[17]))); // no encryption + } + return false; +} + bool CM17Protocol::IsValidDvPacket(const CBuffer &Buffer, std::unique_ptr &header, std::unique_ptr &frame) { uint8_t tag[] = { 'M', '1', '7', ' ' }; if ( (Buffer.size() == sizeof(SM17Frame)) && (0 == Buffer.Compare(tag, sizeof(tag))) && (0x4U == (0x1CU & Buffer[19])) ) - // Buffer[19] is the low-order byte of the uint16_t frametype. - // the 0x1CU mask (00011100 binary) just lets us see: - // 1. the encryptions bytes (mask 0x18U) which must be zero, and - // 2. the msb of the 2-bit payload type (mask 0x4U) which must be set. This bit set means it's voice or voice+data. - // An masked result of 0x4U means the payload contains Codec2 voice data and there is no encryption. { // Make the M17 header CM17Packet m17(Buffer.data()); @@ -411,3 +514,63 @@ void CM17Protocol::EncodeM17Packet(SM17Frame &frame, const CDvHeaderPacket &Head frame.streamid = Header.GetStreamId(); // no host<--->network byte swapping since we never do any math on this value // the CRC will be set in HandleQueue, after lich.dest is set } + +bool CM17Protocol::EncodeDvHeaderPacket(const CDvHeaderPacket &Header, CBuffer &Buffer) const +{ + (void)Header; + (void)Buffer; + return false; // M17 uses EncodeM17Packet +} + +bool CM17Protocol::EncodeDvFramePacket(const CDvFramePacket &Frame, CBuffer &Buffer) const +{ + (void)Frame; + (void)Buffer; + return false; // M17 uses EncodeM17Packet +} + +void CM17Protocol::HandleParrot(const CIp &Ip, const CBuffer &Buffer, bool isStream) +{ + std::string key = Ip.GetAddress(); + auto it = m_ParrotMap.find(key); + + if (it == m_ParrotMap.end()) + { + std::shared_ptr client = g_Reflector.GetClients()->FindClient(Ip, EProtocol::m17); + auto m17client = std::dynamic_pointer_cast(client); + g_Reflector.ReleaseClients(); + + if (m17client) + { + if (isStream) + { + // Extract frametype from SM17Frame + uint16_t ft = (Buffer.data()[12] << 8) | Buffer.data()[13]; + m_ParrotMap[key] = std::make_shared(m17client->GetCallsign(), m17client, ft, this); + } + else + { + // Extract frametype from SM17P (lich part starts at offset 4, but frametype is at offset 16) + uint16_t ft = (Buffer.data()[16] << 8) | Buffer.data()[17]; + m_ParrotMap[key] = std::make_shared(m17client->GetCallsign(), m17client, ft, this); + } + } + } + + it = m_ParrotMap.find(key); + if (it != m_ParrotMap.end() && it->second->GetState() == EParrotState::record) + { + if (isStream) + { + // streamId at offset 4, fn at 38 + uint16_t sid = (Buffer.data()[4] << 8) | Buffer.data()[5]; + uint16_t fn = (Buffer.data()[38] << 8) | Buffer.data()[39]; + it->second->Add(Buffer, sid, fn); + } + else + { + it->second->AddPacket(Buffer); + it->second->Play(); // Packet mode parrot plays back immediately + } + } +} diff --git a/reflector/M17Protocol.h b/reflector/M17Protocol.h index 61c21ea..5887eb3 100644 --- a/reflector/M17Protocol.h +++ b/reflector/M17Protocol.h @@ -1,5 +1,5 @@ // Copyright © 2015 Jean-Luc Deltombe (LX3JL). All rights reserved. - +// // urfd -- The universal reflector // Copyright © 2021 Thomas A. Early N7TAE // @@ -21,13 +21,23 @@ #include "Defines.h" #include "Timer.h" #include "Protocol.h" +#include "SEProtocol.h" #include "DVHeaderPacket.h" #include "DVFramePacket.h" #include "M17CRC.h" +#include +#include +#include +#include + //////////////////////////////////////////////////////////////////////////////////////// // define +//////////////////////////////////////////////////////////////////////////////////////// +// forward declarations +class CParrot; + //////////////////////////////////////////////////////////////////////////////////////// // class @@ -40,15 +50,29 @@ public: uint32_t m_iSeqCounter; }; -class CM17Protocol : public CProtocol +class CM17Protocol : public CSEProtocol { public: + // constructors + CM17Protocol(); + + // destructor + virtual ~CM17Protocol() {} + // initialization bool Initialize(const char *type, const EProtocol ptype, const uint16_t port, const bool has_ipv4, const bool has_ipv6); - // task + // protocol void Task(void); + // packet encoding helpers (public for Parrot access) + void Send(const CBuffer &buf, const CIp &Ip) const { CProtocol::Send(buf, Ip); } + void Send(const char *buf, const CIp &Ip) const { CProtocol::Send(buf, Ip); } + void Send(const SM17Frame &frame, const CIp &Ip) const { CProtocol::Send(frame, Ip); } + + virtual bool EncodeDvHeaderPacket(const CDvHeaderPacket &, CBuffer &) const override; + virtual bool EncodeDvFramePacket(const CDvFramePacket &, CBuffer &) const override; + protected: // queue helper void HandleQueue(void); @@ -59,16 +83,22 @@ protected: // stream helpers void OnDvHeaderPacketIn(std::unique_ptr &, const CIp &); +private: // packet decoding helpers bool IsValidConnectPacket(const CBuffer &, CCallsign &, char &); + bool IsValidListenPacket(const CBuffer &, CCallsign &, char &); bool IsValidDisconnectPacket(const CBuffer &, CCallsign &); bool IsValidKeepAlivePacket(const CBuffer &, CCallsign &); + bool IsValidPacketModePacket(const CBuffer &, CCallsign &, CCallsign &); bool IsValidDvPacket(const CBuffer &, std::unique_ptr &, std::unique_ptr &); // packet encoding helpers void EncodeKeepAlivePacket(CBuffer &); void EncodeM17Packet(SM17Frame &, const CDvHeaderPacket &, const CDvFramePacket *, uint32_t) const; + // parrot + void HandleParrot(const CIp &Ip, const CBuffer &Buffer, bool isStream); + protected: // for keep alive CTimer m_LastKeepaliveTime; @@ -78,4 +108,5 @@ protected: private: CM17CRC m17crc; + std::map> m_ParrotMap; };