Spark server in Perl
Saturday, 17 June 2017 07:19

Particle Photon (formerly called Spark Core) offers easy integration of Wi-Fi connectivity for your home. IoT made easy by a great platform with loads of GPIO pins as well as a stable tool-chain (see also particle-cli) for firmware development and even a Web IDE.

Out of the box, those devices are configured to connect to the particle cloud, which can be used to remotely control the device, query internal variables as well as upload new firmware. For those environments where internet connection is not available or not desirable, a local cloud can be set up by means of the spark server. It implements most of the functionality, the cloud offers, including session management for the clients and AES-encrypted communication between the server and the devices and the server and the clients.

In a small environment with a central control node, however, the overhead of session control and encryption between client and server may be a performance issue and not desirable: I use a couple of Photons to control older devices, such as a 20-year old amplifier for MP3 playback and home theatre. Some of the amplifier's functions are exposed by GPIO pins soldered to optocouplers in parallel to the amp's front-end buttons. The playback server (Raspberry Pi) is the only client ever to talk to the amp Photon directly. The server, in turn, is controlled via a separate protocol. A whole particle cloud would just consume too many resources for just a small subset of its features is required. Instead, this small Perl library is used to enable the playback server (everything written in Perl) to talk to the Photon directly. Large parts of the spark protocol (including CoAP) are implemented: function calls, variable queries and event callbacks work reliably. Firmware uploads are not implemented, but can be added quickly if required.

In order to bind a Photon to the Perl-server, just consider the server to be a local cloud server. Therefore you need to

  1. Generate a new set of RSA keys for the server:

    openssl genrsa -out default_key.pem 2048
    openssl rsa -in default_key.pem -outform PEM -pubout -out default_key.pub.pem

    Alternatively, you can also take the keypair generated by running spark-server for the first time.

  2. Connect the Photon via USB and put the device into listening mode (press the MODE button until the LED blinks blue)

  3. Get the device's id by running

    particle identify
  4. Configure the device for your wireless network.

    particle serial wifi
  5. Put the device into DFU mode (press MODE button and tap RESET button until LED flashes yellow)

  6. Set server IP and upload server public key

    particle keys server default_key.pem IP_ADDRESS
  7. Generate a pair of device keys

    openssl genrsa -out DEVICEID.pem 1024
    openssl rsa -in DEVICEID.pem -pubout -out DEVICEID.pub.pem
    openssl rsa -in DEVICEID.pem -outform DER -out DEVICEID.der
  8. Download private key to the device

    particle keys load DEVICEID.der

Further, you need to set up the Perl-server by adapting the configuration file

  1. {
  2. "keys": {
  3. "server":"spark_keys/default_key.pem",
  4. "devices":"spark_keys"
  5. },
  6. "devices": {
  7. "DEVICEID":{
  8. "name": "amp",
  9. "events": ["power"]
  10. }
  11. }
  12. }
Listing: config.json
such that
  1. the 3rd line points to the server private key
  2. the 4th line points to the directory where the public device keys are stored
  3. the devices section lists all devices you want to use in your code. The name is an alias by which you can access the device in your code and the events array is a list of event names that your server should be listening to.

After this, you are ready to go and once the server is up and running, you can reboot your device (tap RESET) to connect.

Here is a sample implementation to illustrate usage of the library:

  1. #!/usr/bin/perl -w
  2.  
  3. use strict;
  4. use warnings;
  5.  
  6. use lib 'lib';
  7.  
  8. use AnyEvent;
  9. use JSON::PP;
  10. use Spark::Server;
  11.  
  12. # read in configuration file
  13. my $cfg = { };
  14. {
  15. local $/;
  16. open( my $h, "< config.json" );
  17. my $json = <$h>;
  18. close $h;
  19. $cfg = JSON::PP::decode_json( $json );
  20. }
  21.  
  22. # create a new Spark::Server object and define the init callback to be run right after the server read the config file and set up all dummy device objects
  23. my $server = Spark::Server->new( $cfg, { cb_init => sub {
  24. # this one lists the named devices from the config file. they need to be set up, so we can set up connect event handlers etc.
  25. foreach my $o ( keys %{$_[0]->{objects}} ) {
  26. print STDERR "DEVICE $o ".ref( $_[0]->{objects}{$o} )."\n";
  27. }
  28. } } );
  29.  
  30. # bind to USR1 signal
  31. my $sig1 = AnyEvent->signal( signal => 'USR1', cb => sub {
  32. # if device by the name of "amp" is registered, execute the procedure 'on' on the Photon
  33. if ( my $dev = $server->getDevice( name => "amp" ) ) {
  34. $dev->on;
  35. }
  36. } );
  37.  
  38. # bind to USR2 signal
  39. my $sig2 = AnyEvent->signal( signal => 'USR2', cb => sub {
  40. if ( my $dev = $server->getDevice( name => "amp" ) ) {
  41. $dev->off;
  42. }
  43. } );
  44.  
  45. # to bind an init callback you either
  46. # (1) named the device in the config file or
  47. # (2) it is an unnamed device
  48.  
  49. # (1) bind a couple of events for named devices before the server goes life.
  50. $server->_callbacks(
  51. amp => {
  52. # 'connect' and 'disconnect' are fired each time the device reconnects or loses TCP connection.
  53. 'connect' => sub {
  54. # $_[0] contains a reference to the device object
  55. print STDERR "CONNECTED AMP ".$_[0]->{protocol}{device}."\n";
  56.  
  57. # execute the procedure 'eventsOn' on the Photon (which I implemented to turn on the events triggering manually)
  58. $_[0]->eventsOn;
  59. },
  60.  
  61. # 'power' is a custom event sent by the Photon $_[1] and following arguments are parameters sent by the event
  62. 'power' => sub { print STDERR "POWER $_[1]\n"; }
  63. }
  64. );
  65.  
  66. # (2) this callback is run for each unnamed device
  67. $server->on_init( sub {
  68. my $dev = $server->{devices}{$_[0]->{id}}{device};
  69. $dev->on_connect( sub {
  70. print STDERR "CONNECTED (unnamed) ".$dev->{protocol}{device}."\n"; # the device id
  71.  
  72. # run the function 'digitalwrite' with argument 'D7,HIGH' (function of the standard firmware to set GPIO pin 7 high).
  73. # once it returns, the callback is fired with the return value
  74. $dev->digitalwrite( "D7,HIGH", sub { print STDERR "RETURN $_[0]\n"; } );
  75. } );
  76. } );
  77.  
  78. # run the main events loop
  79. my $cv = AnyEvent->condvar;
  80. $cv->recv;

You can see how to register callbacks for connect events (new device connects to the server), how to call functions (switch the amp on and off by means of USR1 and USR2 signals to the server process) on the device, and how to register callbacks for custom events implemented on the device (here, the power event).

There is also still quite some debugging output from the underlying protocol layers — but you will find the print statements to turn it off.