Monday, March 9, 2015

Multicast communication between Arduino (Ethernet library W5100) and a Java application

Multicast is not supported by the Arduino Ethernet library but is supported by the underlying W5100 module. So to start off, this patch should be applied to the library (usually located in the arduino IDE directory under libraries\Ethernet\src).

Note that you may want to update the IP ranges below, I have used the 192.168.100.0/24 network for my tests and 239.100.100.100 multicast group.

Here is a modified version of the UDPSendReceive example:
/*
  UDPSendReceive.pde:
 This sketch receives Multicast UDP message strings, prints them to the serial port
 and sends an "acknowledge" string back to the group.

 created 21 Aug 2010
 by Michael Margolis
 modified 09 Mar 2015
 by Panos Gkikakis

 This code is in the public domain.
 */


#include <SPI.h>         // needed for Arduino versions later than 0018
#include <Ethernet.h>
#include <EthernetUdp.h>         // UDP library from: [email protected] 12/30/2008


// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = {
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};

// Arduino's IP
IPAddress ip(192, 168, 100, 177);

// Multicast group IP
IPAddress mip(239, 100, 100, 100);

// Port 
unsigned int localPort = 9999;      // local port to listen on

// buffers for receiving and sending data
char packetBuffer[50]; //buffer to hold incoming packet,
char  ReplyBuffer[] = "acknowledged";       // a string to send back
char  hiBuffer[] = "0:Client ready";       // a string to send back

int c = 0;
// An EthernetUDP instance to let us send and receive packets over UDP
EthernetUDP Udp;

void setup() {
  // start the Ethernet and UDP:
  Ethernet.begin(mac, ip);
  Udp.beginMulti(mip, localPort);
  

  Serial.begin(9600);
  
}

void loop() {
  // if there's data available, read a packet
  int packetSize = Udp.parsePacket();
  int n = 0;
  if (packetSize)
  {
    Serial.print("Received packet of size ");
    Serial.println(packetSize);
    Serial.print("From ");
    IPAddress remote = Udp.remoteIP();
    for (int i = 0; i < 4; i++)
    {
      Serial.print(remote[i], DEC);
      if (i < 3)
      {
        Serial.print(".");
      }
    }
    Serial.print(", port ");
    Serial.println(Udp.remotePort());

    // read the packet into packetBufffer
    n = Udp.read(packetBuffer, 50);
    packetBuffer[n] = 0;
    
    Serial.println("Contents:");
    Serial.println(packetBuffer);
  
    // send a reply, to the IP address and port that sent us the packet we received
    Udp.beginPacket(mip, Udp.remotePort());
    Udp.write(ReplyBuffer, sizeof(ReplyBuffer));
    Udp.endPacket();
  }
  
  if( c == 0 ) {
    hiBuffer[0] = (hiBuffer[0] > 56) ?  48 : (hiBuffer[0] + 1);
    Udp.beginPacket(mip, localPort);
    Udp.write(hiBuffer, sizeof(hiBuffer));
    Udp.endPacket(); 
    Serial.println(hiBuffer);
    c++;    
  } else if( c > 600) {
    c = -1;
  }
    
  c++;  
  delay(10);
}

And here is a corresponding "server" written in java (MLTestServer.java):

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MulticastSocket;
import java.net.InetAddress;
import java.net.DatagramPacket;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.concurrent.atomic.AtomicReference;

public class MLTestServer {

 
 public static void main(String[] args)  {
  
  String msg = "Server online. Keep 'em coming!\0";
  String myIP = "192.168.100.1";
  String multiAddress = "239.100.100.100";
  int port = 9999;
  byte[] buf = new byte[100];  
  ConsoleReader cr = new ConsoleReader();
  String ack = "acknowledged";
  
  try {
   new Thread(cr).start();
   InetAddress group = InetAddress.getByName(multiAddress);

   // Q: Why use two sockets one for sending and one for receiving?
   // A: There are issues when sending and receiving, namely some messages were not received. If you know why or if that's expected behavior pls let me know.   
   // Q: Why use the long version of MulticastSocket constructor and joinGroup?
   // A: We need to specify the Network interface to get reliable results on  multihomed hosts. 
   MulticastSocket s = new MulticastSocket(new InetSocketAddress(InetAddress.getByName(myIP),port));
   MulticastSocket r = new MulticastSocket(new InetSocketAddress(InetAddress.getByName(myIP),port));

   s.joinGroup(new InetSocketAddress(group,port), NetworkInterface.getByInetAddress(InetAddress.getByName(myIP)));
   r.joinGroup(new InetSocketAddress(group,port), NetworkInterface.getByInetAddress(InetAddress.getByName(myIP)));
   
   // say hello
   DatagramPacket hi = new DatagramPacket(msg.getBytes(), msg.length(),group, port);
   s.send(hi);
   
   System.out.println("Started server: IP " + s.getLocalAddress() + " port " + s.getLocalPort());
   System.out.println("\tMulticast: " + group.getAddress() + " port " + port);

   DatagramPacket recv = new DatagramPacket(buf, buf.length);
   // server loop
   r.setSoTimeout(5);
   while(!ConsoleReader.exit) {
    try { 
     java.util.Arrays.fill(buf, (byte) 0);
     r.receive(recv); 
    } catch(SocketTimeoutException te) {
     msg = cr.send.get();    
     if(msg != null && (msg.length() > 0)) {
      System.out.println("Sending: " + '"' + msg + '"');
      hi = new DatagramPacket(msg.getBytes(), msg.length(),group, port);
      cr.send.set("");
      s.send(hi);
     }     
     continue;
    }
    
    msg = new String(recv.getData());
    
    System.out.println("Message from: " + recv.getAddress() + ":" + recv.getPort() +  ":\n" + msg);  
    
    
   }
   s.leaveGroup(group);
   r.leaveGroup(group);
   s.close();
   r.close();
   
  } catch (Exception e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  
  
 }

}

// thread to read console input 
class ConsoleReader implements Runnable {

 public static boolean exit = false;
 public AtomicReference<String> send = new AtomicReference<String>();
 
 public void run() {
  String cmd;
  BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
  System.out.println("Commands:\t\nsend msg: sends a message (msg) to clients\t\nexit : end program");
  while(true) {
   try {
    cmd = console.readLine();
    if(cmd.compareToIgnoreCase("exit") == 0) {
     break;
    } else if (cmd.toLowerCase().startsWith("send ")) {
     this.send.set(cmd.substring(5) + "\0");
    } else {
     System.out.println("Unknown command");
    }
   } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    break;
   }
    
  }
  ConsoleReader.exit = true;
 }

}

 The code should be self explanatory, I have included comments for some gotchas. To compile the  java code simply run:

#compile
javac MLTestServer.java
#run

java -cp . MLTestServer

assuming off course javac and java is in the PATH.

No comments: