From b42447255aa101035e2d98f5d3d85feebd2c0781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Leischner?= <50262885+trackme518@users.noreply.github.com> Date: Tue, 1 Jul 2025 00:38:01 +0200 Subject: [PATCH] added support for VBAN ping packet --- src/AudioTools/AudioLibs/VBANStream.h | 161 ++++++++++++++++++++++++-- src/AudioTools/AudioLibs/vban/vban.h | 45 ++++++- 2 files changed, 197 insertions(+), 9 deletions(-) diff --git a/src/AudioTools/AudioLibs/VBANStream.h b/src/AudioTools/AudioLibs/VBANStream.h index 315d5fbfbc..9176794bdf 100644 --- a/src/AudioTools/AudioLibs/VBANStream.h +++ b/src/AudioTools/AudioLibs/VBANStream.h @@ -36,6 +36,18 @@ class VBANConfig : public AudioInfo { int max_write_size = DEFAULT_BUFFER_SIZE * 2; // just good enough for 44100 stereo uint8_t format = 0; + + //reply for discovery packet + uint32_t device_flags = 0x00000001; // default: receiver only + uint32_t bitfeature = 0x00000001; // default: audio only + uint32_t device_color = 0x00FF00; // green default + //const char* stream_name_reply = "VBAN SPOT PING"; + const char* device_name = nullptr; // nullptr means use MAC by default + const char* manufacturer_name = "ESP32 AudioTools"; + const char* application_name = "VBAN Streamer"; + const char* host_name = nullptr; // will fallback to WiFi.getHostname() + const char* user_name = "User"; + const char* user_comment = "ESP32 VBAN Audio Device"; }; /** @@ -356,10 +368,8 @@ class VBANStream : public AudioStream { // receive incoming UDP packet // Check if packet length meets VBAN specification: - if (len <= (VBAN_PACKET_HEADER_BYTES + VBAN_PACKET_COUNTER_BYTES) || - len > VBAN_PACKET_MAX_LEN_BYTES) { - LOGE("Packet length %u bytes", len); - rx_buffer.reset(); + if (len < VBAN_PACKET_HEADER_BYTES) { + LOGE("Too short to be VBAN (%u bytes)", len); return; } @@ -369,6 +379,48 @@ class VBANStream : public AudioStream { return; } + uint8_t protocol = udpIncomingPacket[4] & VBAN_PROTOCOL_MASK; + + if (protocol == VBAN_PROTOCOL_SERVICE) { + // Allow up to ~1024 bytes for service packets like Ping0 + if (len > 1024) { + LOGE("Service packet length invalid: %u bytes", len); + return; + } + } else { + // Audio, serial, etc + if (len <= (VBAN_PACKET_HEADER_BYTES + VBAN_PACKET_COUNTER_BYTES) || len > VBAN_PACKET_MAX_LEN_BYTES) { + LOGE("Audio/other packet length invalid: %u bytes", len); + rx_buffer.reset(); + return; + } + } + + //LOGI("VBAN format byte: 0x%02X", udpIncomingPacket[7]); + //LOGD("VBAN protocol mask applied: 0x%02X", udpIncomingPacket[7] & VBAN_PROTOCOL_MASK); + //Serial.printf("Header[7] = 0x%02X\n", udpIncomingPacket[7]); + + + //------------------------------------------------------------------------- + //SUPPORT PING REQUEST + if ( protocol == VBAN_PROTOCOL_SERVICE ) { + + uint8_t service_type = udpIncomingPacket[5]; + uint8_t service_fnct = udpIncomingPacket[6]; + + if (service_type == VBAN_SERVICE_IDENTIFICATION) { + bool isReply = (service_fnct & VBAN_SERVICE_FNCT_REPLY) != 0; + uint8_t function = service_fnct & 0x7F; + + if (!isReply && function == 0) { + LOGI("Received VBAN PING0 request"); + sendVbanPing0Reply(packet); + } + } + return; + } + //-------------------------------------------------------------------------- + vban_rx_data_bytes = len - (VBAN_PACKET_HEADER_BYTES + VBAN_PACKET_COUNTER_BYTES); vban_rx_pkt_nbr = (uint32_t*)&udpIncomingPacket[VBAN_PACKET_HEADER_BYTES]; @@ -378,10 +430,10 @@ class VBANStream : public AudioStream { uint8_t vbanSampleRateIdx = udpIncomingPacket[4] & VBAN_SR_MASK; uint8_t vbchannels = udpIncomingPacket[6] + 1; uint8_t vbframes = udpIncomingPacket[5] + 1; - uint8_t vbformat = udpIncomingPacket[7] & VBAN_PROTOCOL_MASK;; - uint8_t vbformat_bits = udpIncomingPacket[7] & VBAN_BIT_RESOLUTION_MASK;; + uint8_t vbformat = udpIncomingPacket[7] & VBAN_PROTOCOL_MASK; + uint8_t vbformat_bits = udpIncomingPacket[7] & VBAN_BIT_RESOLUTION_MASK; uint32_t vbanSampleRate = VBanSRList[vbanSampleRateIdx]; - + //LOGD("sample_count: %d - frames: %d", vban_rx_sample_count, vbframes); //assert (vban_rx_sample_count == vbframes*vbchannels); @@ -439,6 +491,101 @@ class VBANStream : public AudioStream { } } } +//------------------------------------------------------------------------------------- + //implement ping reply based on VBAN standard + void sendVbanPing0Reply(AsyncUDPPacket& sourcePacket) { + + // Prepare VBAN 28-byte service header + uint8_t header[28]; + memset(header, 0, sizeof(header)); + memcpy(header, "VBAN", 4); + header[4] = VBAN_PROTOCOL_SERVICE; + header[5] = VBAN_SERVICE_FNCT_PING0 | VBAN_SERVICE_FNCT_REPLY; // Service function + reply bit + header[6] = 0x00; // must be zero + // Copy incoming stream name from discovery packet + const uint8_t* data = sourcePacket.data(); + memcpy(&header[8], &data[8], 16); + // Copy frame number (little endian) + + uint32_t frameNumber = (uint32_t)((data[24] & 0xFF) | ((data[25] & 0xFF) << 8) | ((data[26] & 0xFF) << 16) | ((data[27] & 0xFF) << 24)); + memcpy(&header[24], &frameNumber, 4); + + // Construct the PING0 payload using the struct + VBAN_PING0 ping0; + memset(&ping0, 0, sizeof(ping0)); + + // Fill fields with your config data and fixed values + ping0.bitType = cfg.device_flags; + ping0.bitfeature = cfg.bitfeature; + ping0.bitfeatureEx = 0x00000000; + ping0.PreferedRate = 44100; + ping0.MinRate = 8000; + ping0.MaxRate = 96000; + ping0.color_rgb = cfg.device_color; + + // Version string, 8 bytes total (zero padded) + memcpy(ping0.nVersion, "v1.0", 4); + + // GPS_Position left empty (all zero), so no need to set + // USER_Position 8 bytes + memcpy(ping0.USER_Position, "USRPOS", 6); + // LangCode_ascii 8 bytes ("EN" + padding) + memset(ping0.LangCode_ascii, 0, sizeof(ping0.LangCode_ascii)); + memcpy(ping0.LangCode_ascii, "EN", 2); + // reserved_ascii and reservedEx are zeroed by memset + // IP as string, max 32 bytes + + char ipStr[16]; // Enough for "255.255.255.255\0" + sprintf(ipStr, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); + safe_strncpy(ping0.DistantIP_ascii, ipStr, sizeof(ping0.DistantIP_ascii)); + // Ports (network byte order) + ping0.DistantPort = htons(sourcePacket.remotePort()); + ping0.DistantReserved = 0; + + // Device name (64 bytes) + if (cfg.device_name && cfg.device_name[0] != '\0') { + safe_strncpy(ping0.DeviceName_ascii, cfg.device_name, sizeof(ping0.DeviceName_ascii)); + } else { + uint8_t mac[6]; + WiFi.macAddress(mac); + char macStr[64]; + snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + safe_strncpy(ping0.DeviceName_ascii, macStr, sizeof(ping0.DeviceName_ascii)); + } + + // Manufacturer name (64 bytes) + safe_strncpy(ping0.ManufacturerName_ascii, cfg.manufacturer_name, sizeof(ping0.ManufacturerName_ascii)); + // Application name (64 bytes) + safe_strncpy(ping0.ApplicationName_ascii, cfg.application_name, sizeof(ping0.ApplicationName_ascii)); + // Host name (64 bytes) + const char* hostName = cfg.host_name; + if (!hostName || hostName[0] == '\0') { + hostName = WiFi.getHostname(); + if (!hostName) hostName = "ESP32"; + } + safe_strncpy(ping0.HostName_ascii, hostName, sizeof(ping0.HostName_ascii)); + + // UserName_utf8 + safe_strncpy(ping0.UserName_utf8, cfg.user_name, sizeof(ping0.UserName_utf8)); + //UserComment_utf8 + safe_strncpy(ping0.UserComment_utf8, cfg.user_comment, sizeof(ping0.UserComment_utf8)); + + // Prepare final packet: header + payload + uint8_t packet[28 + sizeof(VBAN_PING0)]; + memcpy(packet, header, 28); + memcpy(packet + 28, &ping0, sizeof(VBAN_PING0)); + + // Send UDP packet + udp.writeTo(packet, sizeof(packet), sourcePacket.remoteIP(), sourcePacket.remotePort()); +} + + // Safely copy a C-string with guaranteed null termination + void safe_strncpy(char* dest, const char* src, size_t dest_size) { + if (dest_size == 0) return; + strncpy(dest, src, dest_size - 1); + dest[dest_size - 1] = '\0'; + } + //----------------------------------------------------------------------------------- }; } // namespace audio_tools \ No newline at end of file diff --git a/src/AudioTools/AudioLibs/vban/vban.h b/src/AudioTools/AudioLibs/vban/vban.h index 59785c2853..e5bfeb6356 100644 --- a/src/AudioTools/AudioLibs/vban/vban.h +++ b/src/AudioTools/AudioLibs/vban/vban.h @@ -22,7 +22,8 @@ // MODIFIED by R. Kinnett, https://github.com/rkinnett, 2020 // /////////////////////////////////////////////////////////////////////// - +#include +#include #ifndef __VBAN_H__ #define __VBAN_H__ @@ -104,7 +105,7 @@ enum VBanSampleRates }; -#define VBAN_PROTOCOL_MASK 0xE0 +#define VBAN_PROTOCOL_MASK 0xE0 enum VBanProtocol { VBAN_PROTOCOL_AUDIO = 0x00, @@ -159,6 +160,46 @@ enum VBanCodec }; +/******************************************************** + * SERVICE SUB PROTOCOL * + ********************************************************/ +// VBAN SERVICE PROTOCOL definitions +#define VBAN_PROTOCOL_SERVICE 0x60 + +// Service Types (format_nbc) +#define VBAN_SERVICE_IDENTIFICATION 0x00 +#define VBAN_SERVICE_CHATUTF8 0x01 +#define VBAN_SERVICE_RTPACKETREGISTER 0x20 +#define VBAN_SERVICE_RTPACKET 0x21 + +// Service Functions (format_nbs) +#define VBAN_SERVICE_FNCT_PING0 0x00 +#define VBAN_SERVICE_FNCT_REPLY 0x80 + +struct VBAN_PING0 { + uint32_t bitType; + uint32_t bitfeature; + uint32_t bitfeatureEx; + uint32_t PreferedRate; + uint32_t MinRate; + uint32_t MaxRate; + uint32_t color_rgb; + uint8_t nVersion[4]; + char GPS_Position[8]; // Keep empty (all zero) + char USER_Position[8]; + char LangCode_ascii[8]; + char reserved_ascii[8]; + char reservedEx[64]; + char DistantIP_ascii[32]; + uint16_t DistantPort; + uint16_t DistantReserved; + char DeviceName_ascii[64]; + char ManufacturerName_ascii[64]; + char ApplicationName_ascii[64]; + char HostName_ascii[64]; + char UserName_utf8[128]; + char UserComment_utf8[128]; +} __attribute__((packed)); /******************************************************** * TEXT SUB PROTOCOL * ********************************************************/