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 1/3] 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; }; From c71dfc95a2d263b4abd1bb9252baee09a878f1a7 Mon Sep 17 00:00:00 2001 From: Dave Behnke <916775+dbehnke@users.noreply.github.com> Date: Sun, 28 Dec 2025 00:44:29 -0500 Subject: [PATCH 2/3] fix(m17): allow special chars (-./) in callsign for M17 protocol compatibility --- reflector/Callsign.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reflector/Callsign.cpp b/reflector/Callsign.cpp index b4b9666..ef4e160 100644 --- a/reflector/Callsign.cpp +++ b/reflector/Callsign.cpp @@ -151,10 +151,9 @@ bool CCallsign::IsValid(void) const } } valid = valid && (iNum < 3); - // all remaining char are letter, number or space for ( ; i < CALLSIGN_LEN; i++) { - valid = valid && (IsLetter(m_Callsign.c[i]) || IsNumber(m_Callsign.c[i]) || IsSpace(m_Callsign.c[i])); + valid = valid && (IsLetter(m_Callsign.c[i]) || IsNumber(m_Callsign.c[i]) || IsSpace(m_Callsign.c[i]) || m_Callsign.c[i] == '-' || m_Callsign.c[i] == '.' || m_Callsign.c[i] == '/'); } // prefix From 1230625601edda4d5d2b882dec537758a714847b Mon Sep 17 00:00:00 2001 From: Dave Behnke <916775+dbehnke@users.noreply.github.com> Date: Sun, 28 Dec 2025 01:40:03 -0500 Subject: [PATCH 3/3] Impl: M17 Dual-Mode Support & Legacy Compat Flag Implemented M17LegacyCompat flag (default true). Added dual-mode receiver for 54/56-byte frames. Updated transmitter and Parrot to respect compat flag. --- reflector/Configure.cpp | 8 +++ reflector/JsonKeys.h | 9 ++- reflector/M17Packet.cpp | 122 +++++++++++++++++++++++++++++++++----- reflector/M17Packet.h | 50 ++++++++++++++-- reflector/M17Parrot.cpp | 64 ++++++++++++++------ reflector/M17Protocol.cpp | 78 +++++++++++++++++------- reflector/M17Protocol.h | 5 +- reflector/Protocol.cpp | 16 +---- reflector/Protocol.h | 3 +- 9 files changed, 273 insertions(+), 82 deletions(-) diff --git a/reflector/Configure.cpp b/reflector/Configure.cpp index 445e953..8d87b00 100644 --- a/reflector/Configure.cpp +++ b/reflector/Configure.cpp @@ -62,6 +62,7 @@ #define JIPV6EXTERNAL "IPv6External" #define JJSONPATH "JsonPath" #define JM17 "M17" +#define JM17LEGACYCOMPAT "M17LegacyCompat" #define JMMDVM "MMDVM" #define JMODE "Mode" #define JMODULE "Module" @@ -368,6 +369,8 @@ bool CConfigure::ReadData(const std::string &path) case ESection::m17: if (0 == key.compare(JPORT)) data[g_Keys.m17.port] = getUnsigned(value, "M17 Port", 1024, 65535, 17000); + else if (0 == key.compare(JM17LEGACYCOMPAT)) + data[g_Keys.m17.compat] = IS_TRUE(value[0]); else badParam(key); break; @@ -684,6 +687,11 @@ bool CConfigure::ReadData(const std::string &path) isDefined(ErrorLevel::fatal, JDMRPLUS, JPORT, g_Keys.dmrplus.port, rval); isDefined(ErrorLevel::fatal, JDPLUS, JPORT, g_Keys.dplus.port, rval); isDefined(ErrorLevel::fatal, JM17, JPORT, g_Keys.m17.port, rval); + if (data.contains(g_Keys.m17.compat)) + data[g_Keys.m17.compat] = GetBoolean(g_Keys.m17.compat); + else + data[g_Keys.m17.compat] = true; // Default to Legacy Mode (54 bytes) for compatibility + isDefined(ErrorLevel::fatal, JURF, JPORT, g_Keys.urf.port, rval); // BM diff --git a/reflector/JsonKeys.h b/reflector/JsonKeys.h index 72cfc0d..c29361c 100644 --- a/reflector/JsonKeys.h +++ b/reflector/JsonKeys.h @@ -26,9 +26,12 @@ struct SJsonKeys { dcs { "DCSPort" }, dextra { "DExtraPort" }, dmrplus { "DMRPlusPort" }, - dplus { "DPlusPort" }, - m17 { "M17Port" }, - urf { "URFPort" }; + dplus { "DPlusPort" }; + + struct { const std::string port, compat; } + m17 { "M17Port", "M17LegacyCompat" }; + + PORTONLY urf { "URFPort" }; struct G3 { const std::string enable; } g3 { "G3Enable" }; diff --git a/reflector/M17Packet.cpp b/reflector/M17Packet.cpp index 7ed45a8..e00419a 100644 --- a/reflector/M17Packet.cpp +++ b/reflector/M17Packet.cpp @@ -21,12 +21,21 @@ #include "M17Packet.h" -CM17Packet::CM17Packet(const uint8_t *buf) +CM17Packet::CM17Packet(const uint8_t *buf, bool isStandard) + : m_isStandard(isStandard) { - memcpy(m17.magic, buf, sizeof(SM17Frame)); - - destination.CodeIn(m17.lich.addr_dst); - source.CodeIn(m17.lich.addr_src); + if (m_isStandard) + { + memcpy(m_frame.buffer, buf, sizeof(SM17FrameStandard)); + destination.CodeIn(m_frame.standard.lich.addr_dst); + source.CodeIn(m_frame.standard.lich.addr_src); + } + else + { + memcpy(m_frame.buffer, buf, sizeof(SM17FrameLegacy)); + destination.CodeIn(m_frame.legacy.lich.addr_dst); + source.CodeIn(m_frame.legacy.lich.addr_src); + } } const CCallsign &CM17Packet::GetDestCallsign() const @@ -46,45 +55,130 @@ char CM17Packet::GetDestModule() const uint16_t CM17Packet::GetFrameNumber() const { - return ntohs(m17.framenumber); + if (m_isStandard) + return ntohs(m_frame.standard.framenumber); + else + return ntohs(m_frame.legacy.framenumber); } uint16_t CM17Packet::GetFrameType() const { - return ntohs(m17.lich.frametype); + if (m_isStandard) + return ntohs(m_frame.standard.lich.frametype); + else + return ntohs(m_frame.legacy.lich.frametype); } const uint8_t *CM17Packet::GetPayload() const { - return m17.payload; + if (m_isStandard) + return m_frame.standard.payload; + else + return m_frame.legacy.payload; } const uint8_t *CM17Packet::GetNonce() const { - return m17.lich.nonce; + if (m_isStandard) + return m_frame.standard.lich.nonce; + else + return m_frame.legacy.lich.nonce; } void CM17Packet::SetPayload(const uint8_t *newpayload) { - memcpy(m17.payload, newpayload, 16); + if (m_isStandard) + memcpy(m_frame.standard.payload, newpayload, 16); + else + memcpy(m_frame.legacy.payload, newpayload, 16); } uint16_t CM17Packet::GetStreamId() const { - return ntohs(m17.streamid); + if (m_isStandard) + return ntohs(m_frame.standard.streamid); + else + return ntohs(m_frame.legacy.streamid); } uint16_t CM17Packet::GetCRC() const { - return ntohs(m17.crc); + if (m_isStandard) + return ntohs(m_frame.standard.crc); + else + return ntohs(m_frame.legacy.crc); } void CM17Packet::SetCRC(uint16_t crc) { - m17.crc = htons(crc); + if (m_isStandard) + m_frame.standard.crc = htons(crc); + else + m_frame.legacy.crc = htons(crc); +} + +void CM17Packet::SetDestCallsign(const CCallsign &cs) +{ + destination = cs; + if (m_isStandard) + destination.CodeOut(m_frame.standard.lich.addr_dst); + else + destination.CodeOut(m_frame.legacy.lich.addr_dst); +} + +void CM17Packet::SetSourceCallsign(const CCallsign &cs) +{ + source = cs; + if (m_isStandard) + source.CodeOut(m_frame.standard.lich.addr_src); + else + source.CodeOut(m_frame.legacy.lich.addr_src); +} + +void CM17Packet::SetStreamId(uint16_t id) +{ + if (m_isStandard) + m_frame.standard.streamid = htons(id); + else + m_frame.legacy.streamid = htons(id); +} + +void CM17Packet::SetFrameNumber(uint16_t fn) +{ + if (m_isStandard) + m_frame.standard.framenumber = htons(fn); + else + m_frame.legacy.framenumber = htons(fn); +} + +void CM17Packet::SetFrameType(uint16_t ft) +{ + if (m_isStandard) + m_frame.standard.lich.frametype = htons(ft); + else + m_frame.legacy.lich.frametype = htons(ft); +} + +void CM17Packet::SetMagic() +{ + if (m_isStandard) + memcpy(m_frame.standard.magic, "M17 ", 4); + else + memcpy(m_frame.legacy.magic, "M17 ", 4); +} + +void CM17Packet::SetNonce(const uint8_t *nonce) +{ + if (m_isStandard) + memcpy(m_frame.standard.lich.nonce, nonce, 14); + else + memcpy(m_frame.legacy.lich.nonce, nonce, 14); } bool CM17Packet::IsLastPacket() const { - return ((0x8000u & ntohs(m17.framenumber)) == 0x8000u); + if (m_isStandard) + return ((0x8000u & ntohs(m_frame.standard.framenumber)) == 0x8000u); + else + return ((0x8000u & ntohs(m_frame.legacy.framenumber)) == 0x8000u); } diff --git a/reflector/M17Packet.h b/reflector/M17Packet.h index 3d58ffd..3620e19 100644 --- a/reflector/M17Packet.h +++ b/reflector/M17Packet.h @@ -30,23 +30,42 @@ // M17 Packets //all structures must be big endian on the wire, so you'll want htonl (man byteorder 3) and such. -using SM17Lich = struct __attribute__((__packed__)) lich_tag { +using SM17LichLegacy = struct __attribute__((__packed__)) lich_tag { uint8_t addr_dst[6]; uint8_t addr_src[6]; uint16_t frametype; //frametype flag field per the M17 spec uint8_t nonce[14]; //bytes for the nonce }; // 6 + 6 + 2 + 14 = 28 bytes +// Standard LICH includes CRC +using SM17LichStandard = struct __attribute__((__packed__)) lich_std_tag { + uint8_t addr_dst[6]; + uint8_t addr_src[6]; + uint16_t frametype; + uint8_t nonce[14]; + uint16_t crc; +}; // 6 + 6 + 2 + 14 + 2 = 30 bytes + //without SYNC or other parts -using SM17Frame = struct __attribute__((__packed__)) m17_tag { +using SM17FrameLegacy = struct __attribute__((__packed__)) m17_tag { uint8_t magic[4]; uint16_t streamid; - SM17Lich lich; + SM17LichLegacy lich; uint16_t framenumber; uint8_t payload[16]; uint16_t crc; //16 bit CRC }; // 4 + 2 + 28 + 2 + 16 + 2 = 54 bytes +using SM17FrameStandard = struct __attribute__((__packed__)) m17_std_tag { + uint8_t magic[4]; + uint16_t streamid; + SM17LichStandard lich; + uint16_t framenumber; + uint8_t payload[16]; + uint16_t crc; //16 bit CRC +}; // 4 + 2 + 30 + 2 + 16 + 2 = 56 bytes + + using SLinkPacket = struct __attribute__((__packed__)) link_tag { uint8_t magic[4]; uint8_t fromcs[6]; @@ -57,7 +76,7 @@ class CM17Packet { public: CM17Packet() {} - CM17Packet(const uint8_t *buf); + CM17Packet(const uint8_t *buf, bool isStandard = false); const CCallsign &GetDestCallsign() const; const CCallsign &GetSourceCallsign() const; char GetDestModule() const; @@ -69,9 +88,30 @@ public: uint16_t GetStreamId() const; uint16_t GetCRC() const; void SetCRC(uint16_t crc); + void SetDestCallsign(const CCallsign &cs); + void SetSourceCallsign(const CCallsign &cs); + void SetStreamId(uint16_t id); + void SetFrameNumber(uint16_t fn); + void SetFrameType(uint16_t ft); + void SetNonce(const uint8_t *nonce); + + void SetMagic(); + + uint8_t *GetLICHPointer() { return m_isStandard ? (uint8_t*)&m_frame.standard.lich : (uint8_t*)&m_frame.legacy.lich; } + size_t GetLICHSize() const { return m_isStandard ? sizeof(SM17LichStandard) : sizeof(SM17LichLegacy); } + + const uint8_t *GetBuffer() const { return m_frame.buffer; } + size_t GetSize() const { return m_isStandard ? sizeof(SM17FrameStandard) : sizeof(SM17FrameLegacy); } + bool IsLastPacket() const; private: CCallsign destination, source; - SM17Frame m17; + // Flexible storage for either Legacy or Standard frame + union { + SM17FrameLegacy legacy; + SM17FrameStandard standard; + uint8_t buffer[60]; + } m_frame; + bool m_isStandard; }; diff --git a/reflector/M17Parrot.cpp b/reflector/M17Parrot.cpp index 7fbe3fc..9955fd2 100644 --- a/reflector/M17Parrot.cpp +++ b/reflector/M17Parrot.cpp @@ -22,12 +22,14 @@ void CM17StreamParrot::Add(const CBuffer &Buffer, uint16_t streamId, uint16_t fr { 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 + + bool isStandard = false; + if (Buffer.size() == 56) isStandard = true; + + // Use parser to get payload pointer safely + CM17Packet parser(Buffer.data(), isStandard); + const uint8_t *payload = parser.GetPayload(); - const uint8_t *payload = Buffer.data() + 40; // payload offset in SM17Frame m_data.emplace_back(payload, payload + length); } m_lastHeard.start(); @@ -47,15 +49,28 @@ 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. + // Determine format to send + bool useLegacy = g_Configure.GetBoolean(g_Keys.m17.compat); + + uint8_t buffer[60]; + CM17Packet pkt(buffer, !useLegacy); + memset(buffer, 0, 60); // clear buffer - // Set LICH addresses - memset(frame.lich.addr_dst, 0xFF, 6); // @ALL - m_src.CodeOut(frame.lich.addr_src); - frame.lich.frametype = htons(m_frameType); + pkt.SetMagic(); + pkt.SetStreamId(m_streamId); + + // I will add `SetDestBytes` to CM17Packet? Or just use explicit CCallsign. + // I will try to use the `m_src` as dest? No, that's what `CodeOut` does. + // I will use `pkt.SetDestCallsign` with a dummy, and then manually overwrite if needed? + // Better: `CM17Packet` exposes `GetLichPointer()`. I can write to it manually! + + // Set Source + pkt.SetSourceCallsign(m_src); + pkt.SetFrameType(m_frameType); + + // Set Dest to FF + uint8_t *lich = pkt.GetLICHPointer(); + memset(lich, 0xFF, 6); // Dest is at offset 0 of LICH auto clock = std::chrono::steady_clock::now(); size_t size = m_data.size(); @@ -63,21 +78,32 @@ void CM17StreamParrot::playThread() for (size_t n = 0; n < size; n++) { size_t length = m_is3200 ? 16 : 8; - memcpy(frame.payload, m_data[n].data(), length); + pkt.SetPayload(m_data[n].data()); uint16_t fn = (uint16_t)n; if (n == size - 1) fn |= 0x8000u; - frame.framenumber = htons(fn); + pkt.SetFrameNumber(fn); - CM17CRC m17crc; - frame.crc = htons(m17crc.CalcCRC((uint8_t*)&frame, sizeof(SM17Frame)-2)); + // CRC + CM17CRC m17crc_inst; + if (!useLegacy) { + // Standard LICH CRC + uint16_t l_crc = m17crc_inst.CalcCRC(lich, 28); + ((SM17LichStandard*)lich)->crc = htons(l_crc); + } + + uint16_t p_crc = m17crc_inst.CalcCRC(pkt.GetBuffer(), pkt.GetSize()-2); + pkt.SetCRC(p_crc); clock = clock + std::chrono::milliseconds(40); std::this_thread::sleep_until(clock); - if (m_proto) - m_proto->Send(frame, m_client->GetIp()); + if (m_proto) { + CBuffer sendBuf; + sendBuf.Append(pkt.GetBuffer(), pkt.GetSize()); + m_proto->Send(sendBuf, m_client->GetIp()); + } m_data[n].clear(); } m_data.clear(); diff --git a/reflector/M17Protocol.cpp b/reflector/M17Protocol.cpp index 9da0223..afe674b 100644 --- a/reflector/M17Protocol.cpp +++ b/reflector/M17Protocol.cpp @@ -312,18 +312,22 @@ void CM17Protocol::HandleQueue(void) if ( packet->IsDvHeader() ) { // this relies on queue feeder setting valid module id - // m_StreamsCache[module] will be created if it doesn't exist - m_StreamsCache[module].m_dvHeader = CDvHeaderPacket((const CDvHeaderPacket &)*packet.get()); + // m_StreamsCache[module].m_dvHeader = CDvHeaderPacket((const CDvHeaderPacket &)*packet.get()); m_StreamsCache[module].m_iSeqCounter = 0; } else if (packet->IsDvFrame()) { if ((1 == m_StreamsCache[module].m_iSeqCounter % 2) || packet->IsLastPacket()) { - // encode it - SM17Frame frame; + // Determine if we should send Legacy or Standard packets + // Default to Legacy (true) if key missing, but Configure.cpp handles default. + bool useLegacy = g_Configure.GetBoolean(g_Keys.m17.compat); - EncodeM17Packet(frame, m_StreamsCache[module].m_dvHeader, (CDvFramePacket *)packet.get(), m_StreamsCache[module].m_iSeqCounter); + // encode it using M17Packet wrapper + uint8_t buffer[60]; // Enough for both + CM17Packet m17pkt(buffer, !useLegacy); + + EncodeM17Packet(m17pkt, m_StreamsCache[module].m_dvHeader, (CDvFramePacket *)packet.get(), m_StreamsCache[module].m_iSeqCounter); // push it to all our clients linked to the module and who are not streaming in CClients *clients = g_Reflector.GetClients(); @@ -335,12 +339,27 @@ void CM17Protocol::HandleQueue(void) if ( !client->IsAMaster() && (client->GetReflectorModule() == module) ) { // set the destination - client->GetCallsign().CodeOut(frame.lich.addr_dst); - // set the crc - frame.crc = htons(m17crc.CalcCRC(frame.magic, sizeof(SM17Frame)-2)); - // now send the packet - Send(frame, client->GetIp()); + m17pkt.SetDestCallsign(client->GetCallsign()); + + // Calculate LICH CRC if Standard + if (!useLegacy) { + uint8_t *lich = m17pkt.GetLICHPointer(); + // CRC over first 28 bytes of LICH + uint16_t l_crc = m17crc.CalcCRC(lich, 28); + // Set CRC at offset 28 of LICH (bytes 28,29) + // We can cast to SM17LichStandard to be safe or use set/memcpy + ((SM17LichStandard*)lich)->crc = htons(l_crc); + } + + // set the packet crc + // Legacy: CRC over first 52 bytes. Standard: CRC over first 54 bytes. + uint16_t p_crc = m17crc.CalcCRC(m17pkt.GetBuffer(), m17pkt.GetSize() - 2); + m17pkt.SetCRC(p_crc); + // now send the packet + CBuffer sendBuf; + sendBuf.Append(m17pkt.GetBuffer(), m17pkt.GetSize()); + Send(sendBuf, client->GetIp()); } } g_Reflector.ReleaseClients(); @@ -462,10 +481,23 @@ bool CM17Protocol::IsValidDvPacket(const CBuffer &Buffer, std::unique_ptr(new CDvHeaderPacket(m17)); @@ -490,28 +522,28 @@ void CM17Protocol::EncodeKeepAlivePacket(CBuffer &Buffer) g_Reflector.GetCallsign().CodeOut(Buffer.data() + 4); } -void CM17Protocol::EncodeM17Packet(SM17Frame &frame, const CDvHeaderPacket &Header, const CDvFramePacket *DvFrame, uint32_t iSeq) const +void CM17Protocol::EncodeM17Packet(CM17Packet &packet, const CDvHeaderPacket &Header, const CDvFramePacket *DvFrame, uint32_t iSeq) const { ECodecType codec_in = Header.GetCodecIn(); // We'll need this - // do the lich structure first // first, the src callsign (the lich.dest will be set in HandleQueue) - CCallsign from = Header.GetMyCallsign(); - from.CodeOut(frame.lich.addr_src); + packet.SetSourceCallsign(Header.GetMyCallsign()); + // then the frame type, if the incoming frame is M17 1600, then it will be Voice+Data only, otherwise Voice-Only - frame.lich.frametype = htons((ECodecType::c2_1600==codec_in) ? 0x7U : 0x5U); - memcpy(frame.lich.nonce, DvFrame->GetNonce(), 14); + packet.SetFrameType((ECodecType::c2_1600==codec_in) ? 0x7U : 0x5U); + packet.SetNonce(DvFrame->GetNonce()); // now the main part of the packet - memcpy(frame.magic, "M17 ", 4); + packet.SetMagic(); + // the frame number comes from the stream sequence counter uint16_t fn = (iSeq / 2) % 0x8000U; if (DvFrame->IsLastPacket()) fn |= 0x8000U; - frame.framenumber = htons(fn); - memcpy(frame.payload, DvFrame->GetCodecData(ECodecType::c2_3200), 16); - frame.streamid = Header.GetStreamId(); // no host<--->network byte swapping since we never do any math on this value + packet.SetFrameNumber(fn); + packet.SetPayload(DvFrame->GetCodecData(ECodecType::c2_3200)); + packet.SetStreamId(Header.GetStreamId()); // the CRC will be set in HandleQueue, after lich.dest is set } diff --git a/reflector/M17Protocol.h b/reflector/M17Protocol.h index 5887eb3..efcc57a 100644 --- a/reflector/M17Protocol.h +++ b/reflector/M17Protocol.h @@ -68,7 +68,8 @@ public: // 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; @@ -94,7 +95,7 @@ private: // packet encoding helpers void EncodeKeepAlivePacket(CBuffer &); - void EncodeM17Packet(SM17Frame &, const CDvHeaderPacket &, const CDvFramePacket *, uint32_t) const; + void EncodeM17Packet(CM17Packet &packet, const CDvHeaderPacket &, const CDvFramePacket *, uint32_t) const; // parrot void HandleParrot(const CIp &Ip, const CBuffer &Buffer, bool isStream); diff --git a/reflector/Protocol.cpp b/reflector/Protocol.cpp index 0bcc223..132d6fa 100644 --- a/reflector/Protocol.cpp +++ b/reflector/Protocol.cpp @@ -335,21 +335,7 @@ void CProtocol::Send(const char *buf, const CIp &Ip, uint16_t port) const } } -void CProtocol::Send(const SM17Frame &frame, const CIp &Ip) const -{ - switch (Ip.GetFamily()) - { - case AF_INET: - m_Socket4.Send(frame.magic, sizeof(SM17Frame), Ip); - break; - case AF_INET6: - m_Socket6.Send(frame.magic, sizeof(SM17Frame), Ip); - break; - default: - std::cerr << "WrongFamily: " << Ip.GetFamily() << std::endl; - break; - } -} + #ifdef DEBUG void CProtocol::Dump(const char *title, const uint8_t *data, int length) diff --git a/reflector/Protocol.h b/reflector/Protocol.h index 678b533..d342232 100644 --- a/reflector/Protocol.h +++ b/reflector/Protocol.h @@ -113,7 +113,8 @@ protected: void Send(const char *buf, const CIp &Ip) const; void Send(const CBuffer &buf, const CIp &Ip, uint16_t port) const; void Send(const char *buf, const CIp &Ip, uint16_t port) const; - void Send(const SM17Frame &frame, const CIp &Ip) const; + + #ifdef DEBUG void Dump(const char *title, const uint8_t *data, int length); #endif