Multicast UDP Tutorial

There are a bunch of ways to get data off a Particle onto a server or client on your local network. If the data doesn't change very often, the Particle cloud functions, either variables (pull) or events and SSE (push) are the easiest and where the Particle excels. If you can, you should use those methods.

You can also use a TCP connection to stream large amounts of data in real-time, but this can be a problem when you might need more than one client (receiver of the data from the Particle), or the client moves from machine to machine or the destination IP address changes frequently.

Another option, the one I investigate here, is to use multicast UDP. It works!

Without going into too much detail, UDP, the User Datagram Protocol, is one of the low-level TCP/IP protocols. It allows relatively small packets of data (up to 512 bytes always, and often up to 8 Kbytes, sometimes larger) to be sent between devices. The delivery is unreliable; packets may be dropped, or they may be delivered out of order.

This is a problem in some cases, but in my application, it was fine. I wanted to stream anemometer (wind speed and direction) information in real-time. If the wind was blowing, I wanted to be able to update the values at least once per second, more often than I could send through the cloud. But if I lost a packet, no big deal.

Normal UDP is unicast or point-to-point; you need to know the host name or IP address and port to send the data to. This is fine if the data is going to a known server on your network. Or you can use the Particle function feature to communicate the host and port to the Particle, but this gets messy and at high data rates, the Particle can't serve too many unicast clients.

But there's another way: Multicast UDP. Basically, the Particle sends data once by UDP, and it's received automatically by as many clients as are interested in it. Your home router takes care of all of the work, so the Particle doesn't get overloaded. Multicast can also be used across the Internet, in some cases, but that's more complicated, and security is a problem, so we won't deal with that here.

Normally, IP addresses on your home network will be something like 192.168.x.x or 10.x.x.x, but multicast IP addresses are in special ranges. In my example, I use the 239.x.x.x IP range, which is available for organization-specific use within a single local area network. I arbitrarily picked 239.1.1.234. If you're on a company network, you'll have to make sure you're not using an address already in use.

The other parameter is the UDP port number (1-65535). The port number must be not otherwise used by any computer that's receiving the data. Ports less than 1024 generally require root permissions for Unix-based machines. A safe bet is usually anything above 4096 and less than 49152, though there are a bunch of obscure exceptions. I arbitrarily picked 7123.

Also, remember that this is only suitable for non-sensitive data! Not only are the packets not encrypted, but anyone on your local network or Wi-Fi can subscribe to them and see the data. Also, there's nothing preventing a rogue server from pushing bogus data to the clients. But, again, for anemometer data on my home network, this is acceptable.

The full source code for the examples below is on github:
https://github.com/rickkas7/multicast-tutorial

Test circuit

The test circuit is just a potentiometer connected to A0 as an analog input. When read using analogRead, this results in values from 0-4095. I used this for testing on my desk instead of the actual anemometer, which is unwieldy!

Server software (Particle)

The server software is just a simple app that does some setup, reads from A0 using analogRead, and if the value changes, sends a packet of data using UDP multicast.

In this sample code, I send the data only when it changes, which makes sense. But since it's based on UDP and packets might be lost, it probably would also be a good idea to also transmit a packet periodically, maybe every 15 seconds or something, in case the last change was lost, or to provide initial state for a newly connected client.

// Multicast Sample Code

// Define this to also output debugging information to the serial console
//#define SERIAL_DEBUG

int lastValue = 0;

// UDP support
UDP udp;
const size_t bufferSize = 16; // Make this bigger if you have more data!
unsigned char buffer[bufferSize];
// Note: remoteIP is a multicast address, it should begin with 239.x.x.x or some other
// appropriate multicast address.
// It's not the same as your local IP address (192.168.x.x, for example)!
IPAddress remoteIP(239,1,1,234);

// The remote port must be otherwise unused on any machine that is receiving these packets
int remotePort = 7234;


void setup() {
    #ifdef SERIAL_DEBUG
    Serial.begin(9600);
    #endif

    // We don't listen for UDP packets, so pass 0 here.
    udp.begin(0);

    pinMode(A0, INPUT);
    Particle.variable("value", lastValue);
}

void loop() {
    int value = analogRead(A0);

    int delta = value - lastValue;

    // Only send a packet when data change is sufficiently large
    if (delta < -3 || delta > 3) {
        // Data buffer, we send the 16-bit integer value in network byte order (big endian)
        buffer[0] = value >> 8;
        buffer[1] = (value & 0xff);

        // Send the data
        if (udp.sendPacket(buffer, bufferSize, remoteIP, remotePort) >= 0) {
            // Success
            #ifdef SERIAL_DEBUG
            Serial.printlnf("%d", value);
            #endif
        }
        else {
            #ifdef SERIAL_DEBUG
            Serial.printlnf("send failed");
            #endif
            // On error, wait a moment, then reinitialize UDP and try again.
            delay(1000);
            udp.begin(0);
        }

        lastValue = value;
    }

    delay(100);
}

Client software (Java)

This is a simple Java text-based app that subscribes to UDP multicast and prints the value in any packet received in a loop. I tested this on a Mac, but theoretically it should work on all platforms that support multicast and Java.

The main difference between this and standard UDP code is that you instantiate a MulticastSocket and use the joinGroup() method to specify which multicast IP address to subscribe to. The actual receiving of the data looks like normal UDP code.

MulticastSocket socket = null;
InetAddress group = null;

try {
        socket = new MulticastSocket(7234);
        group = InetAddress.getByName("239.1.1.234");
        socket.joinGroup(group);

        DatagramPacket packet;
        while(true) {
            byte[] buf = new byte[256];
            packet = new DatagramPacket(buf, buf.length);
            socket.receive(packet);

            // Java byte values are signed. Convert to an int so we don't have to deal with negative values for bytes >= 0x7f (unsigned).
            int[] valueBuf = new int[2];
            for(int ii = 0; ii < valueBuf.length; ii++) {
                valueBuf[ii] = (buf[ii] >= 0) ? (int)buf[ii] : (int)buf[ii] + 256;
            }
            
            int value = (valueBuf[0] << 8) | valueBuf[1];
            
            System.out.println(value);
        }

Client software (Python)

Same thing, but written in Python. I tested this on a Mac and Ubuntu Linux, but theoretically it should work on all platforms that support multicast and Python.

#!/usr/bin/python

import socket
import struct

# http://stackoverflow.com/questions/603852/multicast-in-python
MCAST_GRP = '239.1.1.234'
MCAST_PORT = 7234

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

sock.bind((MCAST_GRP, MCAST_PORT))
mreq = struct.pack("4sl", socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)

sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

while True:
  data = sock.recv(16)
  print (ord(data[0]) << 8) | ord(data[1]);

Client software (iOS, Objective-C)

This is just a simple app for iOS to make sure it works. When new packets are received, items are added to the scrolling list with the value.

I did use the GCDAsyncUdpSocket library. You only need the single file from it, and it makes the code much easier to work with.

My code is all in ViewController.m. It should be self-explanatory.

- (void)setupSocket
{
    // http://stackoverflow.com/questions/13459116/how-to-receive-multicast-udp
    self.udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    NSError *error = nil;
    if (![self.udpSocket bindToPort:7234 error:&error])
    {
        NSLog(@"Error binding to port: %@", error);
        return;
    }
    if(![self.udpSocket joinMulticastGroup:@"239.1.1.234" error:&error]){
        NSLog(@"Error connecting to multicast group: %@", error);
        return;
    }
    if (![self.udpSocket beginReceiving:&error])
    {
        NSLog(@"Error receiving: %@", error);
        return;
    }
    NSLog(@"Socket Ready");
}

// GCDAsyncUdpSocketDelegate
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
    fromAddress:(NSData *)address withFilterContext:(id)filterContext
{
    const unsigned char *bytes = [data bytes];
    
    int value = (bytes[0] << 8) | bytes[1];

    NSString *str = [[NSString alloc] initWithFormat:@"%d", value];
    [self.items addObject:str];
    [self.tableView reloadData];

    
    NSLog(@"got data %d", value);
}

Client software (Android)

There are a couple of caveats to using multicast in Android. You must add permissions to AndroidManfest.xml:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>

You must create a multicast lock. By default, multicast packets are discarded because processing them uses extra battery power. You must tell the device you want to process them, perhaps in onCreate().

```
WifiManager wifi = (WifiManager)getSystemService(Context.WIFI_SERVICE);
if (wifi != null){
    WifiManager.MulticastLock lock = wifi.createMulticastLock("HelloAndroid");
    lock.acquire();
}
```

The networking code runs in a worker thread (runThread()) because socket.receive() is blocking and you can't run it from the main UI thread. However updating the user interface must be done from the UI thread, so that's the runOnUiThread() code.

10 Likes

Love this post especially the examples in multiple lanaguages.

@rickkas7 I am learning alot from your tutorials! So… Here’s the app… I am using the photon and serialEvent1() to process serial data constantly updating one-way… The data is type char to represent numbers 0-9… So, I’ve successfully have taken this data and am serving to a browser client. All I want to do is update the values being displayed once a second and so have played around with methods I’ve picked up from the community. I have learned that for faster updates to use the write(buf, len); instead of println(" … "); method. I update the values within the println() method using the String::format() assignment by inserting the var using " + var + " which I see is a problem to use the write(); method. Can I serve up a dynamic data web page using UDP in the first place? Because the ultimate objective is to serve up this to multiple clients…

I know you could embedd a java script to dynamically update the data based on the SoftAP Http Pages example, but I just want to glean from the experts if I would be wasting my time doing this thru the photon instead of developing a node.js app to do the multicast work!

I'm not positive what you want to do, but here's a code example that uses a SoftAP HTTP page and uses Javascript to display the value of a potentiometer in sort of real time.

I'm not sure how the multicast fits in, but you could certainly display a value received by multicast UDP instead of reading an analog pin.

1 Like

Thanks @rickkas7… Imagine a LED Scoreboard displaying a running time and lap splits and/or finish times but on a SMART TV browser… I also want to provide the same data to those having smart phones using their browser…