Deploying your code with Riot Control

RIoT Control is a tool that automates the deployment of your Java code to a Raspberry Pi or similar device. It finds your device on a local network, copies your java archives to it, creates a service configuration file, and starts your application or sets it as a service.

Currently, it is only available as a plugin for the SBT build tool.

Creating an empty RIoT project

The easy way first: There’s a template that will create an empty project that’s already set up. After installing sbt, execute the following from your root git directory or similar:

sbt new riot-framework/streams.g8

Check the template’s github template for more details.

Configuring your existing project to use RIoT Control

If you have an existing SBT project, add sbt-riotctl to your project/plugins.sbtfile:

addSbtPlugin("org.riot-framework" % "sbt-riotctl" % "0.5")

RIoT Control depends on SBT’s JavaServerAppPackaging, and will only be active if you’ve enabled it in your build.sbt:

enablePlugins(JavaServerAppPackaging)

In general, RIoT Control will then try to use sane default for everything, including the host name, username, and password used to log in to your Pi (it will use Raspbian’s defaults). If you need to override any of them, add the corresponding configuration to your project’s settings in build.sbt, for example:

lazy val root = (project in file("."))
  .enablePlugins(JavaServerAppPackaging)
  .settings(
  
    // Deployment Targets (hostname, username, password):
    riotTargets := Seq(
      riotTarget("raspberrypi", "pi", "raspberry")
      ),

    // Port to use for remote debugging:
    riotDbgPort := 8000,

    // Packages and features needed by your code:
    riotPrereqs := "oracle-java8-jdk wiringpi",
    riotRequiresI2C := false,
    riotRequiresSPI := false
    
  )

Deploying your application

RIoT Control will deploy the files generated by SBT in the stage directory, and use the scripts generated by SBT Native Packager’s JavaServerAppPackaging to start your Application.

To copy your application to your device and run it:

sbt riotRun

Then press [Return] twice to terminate it.

To debug your application, similarly run:

sbt riotDebug

and point your Remote Debugger to the device’s address and the port defined in the riotDbgPort setting (e.g. raspberrypi.local:8000).

If you want run the application in the background:

sbt riotStart

then stop it with:

sbt riotStop

Once you’re happy with the result, you can enable the Service that RIoT has created for your Application, which will cause it to start automatically when your device boots:

sbt riotInstall

To disable it, use the command:

sbt riotUninstall

In general, if issuing more than one command, it’s considerably faster to start SBT in interactive mode:

sbt

then enter the commands above (without sbt, for example just riotRun). This allows for quick development: Modify your code in the editor, then change to your terminal and execute riotRun in the interactive SBT shell.

Many IDE have some sort of plugin to integrate with SBT, speeding up the development cycle even more.

Troubleshooting deployment issues

Problems tend to arise most often when installing prerequisite packages. This only happens during the first deployment of an Application, and may take a long time depending on the packages installed.

Out of memory when installing prerequisite packages

As the apt-get traffic is proxied through the installation tool (your Pi may not have internet access), it may run out of memory when downloading large packages (such as the Java JDK). The following error is an indication that this is happening:

[info] E: Failed to fetch http://debian.anexia.at/raspbian/raspbian/pool/main/o/openjdk-11/openjdk-11-jdk-headless_11.0.3+7-5_armhf.deb  Connection failed [IP: 127.0.0.1 8080]
[info] E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?

If this is the case, make sure sbt has enough memory. The easiest way to do this is to add a file named .jvmopts in your project’s root directory:

-Xms1024M
-Xmx4096M
-Xss2M
-XX:MaxMetaspaceSize=1024M
Message ‘Release file … is not valid yet’

A freshly installed Pi without internet access will have a system date that is far in the past. RIoT tries to detect this and set the time to your development machine’s time, but if this fails for some reason, subsequent package installations will fail with rather confusing messages such as:

[info] E: Release file for http://raspbian.raspberrypi.org/raspbian/dists/buster/InRelease is not valid yet (invalid for another 80d 10h 26min 56s). Updates for this repository will not be applied.

In this case, make sure your Pi’s system clock is somewhat correct. It may help to wait for a while after the Raspberry Pi has booted, to allow it to synchronise to internet time sources or others.

Message ‘java.lang.NoClassDefFoundError: sun/misc/SharedSecrets’ with a Stack Trace

If you have Java 11 installed on your system, which handles access to certain classes in a more restrictive manner, you’ll see the following:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.pi4j.io.file.LinuxFile (file:/usr/local/streaming-i2c-bma280/lib/com.pi4j.pi4j-core-1.2.jar) to field java.nio.Buffer.address
WARNING: Please consider reporting this to the maintainers of com.pi4j.io.file.LinuxFile
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Uncaught error from thread [riot-bma280-demo-akka.actor.default-dispatcher-3]: sun/misc/SharedSecrets, shutting down JVM since 'akka.jvm-exit-on-fatal-error' is enabled for ActorSystem[riot-bma280-demo]
java.lang.NoClassDefFoundError: sun/misc/SharedSecrets
	at com.pi4j.io.file.LinuxFile.getFileDescriptor(LinuxFile.java:215)
	at com.pi4j.io.file.LinuxFile.ioctl(LinuxFile.java:103)
	at com.pi4j.io.i2c.impl.I2CBusImpl.selectBusSlave(I2CBusImpl.java:291)
	at com.pi4j.io.i2c.impl.I2CBusImpl.runBusLockedDeviceAction(I2CBusImpl.java:258)
	at com.pi4j.io.i2c.impl.I2CBusImpl.writeByte(I2CBusImpl.java:185)
	at com.pi4j.io.i2c.impl.I2CDeviceImpl.write(I2CDeviceImpl.java:131)

The Pi4J library, on which RIoT builds, is currently not compatible with some newer JDKs. Until this is the case, downgrade to a more widely used version of Java, such as Java 8:

sudo apt-get -y install openjdk-8-jdk-headless
sudo apt-get -y remove openjdk-9-jre-headless openjdk-10-jre-headless openjdk-11-jre-headless

Message ‘no jimage in java.library.path’ with a Stack Trace

Parts of Akka seem to rely on classes that aren’t present in headless JREs. Install the headless JDK:

sudo apt-get -y install openjdk-8-jdk-headless

What happens during deployment

RIoT control runs after SBT Native Packager’s JavaServerAppPackaging, and uses the files it generates.

First, it checks that the prerequisite packages defined in the riotPrereqs setting are installed (by default the JDK 8 and the WiringPi library), and otherwise installs them with apt-get. A local SOCKS5 proxy is started locally through which apt-get accesses the package repositories, so this step works even if your Pi has no direct internet access (this is often the case if it’s connected directly to your laptop).

Then, it checks that certain features, such as SPI or I2C ports, are enabled, depending on the riotRequires... settings.

Next, the stage directory is copied, in which JavaServerAppPackaging has built or copied all JARs, and generated start scripts. The target directory on the Pi is /usr/local/ followed by your project’s name.

A systemd ‘service unit configuration’ file is generated in /etc/systemd/system/ and named after your project (followed by .service, as is the convention).

This systemd service is then started, stopped, enabled or disabled depending on the RIoT Control command issued.

How RIoT Control finds your Pi

RIoT Control will first try to access your Raspberry Pi by Hostname (e.g. if it is called ‘raspberrypi’, RIoT Control will attempt to connect to raspberrypi and raspberrypi.local).

If this fails, then RIoT Control will attempt to use mDNS (a service provided by Bonjour / Zeroconf) to discover an SSH service with the name provided. If your Raspberry Pi has the Avahi service installed and running (this is the case by default), it will publish its SSH port this way, as well as its current IP address.