diff --git a/.gitignore b/.gitignore index 93bd001..6ad1469 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ sdcard *.swp *.swo -CMSIS_5/ -Synth_Dexed/ -circle-stdlib/ \ No newline at end of file +CMSIS_5/** +Synth_Dexed/** +circle-stdlib/** +.vscode/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 064ffe4..a70e358 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,12 @@ [submodule "circle-stdlib"] path = circle-stdlib url = https://github.com/smuehlst/circle-stdlib + ignore = all [submodule "Synth_Dexed"] path = Synth_Dexed url = https://codeberg.org/dcoredump/Synth_Dexed.git + ignore = all [submodule "CMSIS_5"] path = CMSIS_5 url = https://github.com/ARM-software/CMSIS_5 + ignore = all diff --git a/build.sh b/build.sh index b69ba6b..2db6d59 100755 --- a/build.sh +++ b/build.sh @@ -20,6 +20,11 @@ if [ "${RPI}" -gt "1" ]; then OPTIONS="${OPTIONS} -o ARM_ALLOW_MULTI_CORE" fi +# For wireless access +if [ "${RPI}" == "3" ]; then + OPTIONS="${OPTIONS} -o USE_SDHOST" +fi + # USB Vendor and Device ID for use with USB Gadget Mode source USBID.sh if [ "${USB_VID}" ] ; then @@ -39,6 +44,11 @@ make -j cd libs/circle/addon/display/ make clean || true make -j + +cd ../wlan/ +make clean || true +make -j + cd ../sensor/ make clean || true make -j diff --git a/src/Makefile b/src/Makefile index 540ae68..fa4ab10 100644 --- a/src/Makefile +++ b/src/Makefile @@ -5,11 +5,13 @@ CIRCLE_STDLIB_DIR = ../circle-stdlib SYNTH_DEXED_DIR = ../Synth_Dexed/src CMSIS_DIR = ../CMSIS_5/CMSIS +NET_DIR = ./net OBJS = main.o kernel.o minidexed.o config.o userinterface.o uimenu.o \ - mididevice.o midikeyboard.o serialmididevice.o pckeyboard.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 + effect_compressor.o effect_platervbstereo.o uibuttons.o midipin.o \ + net/ftpdaemon.o net/ftpworker.o net/applemidi.o net/udpmidi.o OPTIMIZE = -O3 diff --git a/src/Rules.mk b/src/Rules.mk index 2ebc132..d86f51a 100644 --- a/src/Rules.mk +++ b/src/Rules.mk @@ -11,7 +11,8 @@ include $(CIRCLEHOME)/Rules.mk INCLUDE += \ -I $(CIRCLE_STDLIB_DIR)/include \ - -I $(NEWLIBDIR)/include + -I $(NEWLIBDIR)/include \ + -I $(NET_DIR) LIBS += \ $(NEWLIBDIR)/lib/libm.a \ @@ -28,6 +29,11 @@ LIBS += \ $(CIRCLEHOME)/addon/fatfs/libfatfs.a \ $(CIRCLEHOME)/lib/fs/libfs.a \ $(CIRCLEHOME)/lib/sched/libsched.a \ - $(CIRCLEHOME)/lib/libcircle.a + $(CIRCLEHOME)/lib/libcircle.a \ + $(CIRCLEHOME)/addon/wlan/hostap/wpa_supplicant/libwpa_supplicant.a \ + $(CIRCLEHOME)/addon/wlan/libwlan.a \ + $(CIRCLEHOME)/lib/net/libnet.a + +EXTRACLEAN += $(NET_DIR)/*.d $(NET_DIR)/*.o -include $(DEPS) diff --git a/src/circle_stdlib_app.h b/src/circle_stdlib_app.h index 8a69c82..bf4534a 100644 --- a/src/circle_stdlib_app.h +++ b/src/circle_stdlib_app.h @@ -223,6 +223,7 @@ protected: CEMMCDevice mEMMC; FATFS mFileSystem; CConsole mConsole; + CScheduler mScheduler; }; /** diff --git a/src/config.cpp b/src/config.cpp index 5214713..70c76f3 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -193,6 +193,16 @@ void CConfig::Load (void) m_bProfileEnabled = m_Properties.GetNumber ("ProfileEnabled", 0) != 0; m_bPerformanceSelectToLoad = m_Properties.GetNumber ("PerformanceSelectToLoad", 1) != 0; m_bPerformanceSelectChannel = m_Properties.GetNumber ("PerformanceSelectChannel", 0); + + // Network + m_bNetworkEnabled = m_Properties.GetNumber ("NetworkEnabled", 0) != 0; + m_bNetworkDHCP = m_Properties.GetNumber ("NetworkDHCP", 0) != 0; + m_NetworkType = m_Properties.GetString ("NetworkType", "wifi"); + m_NetworkHostname = m_Properties.GetString ("NetworkHostname", "minidexed"); + m_INetworkIPAddress = m_Properties.GetIPAddress("NetworkIPAddress") != 0; + m_INetworkSubnetMask = m_Properties.GetIPAddress("NetworkSubnetMask") != 0; + m_INetworkDefaultGateway = m_Properties.GetIPAddress("NetworkDefaultGateway") != 0; + m_INetworkDNSServer = m_Properties.GetIPAddress("NetworkDNSServer") != 0; } unsigned CConfig::GetToneGenerators (void) const @@ -680,3 +690,44 @@ unsigned CConfig::GetPerformanceSelectChannel (void) const { return m_bPerformanceSelectChannel; } + +// Network +bool CConfig::GetNetworkEnabled (void) const +{ + return m_bNetworkEnabled; +} + +bool CConfig::GetNetworkDHCP (void) const +{ + return m_bNetworkDHCP; +} + +const char *CConfig::GetNetworkType (void) const +{ + return m_NetworkType.c_str(); +} + +const char *CConfig::GetNetworkHostname (void) const +{ + return m_NetworkHostname.c_str(); +} + +CIPAddress CConfig::GetNetworkIPAddress (void) const +{ + return m_INetworkIPAddress; +} + +CIPAddress CConfig::GetNetworkSubnetMask (void) const +{ + return m_INetworkSubnetMask; +} + +CIPAddress CConfig::GetNetworkDefaultGateway (void) const +{ + return m_INetworkDefaultGateway; +} + +CIPAddress CConfig::GetNetworkDNSServer (void) const +{ + return m_INetworkDNSServer; +} \ No newline at end of file diff --git a/src/config.h b/src/config.h index a454d90..3103fca 100644 --- a/src/config.h +++ b/src/config.h @@ -23,6 +23,7 @@ #ifndef _config_h #define _config_h +#include #include #include #include @@ -232,6 +233,16 @@ public: bool GetPerformanceSelectToLoad (void) const; unsigned GetPerformanceSelectChannel (void) const; + // Network + bool GetNetworkEnabled (void) const; + const char *GetNetworkType (void) const; + bool GetNetworkDHCP (void) const; + const char *GetNetworkHostname (void) const; + CIPAddress GetNetworkIPAddress (void) const; + CIPAddress GetNetworkSubnetMask (void) const; + CIPAddress GetNetworkDefaultGateway (void) const; + CIPAddress GetNetworkDNSServer (void) const; + private: CPropertiesFatFsFile m_Properties; @@ -339,6 +350,16 @@ private: bool m_bProfileEnabled; bool m_bPerformanceSelectToLoad; unsigned m_bPerformanceSelectChannel; + + // Network + bool m_bNetworkEnabled; + bool m_bNetworkDHCP; + std::string m_NetworkType; + std::string m_NetworkHostname; + CIPAddress m_INetworkIPAddress; + CIPAddress m_INetworkSubnetMask; + CIPAddress m_INetworkDefaultGateway; + CIPAddress m_INetworkDNSServer; }; #endif diff --git a/src/kernel.cpp b/src/kernel.cpp index 446747b..c6c86d9 100644 --- a/src/kernel.cpp +++ b/src/kernel.cpp @@ -25,12 +25,15 @@ #include #include "usbminidexedmidigadget.h" +#define NET_DEVICE_TYPE NetDeviceTypeWLAN // or: NetDeviceTypeWLAN + LOGMODULE ("kernel"); CKernel *CKernel::s_pThis = 0; CKernel::CKernel (void) -: CStdlibAppStdio ("minidexed"), +: + CStdlibAppStdio ("minidexed"), m_Config (&mFileSystem), m_GPIOManager (&mInterrupt), m_I2CMaster (CMachineInfo::Get ()->GetDevice (DeviceI2CMaster), TRUE), diff --git a/src/kernel.h b/src/kernel.h index efe5f4f..25ab3ed 100644 --- a/src/kernel.h +++ b/src/kernel.h @@ -26,6 +26,7 @@ #include #include #include +#include #include "config.h" #include "minidexed.h" diff --git a/src/mididevice.cpp b/src/mididevice.cpp index f2b51de..8ea5600 100644 --- a/src/mididevice.cpp +++ b/src/mididevice.cpp @@ -273,7 +273,6 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign { if ((ucChannel == nPerfCh) || (nPerfCh == OmniMode)) { - //printf("Performance Select Channel %d\n", nPerfCh); m_pSynthesizer->ProgramChangePerformance (pMessage[1]); } } @@ -328,7 +327,6 @@ void CMIDIDevice::MIDIMessageHandler (const u8 *pMessage, size_t nLength, unsign { break; } - m_pSynthesizer->keyup (pMessage[1], nTG); break; diff --git a/src/minidexed.cpp b/src/minidexed.cpp index 2e45f46..ceb3491 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -28,6 +28,11 @@ #include #include +const char WLANFirmwarePath[] = "SD:firmware/"; +const char WLANConfigFile[] = "SD:wpa_supplicant.conf"; +#define FTPUSERNAME "admin" +#define FTPPASSWORD "admin" + LOGMODULE ("minidexed"); CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, @@ -51,6 +56,12 @@ CMiniDexed::CMiniDexed (CConfig *pConfig, CInterruptSystem *pInterrupt, m_GetChunkTimer ("GetChunk", 1000000U * pConfig->GetChunkSize ()/2 / pConfig->GetSampleRate ()), m_bProfileEnabled (m_pConfig->GetProfileEnabled ()), + m_pNet(nullptr), + m_pNetDevice(nullptr), + m_WLAN(WLANFirmwarePath), + m_WPASupplicant(WLANConfigFile), + m_bNetworkReady(false), + m_UDPMIDI (this, pConfig, &m_UI), m_bSavePerformance (false), m_bSavePerformanceNewFile (false), m_bSetNewPerformance (false), @@ -344,12 +355,14 @@ bool CMiniDexed::Initialize (void) return false; } #endif - + InitNetwork(); + return true; } void CMiniDexed::Process (bool bPlugAndPlayUpdated) { + CScheduler* const pScheduler = CScheduler::Get(); #ifndef ARM_ALLOW_MULTI_CORE ProcessSound (); #endif @@ -418,6 +431,9 @@ void CMiniDexed::Process (bool bPlugAndPlayUpdated) { m_GetChunkTimer.Dump (); } + UpdateNetwork(); + // Allow other tasks to run + pScheduler->Yield(); } #ifdef ARM_ALLOW_MULTI_CORE @@ -750,6 +766,7 @@ void CMiniDexed::SetMIDIChannel (uint8_t uchChannel, unsigned nTG) { m_SerialMIDI.SetChannel (uchChannel, nTG); } + m_UDPMIDI.SetChannel (uchChannel, nTG); #ifdef ARM_ALLOW_MULTI_CORE /* This doesn't appear to be used anywhere... @@ -2164,3 +2181,100 @@ unsigned CMiniDexed::getModController (unsigned controller, unsigned parameter, } } + +void CMiniDexed::UpdateNetwork() +{ + //CNetSubSystem* const pNet = CNetSubSystem::Get(); + if (!m_pNet) + return; + + bool bNetIsRunning = m_pNet->IsRunning(); + bNetIsRunning &= m_WPASupplicant.IsConnected(); + + if (!m_bNetworkReady && bNetIsRunning) + { + m_bNetworkReady = true; + CString IPString; + m_pNet->GetConfig()->GetIPAddress()->Format(&IPString); + + LOGNOTE("Network up and running at: %s", static_cast(IPString)); + + m_UDPMIDI.Initialize(); + + m_pFTPDaemon = new CFTPDaemon(FTPUSERNAME, FTPPASSWORD); + + if (!m_pFTPDaemon->Initialize()) + { + LOGERR("Failed to init FTP daemon"); + delete m_pFTPDaemon; + m_pFTPDaemon = nullptr; + } + else + { + LOGNOTE("FTP daemon initialized"); + } + + m_UI.DisplayWrite ("IP", + "Network", + IPString, + 0, + 1); + } + else if (m_bNetworkReady && !bNetIsRunning) + { + m_bNetworkReady = false; + LOGNOTE("Network disconnected."); + + } +} + +bool CMiniDexed::InitNetwork() +{ + assert(m_pNet == nullptr); + + TNetDeviceType NetDeviceType = NetDeviceTypeUnknown; + + if (m_pConfig->GetNetworkEnabled () && (strcmp(m_pConfig->GetNetworkType(), "wifi") == 0)) + { + LOGNOTE("Initializing Wi-Fi"); + + if (m_WLAN.Initialize() && m_WPASupplicant.Initialize()) + { + LOGNOTE("wlan and wpasupplicant initialized"); + NetDeviceType = NetDeviceTypeWLAN; + + } + else + LOGERR("Failed to initialize Wi-Fi"); + } + else if (m_pConfig->GetNetworkEnabled () && (strcmp(m_pConfig->GetNetworkType(), "ethernet") == 0)) + { + LOGNOTE("Initializing Ethernet"); + NetDeviceType = NetDeviceTypeEthernet; + } + + if (NetDeviceType != NetDeviceTypeUnknown) + { + if (m_pConfig->GetNetworkDHCP()) + m_pNet = new CNetSubSystem(0, 0, 0, 0, m_pConfig->GetNetworkHostname(), NetDeviceType); + else + m_pNet = new CNetSubSystem( + m_pConfig->GetNetworkIPAddress().Get(), + m_pConfig->GetNetworkSubnetMask().Get(), + m_pConfig->GetNetworkDefaultGateway().Get(), + m_pConfig->GetNetworkDNSServer().Get(), + m_pConfig->GetNetworkHostname(), + NetDeviceType + ); + + if (!m_pNet->Initialize()) + { + LOGERR("Failed to initialize network subsystem"); + delete m_pNet; + m_pNet = nullptr; + } + + m_pNetDevice = CNetDevice::GetNetDevice(NetDeviceType); + } + return m_pNet != nullptr; +} \ No newline at end of file diff --git a/src/minidexed.h b/src/minidexed.h index 69dcf9c..2d73e74 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -39,11 +39,17 @@ #include #include #include +#include +#include +#include +#include #include #include "common.h" #include "effect_mixer.hpp" #include "effect_platervbstereo.h" #include "effect_compressor.h" +#include "udpmididevice.h" +#include "net/ftpdaemon.h" class CMiniDexed #ifdef ARM_ALLOW_MULTI_CORE @@ -61,7 +67,6 @@ public: #ifdef ARM_ALLOW_MULTI_CORE void Run (unsigned nCore); #endif - CSysExFileLoader *GetSysExFileLoader (void); CPerformanceConfig *GetPerformanceConfig (void); @@ -227,12 +232,15 @@ public: bool DoSavePerformance (void); void setMasterVolume (float32_t vol); + bool InitNetwork(); + void UpdateNetwork(); private: int16_t ApplyNoteLimits (int16_t pitch, unsigned nTG); // returns < 0 to ignore note uint8_t m_uchOPMask[CConfig::AllToneGenerators]; void LoadPerformanceParameters(void); void ProcessSound (void); + const char* GetNetworkDeviceShortName() const; #ifdef ARM_ALLOW_MULTI_CORE enum TCoreStatus @@ -323,6 +331,15 @@ private: CSpinLock m_ReverbSpinLock; + // Network + CNetSubSystem* m_pNet; + CNetDevice* m_pNetDevice; + CBcm4343Device m_WLAN; + CWPASupplicant m_WPASupplicant; + bool m_bNetworkReady; + CUDPMIDIDevice m_UDPMIDI; + CFTPDaemon* m_pFTPDaemon; + bool m_bSavePerformance; bool m_bSavePerformanceNewFile; bool m_bSetNewPerformance; @@ -335,6 +352,9 @@ private: bool m_bLoadPerformanceBusy; bool m_bLoadPerformanceBankBusy; bool m_bSaveAsDeault; + + + }; #endif diff --git a/src/net/applemidi.cpp b/src/net/applemidi.cpp new file mode 100644 index 0000000..3c96629 --- /dev/null +++ b/src/net/applemidi.cpp @@ -0,0 +1,874 @@ +// +// applemidi.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#include +#include +#include +#include +#include +#include +#include + +#include "applemidi.h" +#include "byteorder.h" + +// #define APPLEMIDI_DEBUG + +LOGMODULE("applemidi"); + +constexpr u16 ControlPort = 5004; +constexpr u16 MIDIPort = ControlPort + 1; + +constexpr u16 AppleMIDISignature = 0xFFFF; +constexpr u8 AppleMIDIVersion = 2; + +constexpr u8 RTPMIDIPayloadType = 0x61; +constexpr u8 RTPMIDIVersion = 2; + +// Arbitrary value +constexpr size_t MaxNameLength = 256; + +// Timeout period for invitation (5 seconds in 100 microsecond units) +constexpr unsigned int InvitationTimeout = 5 * 10000; + +// Timeout period for sync packets (60 seconds in 100 microsecond units) +constexpr unsigned int SyncTimeout = 60 * 10000; + +// Receiver feedback packet frequency (1 second in 100 microsecond units) +constexpr unsigned int ReceiverFeedbackPeriod = 1 * 10000; + +constexpr u16 CommandWord(const char Command[2]) { return Command[0] << 8 | Command[1]; } + +enum TAppleMIDICommand : u16 +{ + Invitation = CommandWord("IN"), + InvitationAccepted = CommandWord("OK"), + InvitationRejected = CommandWord("NO"), + Sync = CommandWord("CK"), + ReceiverFeedback = CommandWord("RS"), + EndSession = CommandWord("BY"), +}; + +struct TAppleMIDISession +{ + u16 nSignature; + u16 nCommand; + u32 nVersion; + u32 nInitiatorToken; + u32 nSSRC; + char Name[MaxNameLength]; +} +PACKED; + +// The Name field is optional +constexpr size_t NamelessSessionPacketSize = sizeof(TAppleMIDISession) - sizeof(TAppleMIDISession::Name); + +struct TAppleMIDISync +{ + u16 nSignature; + u16 nCommand; + u32 nSSRC; + u8 nCount; + u8 Padding[3]; + u64 Timestamps[3]; +} +PACKED; + +struct TAppleMIDIReceiverFeedback +{ + u16 nSignature; + u16 nCommand; + u32 nSSRC; + u32 nSequence; +} +PACKED; + +struct TRTPMIDI +{ + u16 nFlags; + u16 nSequence; + u32 nTimestamp; + u32 nSSRC; +} +PACKED; + +u64 GetSyncClock() +{ + static const u64 nStartTime = CTimer::GetClockTicks(); + const u64 nMicrosSinceEpoch = CTimer::GetClockTicks(); + + // Units of 100 microseconds + return (nMicrosSinceEpoch - nStartTime ) / 100; +} + +bool ParseInvitationPacket(const u8* pBuffer, size_t nSize, TAppleMIDISession* pOutPacket) +{ + const TAppleMIDISession* const pInPacket = reinterpret_cast(pBuffer); + + if (nSize < NamelessSessionPacketSize) + return false; + + const u16 nSignature = ntohs(pInPacket->nSignature); + if (nSignature != AppleMIDISignature) + return false; + + const u16 nCommand = ntohs(pInPacket->nCommand); + if (nCommand != Invitation) + return false; + + const u32 nVersion = ntohl(pInPacket->nVersion); + if (nVersion != AppleMIDIVersion) + return false; + + pOutPacket->nSignature = nSignature; + pOutPacket->nCommand = nCommand; + pOutPacket->nVersion = nVersion; + pOutPacket->nInitiatorToken = ntohl(pInPacket->nInitiatorToken); + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + + if (nSize > NamelessSessionPacketSize) + strncpy(pOutPacket->Name, pInPacket->Name, sizeof(pOutPacket->Name)); + else + strncpy(pOutPacket->Name, "", sizeof(pOutPacket->Name)); + + return true; +} + +bool ParseEndSessionPacket(const u8* pBuffer, size_t nSize, TAppleMIDISession* pOutPacket) +{ + const TAppleMIDISession* const pInPacket = reinterpret_cast(pBuffer); + + if (nSize < NamelessSessionPacketSize) + return false; + + const u16 nSignature = ntohs(pInPacket->nSignature); + if (nSignature != AppleMIDISignature) + return false; + + const u16 nCommand = ntohs(pInPacket->nCommand); + if (nCommand != EndSession) + return false; + + const u32 nVersion = ntohl(pInPacket->nVersion); + if (nVersion != AppleMIDIVersion) + return false; + + pOutPacket->nSignature = nSignature; + pOutPacket->nCommand = nCommand; + pOutPacket->nVersion = nVersion; + pOutPacket->nInitiatorToken = ntohl(pInPacket->nInitiatorToken); + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + + return true; +} + +bool ParseSyncPacket(const u8* pBuffer, size_t nSize, TAppleMIDISync* pOutPacket) +{ + const TAppleMIDISync* const pInPacket = reinterpret_cast(pBuffer); + + if (nSize < sizeof(TAppleMIDISync)) + return false; + + const u32 nSignature = ntohs(pInPacket->nSignature); + if (nSignature != AppleMIDISignature) + return false; + + const u32 nCommand = ntohs(pInPacket->nCommand); + if (nCommand != Sync) + return false; + + pOutPacket->nSignature = nSignature; + pOutPacket->nCommand = nCommand; + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + pOutPacket->nCount = pInPacket->nCount; + pOutPacket->Timestamps[0] = ntohll(pInPacket->Timestamps[0]); + pOutPacket->Timestamps[1] = ntohll(pInPacket->Timestamps[1]); + pOutPacket->Timestamps[2] = ntohll(pInPacket->Timestamps[2]); + + return true; +} + +u8 ParseMIDIDeltaTime(const u8* pBuffer) +{ + u8 nLength = 0; + u32 nDeltaTime = 0; + + while (nLength < 4) + { + nDeltaTime <<= 7; + nDeltaTime |= pBuffer[nLength] & 0x7F; + + // Upper bit not set; end of timestamp + if ((pBuffer[nLength++] & (1 << 7)) == 0) + break; + } + + return nLength; +} + +size_t ParseSysExCommand(const u8* pBuffer, size_t nSize, CAppleMIDIHandler* pHandler) +{ + size_t nBytesParsed = 1; + const u8 nHead = pBuffer[0]; + u8 nTail = 0; + + while (nBytesParsed < nSize && !(nTail == 0xF0 || nTail == 0xF7 || nTail == 0xF4)) + nTail = pBuffer[nBytesParsed++]; + + size_t nReceiveLength = nBytesParsed; + + // First segmented SysEx packet + if (nHead == 0xF0 && nTail == 0xF0) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received segmented SysEx (first)"); +#endif + --nReceiveLength; + } + + // Middle segmented SysEx packet + else if (nHead == 0xF7 && nTail == 0xF0) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received segmented SysEx (middle)"); +#endif + ++pBuffer; + nBytesParsed -= 2; + } + + // Last segmented SysEx packet + else if (nHead == 0xF7 && nTail == 0xF7) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received segmented SysEx (last)"); +#endif + ++pBuffer; + --nReceiveLength; + } + + // Cancelled segmented SysEx packet + else if (nHead == 0xF7 && nTail == 0xF4) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Received cancelled SysEx"); +#endif + nReceiveLength = 1; + } + +#ifdef APPLEMIDI_DEBUG + else + { + LOGNOTE("Received complete SysEx"); + } +#endif + + pHandler->OnAppleMIDIDataReceived(pBuffer, nReceiveLength); + + return nBytesParsed; +} + +size_t ParseMIDICommand(const u8* pBuffer, size_t nSize, u8& nRunningStatus, CAppleMIDIHandler* pHandler) +{ + size_t nBytesParsed = 0; + u8 nByte = pBuffer[0]; + + // System Real-Time message - single byte, handle immediately + // Can appear anywhere in the stream, even in between status/data bytes + if (nByte >= 0xF8) + { + // Ignore undefined System Real-Time + if (nByte != 0xF9 && nByte != 0xFD) + pHandler->OnAppleMIDIDataReceived(&nByte, 1); + + return 1; + } + + // Is it a status byte? + if (nByte & 0x80) + { + // Update running status if non Real-Time System status + if (nByte < 0xF0) + nRunningStatus = nByte; + else + nRunningStatus = 0; + + ++nBytesParsed; + } + else + { + // First byte not a status byte and no running status - invalid + if (!nRunningStatus) + return 0; + + // Use running status + nByte = nRunningStatus; + } + + // Channel messages + if (nByte < 0xF0) + { + // How many data bytes? + switch (nByte & 0xF0) + { + case 0x80: // Note off + case 0x90: // Note on + case 0xA0: // Polyphonic key pressure/aftertouch + case 0xB0: // Control change + case 0xE0: // Pitch bend + nBytesParsed += 2; + break; + + case 0xC0: // Program change + case 0xD0: // Channel pressure/aftertouch + nBytesParsed += 1; + break; + } + + // Handle command + pHandler->OnAppleMIDIDataReceived(pBuffer, nBytesParsed); + return nBytesParsed; + } + + // System common commands + switch (nByte) + { + case 0xF0: // Start of System Exclusive + case 0xF7: // End of Exclusive + return ParseSysExCommand(pBuffer, nSize, pHandler); + + case 0xF1: // MIDI Time Code Quarter Frame + case 0xF3: // Song Select + ++nBytesParsed; + break; + + case 0xF2: // Song Position Pointer + nBytesParsed += 2; + break; + } + + pHandler->OnAppleMIDIDataReceived(pBuffer, nBytesParsed); + return nBytesParsed; +} + +bool ParseMIDICommandSection(const u8* pBuffer, size_t nSize, CAppleMIDIHandler* pHandler) +{ + // Must have at least a header byte and a single status byte + if (nSize < 2) + return false; + + size_t nMIDICommandsProcessed = 0; + size_t nBytesRemaining = nSize - 1; + u8 nRunningStatus = 0; + + const u8 nMIDIHeader = pBuffer[0]; + const u8* pMIDICommands = pBuffer + 1; + + // Lower 4 bits of the header is length + u16 nMIDICommandLength = nMIDIHeader & 0x0F; + + // If B flag is set, length value is 12 bits + if (nMIDIHeader & (1 << 7)) + { + nMIDICommandLength <<= 8; + nMIDICommandLength |= pMIDICommands[0]; + ++pMIDICommands; + --nBytesRemaining; + } + + if (nMIDICommandLength > nBytesRemaining) + { + LOGERR("Invalid MIDI command length"); + return false; + } + + // Begin decoding the command list + while (nMIDICommandLength) + { + // If Z flag is set, first list entry is a delta time + if (nMIDICommandsProcessed || nMIDIHeader & (1 << 5)) + { + const u8 nBytesParsed = ParseMIDIDeltaTime(pMIDICommands); + nMIDICommandLength -= nBytesParsed; + pMIDICommands += nBytesParsed; + } + + if (nMIDICommandLength) + { + const size_t nBytesParsed = ParseMIDICommand(pMIDICommands, nMIDICommandLength, nRunningStatus, pHandler); + nMIDICommandLength -= nBytesParsed; + pMIDICommands += nBytesParsed; + ++nMIDICommandsProcessed; + } + } + + return true; +} + +bool ParseMIDIPacket(const u8* pBuffer, size_t nSize, TRTPMIDI* pOutPacket, CAppleMIDIHandler* pHandler) +{ + assert(pHandler != nullptr); + + const TRTPMIDI* const pInPacket = reinterpret_cast(pBuffer); + const u16 nRTPFlags = ntohs(pInPacket->nFlags); + + // Check size (RTP-MIDI header plus MIDI command section header) + if (nSize < sizeof(TRTPMIDI) + 1) + return false; + + // Check version + if (((nRTPFlags >> 14) & 0x03) != RTPMIDIVersion) + return false; + + // Ensure no CSRC identifiers + if (((nRTPFlags >> 8) & 0x0F) != 0) + return false; + + // Check payload type + if ((nRTPFlags & 0xFF) != RTPMIDIPayloadType) + return false; + + pOutPacket->nFlags = nRTPFlags; + pOutPacket->nSequence = ntohs(pInPacket->nSequence); + pOutPacket->nTimestamp = ntohl(pInPacket->nTimestamp); + pOutPacket->nSSRC = ntohl(pInPacket->nSSRC); + + // RTP-MIDI variable-length header + const u8* const pMIDICommandSection = pBuffer + sizeof(TRTPMIDI); + size_t nRemaining = nSize - sizeof(TRTPMIDI); + return ParseMIDICommandSection(pMIDICommandSection, nRemaining, pHandler); +} + +CAppleMIDIParticipant::CAppleMIDIParticipant(CBcmRandomNumberGenerator* pRandom, CAppleMIDIHandler* pHandler) + : CTask(TASK_STACK_SIZE, true), + + m_pRandom(pRandom), + + m_pControlSocket(nullptr), + m_pMIDISocket(nullptr), + + m_nForeignControlPort(0), + m_nForeignMIDIPort(0), + m_nInitiatorControlPort(0), + m_nInitiatorMIDIPort(0), + m_ControlBuffer{0}, + m_MIDIBuffer{0}, + + m_nControlResult(0), + m_nMIDIResult(0), + + m_pHandler(pHandler), + + m_State(TState::ControlInvitation), + + m_nInitiatorToken(0), + m_nInitiatorSSRC(0), + m_nSSRC(0), + m_nLastMIDISequenceNumber(0), + + m_nOffsetEstimate(0), + m_nLastSyncTime(0), + + m_nSequence(0), + m_nLastFeedbackSequence(0), + m_nLastFeedbackTime(0) +{ +} + +CAppleMIDIParticipant::~CAppleMIDIParticipant() +{ + if (m_pControlSocket) + delete m_pControlSocket; + + if (m_pMIDISocket) + delete m_pMIDISocket; +} + +bool CAppleMIDIParticipant::Initialize() +{ + assert(m_pControlSocket == nullptr); + assert(m_pMIDISocket == nullptr); + + CNetSubSystem* const pNet = CNetSubSystem::Get(); + + if ((m_pControlSocket = new CSocket(pNet, IPPROTO_UDP)) == nullptr) + return false; + + if ((m_pMIDISocket = new CSocket(pNet, IPPROTO_UDP)) == nullptr) + return false; + + if (m_pControlSocket->Bind(ControlPort) != 0) + { + LOGERR("Couldn't bind to port %d", ControlPort); + return false; + } + + if (m_pMIDISocket->Bind(MIDIPort) != 0) + { + LOGERR("Couldn't bind to port %d", MIDIPort); + return false; + } + + // We started as a suspended task; run now that initialization is successful + Start(); + + return true; +} + +void CAppleMIDIParticipant::Run() +{ + assert(m_pControlSocket != nullptr); + assert(m_pMIDISocket != nullptr); + + CScheduler* const pScheduler = CScheduler::Get(); + + while (true) + { + if ((m_nControlResult = m_pControlSocket->ReceiveFrom(m_ControlBuffer, sizeof(m_ControlBuffer), MSG_DONTWAIT, &m_ForeignControlIPAddress, &m_nForeignControlPort)) < 0) + LOGERR("Control socket receive error: %d", m_nControlResult); + + if ((m_nMIDIResult = m_pMIDISocket->ReceiveFrom(m_MIDIBuffer, sizeof(m_MIDIBuffer), MSG_DONTWAIT, &m_ForeignMIDIIPAddress, &m_nForeignMIDIPort)) < 0) + LOGERR("MIDI socket receive error: %d", m_nMIDIResult); + + switch (m_State) + { + case TState::ControlInvitation: + ControlInvitationState(); + break; + + case TState::MIDIInvitation: + MIDIInvitationState(); + break; + + case TState::Connected: + ConnectedState(); + break; + } + + // Allow other tasks to run + pScheduler->Yield(); + } +} + +void CAppleMIDIParticipant::ControlInvitationState() +{ + TAppleMIDISession SessionPacket; + + if (m_nControlResult == 0) + return; + + if (!ParseInvitationPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { + LOGERR("Unexpected packet"); + return; + } + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- Control invitation"); +#endif + + // Store initiator details + m_InitiatorIPAddress.Set(m_ForeignControlIPAddress); + m_nInitiatorControlPort = m_nForeignControlPort; + m_nInitiatorToken = SessionPacket.nInitiatorToken; + m_nInitiatorSSRC = SessionPacket.nSSRC; + + // Generate random SSRC and accept + m_nSSRC = m_pRandom->GetNumber(); + if (!SendAcceptInvitationPacket(m_pControlSocket, &m_InitiatorIPAddress, m_nInitiatorControlPort)) + { + LOGERR("Couldn't accept control invitation"); + return; + } + + m_nLastSyncTime = GetSyncClock(); + m_State = TState::MIDIInvitation; +} + +void CAppleMIDIParticipant::MIDIInvitationState() +{ + TAppleMIDISession SessionPacket; + + if (m_nControlResult > 0) + { + if (ParseInvitationPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { + // Unexpected peer; reject invitation + if (m_ForeignControlIPAddress != m_InitiatorIPAddress || m_nForeignControlPort != m_nInitiatorControlPort) + SendRejectInvitationPacket(m_pControlSocket, &m_ForeignControlIPAddress, m_nForeignControlPort, SessionPacket.nInitiatorToken); + else + LOGERR("Unexpected packet"); + } + } + + if (m_nMIDIResult > 0) + { + if (!ParseInvitationPacket(m_MIDIBuffer, m_nMIDIResult, &SessionPacket)) + { + LOGERR("Unexpected packet"); + return; + } + + // Unexpected peer; reject invitation + if (m_ForeignMIDIIPAddress != m_InitiatorIPAddress) + { + SendRejectInvitationPacket(m_pMIDISocket, &m_ForeignMIDIIPAddress, m_nForeignMIDIPort, SessionPacket.nInitiatorToken); + return; + } + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- MIDI invitation"); +#endif + + m_nInitiatorMIDIPort = m_nForeignMIDIPort; + + if (SendAcceptInvitationPacket(m_pMIDISocket, &m_InitiatorIPAddress, m_nInitiatorMIDIPort)) + { + CString IPAddressString; + m_InitiatorIPAddress.Format(&IPAddressString); + LOGNOTE("Connection to %s (%s) established", SessionPacket.Name, static_cast(IPAddressString)); + m_nLastSyncTime = GetSyncClock(); + m_State = TState::Connected; + m_pHandler->OnAppleMIDIConnect(&m_InitiatorIPAddress, SessionPacket.Name); + } + else + { + LOGERR("Couldn't accept MIDI invitation"); + Reset(); + } + } + + // Timeout + else if ((GetSyncClock() - m_nLastSyncTime) > InvitationTimeout) + { + LOGERR("MIDI port invitation timed out"); + Reset(); + } +} + +void CAppleMIDIParticipant::ConnectedState() +{ + TAppleMIDISession SessionPacket; + TRTPMIDI MIDIPacket; + TAppleMIDISync SyncPacket; + + if (m_nControlResult > 0) + { + if (ParseEndSessionPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- End session"); +#endif + + if (m_ForeignControlIPAddress == m_InitiatorIPAddress && + m_nForeignControlPort == m_nInitiatorControlPort && + SessionPacket.nSSRC == m_nInitiatorSSRC) + { + LOGNOTE("Initiator ended session"); + m_pHandler->OnAppleMIDIDisconnect(&m_InitiatorIPAddress, SessionPacket.Name); + Reset(); + return; + } + } + else if (ParseInvitationPacket(m_ControlBuffer, m_nControlResult, &SessionPacket)) + { + // Unexpected peer; reject invitation + if (m_ForeignControlIPAddress != m_InitiatorIPAddress || m_nForeignControlPort != m_nInitiatorControlPort) + SendRejectInvitationPacket(m_pControlSocket, &m_ForeignControlIPAddress, m_nForeignControlPort, SessionPacket.nInitiatorToken); + else + LOGERR("Unexpected packet"); + } + } + + if (m_nMIDIResult > 0) + { + if (m_ForeignMIDIIPAddress != m_InitiatorIPAddress || m_nForeignMIDIPort != m_nInitiatorMIDIPort) + LOGERR("Unexpected packet"); + else if (ParseMIDIPacket(m_MIDIBuffer, m_nMIDIResult, &MIDIPacket, m_pHandler)) + m_nSequence = MIDIPacket.nSequence; + else if (ParseSyncPacket(m_MIDIBuffer, m_nMIDIResult, &SyncPacket)) + { +#ifdef APPLEMIDI_DEBUG + LOGNOTE("<-- Sync %d", SyncPacket.nCount); +#endif + + if (SyncPacket.nSSRC == m_nInitiatorSSRC && (SyncPacket.nCount == 0 || SyncPacket.nCount == 2)) + { + if (SyncPacket.nCount == 0) + SendSyncPacket(SyncPacket.Timestamps[0], GetSyncClock()); + else if (SyncPacket.nCount == 2) + { + m_nOffsetEstimate = ((SyncPacket.Timestamps[2] + SyncPacket.Timestamps[0]) / 2) - SyncPacket.Timestamps[1]; +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Offset estimate: %llu", m_nOffsetEstimate); +#endif + } + + m_nLastSyncTime = GetSyncClock(); + } + else + { + LOGERR("Unexpected sync packet"); + } + } + } + + const u64 nTicks = GetSyncClock(); + + if ((nTicks - m_nLastFeedbackTime) > ReceiverFeedbackPeriod) + { + if (m_nSequence != m_nLastFeedbackSequence) + { + SendFeedbackPacket(); + m_nLastFeedbackSequence = m_nSequence; + } + m_nLastFeedbackTime = nTicks; + } + + if ((nTicks - m_nLastSyncTime) > SyncTimeout) + { + LOGERR("Initiator timed out"); + Reset(); + } +} + +void CAppleMIDIParticipant::Reset() +{ + m_State = TState::ControlInvitation; + + m_nInitiatorToken = 0; + m_nInitiatorSSRC = 0; + m_nSSRC = 0; + m_nLastMIDISequenceNumber = 0; + + m_nOffsetEstimate = 0; + m_nLastSyncTime = 0; + + m_nSequence = 0; + m_nLastFeedbackSequence = 0; + m_nLastFeedbackTime = 0; +} + +bool CAppleMIDIParticipant::SendPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, const void* pData, size_t nSize) +{ + const int nResult = pSocket->SendTo(pData, nSize, MSG_DONTWAIT, *pIPAddress, nPort); + + if (nResult < 0) + { + LOGERR("Send failure, error code: %d", nResult); + return false; + } + + if (static_cast(nResult) != nSize) + { + LOGERR("Send failure, only %d/%d bytes sent", nResult, nSize); + return false; + } + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("Sent %d bytes to port %d", nResult, nPort); +#endif + + return true; +} + +bool CAppleMIDIParticipant::SendAcceptInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort) +{ + TAppleMIDISession AcceptPacket = + { + htons(AppleMIDISignature), + htons(InvitationAccepted), + htonl(AppleMIDIVersion), + htonl(m_nInitiatorToken), + htonl(m_nSSRC), + {'\0'} + }; + + // TODO: configurable name + strncpy(AcceptPacket.Name, "minidexed", sizeof(AcceptPacket.Name)); + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Accept invitation"); +#endif + + const size_t nSendSize = NamelessSessionPacketSize + strlen(AcceptPacket.Name) + 1; + return SendPacket(pSocket, pIPAddress, nPort, &AcceptPacket, nSendSize); +} + +bool CAppleMIDIParticipant::SendRejectInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, u32 nInitiatorToken) +{ + TAppleMIDISession RejectPacket = + { + htons(AppleMIDISignature), + htons(InvitationRejected), + htonl(AppleMIDIVersion), + htonl(nInitiatorToken), + htonl(m_nSSRC), + {'\0'} + }; + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Reject invitation"); +#endif + + // Send without name + return SendPacket(pSocket, pIPAddress, nPort, &RejectPacket, NamelessSessionPacketSize); +} + +bool CAppleMIDIParticipant::SendSyncPacket(u64 nTimestamp1, u64 nTimestamp2) +{ + const TAppleMIDISync SyncPacket = + { + htons(AppleMIDISignature), + htons(Sync), + htonl(m_nSSRC), + 1, + {0}, + { + htonll(nTimestamp1), + htonll(nTimestamp2), + 0 + } + }; + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Sync 1"); +#endif + + return SendPacket(m_pMIDISocket, &m_InitiatorIPAddress, m_nInitiatorMIDIPort, &SyncPacket, sizeof(SyncPacket)); +} + +bool CAppleMIDIParticipant::SendFeedbackPacket() +{ + const TAppleMIDIReceiverFeedback FeedbackPacket = + { + htons(AppleMIDISignature), + htons(ReceiverFeedback), + htonl(m_nSSRC), + htonl(m_nSequence << 16) + }; + +#ifdef APPLEMIDI_DEBUG + LOGNOTE("--> Feedback"); +#endif + + return SendPacket(m_pControlSocket, &m_InitiatorIPAddress, m_nInitiatorControlPort, &FeedbackPacket, sizeof(FeedbackPacket)); +} \ No newline at end of file diff --git a/src/net/applemidi.h b/src/net/applemidi.h new file mode 100644 index 0000000..3df68ae --- /dev/null +++ b/src/net/applemidi.h @@ -0,0 +1,111 @@ +// +// applemidi.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _applemidi_h +#define _applemidi_h + +#include +#include +#include +#include + +class CAppleMIDIHandler +{ +public: + virtual void OnAppleMIDIDataReceived(const u8* pData, size_t nSize) = 0; + virtual void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) = 0; + virtual void OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) = 0; +}; + +class CAppleMIDIParticipant : protected CTask +{ +public: + CAppleMIDIParticipant(CBcmRandomNumberGenerator* pRandom, CAppleMIDIHandler* pHandler); + virtual ~CAppleMIDIParticipant() override; + + bool Initialize(); + + virtual void Run() override; + +private: + void ControlInvitationState(); + void MIDIInvitationState(); + void ConnectedState(); + void Reset(); + + bool SendPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, const void* pData, size_t nSize); + bool SendAcceptInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort); + bool SendRejectInvitationPacket(CSocket* pSocket, CIPAddress* pIPAddress, u16 nPort, u32 nInitiatorToken); + bool SendSyncPacket(u64 nTimestamp1, u64 nTimestamp2); + bool SendFeedbackPacket(); + + CBcmRandomNumberGenerator* m_pRandom; + + // UDP sockets + CSocket* m_pControlSocket; + CSocket* m_pMIDISocket; + + // Foreign peers + CIPAddress m_ForeignControlIPAddress; + CIPAddress m_ForeignMIDIIPAddress; + u16 m_nForeignControlPort; + u16 m_nForeignMIDIPort; + + // Connected peer + CIPAddress m_InitiatorIPAddress; + u16 m_nInitiatorControlPort; + u16 m_nInitiatorMIDIPort; + + // Socket receive buffers + u8 m_ControlBuffer[FRAME_BUFFER_SIZE]; + u8 m_MIDIBuffer[FRAME_BUFFER_SIZE]; + + int m_nControlResult; + int m_nMIDIResult; + + // Callback handler + CAppleMIDIHandler* m_pHandler; + + // Participant state machine + enum class TState + { + ControlInvitation, + MIDIInvitation, + Connected + }; + + TState m_State; + + u32 m_nInitiatorToken = 0; + u32 m_nInitiatorSSRC = 0; + u32 m_nSSRC = 0; + u32 m_nLastMIDISequenceNumber = 0; + + u64 m_nOffsetEstimate = 0; + u64 m_nLastSyncTime = 0; + + u16 m_nSequence = 0; + u16 m_nLastFeedbackSequence = 0; + u64 m_nLastFeedbackTime = 0; +}; + +#endif \ No newline at end of file diff --git a/src/net/byteorder.h b/src/net/byteorder.h new file mode 100644 index 0000000..5160119 --- /dev/null +++ b/src/net/byteorder.h @@ -0,0 +1,42 @@ +// +// byteorder.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _byteorder_h +#define _byteorder_h + +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define htons(VALUE) (VALUE) +#define htonl(VALUE) (VALUE) +#define htonll(VALUE) (VALUE) +#define ntohs(VALUE) (VALUE) +#define ntohl(VALUE) (VALUE) +#define ntohll(VALUE) (VALUE) +#else +#define htons(VALUE) __builtin_bswap16(VALUE) +#define htonl(VALUE) __builtin_bswap32(VALUE) +#define htonll(VALUE) __builtin_bswap64(VALUE) +#define ntohs(VALUE) __builtin_bswap16(VALUE) +#define ntohl(VALUE) __builtin_bswap32(VALUE) +#define ntohll(VALUE) __builtin_bswap64(VALUE) +#endif + +#endif \ No newline at end of file diff --git a/src/net/ftpdaemon.cpp b/src/net/ftpdaemon.cpp new file mode 100644 index 0000000..0cab51c --- /dev/null +++ b/src/net/ftpdaemon.cpp @@ -0,0 +1,111 @@ +// +// ftpdaemon.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#include +#include +#include +#include +#include + +#include "ftpdaemon.h" +#include "ftpworker.h" + +LOGMODULE("ftpd"); + +constexpr u16 ListenPort = 21; +constexpr u8 MaxConnections = 1; + +CFTPDaemon::CFTPDaemon(const char* pUser, const char* pPassword) + : CTask(TASK_STACK_SIZE, true), + m_pListenSocket(nullptr), + m_pUser(pUser), + m_pPassword(pPassword) +{ +} + +CFTPDaemon::~CFTPDaemon() +{ + if (m_pListenSocket) + delete m_pListenSocket; +} + +bool CFTPDaemon::Initialize() +{ + CNetSubSystem* const pNet = CNetSubSystem::Get(); + + if ((m_pListenSocket = new CSocket(pNet, IPPROTO_TCP)) == nullptr) + return false; + + if (m_pListenSocket->Bind(ListenPort) != 0) + { + LOGERR("Couldn't bind to port %d", ListenPort); + return false; + } + + if (m_pListenSocket->Listen() != 0) + { + LOGERR("Failed to listen on control socket"); + return false; + } + + // We started as a suspended task; run now that initialization is successful + Start(); + + return true; +} + +void CFTPDaemon::Run() +{ + assert(m_pListenSocket != nullptr); + + LOGNOTE("Listener task spawned"); + + while (true) + { + CIPAddress ClientIPAddress; + u16 nClientPort; + + LOGDBG("Listener: waiting for connection"); + CSocket* pConnection = m_pListenSocket->Accept(&ClientIPAddress, &nClientPort); + + if (pConnection == nullptr) + { + LOGERR("Unable to accept connection"); + continue; + } + + CString IPAddressString; + ClientIPAddress.Format(&IPAddressString); + LOGNOTE("Incoming connection from %s:%d", static_cast(IPAddressString), nClientPort); + + if (CFTPWorker::GetInstanceCount() >= MaxConnections) + { + pConnection->Send("421 Maximum number of connections reached.\r\n", 45, 0); + delete pConnection; + LOGWARN("Maximum number of connections reached"); + continue; + } + + // Spawn new worker + new CFTPWorker(pConnection, m_pUser, m_pPassword); + } +} \ No newline at end of file diff --git a/src/net/ftpdaemon.h b/src/net/ftpdaemon.h new file mode 100644 index 0000000..4d75762 --- /dev/null +++ b/src/net/ftpdaemon.h @@ -0,0 +1,47 @@ +// +// ftpdaemon.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _ftpdaemon_h +#define _ftpdaemon_h + +#include +#include + +class CFTPDaemon : protected CTask +{ +public: + CFTPDaemon(const char* pUser, const char* pPassword); + virtual ~CFTPDaemon() override; + + bool Initialize(); + + virtual void Run() override; + +private: + // TCP sockets + CSocket* m_pListenSocket; + + const char* m_pUser; + const char* m_pPassword; +}; + +#endif \ No newline at end of file diff --git a/src/net/ftpworker.cpp b/src/net/ftpworker.cpp new file mode 100644 index 0000000..f61b07b --- /dev/null +++ b/src/net/ftpworker.cpp @@ -0,0 +1,1206 @@ +// +// ftpworker.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +//#define FTPDAEMON_DEBUG + +#include +#include +#include +#include +#include +#include + +#include + +#include "ftpworker.h" +#include "utility.h" + +// Use a per-instance name for the log macros +#define From m_LogName + +constexpr u16 PassivePortBase = 9000; +constexpr size_t TextBufferSize = 512; +constexpr unsigned int SocketTimeout = 20; +constexpr unsigned int NumRetries = 3; + +#ifndef MT32_PI_VERSION +#define MT32_PI_VERSION "alpha version" +#endif + +const char MOTDBanner[] = "Welcome to the minidexed " MT32_PI_VERSION " embedded FTP server!"; + +enum class TDirectoryListEntryType +{ + File, + Directory, +}; + +struct TDirectoryListEntry +{ + char Name[FF_LFN_BUF + 1]; + TDirectoryListEntryType Type; + u32 nSize; + u16 nLastModifedDate; + u16 nLastModifedTime; +}; + +using TCommandHandler = bool (CFTPWorker::*)(const char* pArgs); + +struct TFTPCommand +{ + const char* pCmdStr; + TCommandHandler pHandler; +}; + +const TFTPCommand CFTPWorker::Commands[] = +{ + { "SYST", &CFTPWorker::System }, + { "USER", &CFTPWorker::Username }, + { "PASS", &CFTPWorker::Password }, + { "TYPE", &CFTPWorker::Type }, + { "PASV", &CFTPWorker::Passive }, + { "PORT", &CFTPWorker::Port }, + { "RETR", &CFTPWorker::Retrieve }, + { "STOR", &CFTPWorker::Store }, + { "DELE", &CFTPWorker::Delete }, + { "RMD", &CFTPWorker::Delete }, + { "MKD", &CFTPWorker::MakeDirectory }, + { "CWD", &CFTPWorker::ChangeWorkingDirectory }, + { "CDUP", &CFTPWorker::ChangeToParentDirectory }, + { "PWD", &CFTPWorker::PrintWorkingDirectory }, + { "LIST", &CFTPWorker::List }, + { "NLST", &CFTPWorker::ListFileNames }, + { "RNFR", &CFTPWorker::RenameFrom }, + { "RNTO", &CFTPWorker::RenameTo }, + { "BYE", &CFTPWorker::Bye }, + { "QUIT", &CFTPWorker::Bye }, + { "NOOP", &CFTPWorker::NoOp }, +}; + +u8 CFTPWorker::s_nInstanceCount = 0; + +// Volume names from ffconf.h +// TODO: Share with soundfontmanager.cpp +const char* const VolumeNames[] = { FF_VOLUME_STRS }; + +bool ValidateVolumeName(const char* pVolumeName) +{ + for (const auto pName : VolumeNames) + { + if (strcasecmp(pName, pVolumeName) == 0) + return true; + } + + return false; +} + +// Comparator for sorting directory listings +inline bool DirectoryCaseInsensitiveAscending(const TDirectoryListEntry& EntryA, const TDirectoryListEntry& EntryB) +{ + // Directories first in ascending order + if (EntryA.Type != EntryB.Type) + return EntryA.Type == TDirectoryListEntryType::Directory; + + return strncasecmp(EntryA.Name, EntryB.Name, sizeof(TDirectoryListEntry::Name)) < 0; +} + + +CFTPWorker::CFTPWorker(CSocket* pControlSocket, const char* pExpectedUser, const char* pExpectedPassword) + : CTask(TASK_STACK_SIZE), + m_LogName(), + m_pExpectedUser(pExpectedUser), + m_pExpectedPassword(pExpectedPassword), + m_pControlSocket(pControlSocket), + m_pDataSocket(nullptr), + m_nDataSocketPort(0), + m_DataSocketIPAddress(), + m_CommandBuffer{'\0'}, + m_DataBuffer{0}, + m_User(), + m_Password(), + m_DataType(TDataType::ASCII), + m_TransferMode(TTransferMode::Active), + m_CurrentPath(), + m_RenameFrom() +{ + ++s_nInstanceCount; + m_LogName.Format("ftpd[%d]", s_nInstanceCount); +} + +CFTPWorker::~CFTPWorker() +{ + if (m_pControlSocket) + delete m_pControlSocket; + + if (m_pDataSocket) + delete m_pDataSocket; + + --s_nInstanceCount; + + LOGNOTE("Instance count is now %d", s_nInstanceCount); +} + +void CFTPWorker::Run() +{ + assert(m_pControlSocket != nullptr); + + const size_t nWorkerNumber = s_nInstanceCount; + CScheduler* const pScheduler = CScheduler::Get(); + + LOGNOTE("Worker task %d spawned", nWorkerNumber); + + if (!SendStatus(TFTPStatus::ReadyForNewUser, MOTDBanner)) + return; + + CTimer* const pTimer = CTimer::Get(); + unsigned int nTimeout = pTimer->GetTicks(); + + while (m_pControlSocket) + { + // Block while waiting to receive +#ifdef FTPDAEMON_DEBUG + LOGDBG("Waiting for command"); +#endif + const int nReceiveBytes = m_pControlSocket->Receive(m_CommandBuffer, sizeof(m_CommandBuffer), MSG_DONTWAIT); + + if (nReceiveBytes == 0) + { + if (pTimer->GetTicks() - nTimeout >= SocketTimeout * HZ) + { + LOGERR("Socket timed out"); + break; + } + + pScheduler->Yield(); + continue; + } + + if (nReceiveBytes < 0) + { + LOGNOTE("Connection closed"); + break; + } + + // FIXME + m_CommandBuffer[nReceiveBytes - 2] = '\0'; + +#ifdef FTPDAEMON_DEBUG + const u8* pIPAddress = m_pControlSocket->GetForeignIP(); + LOGDBG("<-- Received %d bytes from %d.%d.%d.%d: '%s'", nReceiveBytes, pIPAddress[0], pIPAddress[1], pIPAddress[2], pIPAddress[3], m_CommandBuffer); +#endif + + char* pSavePtr; + char* pToken = strtok_r(m_CommandBuffer, " \r\n", &pSavePtr); + + if (!pToken) + { + LOGERR("String tokenization error (received: '%s')", m_CommandBuffer); + continue; + } + + TCommandHandler pHandler = nullptr; + for (size_t i = 0; i < Utility::ArraySize(Commands); ++i) + { + if (strcasecmp(pToken, Commands[i].pCmdStr) == 0) + { + pHandler = Commands[i].pHandler; + break; + } + } + + if (pHandler) + (this->*pHandler)(pSavePtr); + else + SendStatus(TFTPStatus::CommandNotImplemented, "Command not implemented."); + + nTimeout = pTimer->GetTicks(); + } + + LOGNOTE("Worker task %d shutting down", nWorkerNumber); + + delete m_pControlSocket; + m_pControlSocket = nullptr; +} + +CSocket* CFTPWorker::OpenDataConnection() +{ + CSocket* pDataSocket = nullptr; + u8 nRetries = NumRetries; + + while (pDataSocket == nullptr && nRetries > 0) + { + // Active: Create new socket and connect to client + if (m_TransferMode == TTransferMode::Active) + { + CNetSubSystem* const pNet = CNetSubSystem::Get(); + pDataSocket = new CSocket(pNet, IPPROTO_TCP); + + if (pDataSocket == nullptr) + { + SendStatus(TFTPStatus::DataConnectionFailed, "Could not open socket."); + return nullptr; + } + + if (pDataSocket->Connect(m_DataSocketIPAddress, m_nDataSocketPort) < 0) + { + SendStatus(TFTPStatus::DataConnectionFailed, "Could not connect to data port."); + delete pDataSocket; + pDataSocket = nullptr; + } + } + + // Passive: Use previously-created socket and accept connection from client + else if (m_TransferMode == TTransferMode::Passive && m_pDataSocket != nullptr) + { + CIPAddress ClientIPAddress; + u16 nClientPort; + pDataSocket = m_pDataSocket->Accept(&ClientIPAddress, &nClientPort); + } + + --nRetries; + } + + if (pDataSocket == nullptr) + { + LOGERR("Unable to open data socket after %d attempts", NumRetries); + SendStatus(TFTPStatus::DataConnectionFailed, "Couldn't open data connection."); + } + + return pDataSocket; +} + +bool CFTPWorker::SendStatus(TFTPStatus StatusCode, const char* pMessage) +{ + assert(m_pControlSocket != nullptr); + + const int nLength = snprintf(m_CommandBuffer, sizeof(m_CommandBuffer), "%d %s\r\n", StatusCode, pMessage); + if (m_pControlSocket->Send(m_CommandBuffer, nLength, 0) < 0) + { + LOGERR("Failed to send status"); + return false; + } +#ifdef FTPDAEMON_DEBUG + else + { + m_CommandBuffer[nLength - 2] = '\0'; + LOGDBG("--> Sent: '%s'", m_CommandBuffer); + } +#endif + + return true; +} + +bool CFTPWorker::CheckLoggedIn() +{ +#ifdef FTPDAEMON_DEBUG + LOGDBG("Username compare: expected '%s', actual '%s'", static_cast(m_pExpectedUser), static_cast(m_User)); + LOGDBG("Password compare: expected '%s', actual '%s'", static_cast(m_pExpectedPassword), static_cast(m_Password)); +#endif + + if (m_User.Compare(m_pExpectedUser) == 0 && m_Password.Compare(m_pExpectedPassword) == 0) + return true; + + SendStatus(TFTPStatus::NotLoggedIn, "Not logged in."); + return false; +} + +CString CFTPWorker::RealPath(const char* pInBuffer) const +{ + assert(pInBuffer != nullptr); + + CString Path; + const bool bAbsolute = pInBuffer[0] == '/'; + + if (bAbsolute) + { + char Buffer[TextBufferSize]; + FTPPathToFatFsPath(pInBuffer, Buffer, sizeof(Buffer)); + Path = Buffer; + } + else + Path.Format("%s/%s", static_cast(m_CurrentPath), pInBuffer); + + return Path; +} + +const TDirectoryListEntry* CFTPWorker::BuildDirectoryList(size_t& nOutEntries) const +{ + DIR Dir; + FILINFO FileInfo; + FRESULT Result; + + TDirectoryListEntry* pEntries = nullptr; + nOutEntries = 0; + + // Volume list + if (m_CurrentPath.GetLength() == 0) + { + constexpr size_t nVolumes = Utility::ArraySize(VolumeNames); + bool VolumesAvailable[nVolumes] = { false }; + + for (size_t i = 0; i < nVolumes; ++i) + { + char VolumeName[6]; + strncpy(VolumeName, VolumeNames[i], sizeof(VolumeName)); + strcat(VolumeName, ":"); + + // Returns FR_ + if ((Result = f_opendir(&Dir, VolumeName)) == FR_OK) + { + f_closedir(&Dir); + VolumesAvailable[i] = true; + ++nOutEntries; + } + } + + pEntries = new TDirectoryListEntry[nOutEntries]; + + size_t nCurrentEntry = 0; + for (size_t i = 0; i < nVolumes && nCurrentEntry < nOutEntries; ++i) + { + if (VolumesAvailable[i]) + { + TDirectoryListEntry& Entry = pEntries[nCurrentEntry++]; + strncpy(Entry.Name, VolumeNames[i], sizeof(Entry.Name)); + Entry.Type = TDirectoryListEntryType::Directory; + Entry.nSize = 0; + Entry.nLastModifedDate = 0; + Entry.nLastModifedTime = 0; + } + } + + return pEntries; + } + + // Directory list + Result = f_findfirst(&Dir, &FileInfo, m_CurrentPath, "*"); + if (Result == FR_OK && *FileInfo.fname) + { + // Count how many entries we need + do + { + ++nOutEntries; + Result = f_findnext(&Dir, &FileInfo); + } while (Result == FR_OK && *FileInfo.fname); + + f_closedir(&Dir); + + if (nOutEntries && (pEntries = new TDirectoryListEntry[nOutEntries])) + { + size_t nCurrentEntry = 0; + Result = f_findfirst(&Dir, &FileInfo, m_CurrentPath, "*"); + while (Result == FR_OK && *FileInfo.fname) + { + TDirectoryListEntry& Entry = pEntries[nCurrentEntry++]; + strncpy(Entry.Name, FileInfo.fname, sizeof(Entry.Name)); + + if (FileInfo.fattrib & AM_DIR) + { + Entry.Type = TDirectoryListEntryType::Directory; + Entry.nSize = 0; + } + else + { + Entry.Type = TDirectoryListEntryType::File; + Entry.nSize = FileInfo.fsize; + } + + Entry.nLastModifedDate = FileInfo.fdate; + Entry.nLastModifedTime = FileInfo.ftime; + + Result = f_findnext(&Dir, &FileInfo); + } + + f_closedir(&Dir); + + Utility::QSort(pEntries, DirectoryCaseInsensitiveAscending, 0, nOutEntries - 1); + } + } + + return pEntries; +} + +bool CFTPWorker::System(const char* pArgs) +{ + // Some FTP clients (e.g. Directory Opus) will only attempt to parse LIST responses as IIS/DOS-style if we pretend to be Windows NT + SendStatus(TFTPStatus::SystemType, "Windows_NT"); + return true; +} + +bool CFTPWorker::Username(const char* pArgs) +{ + m_User = pArgs; + char Buffer[TextBufferSize]; + snprintf(Buffer, sizeof(Buffer), "Password required for '%s'.", static_cast(m_User)); + SendStatus(TFTPStatus::PasswordRequired, Buffer); + return true; +} + +bool CFTPWorker::Port(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + char Buffer[TextBufferSize]; + strncpy(Buffer, pArgs, sizeof(Buffer)); + + if (m_pDataSocket != nullptr) + { + delete m_pDataSocket; + m_pDataSocket = nullptr; + } + + m_TransferMode = TTransferMode::Active; + + // TODO: PORT IP Address should match original IP address + + u8 PortBytes[6]; + char* pSavePtr; + char* pToken = strtok_r(Buffer, " ,", &pSavePtr); + bool bParseError = (pToken == nullptr); + + if (!bParseError) + { + PortBytes[0] = static_cast(atoi(pToken)); + + for (u8 i = 0; i < 5; ++i) + { + pToken = strtok_r(nullptr, " ,", &pSavePtr); + if (pToken == nullptr) + { + bParseError = true; + break; + } + + PortBytes[i + 1] = static_cast(atoi(pToken)); + } + } + + if (bParseError) + { + SendStatus(TFTPStatus::SyntaxError, "Syntax error."); + return false; + } + + m_DataSocketIPAddress.Set(PortBytes); + m_nDataSocketPort = (PortBytes[4] << 8) + PortBytes[5]; + +#ifdef FTPDAEMON_DEBUG + CString IPAddressString; + m_DataSocketIPAddress.Format(&IPAddressString); + LOGDBG("PORT set to: %s:%d", static_cast(IPAddressString), m_nDataSocketPort); +#endif + + SendStatus(TFTPStatus::Success, "Command OK."); + return true; +} + +bool CFTPWorker::Passive(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (m_pDataSocket == nullptr) + { + m_TransferMode = TTransferMode::Passive; + m_nDataSocketPort = PassivePortBase + s_nInstanceCount - 1; + + CNetSubSystem* const pNet = CNetSubSystem::Get(); + m_pDataSocket = new CSocket(pNet, IPPROTO_TCP); + + if (m_pDataSocket == nullptr) + { + SendStatus(TFTPStatus::ServiceNotAvailable, "Failed to open port for passive mode."); + return false; + } + + if (m_pDataSocket->Bind(m_nDataSocketPort) < 0) + { + SendStatus(TFTPStatus::DataConnectionFailed, "Could not bind to data port."); + delete m_pDataSocket; + m_pDataSocket = nullptr; + return false; + } + + if (m_pDataSocket->Listen() < 0) + { + SendStatus(TFTPStatus::DataConnectionFailed, "Could not listen on data port."); + delete m_pDataSocket; + m_pDataSocket = nullptr; + return false; + } + } + + u8 IPAddress[IP_ADDRESS_SIZE]; + CNetSubSystem::Get()->GetConfig()->GetIPAddress()->CopyTo(IPAddress); + + char Buffer[TextBufferSize]; + snprintf(Buffer, sizeof(Buffer), "Entering passive mode (%d,%d,%d,%d,%d,%d).", + IPAddress[0], + IPAddress[1], + IPAddress[2], + IPAddress[3], + (m_nDataSocketPort >> 8) & 0xFF, + m_nDataSocketPort & 0xFF + ); + + SendStatus(TFTPStatus::EnteringPassiveMode, Buffer); + return true; +} + +bool CFTPWorker::Password(const char* pArgs) +{ + if (m_User.GetLength() == 0) + { + SendStatus(TFTPStatus::AccountRequired, "Need account for login."); + return false; + } + + m_Password = pArgs; + + if (!CheckLoggedIn()) + return false; + + SendStatus(TFTPStatus::UserLoggedIn, "User logged in."); + return true; +} + +bool CFTPWorker::Type(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (strcasecmp(pArgs, "A") == 0) + { + m_DataType = TDataType::ASCII; + SendStatus(TFTPStatus::Success, "Type set to ASCII."); + return true; + } + + if (strcasecmp(pArgs, "I") == 0) + { + m_DataType = TDataType::Binary; + SendStatus(TFTPStatus::Success, "Type set to binary."); + return true; + } + + SendStatus(TFTPStatus::SyntaxError, "Syntax error."); + return false; +} + +bool CFTPWorker::Retrieve(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + FIL File; + CString Path = RealPath(pArgs); + + if (f_open(&File, Path, FA_READ) != FR_OK) + { + SendStatus(TFTPStatus::FileActionNotTaken, "Could not open file for reading."); + return false; + } + + if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) + return false; + + CSocket* pDataSocket = OpenDataConnection(); + if (pDataSocket == nullptr) + return false; + + size_t nSize = f_size(&File); + size_t nSent = 0; + + while (nSent < nSize) + { + UINT nBytesRead; +#ifdef FTPDAEMON_DEBUG + LOGDBG("Sending data"); +#endif + if (f_read(&File, m_DataBuffer, sizeof(m_DataBuffer), &nBytesRead) != FR_OK || pDataSocket->Send(m_DataBuffer, nBytesRead, 0) < 0) + { + delete pDataSocket; + f_close(&File); + SendStatus(TFTPStatus::ActionAborted, "File action aborted, local error."); + return false; + } + + nSent += nBytesRead; + assert(nSent <= nSize); + } + + delete pDataSocket; + f_close(&File); + SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); + + return false; +} + +bool CFTPWorker::Store(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + FIL File; + CString Path = RealPath(pArgs); + + if (f_open(&File, Path, FA_CREATE_ALWAYS | FA_WRITE) != FR_OK) + { + SendStatus(TFTPStatus::FileActionNotTaken, "Could not open file for writing."); + return false; + } + + f_sync(&File); + + if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) + return false; + + CSocket* pDataSocket = OpenDataConnection(); + if (pDataSocket == nullptr) + return false; + + bool bSuccess = true; + + CTimer* const pTimer = CTimer::Get(); + unsigned int nTimeout = pTimer->GetTicks(); + + while (true) + { +#ifdef FTPDAEMON_DEBUG + LOGDBG("Waiting to receive"); +#endif + int nReceiveResult = pDataSocket->Receive(m_DataBuffer, sizeof(m_DataBuffer), MSG_DONTWAIT); + FRESULT nWriteResult; + UINT nWritten; + + if (nReceiveResult == 0) + { + if (pTimer->GetTicks() - nTimeout >= SocketTimeout * HZ) + { + LOGERR("Socket timed out"); + bSuccess = false; + break; + } + CScheduler::Get()->Yield(); + continue; + } + + // All done + if (nReceiveResult < 0) + { + LOGNOTE("Receive done, no more data"); + break; + } + +#ifdef FTPDAEMON_DEBUG + //LOGDBG("Received %d bytes", nReceiveResult); +#endif + + if ((nWriteResult = f_write(&File, m_DataBuffer, nReceiveResult, &nWritten)) != FR_OK) + { + LOGERR("Write FAILED, return code %d", nWriteResult); + bSuccess = false; + break; + } + + f_sync(&File); + CScheduler::Get()->Yield(); + + nTimeout = pTimer->GetTicks(); + } + + if (bSuccess) + SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); + else + SendStatus(TFTPStatus::ActionAborted, "File action aborted, local error."); + +#ifdef FTPDAEMON_DEBUG + LOGDBG("Closing socket/file"); +#endif + delete pDataSocket; + f_close(&File); + + return true; +} + +bool CFTPWorker::Delete(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + CString Path = RealPath(pArgs); + + if (f_unlink(Path) != FR_OK) + SendStatus(TFTPStatus::FileActionNotTaken, "File was not deleted."); + else + SendStatus(TFTPStatus::FileActionOk, "File deleted."); + + return true; +} + +bool CFTPWorker::MakeDirectory(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + CString Path = RealPath(pArgs); + + if (f_mkdir(Path) != FR_OK) + SendStatus(TFTPStatus::FileActionNotTaken, "Directory creation failed."); + else + { + char Buffer[TextBufferSize]; + FatFsPathToFTPPath(Path, Buffer, sizeof(Buffer)); + strcat(Buffer, " directory created."); + SendStatus(TFTPStatus::PathCreated, Buffer); + } + + return true; +} + +bool CFTPWorker::ChangeWorkingDirectory(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + char Buffer[TextBufferSize]; + bool bSuccess = false; + + const bool bAbsolute = pArgs[0] == '/'; + if (bAbsolute) + { + // Root + if (pArgs[1] == '\0') + { + m_CurrentPath = ""; + bSuccess = true; + } + else + { + DIR Dir; + FTPPathToFatFsPath(pArgs, Buffer, sizeof(Buffer)); + + // f_stat() will fail if we're trying to CWD to the root of a volume, so use f_opendir() + if (f_opendir(&Dir, Buffer) == FR_OK) + { + f_closedir(&Dir); + m_CurrentPath = Buffer; + bSuccess = true; + } + } + } + else + { + const bool bAtRoot = m_CurrentPath.GetLength() == 0; + if (bAtRoot) + { + if (ValidateVolumeName(pArgs)) + { + m_CurrentPath.Format("%s:", pArgs); + bSuccess = true; + } + } + else + { + CString NewPath; + NewPath.Format("%s/%s", static_cast(m_CurrentPath), pArgs); + + if (f_stat(NewPath, nullptr) == FR_OK) + { + m_CurrentPath = NewPath; + bSuccess = true; + } + } + } + + if (bSuccess) + { + const bool bAtRoot = m_CurrentPath.GetLength() == 0; + if (bAtRoot) + strncpy(Buffer, "\"/\"", sizeof(Buffer)); + else + FatFsPathToFTPPath(m_CurrentPath, Buffer, sizeof(Buffer)); + SendStatus(TFTPStatus::FileActionOk, Buffer); + } + else + SendStatus(TFTPStatus::FileNotFound, "Directory unavailable."); + + return bSuccess; +} + +bool CFTPWorker::ChangeToParentDirectory(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + char Buffer[TextBufferSize]; + bool bSuccess = false; + bool bAtRoot = m_CurrentPath.GetLength() == 0; + + if (!bAtRoot) + { + DIR Dir; + FatFsParentPath(m_CurrentPath, Buffer, sizeof(Buffer)); + + bAtRoot = Buffer[0] == '\0'; + if (bAtRoot) + { + m_CurrentPath = Buffer; + bSuccess = true; + } + else if (f_opendir(&Dir, Buffer) == FR_OK) + { + f_closedir(&Dir); + m_CurrentPath = Buffer; + bSuccess = true; + } + } + + if (bSuccess) + { + bAtRoot = m_CurrentPath.GetLength() == 0; + if (bAtRoot) + strncpy(Buffer, "\"/\"", sizeof(Buffer)); + else + FatFsPathToFTPPath(m_CurrentPath, Buffer, sizeof(Buffer)); + SendStatus(TFTPStatus::FileActionOk, Buffer); + } + else + SendStatus(TFTPStatus::FileNotFound, "Directory unavailable."); + + return false; +} + +bool CFTPWorker::PrintWorkingDirectory(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + char Buffer[TextBufferSize]; + + const bool bAtRoot = m_CurrentPath.GetLength() == 0; + if (bAtRoot) + strncpy(Buffer, "\"/\"", sizeof(Buffer)); + else + FatFsPathToFTPPath(m_CurrentPath, Buffer, sizeof(Buffer)); + + SendStatus(TFTPStatus::PathCreated, Buffer); + + return true; +} + +bool CFTPWorker::List(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) + return false; + + CSocket* pDataSocket = OpenDataConnection(); + if (pDataSocket == nullptr) + return false; + + char Buffer[TextBufferSize]; + char Date[9]; + char Time[8]; + + size_t nEntries; + const TDirectoryListEntry* pDirEntries = BuildDirectoryList(nEntries); + + if (pDirEntries) + { + for (size_t i = 0; i < nEntries; ++i) + { + const TDirectoryListEntry& Entry = pDirEntries[i]; + int nLength; + + // Mimic the Microsoft IIS LIST format + FormatLastModifiedDate(Entry.nLastModifedDate, Date, sizeof(Date)); + FormatLastModifiedTime(Entry.nLastModifedTime, Time, sizeof(Time)); + + if (Entry.Type == TDirectoryListEntryType::Directory) + nLength = snprintf(Buffer, sizeof(Buffer), "%-9s %-13s %-14s %s\r\n", Date, Time, "", Entry.Name); + else + nLength = snprintf(Buffer, sizeof(Buffer), "%-9s %-13s %14d %s\r\n", Date, Time, Entry.nSize, Entry.Name); + + if (pDataSocket->Send(Buffer, nLength, 0) < 0) + { + delete[] pDirEntries; + delete pDataSocket; + SendStatus(TFTPStatus::DataConnectionFailed, "Transfer error."); + return false; + } + } + + delete[] pDirEntries; + } + + delete pDataSocket; + SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); + return true; +} + +bool CFTPWorker::ListFileNames(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) + return false; + + CSocket* pDataSocket = OpenDataConnection(); + if (pDataSocket == nullptr) + return false; + + char Buffer[TextBufferSize]; + size_t nEntries; + const TDirectoryListEntry* pDirEntries = BuildDirectoryList(nEntries); + + if (pDirEntries) + { + for (size_t i = 0; i < nEntries; ++i) + { + const TDirectoryListEntry& Entry = pDirEntries[i]; + if (Entry.Type == TDirectoryListEntryType::Directory) + continue; + + const int nLength = snprintf(Buffer, sizeof(Buffer), "%s\r\n", Entry.Name); + if (pDataSocket->Send(Buffer, nLength, 0) < 0) + { + delete[] pDirEntries; + delete pDataSocket; + SendStatus(TFTPStatus::DataConnectionFailed, "Transfer error."); + return false; + } + } + + delete[] pDirEntries; + } + + delete pDataSocket; + SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); + return true; +} + +bool CFTPWorker::RenameFrom(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + m_RenameFrom = pArgs; + SendStatus(TFTPStatus::PendingFurtherInfo, "Requested file action pending further information."); + + return false; +} + +bool CFTPWorker::RenameTo(const char* pArgs) +{ + if (!CheckLoggedIn()) + return false; + + if (m_RenameFrom.GetLength() == 0) + { + SendStatus(TFTPStatus::BadCommandSequence, "Bad sequence of commands."); + return false; + } + + CString SourcePath = RealPath(m_RenameFrom); + CString DestPath = RealPath(pArgs); + + if (f_rename(SourcePath, DestPath) != FR_OK) + SendStatus(TFTPStatus::FileNameNotAllowed, "File name not allowed."); + else + SendStatus(TFTPStatus::FileActionOk, "File renamed."); + + m_RenameFrom = ""; + + return false; +} + +bool CFTPWorker::Bye(const char* pArgs) +{ + SendStatus(TFTPStatus::ClosingControl, "Goodbye."); + delete m_pControlSocket; + m_pControlSocket = nullptr; + return true; +} + +bool CFTPWorker::NoOp(const char* pArgs) +{ + SendStatus(TFTPStatus::Success, "Command OK."); + return true; +} + +void CFTPWorker::FatFsPathToFTPPath(const char* pInBuffer, char* pOutBuffer, size_t nSize) +{ + assert(pOutBuffer && nSize > 2); + const char* pEnd = pOutBuffer + nSize; + const char* pInChar = pInBuffer; + char* pOutChar = pOutBuffer; + + *pOutChar++ = '"'; + *pOutChar++ = '/'; + + while (*pInChar != '\0' && pOutChar < pEnd) + { + // Kill the volume colon + if (*pInChar == ':') + { + *pOutChar++ = '/'; + ++pInChar; + + // Kill any slashes after the colon + while (*pInChar == '/') ++pInChar; + continue; + } + + // Kill duplicate slashes + if (*pInChar == '/') + { + *pOutChar++ = *pInChar++; + while (*pInChar == '/') ++pInChar; + continue; + } + + *pOutChar++ = *pInChar++; + } + + // Kill trailing slash + if (*(pOutChar - 1) == '/') + --pOutChar; + + assert(pOutChar < pEnd - 2); + *pOutChar++ = '"'; + *pOutChar++ = '\0'; +} + +void CFTPWorker::FTPPathToFatFsPath(const char* pInBuffer, char* pOutBuffer, size_t nSize) +{ + assert(pInBuffer && pOutBuffer); + const char* pEnd = pOutBuffer + nSize; + const char* pInChar = pInBuffer; + char* pOutChar = pOutBuffer; + + // Kill leading slashes + while (*pInChar == '/') ++pInChar; + + bool bGotVolume = false; + while (*pInChar != '\0' && pOutChar < pEnd) + { + // Kill the volume colon + if (!bGotVolume && *pInChar == '/') + { + bGotVolume = true; + *pOutChar++ = ':'; + ++pInChar; + + // Kill any slashes after the colon + while (*pInChar == '/') ++pInChar; + continue; + } + + // Kill duplicate slashes + if (*pInChar == '/') + { + *pOutChar++ = *pInChar++; + while (*pInChar == '/') ++pInChar; + continue; + } + + *pOutChar++ = *pInChar++; + } + + assert(pOutChar < pEnd - 2); + + // Kill trailing slash + if (*(pOutChar - 1) == '/') + --pOutChar; + + // Add volume colon + if (!bGotVolume) + *pOutChar++ = ':'; + + *pOutChar++ = '\0'; +} + +void CFTPWorker::FatFsParentPath(const char* pInBuffer, char* pOutBuffer, size_t nSize) +{ + assert(pInBuffer != nullptr && pOutBuffer != nullptr); + + size_t nLength = strlen(pInBuffer); + assert(nLength > 0 && nSize >= nLength); + + const char* pLastChar = pInBuffer + nLength - 1; + const char* pInChar = pLastChar; + + // Kill trailing slashes + while (*pInChar == '/' && pInChar > pInBuffer) --pInChar; + + // Kill subdirectory name + while (*pInChar != '/' && *pInChar != ':' && pInChar > pInBuffer) --pInChar; + + // Kill trailing slashes + while (*pInChar == '/' && pInChar > pInBuffer) --pInChar; + + // Pointer didn't move (we're already at a volume root), or we reached the start of the string (path invalid) + if (pInChar == pLastChar || pInChar == pInBuffer) + { + *pOutBuffer = '\0'; + return; + } + + // Truncate string + nLength = pInChar - pInBuffer + 1; + memcpy(pOutBuffer, pInBuffer, nLength); + pOutBuffer[nLength] = '\0'; +} + +void CFTPWorker::FormatLastModifiedDate(u16 nDate, char* pOutBuffer, size_t nSize) +{ + // 2-digit year + const u16 nYear = (1980 + (nDate >> 9)) % 100; + u16 nMonth = (nDate >> 5) & 0x0F; + u16 nDay = nDate & 0x1F; + + if (nMonth == 0) + nMonth = 1; + if (nDay == 0) + nDay = 1; + + snprintf(pOutBuffer, nSize, "%02d-%02d-%02d", nMonth, nDay, nYear); +} + +void CFTPWorker::FormatLastModifiedTime(u16 nDate, char* pOutBuffer, size_t nSize) +{ + u16 nHour = (nDate >> 11) & 0x1F; + const u16 nMinute = (nDate >> 5) & 0x3F; + const char* pSuffix = nHour < 12 ? "AM" : "PM"; + + if (nHour == 0) + nHour = 12; + else if (nHour >= 12) + nHour -= 12; + + snprintf(pOutBuffer, nSize, "%02d:%02d%s", nHour, nMinute, pSuffix); +} \ No newline at end of file diff --git a/src/net/ftpworker.h b/src/net/ftpworker.h new file mode 100644 index 0000000..62e60ed --- /dev/null +++ b/src/net/ftpworker.h @@ -0,0 +1,157 @@ +// +// ftpworker.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _ftpworker_h +#define _ftpworker_h + +#include +#include +#include +#include + +// TODO: These may be incomplete/inaccurate +enum TFTPStatus +{ + FileStatusOk = 150, + + Success = 200, + SystemType = 215, + ReadyForNewUser = 220, + ClosingControl = 221, + TransferComplete = 226, + EnteringPassiveMode = 227, + UserLoggedIn = 230, + FileActionOk = 250, + PathCreated = 257, + + PasswordRequired = 331, + AccountRequired = 332, + PendingFurtherInfo = 350, + + ServiceNotAvailable = 421, + DataConnectionFailed = 425, + FileActionNotTaken = 450, + ActionAborted = 451, + + CommandUnrecognized = 500, + SyntaxError = 501, + CommandNotImplemented = 502, + BadCommandSequence = 503, + NotLoggedIn = 530, + FileNotFound = 550, + FileNameNotAllowed = 553, +}; + +enum class TTransferMode +{ + Active, + Passive, +}; + +enum class TDataType +{ + ASCII, + Binary, +}; + +struct TFTPCommand; +struct TDirectoryListEntry; + +class CFTPWorker : protected CTask +{ +public: + CFTPWorker(CSocket* pControlSocket, const char* pExpectedUser, const char* pExpectedPassword); + virtual ~CFTPWorker() override; + + virtual void Run() override; + + static u8 GetInstanceCount() { return s_nInstanceCount; } + +private: + CSocket* OpenDataConnection(); + + bool SendStatus(TFTPStatus StatusCode, const char* pMessage); + + bool CheckLoggedIn(); + + // Directory navigation + CString RealPath(const char* pInBuffer) const; + const TDirectoryListEntry* BuildDirectoryList(size_t& nOutEntries) const; + + // FTP command handlers + bool System(const char* pArgs); + bool Username(const char* pArgs); + bool Port(const char* pArgs); + bool Passive(const char* pArgs); + bool Password(const char* pArgs); + bool Type(const char* pArgs); + bool Retrieve(const char* pArgs); + bool Store(const char* pArgs); + bool Delete(const char* pArgs); + bool MakeDirectory(const char* pArgs); + bool ChangeWorkingDirectory(const char* pArgs); + bool ChangeToParentDirectory(const char* pArgs); + bool PrintWorkingDirectory(const char* pArgs); + bool List(const char* pArgs); + bool ListFileNames(const char* pArgs); + bool RenameFrom(const char* pArgs); + bool RenameTo(const char* pArgs); + bool Bye(const char* pArgs); + bool NoOp(const char* pArgs); + + CString m_LogName; + + // Authentication + const char* m_pExpectedUser; + const char* m_pExpectedPassword; + + // TCP sockets + CSocket* m_pControlSocket; + CSocket* m_pDataSocket; + u16 m_nDataSocketPort; + CIPAddress m_DataSocketIPAddress; + + // Command/data buffers + char m_CommandBuffer[FRAME_BUFFER_SIZE]; + u8 m_DataBuffer[FRAME_BUFFER_SIZE]; + + // Session state + CString m_User; + CString m_Password; + TDataType m_DataType; + TTransferMode m_TransferMode; + CString m_CurrentPath; + CString m_RenameFrom; + + static void FatFsPathToFTPPath(const char* pInBuffer, char* pOutBuffer, size_t nSize); + static void FTPPathToFatFsPath(const char* pInBuffer, char* pOutBuffer, size_t nSize); + + static void FatFsParentPath(const char* pInBuffer, char* pOutBuffer, size_t nSize); + + static void FormatLastModifiedDate(u16 nDate, char* pOutBuffer, size_t nSize); + static void FormatLastModifiedTime(u16 nDate, char* pOutBuffer, size_t nSize); + + static const TFTPCommand Commands[]; + static u8 s_nInstanceCount; +}; + +#endif \ No newline at end of file diff --git a/src/net/udpmidi.cpp b/src/net/udpmidi.cpp new file mode 100644 index 0000000..2f25eda --- /dev/null +++ b/src/net/udpmidi.cpp @@ -0,0 +1,89 @@ +// +// udpmidi.cpp +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#include +#include +#include +#include + +#include "udpmidi.h" + +LOGMODULE("udpmidi"); + +constexpr u16 MIDIPort = 1999; + +CUDPMIDIReceiver::CUDPMIDIReceiver(CUDPMIDIHandler* pHandler) + : CTask(TASK_STACK_SIZE, true), + m_pMIDISocket(nullptr), + m_MIDIBuffer{0}, + m_pHandler(pHandler) +{ +} + +CUDPMIDIReceiver::~CUDPMIDIReceiver() +{ + if (m_pMIDISocket) + delete m_pMIDISocket; +} + +bool CUDPMIDIReceiver::Initialize() +{ + assert(m_pMIDISocket == nullptr); + + CNetSubSystem* const pNet = CNetSubSystem::Get(); + + if ((m_pMIDISocket = new CSocket(pNet, IPPROTO_UDP)) == nullptr) + return false; + + if (m_pMIDISocket->Bind(MIDIPort) != 0) + { + LOGERR("Couldn't bind to port %d", MIDIPort); + return false; + } + + // We started as a suspended task; run now that initialization is successful + Start(); + + return true; +} + +void CUDPMIDIReceiver::Run() +{ + assert(m_pHandler != nullptr); + assert(m_pMIDISocket != nullptr); + + CScheduler* const pScheduler = CScheduler::Get(); + + while (true) + { + // Blocking call + const int nMIDIResult = m_pMIDISocket->Receive(m_MIDIBuffer, sizeof(m_MIDIBuffer), 0); + + if (nMIDIResult < 0) + LOGERR("MIDI socket receive error: %d", nMIDIResult); + else if (nMIDIResult > 0) + m_pHandler->OnUDPMIDIDataReceived(m_MIDIBuffer, nMIDIResult); + + // Allow other tasks to run + pScheduler->Yield(); + } +} \ No newline at end of file diff --git a/src/net/udpmidi.h b/src/net/udpmidi.h new file mode 100644 index 0000000..102d339 --- /dev/null +++ b/src/net/udpmidi.h @@ -0,0 +1,57 @@ +// +// udpmidi.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _udpmidi_h +#define _udpmidi_h + +#include +#include +#include + +class CUDPMIDIHandler +{ +public: + virtual void OnUDPMIDIDataReceived(const u8* pData, size_t nSize) = 0; +}; + +class CUDPMIDIReceiver : protected CTask +{ +public: + CUDPMIDIReceiver(CUDPMIDIHandler* pHandler); + virtual ~CUDPMIDIReceiver() override; + + bool Initialize(); + + virtual void Run() override; + +private: + // UDP sockets + CSocket* m_pMIDISocket; + + // Socket receive buffer + u8 m_MIDIBuffer[FRAME_BUFFER_SIZE]; + + // Callback handler + CUDPMIDIHandler* m_pHandler; +}; + +#endif \ No newline at end of file diff --git a/src/net/utility.h b/src/net/utility.h new file mode 100644 index 0000000..3b64395 --- /dev/null +++ b/src/net/utility.h @@ -0,0 +1,193 @@ + +// +// utility.h +// +// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi +// Copyright (C) 2020-2023 Dale Whinham +// +// This file is part of mt32-pi. +// +// mt32-pi 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. +// +// mt32-pi 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 +// mt32-pi. If not, see . +// + +#ifndef _utility_h +#define _utility_h + +#include +#include + +// Macro to extract the string representation of an enum +#define CONFIG_ENUM_VALUE(VALUE, STRING) VALUE, + +// Macro to extract the enum value +#define CONFIG_ENUM_STRING(VALUE, STRING) #STRING, + +// Macro to declare the enum itself +#define CONFIG_ENUM(NAME, VALUES) enum class NAME { VALUES(CONFIG_ENUM_VALUE) } + +// Macro to declare an array of string representations for an enum +#define CONFIG_ENUM_STRINGS(NAME, DATA) static const char* NAME##Strings[] = { DATA(CONFIG_ENUM_STRING) } + +namespace Utility +{ + // Templated function for clamping a value between a minimum and a maximum + template + constexpr T Clamp(const T& nValue, const T& nMin, const T& nMax) + { + return (nValue < nMin) ? nMin : (nValue > nMax) ? nMax : nValue; + } + + // Templated function for taking the minimum of two values + template + constexpr T Min(const T& nLHS, const T& nRHS) + { + return nLHS < nRHS ? nLHS : nRHS; + } + + // Templated function for taking the maximum of two values + template + constexpr T Max(const T& nLHS, const T& nRHS) + { + return nLHS > nRHS ? nLHS : nRHS; + } + + // Function for performing a linear interpolation of a value + constexpr float Lerp(float nValue, float nMinA, float nMaxA, float nMinB, float nMaxB) + { + return nMinB + (nValue - nMinA) * ((nMaxB - nMinB) / (nMaxA - nMinA)); + } + + // Return number of elements in an array + template + constexpr size_t ArraySize(const T(&)[N]) { return N; } + + // Returns whether some value is a power of 2 + template + constexpr bool IsPowerOfTwo(const T& nValue) + { + return nValue && ((nValue & (nValue - 1)) == 0); + } + + // Rounds a number to a nearest multiple; only works for integer values/multiples + template + constexpr T RoundToNearestMultiple(const T& nValue, const T& nMultiple) + { + return ((nValue + nMultiple / 2) / nMultiple) * nMultiple; + } + + // Convert between milliseconds and ticks of a 1MHz clock + template + constexpr T MillisToTicks(const T& nMillis) + { + return nMillis * 1000; + } + + template + constexpr T TicksToMillis(const T& nTicks) + { + return nTicks / 1000; + } + + // Computes the Roland checksum + constexpr u8 RolandChecksum(const u8* pData, size_t nSize) + { + u8 nSum = 0; + for (size_t i = 0; i < nSize; ++i) + nSum = (nSum + pData[i]) & 0x7F; + + return 128 - nSum; + } + + // Comparators for sorting + namespace Comparator + { + template + using TComparator = bool (*)(const T&, const T&); + + template + inline bool LessThan(const T& ObjectA, const T& ObjectB) + { + return ObjectA < ObjectB; + } + + template + inline bool GreaterThan(const T& ObjectA, const T& ObjectB) + { + return ObjectA > ObjectB; + } + + inline bool CaseInsensitiveAscending(const CString& StringA, const CString& StringB) + { + return strcasecmp(StringA, StringB) < 0; + } + } + + // Swaps two objects in-place + template + inline void Swap(T& ObjectA, T& ObjectB) + { + u8 Buffer[sizeof(T)]; + memcpy(Buffer, &ObjectA, sizeof(T)); + memcpy(&ObjectA, &ObjectB, sizeof(T)); + memcpy(&ObjectB, Buffer, sizeof(T)); + } + + namespace + { + // Quicksort partition function (private) + template + size_t Partition(T* Items, Comparator::TComparator Comparator, size_t nLow, size_t nHigh) + { + const size_t nPivotIndex = (nHigh + nLow) / 2; + T* Pivot = &Items[nPivotIndex]; + + while (true) + { + while (Comparator(Items[nLow], *Pivot)) + ++nLow; + + while (Comparator(*Pivot, Items[nHigh])) + --nHigh; + + if (nLow >= nHigh) + return nHigh; + + Swap(Items[nLow], Items[nHigh]); + + // Update pointer if pivot was swapped + if (nPivotIndex == nLow) + Pivot = &Items[nHigh]; + else if (nPivotIndex == nHigh) + Pivot = &Items[nLow]; + + ++nLow; + --nHigh; + } + } + } + + // Sorts an array in-place using the Tony Hoare Quicksort algorithm + template + void QSort(T* Items, Comparator::TComparator Comparator, size_t nLow, size_t nHigh) + { + if (nLow < nHigh) + { + size_t p = Partition(Items, Comparator, nLow, nHigh); + QSort(Items, Comparator, nLow, p); + QSort(Items, Comparator, p + 1, nHigh); + } + } +} + +#endif diff --git a/src/rtpmididevice.cpp b/src/rtpmididevice.cpp new file mode 100644 index 0000000..2c29a94 --- /dev/null +++ b/src/rtpmididevice.cpp @@ -0,0 +1,80 @@ +// +// serialmididevice.cpp +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2022 The MiniDexed Team +// +// Original author of this class: +// 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 +#include +#include "rtpmididevice.h" +#include + +LOGMODULE("rtpmididevice"); + +CRTPMIDIDevice::CRTPMIDIDevice (CMiniDexed *pSynthesizer, + CConfig *pConfig, CUserInterface *pUI) +: CMIDIDevice (pSynthesizer, pConfig, pUI), + m_pConfig (pConfig) + + //m_Serial (pInterrupt, TRUE), + //m_nSerialState (0), + //m_nSysEx (0), + //m_SendBuffer (&m_Serial) +{ + AddDevice ("rtpdummy"); +} + +CRTPMIDIDevice::~CRTPMIDIDevice (void) +{ + //m_nSerialState = 255; +} + +boolean CRTPMIDIDevice::Initialize (void) +{ + m_pAppleMIDIParticipant = new CAppleMIDIParticipant(&m_Random, this); + if (!m_pAppleMIDIParticipant->Initialize()) + { + LOGERR("Failed to init RTP listener"); + return false; //continue without rtp midi + } + else + LOGNOTE("RTP Listener initialized"); + return true; +} + +// Methods to handle MIDI events + +void CRTPMIDIDevice::OnAppleMIDIDataReceived(const u8* pData, size_t nSize) +{ + LOGNOTE("Recieved RTP MIDI Data"); + printf ("MIDI-RTP: %02X %02X\n", + (unsigned) pData[0], (unsigned) pData[1]); + MIDIMessageHandler(pData, nSize); +} + +void CRTPMIDIDevice::OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) +{ + LOGNOTE("RTP Device connected"); +} + +void CRTPMIDIDevice::OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) +{ + // RemoveRTPDevice +} diff --git a/src/rtpmididevice.h b/src/rtpmididevice.h new file mode 100644 index 0000000..418fcde --- /dev/null +++ b/src/rtpmididevice.h @@ -0,0 +1,69 @@ +// +// .h +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2022 The MiniDexed Team +// +// Original author of this class: +// 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 _rtpmididevice_h +#define _rtpmididevice_h + +#include "mididevice.h" +#include "config.h" +#include "net/applemidi.h" + +#include +#include +#include +#include + +class CMiniDexed; + +class CRTPMIDIDevice : CAppleMIDIHandler, CMIDIDevice +{ +public: + CRTPMIDIDevice (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI); + ~CRTPMIDIDevice (void); + + boolean Initialize (void); + virtual void OnAppleMIDIDataReceived(const u8* pData, size_t nSize) override; + virtual void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) override; + virtual void OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) override; + //void OnAppleMIDIDataReceived(const u8* pData, size_t nSize); + //void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName); + //void OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName); + + //void Process (void); + + //void Send (const u8 *pMessage, size_t nLength, unsigned nCable = 0) override; + +private: + CConfig *m_pConfig; + + //CSerialDevice m_Serial; + //unsigned m_nSerialState; + //unsigned m_nSysEx; + //u8 m_SerialMessage[MAX_MIDI_MESSAGE]; + + //CWriteBufferDevice m_SendBuffer; + CBcmRandomNumberGenerator m_Random; + //CAppleMIDIHandler* m_MIDIHandler; + CAppleMIDIParticipant* m_pAppleMIDIParticipant; // AppleMIDI participant instance +}; + +#endif diff --git a/src/udpmididevice.cpp b/src/udpmididevice.cpp new file mode 100644 index 0000000..6080132 --- /dev/null +++ b/src/udpmididevice.cpp @@ -0,0 +1,89 @@ +// +// udpmididevice.cpp +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2022 The MiniDexed Team +// +// Original author of this class: +// 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 +#include +#include "udpmididevice.h" +#include + +#define VIRTUALCABLE 24 + +LOGMODULE("rtpmididevice"); + +CUDPMIDIDevice::CUDPMIDIDevice (CMiniDexed *pSynthesizer, + CConfig *pConfig, CUserInterface *pUI) +: CMIDIDevice (pSynthesizer, pConfig, pUI), + m_pSynthesizer (pSynthesizer), + m_pConfig (pConfig) +{ + AddDevice ("udp"); +} + +CUDPMIDIDevice::~CUDPMIDIDevice (void) +{ + //m_pSynthesizer = 0; +} + +boolean CUDPMIDIDevice::Initialize (void) +{ + m_pAppleMIDIParticipant = new CAppleMIDIParticipant(&m_Random, this); + if (!m_pAppleMIDIParticipant->Initialize()) + { + LOGERR("Failed to init RTP listener"); + return false; //continue without rtp midi + } + else + LOGNOTE("RTP Listener initialized"); + return true; + m_pUDPMIDIReceiver = new CUDPMIDIReceiver(this); + if (!m_pUDPMIDIReceiver->Initialize()) + { + LOGERR("Failed to init UDP MIDI receiver"); + delete m_pUDPMIDIReceiver; + m_pUDPMIDIReceiver = nullptr; + } + else + LOGNOTE("UDP MIDI receiver initialized"); +} + +// Methods to handle MIDI events + +void CUDPMIDIDevice::OnAppleMIDIDataReceived(const u8* pData, size_t nSize) +{ + MIDIMessageHandler(pData, nSize, VIRTUALCABLE); +} + +void CUDPMIDIDevice::OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) +{ + LOGNOTE("RTP Device connected"); +} + +void CUDPMIDIDevice::OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) +{ + LOGNOTE("RTP Device disconnected"); +} + +void CUDPMIDIDevice::OnUDPMIDIDataReceived(const u8* pData, size_t nSize) +{ + MIDIMessageHandler(pData, nSize, VIRTUALCABLE); +} \ No newline at end of file diff --git a/src/udpmididevice.h b/src/udpmididevice.h new file mode 100644 index 0000000..55bc7a2 --- /dev/null +++ b/src/udpmididevice.h @@ -0,0 +1,56 @@ +// +// udpmididevice.h +// +// Virtual midi device for data recieved on network +// +// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi +// Copyright (C) 2022 The MiniDexed Team +// +// Original author of this class: +// 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 _rtpmididevice_h +#define _rtpmididevice_h + +#include "mididevice.h" +#include "config.h" +#include "net/applemidi.h" +#include "net/udpmidi.h" + +class CMiniDexed; + +class CUDPMIDIDevice : CAppleMIDIHandler, CUDPMIDIHandler, public CMIDIDevice +{ +public: + CUDPMIDIDevice (CMiniDexed *pSynthesizer, CConfig *pConfig, CUserInterface *pUI); + ~CUDPMIDIDevice (void); + + boolean Initialize (void); + virtual void OnAppleMIDIDataReceived(const u8* pData, size_t nSize) override; + virtual void OnAppleMIDIConnect(const CIPAddress* pIPAddress, const char* pName) override; + virtual void OnAppleMIDIDisconnect(const CIPAddress* pIPAddress, const char* pName) override; + virtual void OnUDPMIDIDataReceived(const u8* pData, size_t nSize) override; + +private: + CMiniDexed *m_pSynthesizer; + CConfig *m_pConfig; + CBcmRandomNumberGenerator m_Random; + CAppleMIDIParticipant* m_pAppleMIDIParticipant; // AppleMIDI participant instance + CUDPMIDIReceiver* m_pUDPMIDIReceiver; + +}; + +#endif