diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc5ad01..9a0381e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: # Put git hash in startup message sed -i "s/Loading.../$(date +%Y%m%d)-$(git rev-parse --short HEAD)/g" src/userinterface.cpp # https://github.com/rsta2/circle/discussions/427#discussioncomment-11198505 - # sed -i -e 's|TTLShort = 120|TTLShort = 15|g' circle-stdlib/libs/circle/include/circle/net/mdnspublisher.h + sed -i -e 's|TTLShort = 120|TTLShort = 15|g' circle-stdlib/libs/circle/include/circle/net/mdnspublisher.h - name: Install toolchains run: | set -ex diff --git a/src/Makefile b/src/Makefile index fa4ab10..ed5e050 100644 --- a/src/Makefile +++ b/src/Makefile @@ -11,7 +11,7 @@ OBJS = main.o kernel.o minidexed.o config.o userinterface.o uimenu.o \ mididevice.o udpmididevice.o midikeyboard.o serialmididevice.o pckeyboard.o \ sysexfileloader.o performanceconfig.o perftimer.o \ effect_compressor.o effect_platervbstereo.o uibuttons.o midipin.o \ - net/ftpdaemon.o net/ftpworker.o net/applemidi.o net/udpmidi.o + net/ftpdaemon.o net/ftpworker.o net/applemidi.o net/udpmidi.o net/mdnspublisher.o OPTIMIZE = -O3 diff --git a/src/minidexed.cpp b/src/minidexed.cpp index c198c6f..14d0c3f 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -64,7 +64,7 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_bNetworkReady(false), m_bNetworkInit(false), m_UDPMIDI (this, pConfig, &m_UI), - //m_pmDNSPublisher (nullptr), + m_pmDNSPublisher (nullptr), m_bSavePerformance (false), m_bSavePerformanceNewFile (false), m_bSetNewPerformance (false), @@ -2232,7 +2232,7 @@ void CMiniDexed::UpdateNetwork() } m_UI.DisplayWrite (IPString, "", "TG1", 0, 1); // FIXME: Do not hardcode "TG1" here - /*m_pmDNSPublisher = new CmDNSPublisher (m_pNet); + m_pmDNSPublisher = new CmDNSPublisher (m_pNet); assert (m_pmDNSPublisher); //static const char *ppText[] = {"RTP-MIDI Receiver", nullptr}; // dont bother adding additional data @@ -2241,26 +2241,26 @@ void CMiniDexed::UpdateNetwork() { LOGPANIC ("Cannot publish mdns service"); } - */ + m_bNetworkReady = true; } if (m_bNetworkReady && !bNetIsRunning) { m_bNetworkReady = false; - //m_pmDNSPublisher->UnpublishService (MDNSSERVICENAME); + m_pmDNSPublisher->UnpublishService (MDNSSERVICENAME); LOGNOTE("Network disconnected."); } else if (!m_bNetworkReady && bNetIsRunning) { m_bNetworkReady = true; - /* + if (!m_pmDNSPublisher->PublishService (MDNSSERVICENAME, CmDNSPublisher::ServiceTypeAppleMIDI, 5004)) { LOGPANIC ("Cannot publish mdns service"); } - */ + LOGNOTE("Network connection reestablished."); } diff --git a/src/minidexed.h b/src/minidexed.h index 0c29978..79e857d 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -43,7 +43,7 @@ #include #include #include -//#include +#include "net/mdnspublisher.h" #include #include "common.h" #include "effect_mixer.hpp" @@ -341,7 +341,7 @@ private: bool m_bNetworkInit; CUDPMIDIDevice m_UDPMIDI; CFTPDaemon* m_pFTPDaemon; - //CmDNSPublisher *m_pmDNSPublisher; + CmDNSPublisher *m_pmDNSPublisher; bool m_bSavePerformance; bool m_bSavePerformanceNewFile; diff --git a/src/net/mdnspublisher.cpp b/src/net/mdnspublisher.cpp new file mode 100644 index 0000000..23052db --- /dev/null +++ b/src/net/mdnspublisher.cpp @@ -0,0 +1,345 @@ +// +// mdnspublisher.cpp +// +// Circle - A C++ bare metal environment for Raspberry Pi +// Copyright (C) 2024 R. Stange +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "mdnspublisher.h" +#include +#include +#include +#include +#include +#define MDNS_HOST_GROUP {224, 0, 0, 251} +#define MDNS_PORT 5353 +#define MDNS_DOMAIN "local" +#define RR_TYPE_A 1 +#define RR_TYPE_PTR 12 +#define RR_TYPE_TXT 16 +#define RR_TYPE_SRV 33 +#define RR_CLASS_IN 1 +#define RR_CACHE_FLUSH 0x8000 +LOGMODULE ("mdnspub"); +CmDNSPublisher::CmDNSPublisher (CNetSubSystem *pNet) +: m_pNet (pNet), + m_pSocket (nullptr), + m_bRunning (FALSE), + m_pWritePtr (nullptr), + m_pDataLen (nullptr) +{ + SetName ("mdnspub"); +} +CmDNSPublisher::~CmDNSPublisher (void) +{ + assert (!m_pSocket); + m_bRunning = FALSE; +} +boolean CmDNSPublisher::PublishService (const char *pServiceName, const char *pServiceType, + u16 usServicePort, const char *ppText[]) +{ + if (!m_bRunning) + { + // Let task can run once to initialize + CScheduler::Get ()->Yield (); + if (!m_bRunning) + { + return FALSE; + } + } + assert (pServiceName); + assert (pServiceType); + TService *pService = new TService {pServiceName, pServiceType, usServicePort, 0}; + assert (pService); + if (ppText) + { + for (unsigned i = 0; i < MaxTextRecords && ppText[i]; i++) + { + pService->ppText[i] = new CString (ppText[i]); + assert (pService->ppText[i]); + pService->nTextRecords++; + } + } + m_Mutex.Acquire (); + // Insert as first element into list + TPtrListElement *pElement = m_ServiceList.GetFirst (); + if (pElement) + { + m_ServiceList.InsertBefore (pElement, pService); + } + else + { + m_ServiceList.InsertAfter (nullptr, pService); + } + m_Mutex.Release (); + LOGDBG ("Publish service %s", (const char *) pService->ServiceName); + m_Event.Set (); // Trigger resent for everything + return TRUE; +} +boolean CmDNSPublisher::UnpublishService (const char *pServiceName) +{ + if (!m_bRunning) + { + return FALSE; + } + assert (pServiceName); + m_Mutex.Acquire (); + // Find service in the list and remove it + TService *pService = nullptr; + TPtrListElement *pElement = m_ServiceList.GetFirst (); + while (pElement) + { + pService = static_cast (CPtrList::GetPtr (pElement)); + assert (pService); + if (pService->ServiceName.Compare (pServiceName) == 0) + { + m_ServiceList.Remove (pElement); + break; + } + pService = nullptr; + pElement = m_ServiceList.GetNext (pElement); + } + m_Mutex.Release (); + if (!pService) + { + return FALSE; + } + LOGDBG ("Unpublish service %s", (const char *) pService->ServiceName); + if (!SendResponse (pService, TRUE)) + { + LOGWARN ("Send failed"); + } + for (unsigned i = 0; i < pService->nTextRecords; i++) + { + delete pService->ppText[i]; + } + delete pService; + return TRUE; +} +void CmDNSPublisher::Run (void) +{ + assert (m_pNet); + assert (!m_pSocket); + m_pSocket = new CSocket (m_pNet, IPPROTO_UDP); + assert (m_pSocket); + if (m_pSocket->Bind (MDNS_PORT) < 0) + { + LOGERR ("Cannot bind to port %u", MDNS_PORT); + delete m_pSocket; + m_pSocket = nullptr; + while (1) + { + m_Event.Clear (); + m_Event.Wait (); + } + } + static const u8 mDNSIPAddress[] = MDNS_HOST_GROUP; + CIPAddress mDNSIP (mDNSIPAddress); + if (m_pSocket->Connect (mDNSIP, MDNS_PORT) < 0) + { + LOGERR ("Cannot connect to mDNS host group"); + delete m_pSocket; + m_pSocket = nullptr; + while (1) + { + m_Event.Clear (); + m_Event.Wait (); + } + } + m_bRunning = TRUE; + while (1) + { + m_Event.Clear (); + m_Event.WaitWithTimeout ((TTLShort - 10) * 1000000); + for (unsigned i = 1; i <= 3; i++) + { + m_Mutex.Acquire (); + TPtrListElement *pElement = m_ServiceList.GetFirst (); + while (pElement) + { + TService *pService = + static_cast (CPtrList::GetPtr (pElement)); + assert (pService); + if (!SendResponse (pService, FALSE)) + { + LOGWARN ("Send failed"); + } + pElement = m_ServiceList.GetNext (pElement); + } + m_Mutex.Release (); + CScheduler::Get ()->Sleep (1); + } + } +} +boolean CmDNSPublisher::SendResponse (TService *pService, boolean bDelete) +{ + assert (pService); + assert (m_pNet); + // Collect data + static const char Domain[] = "." MDNS_DOMAIN; + CString ServiceType (pService->ServiceType); + ServiceType.Append (Domain); + CString ServiceName (pService->ServiceName); + ServiceName.Append ("."); + ServiceName.Append (ServiceType); + CString Hostname (m_pNet->GetHostname ()); + Hostname.Append (Domain); + // Start writing buffer + assert (!m_pWritePtr); + m_pWritePtr = m_Buffer; + // mDNS Header + PutWord (0); // Transaction ID + PutWord (0x8400); // Message is a response, Server is an authority for the domain + PutWord (0); // Questions + PutWord (5); // Answer RRs + PutWord (0); // Authority RRs + PutWord (0); // Additional RRs + // Answer RRs + // PTR + PutDNSName ("_services._dns-sd._udp.local"); + PutWord (RR_TYPE_PTR); + PutWord (RR_CLASS_IN); + PutDWord (bDelete ? TTLDelete : TTLLong); + ReserveDataLength (); + u8 *pServiceTypePtr = m_pWritePtr; + PutDNSName (ServiceType); + SetDataLength (); + // PTR + PutCompressedString (pServiceTypePtr); + PutWord (RR_TYPE_PTR); + PutWord (RR_CLASS_IN); + PutDWord (bDelete ? TTLDelete : TTLLong); + ReserveDataLength (); + u8 *pServiceNamePtr = m_pWritePtr; + PutDNSName (ServiceName); + SetDataLength (); + // SRV + PutCompressedString (pServiceNamePtr); + PutWord (RR_TYPE_SRV); + PutWord (RR_CLASS_IN | RR_CACHE_FLUSH); + PutDWord (bDelete ? TTLDelete : TTLShort); + ReserveDataLength (); + PutWord (0); // Priority + PutWord (0); // Weight + PutWord (pService->usServicePort); + u8 *pHostnamePtr = m_pWritePtr; + PutDNSName (Hostname); + SetDataLength (); + // A + PutCompressedString (pHostnamePtr); + PutWord (RR_TYPE_A); + PutWord (RR_CLASS_IN | RR_CACHE_FLUSH); + PutDWord (TTLShort); + ReserveDataLength (); + PutIPAddress (*m_pNet->GetConfig ()->GetIPAddress ()); + SetDataLength (); + // TXT + PutCompressedString (pServiceNamePtr); + PutWord (RR_TYPE_TXT); + PutWord (RR_CLASS_IN | RR_CACHE_FLUSH); + PutDWord (bDelete ? TTLDelete : TTLLong); + ReserveDataLength (); + for (int i = pService->nTextRecords-1; i >= 0; i--) // In reverse order + { + assert (pService->ppText[i]); + PutString (*pService->ppText[i]); + } + SetDataLength (); + unsigned nMsgSize = m_pWritePtr - m_Buffer; + m_pWritePtr = nullptr; + if (nMsgSize >= MaxMessageSize) + { + return FALSE; + } + assert (m_pSocket); + return m_pSocket->Send (m_Buffer, nMsgSize, MSG_DONTWAIT) == (int) nMsgSize; +} +void CmDNSPublisher::PutByte (u8 uchValue) +{ + assert (m_pWritePtr); + if ((unsigned) (m_pWritePtr - m_Buffer) < MaxMessageSize) + { + *m_pWritePtr++ = uchValue; + } +} +void CmDNSPublisher::PutWord (u16 usValue) +{ + PutByte (usValue >> 8); + PutByte (usValue & 0xFF); +} +void CmDNSPublisher::PutDWord (u32 nValue) +{ + PutWord (nValue >> 16); + PutWord (nValue & 0xFFFF); +} +void CmDNSPublisher::PutString (const char *pValue) +{ + assert (pValue); + size_t nLen = strlen (pValue); + assert (nLen <= 255); + PutByte (nLen); + while (*pValue) + { + PutByte (static_cast (*pValue++)); + } +} +void CmDNSPublisher::PutCompressedString (const u8 *pWritePtr) +{ + assert (m_pWritePtr); + assert (pWritePtr < m_pWritePtr); + unsigned nOffset = pWritePtr - m_Buffer; + assert (nOffset < MaxMessageSize); + nOffset |= 0xC000; + PutWord (static_cast (nOffset)); +} +void CmDNSPublisher::PutDNSName (const char *pValue) +{ + char Buffer[256]; + assert (pValue); + strncpy (Buffer, pValue, sizeof Buffer); + Buffer[sizeof Buffer-1] = '\0'; + char *pSavePtr = nullptr; + char *pToken = strtok_r (Buffer, ".", &pSavePtr); + while (pToken) + { + PutString (pToken); + pToken = strtok_r (nullptr, ".", &pSavePtr); + } + PutByte (0); +} +void CmDNSPublisher::PutIPAddress (const CIPAddress &rValue) +{ + u8 Buffer[IP_ADDRESS_SIZE]; + rValue.CopyTo (Buffer); + for (unsigned i = 0; i < IP_ADDRESS_SIZE; i++) + { + PutByte (Buffer[i]); + } +} +void CmDNSPublisher::ReserveDataLength (void) +{ + assert (!m_pDataLen); + m_pDataLen = m_pWritePtr; + assert (m_pDataLen); + PutWord (0); +} +void CmDNSPublisher::SetDataLength (void) +{ + assert (m_pDataLen); + assert (m_pWritePtr); + assert (m_pWritePtr > m_pDataLen); + *reinterpret_cast (m_pDataLen) = le2be16 (m_pWritePtr - m_pDataLen - sizeof (u16)); + m_pDataLen = nullptr; +} \ No newline at end of file diff --git a/src/net/mdnspublisher.h b/src/net/mdnspublisher.h new file mode 100644 index 0000000..6b132a7 --- /dev/null +++ b/src/net/mdnspublisher.h @@ -0,0 +1,90 @@ +// +// mdnspublisher.h +// +// Circle - A C++ bare metal environment for Raspberry Pi +// Copyright (C) 2024 R. Stange +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#ifndef _circle_net_mdnspublisher_h +#define _circle_net_mdnspublisher_h +#include +#include +#include +#include +#include +#include +#include +#include +#include +class CmDNSPublisher : public CTask /// mDNS / Bonjour client task +{ +public: + static constexpr const char *ServiceTypeAppleMIDI = "_apple-midi._udp"; +public: + /// \param pNet Pointer to the network subsystem object + CmDNSPublisher (CNetSubSystem *pNet); + ~CmDNSPublisher (void); + /// \brief Start publishing a service + /// \param pServiceName Name of the service to be published + /// \param pServiceType Type of the service to be published (e.g. ServiceTypeAppleMIDI) + /// \param usServicePort Port number of the service to be published (in host byte order) + /// \param ppText Descriptions of the service (terminated with a nullptr, or nullptr itself) + /// \return Operation successful? + boolean PublishService (const char *pServiceName, + const char *pServiceType, + u16 usServicePort, + const char *ppText[] = nullptr); + /// \brief Stop publishing a service + /// \param pServiceName Name of the service to be unpublished (same as when published) + /// \return Operation successful? + boolean UnpublishService (const char *pServiceName); + void Run (void) override; +private: + static const unsigned MaxTextRecords = 10; + static const unsigned MaxMessageSize = 1400; // safe UDP payload in an Ethernet frame + static const unsigned TTLShort = 15; // seconds + static const unsigned TTLLong = 4500; + static const unsigned TTLDelete = 0; + struct TService + { + CString ServiceName; + CString ServiceType; + u16 usServicePort; + unsigned nTextRecords; + CString *ppText[MaxTextRecords]; + }; + boolean SendResponse (TService *pService, boolean bDelete); + // Helpers for writing to buffer + void PutByte (u8 uchValue); + void PutWord (u16 usValue); + void PutDWord (u32 nValue); + void PutString (const char *pValue); + void PutCompressedString (const u8 *pWritePtr); + void PutDNSName (const char *pValue); + void PutIPAddress (const CIPAddress &rValue); + void ReserveDataLength (void); + void SetDataLength (void); +private: + CNetSubSystem *m_pNet; + CPtrList m_ServiceList; + CMutex m_Mutex; + CSocket *m_pSocket; + boolean m_bRunning; + CSynchronizationEvent m_Event; + u8 m_Buffer[MaxMessageSize]; + u8 *m_pWritePtr; + u8 *m_pDataLen; +}; +#endif \ No newline at end of file