diff --git a/bindings/csharp/multicast/README.txt b/bindings/csharp/multicast/README.txt index 2e4004b96..fbb130f50 100644 --- a/bindings/csharp/multicast/README.txt +++ b/bindings/csharp/multicast/README.txt @@ -2,15 +2,15 @@ Works on Windows and Linux Requires you get dotnet installed of course -For Wiundows install Visual Studio or such to get dotnet +For Windows install Visual Studio or such to get dotnet -On Ubuntu 20.04 it was this +On Ubuntu 21.04 it was this apt install snap apt install mono-complete -snap install dotnet-sdk --classic --channel=5.0 +snap install dotnet-sdk --classic --channel=6.0 snap alias dotnet-sdk.dotnet dotnet -snap install dotnet-runtime-50 --classic -snap alias dotnet-runtime-50.dotnet dotnet +snap install dotnet-runtime-60 --classic +snap alias dotnet-runtime-60.dotnet dotnet export DOTNET_ROOT=/snap/dotnet-sdk/current Once dotnet is OK @@ -19,4 +19,20 @@ dotnet build You should then be able to run -./bin/Debug/net5.0/multicast +./bin/Debug/net6.0/multicast + +====================================================== +Waiting for Net 7.0/8.0 to be in Ubunut main packages +Following did not work + +sudo apt remove 'dotnet*' 'aspnet*' 'netstandard*' +touch /etc/apt/preferences +// add to preferences +Package: dotnet* aspnet* netstandard* +Pin: origin "archive.ubuntu.com" +Pin-Priority: -10 +// end preferences +snap remove dotnet-sdk +snap install dotnet-sdk --classic --channel=7.0 +snap alias dotnet-runtime-70.dotnet dotnet + diff --git a/bindings/csharp/multicast/multicast.cs b/bindings/csharp/multicast/multicast.cs index 6e321cd09..9640e7704 100644 --- a/bindings/csharp/multicast/multicast.cs +++ b/bindings/csharp/multicast/multicast.cs @@ -9,6 +9,7 @@ namespace HamlibMultiCast { public class HamlibMulticastClient { + public string __comment1__; public class VFO { public string Name; @@ -17,19 +18,25 @@ namespace HamlibMultiCast public int Width; public bool RX; public bool TX; + public int WidthLower; + public int WidthUpper; } + public string __comment_spectrum__; public class SpectrumClass { public string Name; public int Length; + public string __comment_spectrum_data__; public string Data; public string Type; public int MinLevel; public int MaxLevel; public int MinStrength; public int MaxStrength; + public string __comment_spectrum_center__; public double CenterFreq; public int Span; + public string __comment_spectrum_fixed__; public double LowFreq; public double HighFreq; } @@ -38,14 +45,26 @@ namespace HamlibMultiCast public string Command; public string Status; } + public string __comment_gps__; + public class GPS { + public double Latitude; + public double Longitude; + public double Altitude; + } public string ID; + public string VFOCurr; public List VFOs { get; set; } public bool Split; + public string __comment_time__; + public string Time; public bool SatMode; public string Rig; public string App; + public string __comment_version__; public string Version; + public string __comment_seq__; public UInt32 Seq; + public string __comment_crc__; public string CRC; public List Spectra { get; set; } } diff --git a/bindings/csharp/multicast/multicast.csproj b/bindings/csharp/multicast/multicast.csproj index d08b4c72a..81870de07 100644 --- a/bindings/csharp/multicast/multicast.csproj +++ b/bindings/csharp/multicast/multicast.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net6.0 diff --git a/bindings/csharp/multicast/test.json b/bindings/csharp/multicast/test.json index 91828ab79..ba531aa4b 100644 --- a/bindings/csharp/multicast/test.json +++ b/bindings/csharp/multicast/test.json @@ -1,93 +1,16 @@ +"ID": "FT-991:/dev/ttyUSB0", +"Time": 2023-05-05T04:32:47.834952-0000, +"Sequence": 892, +"VFOCurr": "VFOA", +"VFOs": [ { - "__comment1__": "customizable rig identification -- will allow multiple rigs to be on the multicast", - "ID": "Rig#1", - "VFOs": [ - { - "Name": "VFOA", - "Freq": 14074000, - "Mode": "USB", - "Width": 2800, - "WidthLower": 200, - "WidthUpper": 3000, - "RX": true, - "TX": false - }, - { - "Name": "VFOB", - "Freq": 14076000, - "Mode": "USB", - "Width": 2800, - "WidthLower": 200, - "WidthUpper": 3000, - "RX": false, - "TX": true - }], - - "__comment_spectrum__": "Rigs that have spectrum output may include this data", - "Spectra": [ - { - "Name": "Main", - "Length": 475, - "__comment_spectrum_data__": "2-char hex bytes so data len=2*Length", - "Data": "00AAFF75BD2AAA...", - "Type": "FIXED|CENTER", - "MinLevel": 0, - "MaxLevel": 140, - "MinStrength": -100, - "MaxStrength": 0, - - "__comment_spectrum_center__": "If Type=CENTER, the following fields will be present:", - "CenterFreq": 14267000, - "Span": 25000, - - "__comment_spectrum_fixed__": "If SpectrumType=FIXED, the following fields will be present:", - "LowFreq": 14000000, - "HighFreq": 14250000 - }, - { - "Name": "Sub", - "Length": 475, - "__comment_spectrum_data__": "2-char hex bytes so data len=2*Length", - "Data": "00AAFF75BD2AAA...", - "Type": "FIXED|CENTER", - "MinLevel": 0, - "MaxLevel": 140, - "MinStrength": -100, - "MaxStrength": 0, - - "__comment_spectrum_center__": "If Type=CENTER, the following fields will be present:", - "CenterFreq": 14267000, - "Span": 25000, - - "__comment_spectrum_fixed__": "If SpectrumType=FIXED, the following fields will be present:", - "LowFreq": 14000000, - "HighFreq": 14250000 - }], - "LastCommand": { - "ID": "MyApp 123", - "Command": "set_freq VFOA 14074000", - "Status": "OK" - }, - "__comment_gps__": "Rigs that have gps output may include this data Lat -180 to 180 Lat -90 to 90, altitude in meters", - "GPS": - { - "Latitude": 31.6543, - "Longitude": -91.6543, - "Altitude": 640.1, - "__comment_gps__": "Time is in UTC gps synchronized for this block", - "Time": "2023-01-18T14:29:18.740561" - }, - - "Split": true, - "SatMode": false, - "__comment_Time__": "Time is from internal rig not GPS synced so may be inaccurate -- see GPS Time", - "Time": "2023-01-18T14:29:18.740561", - "Rig": "Dummy", - "App": "Hamlib", - "__comment_version__": "protocol version date YYYYMMDD", - "Version": "20210520", - "__comment_seq__": "Seq is 1-up sequence number 32-bit -- wraps around to 1 from 2^32-1", - "Seq": 1, - "__comment_crc__": "32-bit CRC of entire JSON record replacing the CRC value with 0x00000000", - "CRC": "0x00000000" +{ +, +"Name": "VFOA", +"Freq": 14086800, +"Mode": "AM", +"Width": 9000, +"RX": True, +"TX": False } + diff --git a/include/hamlib/rig.h b/include/hamlib/rig.h index d8d5ad3f8..69662c75a 100644 --- a/include/hamlib/rig.h +++ b/include/hamlib/rig.h @@ -2464,6 +2464,21 @@ struct rig_cache { int satmode; // if rig is in satellite mode }; +/** + * \brief Multicast data items the are unique per rig instantiation + * This is meant for internal Hamlib use only + */ +#include +struct multicast_s +{ + int multicast_running; + int sock; + int seqnumber; + int runflag; // = 0; + struct ip_mreq mreq; // = {0}; + pthread_t threadid; + struct sockaddr_in dest_addr; // = {0}; +}; /** * \brief Rig state containing live data and customized fields. @@ -2594,6 +2609,7 @@ struct rig_state { char client_version[32]; /*! +#include +#include +#include +#include +#include +//#include +#include +#include "hamlib/rig.h" +#include "misc.h" +#include "multicast.h" + +#define RIG_MULTICAST_ADDR "224.0.0.1" +#define RIG_MULTICAST_PORT 4532 + +#if 0 +static int multicast_running; +static int sock; +static int seqnumber; +static int runflag = 0; +static struct ip_mreq mreq = {0}; +static pthread_t threadid; +static struct sockaddr_in dest_addr = {0}; +#endif + + +static int multicast_status_changed(RIG *rig) +{ + int retval; +#if 0 + freq_t freq, freqsave = rig->state.cache.freqMainA; + + if ((retval = rig_get_freq(rig, RIG_VFO_A, &freq)) != RIG_OK) + { + rig_debug(RIG_DEBUG_ERR, "%s: rig_get_freq:%s\n", __func__, rigerror(retval)); + } + + if (freq != freqsave) { return 1; } + +#endif + + rmode_t mode, modesave = rig->state.cache.modeMainA; + pbwidth_t width, widthsave = rig->state.cache.widthMainA; + + if (rig->state.multicast->seqnumber % 2 == 0 + && (retval = rig_get_mode(rig, RIG_VFO_A, &mode, &width)) != RIG_OK) + { + rig_debug(RIG_DEBUG_ERR, "%s: rig_get_freq:%s\n", __func__, rigerror(retval)); + } + + if (mode != modesave) { return 1; } + + if (width != widthsave) { return 1; } + + ptt_t ptt, pttsave = rig->state.cache.ptt; + + if (rig->state.multicast->seqnumber % 2 == 0 + && (retval = rig_get_ptt(rig, RIG_VFO_A, &ptt)) != RIG_OK) + if (ptt != pttsave) { return 1; } + + split_t split, splitsave = rig->state.cache.split; + vfo_t txvfo; + + if (rig->state.multicast->seqnumber % 2 == 0 + && (retval = rig_get_split_vfo(rig, RIG_VFO_A, &split, &txvfo)) != RIG_OK) + if (split != splitsave) { return 1; } + + return 0; +} + +void json_add_string(char *msg, const char *key, const char *value) +{ + if (strlen(msg) != 0) + { + strcat(msg, ",\n"); + } + + strcat(msg, "\""); + strcat(msg, key); + strcat(msg, "\": "); + strcat(msg, "\""); + strcat(msg, value); + strcat(msg, "\""); +} + +void json_add_int(char *msg, const char *key, const int number) +{ + if (strlen(msg) != 0) + { + strcat(msg, ",\n"); + } + + strcat(msg, "\""); + strcat(msg, key); + strcat(msg, "\": "); + char tmp[64]; + sprintf(tmp, "%d", number); + strcat(msg, tmp); +} + +void json_add_double(char *msg, const char *key, const double value) +{ + if (strlen(msg) != 0) + { + strcat(msg, ",\n"); + } + + strcat(msg, "\""); + strcat(msg, key); + strcat(msg, "\": "); + char tmp[64]; + sprintf(tmp, "%f", value); + strcat(msg, tmp); +} + +void json_add_boolean(char *msg, const char *key, const int value) +{ + if (strlen(msg) != 0) + { + strcat(msg, ",\n"); + } + + strcat(msg, "\""); + strcat(msg, key); + strcat(msg, "\": "); + char tmp[64]; + sprintf(tmp, "%s", value == 0 ? "False" : "True"); + strcat(msg, tmp); +} + +void json_add_time(char *msg) +{ + char mydate[256]; + date_strget(mydate, sizeof(mydate), 0); + + if (strlen(msg) != 0) + { + strcat(msg, ",\n"); + } + + strcat(msg, "\"Time\": "); + strcat(msg, mydate); +} + +void json_add_vfoA(RIG *rig, char *msg) +{ + strcat(msg, "{\n"); + json_add_string(msg, "Name", "VFOA"); + json_add_int(msg, "Freq", rig->state.cache.freqMainA); + + if (strlen(rig_strrmode(rig->state.cache.modeMainA)) > 0) + { + json_add_string(msg, "Mode", rig_strrmode(rig->state.cache.modeMainA)); + } + + if (rig->state.cache.widthMainA > 0) + { + json_add_int(msg, "Width", rig->state.cache.widthMainA); + } + + json_add_boolean(msg, "RX", !rig->state.cache.ptt); + json_add_boolean(msg, "TX", rig->state.cache.ptt); + strcat(msg, "\n}\n"); +} + +void json_add_vfoB(RIG *rig, char *msg) +{ + strcat(msg, "{\n"); + json_add_string(msg, "Name", "VFOB"); + json_add_int(msg, "Freq", rig->state.cache.freqMainB); + + if (strlen(rig_strrmode(rig->state.cache.modeMainB)) > 0) + { + json_add_string(msg, "Mode", rig_strrmode(rig->state.cache.modeMainB)); + } + + if (rig->state.cache.widthMainB > 0) + { + json_add_int(msg, "Width", rig->state.cache.widthMainB); + } + + json_add_boolean(msg, "RX", !rig->state.cache.ptt); + json_add_boolean(msg, "TX", rig->state.cache.ptt); + strcat(msg, "\n},\n"); +} + +static int multicast_send_json(RIG *rig) +{ + char msg[8192]; // could be pretty big + char buf[4096]; +// sprintf(msg,"%s:f=%.1f", date_strget(msg, (int)sizeof(msg), 0), f); + msg[0] = 0; + snprintf(buf, sizeof(buf), "%s:%s", rig->caps->model_name, + rig->state.rigport.pathname); + json_add_string(msg, "ID", buf); + json_add_time(msg); + json_add_int(msg, "Sequence", rig->state.multicast->seqnumber++); + json_add_string(msg, "VFOCurr", rig_strvfo(rig->state.current_vfo)); + json_add_int(msg, "Split", rig->state.cache.split); + strcat(msg, ",\n\"VFOs\": [\n{\n"); + json_add_vfoA(rig, msg); + + // send the thing + multicast_send(rig, (unsigned char *)msg, strlen(msg)); + return 0; +} + +void *multicast_thread(void *vrig) +{ + int retval; + RIG *rig = (RIG *)vrig; + rig_debug(RIG_DEBUG_TRACE, "%s: multicast_thread started\n", __func__); + + // do the 1st packet all the time + multicast_status_changed(rig); + multicast_send_json(rig); + int loopcount = 4; + + while (rig->state.multicast->runflag) + { + hl_usleep(100 * 1000); + freq_t freq, freqsave = 0; + + if ((retval = rig_get_freq(rig, RIG_VFO_A, &freq)) != RIG_OK) + { + rig_debug(RIG_DEBUG_ERR, "%s: rig_get_freq:%s\n", __func__, rigerror(retval)); + } + + if (freq != freqsave || loopcount-- == 0) + { + multicast_status_changed(rig); + multicast_send_json(rig); + loopcount = 4; + } + + } + + return NULL; +} + +int multicast_init(RIG *rig, char *addr, int port) +{ + if (rig->state.multicast == NULL) + { + rig->state.multicast = calloc(1, sizeof(struct multicast_s)); + } + else if (rig->state.multicast->multicast_running) { return RIG_OK; } // we only need one port + + if (addr == NULL) { addr = RIG_MULTICAST_ADDR; } + + if (port == 0) { port = RIG_MULTICAST_PORT; } + + // Create a UDP socket + rig->state.multicast->sock = socket(AF_INET, SOCK_DGRAM, 0); + + if (rig->state.multicast->sock < 0) + { + rig_debug(RIG_DEBUG_ERR, "%s: socket: %s\n", __func__, strerror(errno)); + return -RIG_EIO; + } + + // Set the SO_REUSEADDR option to allow multiple processes to use the same address + int optval = 1; + + if (setsockopt(rig->state.multicast->sock, SOL_SOCKET, SO_REUSEADDR, &optval, + sizeof(optval)) < 0) + { + rig_debug(RIG_DEBUG_ERR, "%s: setsockopt: %s\n", __func__, strerror(errno)); + return -RIG_EIO; + } + + // Bind the socket to any available local address and the specified port + struct sockaddr_in saddr = {0}; + saddr.sin_family = AF_INET; + saddr.sin_addr.s_addr = htonl(INADDR_ANY); + saddr.sin_port = htons(port); + + if (bind(rig->state.multicast->sock, (struct sockaddr *)&saddr, + sizeof(saddr)) < 0) + { + rig_debug(RIG_DEBUG_ERR, "%s: bind: %s\n", __func__, strerror(errno)); + return -RIG_EIO; + } + + // Construct the multicast group address + // struct ip_mreq mreq = {0}; + rig->state.multicast->mreq.imr_multiaddr.s_addr = inet_addr(addr); + rig->state.multicast->mreq.imr_interface.s_addr = htonl(INADDR_ANY); + + // Set the multicast TTL (time-to-live) to limit the scope of the packets + unsigned char ttl = 1; + + if (setsockopt(rig->state.multicast->sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, + sizeof(ttl)) < 0) + { + rig_debug(RIG_DEBUG_ERR, "%s: setsockopt: %s\n", __func__, strerror(errno)); + return -RIG_EIO; + } + + // Join the multicast group + if (setsockopt(rig->state.multicast->sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, + &rig->state.multicast->mreq, sizeof(rig->state.multicast->mreq)) < 0) + { + rig_debug(RIG_DEBUG_ERR, "%s: setsockopt: %s\n", __func__, strerror(errno)); + return -RIG_EIO; + } + + // prime the dest_addr for the send routine + rig->state.multicast->dest_addr.sin_family = AF_INET; + rig->state.multicast->dest_addr.sin_addr.s_addr = inet_addr(addr); + rig->state.multicast->dest_addr.sin_port = htons(port); + + printf("starting thread\n"); + + rig->state.multicast->runflag = 1; + pthread_create(&rig->state.multicast->threadid, NULL, multicast_thread, + (void *)rig); + rig->state.multicast->multicast_running = 1; + return RIG_OK; +} + +void multicast_close(RIG *rig) +{ + int retval; + + // Leave the multicast group + if ((retval = setsockopt(rig->state.multicast->sock, IPPROTO_IP, + IP_DROP_MEMBERSHIP, &rig->state.multicast->mreq, + sizeof(rig->state.multicast->mreq))) < 0) + { + rig_debug(RIG_DEBUG_ERR, "%s: setsockopt: %s\n", __func__, strerror(errno)); + return; + } + + // Close the socket + if ((retval = close(rig->state.multicast->sock))) + { + rig_debug(RIG_DEBUG_ERR, "%s: close: %s\n", __func__, strerror(errno)); + } +} + +// if msglen=0 msg is assumed to be a string +int multicast_send(RIG *rig, unsigned char *msg, int msglen) +{ + // Construct the message to send + if (msglen == 0) { msglen = strlen((char *)msg); } + + // Send the message to the multicast group + ssize_t num_bytes = sendto(rig->state.multicast->sock, msg, msglen, 0, + (struct sockaddr *)&rig->state.multicast->dest_addr, + sizeof(rig->state.multicast->dest_addr)); + + if (num_bytes < 0) + { + rig_debug(RIG_DEBUG_ERR, "%s: sendto: %s\n", __func__, strerror(errno)); + return -RIG_EIO; + } + else + { +// printf("Sent %zd bytes to multicast group %s:%d\n", num_bytes, MULTICAST_ADDR, +// PORT); + } + + return num_bytes; +} + +#define TEST +#ifdef TEST +int main(int argc, char *argv[]) +{ + RIG *rig; + rig_model_t myrig_model; + rig_set_debug_level(RIG_DEBUG_NONE); + + if (argc > 1) { myrig_model = atoi(argv[1]); } + else + { + myrig_model = 1035; + } + + rig = rig_init(myrig_model); + + if (rig == NULL) + { + + } + + strncpy(rig->state.rigport.pathname, "/dev/ttyUSB0", HAMLIB_FILPATHLEN - 1); + rig->state.rigport.parm.serial.rate = 38400; + rig_open(rig); + multicast_init(rig, NULL, 0); + pthread_join(rig->state.multicast->threadid, NULL); + pthread_exit(NULL); + return 0; +} +#endif diff --git a/src/multicast.h b/src/multicast.h new file mode 100644 index 000000000..dc97015de --- /dev/null +++ b/src/multicast.h @@ -0,0 +1,47 @@ +//include +//#include +//#include +//#include +//#include +#include +//#include +//#include +//#include + +#ifndef MULTICAST_H +#define MULTICAST_H +#if 0 // moved to rig.h +struct multicast_s +{ + int multicast_running; + int sock; + int seqnumber; + int runflag; // = 0; + struct ip_mreq mreq; // = {0}; + pthread_t threadid; + struct sockaddr_in dest_addr; // = {0}; +}; +#endif + +struct multicast_vfo +{ + char *name; + double freq; + char *mode; + int width; + int widthLower; + int widthUpper; + unsigned char rx; // true if in rx mode + unsigned char tx; // true in in tx mode +}; + +struct multicast_broadcast +{ + char *ID; + struct multicast_vfo **vfo; +}; + +// returns # of bytes sent +int multicast_init(RIG *rig, char *addr, int port); +int multicast_send(RIG *rig, unsigned char *msg, int msglen); +#endif //MULTICAST_H diff --git a/tests/multicastclient.c b/tests/multicastclient.c new file mode 100644 index 000000000..d5a6f5a30 --- /dev/null +++ b/tests/multicastclient.c @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include +#include + +#define MULTICAST_ADDR "224.0.0.1" +#define PORT 4532 +#define BUFFER_SIZE 1024 + +int main() +{ + // Create a UDP socket + int sock = socket(AF_INET, SOCK_DGRAM, 0); + + if (sock < 0) + { + perror("socket creation failed"); + exit(EXIT_FAILURE); + } + + // Set the SO_REUSEADDR option to allow multiple processes to use the same address + int optval = 1; + + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) + { + perror("setsockopt failed"); + exit(EXIT_FAILURE); + } + + // Bind the socket to any available local address and the specified port + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(PORT); + + if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) + { + perror("bind failed"); + exit(EXIT_FAILURE); + } + + // Construct the multicast group address + struct ip_mreq mreq = {0}; + mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_ADDR); + mreq.imr_interface.s_addr = htonl(INADDR_ANY); + + // Join the multicast group + if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) + { + perror("setsockopt failed"); + exit(EXIT_FAILURE); + } + + // Receive multicast packets in a loop + while (1) + { + char buffer[BUFFER_SIZE]; + struct sockaddr_in src_addr = {0}; + socklen_t addrlen = sizeof(src_addr); + ssize_t num_bytes = recvfrom(sock, buffer, BUFFER_SIZE - 1, 0, + (struct sockaddr *)&src_addr, &addrlen); + + if (num_bytes < 0) + { + perror("recvfrom failed"); + exit(EXIT_FAILURE); + } + + buffer[num_bytes] = '\0'; + printf("Received %zd bytes from %s:%d\n%s\n", num_bytes, + inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port), buffer); + } + + // Leave the multicast group + if (setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) + { + perror("setsockopt failed"); + exit(EXIT_FAILURE); + } + + // Close the socket + close(sock); + + return 0; +}