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.

Tuesday, March 3, 2015

Openldap 2.4 on Centos 7 using mdb

A bit confusing to set up especially for people used to the older slapd.conf. This is not used, do not try to set it, it will be ignored.

My setup uses the new back-end mdb.

The very basics is to install the server and client libraries (yum install openldap openldap-clients)

Configuration is stored under /etc/openldap/slapd.d/. You can view the contents but do not edit the files directly.

Remove the default configuration rm -rf /etc/openldap/slapd.d/*

Create a file for the initial configuration, let's say main.ldif:

#global configuration
dn: cn=config
objectClass: olcGlobal
cn: config
olcArgsFile: /var/run/openldap/slapd.args
olcPidFile: /var/run/openldap/slapd.pid
olcTLSCACertificatePath: /etc/openldap/certs
olcTLSCertificateFile: "OpenLDAP Server"
olcTLSCertificateKeyFile: /etc/openldap/certs/password
structuralObjectClass: olcGlobal
entryUUID: 0ca4a796-53e8-1034-90ac-5fa43e938d62
creatorsName: cn=config
createTimestamp: 20150228225117Z
entryCSN: 20150228225117.358858Z#000000#000#000000
modifiersName: cn=config
modifyTimestamp: 20150228225117Z


# Load the mdb backend
dn: cn=module,cn=config
objectClass: olcModuleList
cn: module
olcModulepath: /usr/lib64/openldap
olcModuleload: back_mdb


# Load external definitions
dn: cn=schema,cn=config
objectClass: olcSchemaConfig
cn: schema


# You may want to add more, make sure to satisfy any dependencies though
include: file:///etc/openldap/schema/core.ldif
include: file:///etc/openldap/schema/cosine.ldif
include: file:///etc/openldap/schema/nis.ldif
include: file:///etc/openldap/schema/inetorgperson.ldif
include: file:///etc/openldap/schema/openldap.ldif


# front end db
dn: olcDatabase=frontend,cn=config
objectClass: olcDatabaseConfig
objectClass: olcFrontendConfig
olcDatabase: frontend
olcAccess: to * by * read
olcSizelimit: size.soft=10000 size.hard=1000000
olcTimelimit: time.soft=300 time.hard=3600


# configuration db, we define a separate RootDN and passwd, use this one to change the config
dn: olcDatabase=config,cn=config
objectClass: olcDatabaseConfig
olcDatabase: config
olcRootDN: cn=Manager,cn=config
# generate a password by running slappasswd
olcRootPW: {SSHA}XXXXXXXXXXXXXXXXXXXX
olcMonitoring: FALSE
olcAccess: to * by * none
dn: olcDatabase=monitor,cn=config
objectClass: olcDatabaseConfig
olcDatabase: monitor
olcAddContentAcl: FALSE
olcLastMod: TRUE
olcMaxDerefDepth: 15
olcReadOnly: FALSE
olcSyncUseSubentry: FALSE
olcMonitoring: FALSE
olcAccess: to dn.subtree="cn=monitor"
  by dn.exact="cn=Manager,cn=config" read
  by dn.exact="cn=Manager,dc=example,dc=com" read
  by * none
dn: olcDatabase=mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: mdb
olcMonitoring: TRUE
olcDbMaxSize: 42949672960
olcSuffix: dc=example,dc=com
olcDbDirectory: /var/lib/ldap
olcRootDN: cn=Manager,dc=example,dc=com
# generate a password by running slappasswd
olcRootPW: {SSHA}XXXXXXXXXXXXXXXXXXXX
olcDbIndex: uid pres,eq
olcDbIndex: cn,sn pres,eq,approx,sub
olcDbIndex: mail pres,eq,sub
olcDbIndex: objectClass pres,eq
olcDbIndex: loginShell pres,eq

# Tuning, not sure what these do maybe test them
#olcDbConfig: set_cachesize 0 2097152 0
#olcDbConfig: set_lk_max_objects 1500
#olcDbConfig: set_lk_max_locks 1500
#olcDbConfig: set_lk_max_lockers 1500
#olcLastMod: TRUE
#olcMonitoring: TRUE
#olcDbCheckpoint: 512 30
olcAccess: to attrs=userPassword
   by self write
   by anonymous auth
   by dn.base="cn=Manager,dc=example,dc=com" write
   by * none
olcAccess: to attrs=shadowLastChange
   by self write
   by * read
olcAccess: to *
   by dn.exact="cn=Manager,dc=example,dc=com" write
   by * read
Now add it to the configuration:

cat main.ldif | slapadd -v -F /etc/openldap/slapd.d -n 0

Start the server:

systemctl start slapd

Add a root object:

ldapadd -h localhost -D "cn=Manager,dc=example,dc=com" -w YOURPASSWORDHERE <<EOF
dn: dc=example,dc=com
objectClass:top
objectClass:organization
objectClass: dcobject
o: Your organization's name
<<EOF

You should now install phpldapadmin or a client of your liking to add more data. A note for phpldapadmin, if you want to connect as the manager you should edit the configuration (/usr/share/phpldapadmin/config/config.php) and make sure

$servers->setValue('login','attr','dn');

is set. The default seems to be 'uid' which does not work with our manager definition.

To add to the configuration, for instance to define a custom object, attributes and so on you should use an LDAP client,  with the base dn cn=config and the user/pass combination specified for the config database, in our example user name is  cn=Manager,cn=config.

Useful links (somewhat contradicted info though):
  1. http://www.server-world.info/en/note?os=CentOS_7&p=openldap 
  2. http://www.nies.ch/doc/openldap-replication.en.php 
  3. Fixes for the OpenLDAP example config and deployment tips
  4. Admin Guide (clearly not uptodate, still useful though)