LEGO ecosystem

LEGO has made several generations of motorised, remote-controlled systems. Juno drives two of them today from the same kind of Java program, and three more are pending. Each is a small remote-control class: the board plays the part of LEGO's remote, app or infrared tower, and the LEGO hardware does the rest.

System Since What it is Protocol used Board with support Juno class
Powered Up 2017 Hubs (City, Technic, BOOST, DUPLO Train) with motors and sensors, driven by LEGO's apps Bluetooth LE UNO R4 WiFi, UNO Q RemotePoweredUpHub
Mindstorms EV3 2013 The third Mindstorms brick: four motor outputs, four sensor inputs, a screen and a speaker, running Linux Bluetooth Classic (serial port profile), LEGO's EV3 direct commands — Pending
Power Functions 2007 Motors, lights and servos plugged into an IR Receiver (8884), normally driven by the IR remote (8885) Infrared, through an IR LED on the board — Pending
Mindstorms NXT 2006 The second Mindstorms brick: three motor outputs, four sensor inputs, a screen and a speaker Bluetooth Classic (serial port profile), LEGO's NXT direct commands — Pending
Mindstorms RCX 1998 The yellow programmable brick: three motor outputs, three sensor inputs, an LCD and a speaker Infrared, through an IR LED and receiver on the board UNO R4 WiFi, UNO Q, Nano ESP32 RemoteRcx

LEGO Powered Up

RemotePoweredUpHub (io.github.jabrena.juno.remote.lego.poweredup) lets a Juno program remote-control a LEGO Powered Up hub — run its motors and set its status LED — the same way the LEGO apps do. The board acts as a Bluetooth Low Energy central and speaks the LEGO Wireless Protocol 3.0 to the hub. It is a compiler intrinsic backed by the Arduino ArduinoBLE library.

LEGO Technic Hub 88012 (Powered Up)

Connect to a hub

Create one RemotePoweredUpHub at startup, press the hub's green button so it advertises, and call connect. It scans for the first hub it finds, for up to the given milliseconds (0 waits forever). The hub drops the link when it switches off or goes out of range, so check isConnected() in long-running loops and connect again:

RemotePoweredUpHub hub = new RemotePoweredUpHub();

while (true) {
    if (!hub.isConnected()) {
        if (hub.connect(10_000)) {
            Serial.print("connected to hub type ");
            Serial.println(hub.hubType());   // TYPE_CITY_HUB, TYPE_TECHNIC_HUB, ...
        }
    }
    Delay.millis(500);
}

PoweredUpHubHello is the smallest check that a hub connects: it prints the battery level and waits for the hub again if the link drops. Close the LEGO apps first: a hub accepts one connection at a time.

Set the LED color

The hub's status LED takes a LEGO color index, from COLOR_OFF through COLOR_PINK, COLOR_BLUE, COLOR_GREEN, COLOR_YELLOW, COLOR_RED to COLOR_WHITE:

int[] colors = {
    HubColor.RED, HubColor.GREEN, HubColor.BLUE
};

while (hub.isConnected()) {
    for (int color : colors) {
        hub.setLedColor(color);
        Delay.millis(1000);
    }
}

PoweredUpHubHello connects and then cycles the LED through several colors.

Drive the motors

setMotorPower(port, percent) runs any Powered Up motor on PORT_A to PORT_D, from -100 (full reverse) to 100 (full forward). There are three ways to stop: setMotorPower(port, 0) lets the motor coast, brakeMotor stops it actively, and holdMotor holds a motor with a rotation sensor in place. portDevice(port) tells which motor the hub detected on a port:

if (hub.portDevice(HubPort.A) != RemotePoweredUpHub.DEVICE_NONE) {
    hub.getMotor(HubPort.A).drive(50);    // half speed forward
    Delay.millis(2000);
    hub.getMotor(HubPort.A).drive(-50);   // half speed backward
    Delay.millis(2000);
    hub.getMotor(HubPort.A).stop();
}

MagicSquarePoweredUpHub drives a two-motor vehicle (ports A and B) around a square. To drive two motors as one, as a vehicle's left and right wheels, pair them with linkMotors(PORT_A, PORT_B) and drive the pair with setLinkedMotorPower.

Lego mindstorms RCX

RemoteRcx (io.github.jabrena.juno.remote.lego.rcx) lets a Juno program remote-control a LEGO Mindstorms RCX brick over infrared: run its motors, read its sensors and buttons, play sounds and read its batteries. It follows LeJOS's RemoteNXT: one object for the brick, whose getters hand out its parts, all allocated once when the RemoteRcx is created. The board needs an IR receiver module and an IR LED, here on pins 2 and 3, pointed at the brick's infrared window. The motors, sensors, sounds and battery work with LEGO's own firmware; reading the buttons and writing text on the display need Juno's RCX firmware, downloaded to the brick once with RcxFirmwareDownload. The Nano ESP32 page shows the same setup with Grove infrared modules.

LEGO Mindstorms RCX 1.0

Installing the JUno firmware to lego mindstorms RCX

Download Juno's firmware to the brick once. Wire the IR receiver to pin 2 and the IR LED to pin 3, point both at the switched-on RCX from 10 to 50 cm, and keep them still for about ten minutes; the serial monitor shows the progress:

./mvnw -f juno-examples/pom.xml compile juno:upload \
  -Djuno.main=io.github.jabrena.juno.api.remote.lego.rcx.RcxFirmwareDownload \
  -Djuno.board=arduino-uno-r4-wifi
./mvnw -f juno-examples/pom.xml juno:monitor -Djuno.board=arduino-uno-r4-wifi

On a Nano ESP32 with the Nano Connector Carrier, put a Grove infrared emitter in the A0/A1 socket and a Grove infrared receiver on the I2C socket's SCL line, and run the carrier version instead:

./mvnw -f juno-examples/pom.xml compile juno:upload \
  -Djuno.main=io.github.jabrena.juno.api.remote.lego.rcx.NanoCarrierRcxFirmwareDownload \
  -Djuno.board=arduino-nano-esp32
./mvnw -f juno-examples/pom.xml juno:monitor -Djuno.board=arduino-nano-esp32

Connecting with the brick

A brick that already runs firmware is left alone, so resetting the board afterwards cannot delete it.

A program reaches the brick through an infrared receiver and emitter pair, either HX modules wired to plain pins or Grove modules plugged into Grove sockets. Create the RemoteRcx once, at startup, and call begin().

HX infrared modules

With an HX-M121 receiver and an HX-53 emitter, here on pins 2 and 3 of an UNO board:

HX-53 infrared emitter and HX-M121 infrared receiver modules
HXM121InfraredReceiver receiver = new HXM121InfraredReceiver(Gpio.getD2());   // DAT on D2
HX53InfraredEmitter emitter = new HX53InfraredEmitter(Gpio.getD3());         // DAT on D3
RemoteRcx rcx = new RemoteRcx(new RcxLink(), receiver, emitter);
rcx.begin();

Grove infrared modules

With Grove infrared modules on a Nano ESP32's Nano Connector Carrier, the emitter in the A0/A1 socket and the receiver on the I2C socket's SCL line, passing the Nano's clock:

Grove Infrared Emitter Grove Infrared Receiver
NanoConnectorCarrier carrier = new NanoConnectorCarrier();
GroveInfraredEmitter emitter = new GroveInfraredEmitter(carrier.getGroveA0());
GroveInfraredReceiver receiver = new GroveInfraredReceiver(carrier.getI2cScl());
RemoteRcx rcx = new RemoteRcx(new RcxLink(ArduinoNano.getClock()), receiver, emitter, ArduinoNano.getClock());
rcx.begin();

Either way, the examples below use the same rcx.

Use the motors

The brick has three motor outputs, getMotorA(), getMotorB() and getMotorC(); and combines two into one, as a vehicle's left and right wheels. drive(speed) runs them from -RcxMotor.MAX_LEVEL (full reverse) to MAX_LEVEL (7, full forward). stop() brakes them and flt() lets them coast:

RcxMotor wheels = rcx.getMotorA().and(rcx.getMotorC());

wheels.drive(RcxMotor.MAX_LEVEL);    // forward
Delay.millis(2000);
wheels.drive(-RcxMotor.MAX_LEVEL);   // backward
Delay.millis(2000);
wheels.stop();

RcxDriveAC drives a vehicle with its motors on outputs A and C.

Use the rotation sensor

A rotation sensor counts 16 steps per turn of its axle, signed by direction. Plug it into one of the brick's three inputs, getSensor1() to getSensor3(), and call begin() after rcx.begin(); reset() sets the count back to zero:

RcxRotationSensor rotation = new RcxRotationSensor(rcx.getSensor1());
rotation.begin();
rotation.reset();

while (true) {
    Serial.print(rotation.steps());       // 16 per turn
    Serial.print(" steps, ");
    Serial.print(rotation.degrees());     // 22.5 per step
    Serial.println(" degrees");
    Delay.millis(200);
}

RcxRotationSensorExample prints the steps and the angle each time the axle turns.

Use the touch sensor

isPressed() tells whether the touch sensor's button is down:

RcxTouchSensor touch = new RcxTouchSensor(rcx.getSensor1());
touch.begin();

while (!touch.isPressed()) {
    Delay.millis(50);
}
Serial.println("Pressed");

RcxTouchSensorExample prints Pressed or Released each time it changes.

Use the temperature sensor

temperatureTenths() returns the temperature in tenths of a degree Celsius, so 235 is 23.5 °C:

RcxTemperatureSensor temperature = new RcxTemperatureSensor(rcx.getSensor1());
temperature.begin();

int tenths = temperature.temperatureTenths();
Serial.print(tenths / 10);
Serial.print(".");
Serial.print(tenths % 10);
Serial.println(" C");

RcxTemperatureSensorExample prints the temperature once a second.

Use the light sensor

lightPercent() returns the light level from 0 to 100, or -1 if the brick did not answer:

RcxLightSensor light = new RcxLightSensor(rcx.getSensor1());
light.begin();

int percent = light.lightPercent();
if (percent >= 0) {
    Serial.print(percent);
    Serial.println(" %");
}

RcxLightSensorExample prints the light level twice a second.

Play music

The brick's speaker plays one of six built-in sounds (RcxAudio.SOUND_BEEP, SOUND_SWEEP_UP, ...) or a tone of a given frequency, from about 31 to 20000 Hz, for a duration in hundredths of a second. A tone replaces any sound still playing, so wait for it to end before the next:

RcxAudio audio = rcx.getAudio();
audio.playSound(RcxAudio.SOUND_BEEP);
Delay.millis(500);

int[] notes = {262, 294, 330, 349, 392};   // C, D, E, F, G
for (int note : notes) {
    audio.playTone(note, 30);              // 0.3 seconds
    Delay.millis(350);
}

RcxFurElise plays the melody of Beethoven's "Für Elise" on the brick.

Read the battery

getBattery().millivolts() asks the brick for its battery voltage. A negative value means the brick did not answer: it is off, out of range, or not facing the IR LED. That makes it a good first check that the link works:

int millivolts = rcx.getBattery().millivolts();
if (millivolts < 0) {
    Serial.println("No answer from the RCX");
} else {
    Serial.print(millivolts);
    Serial.println(" mV");
}

RcxPing reads the battery in a loop and reports whether the brick answers.

Read the buttons

With Juno's firmware, clicking View, Prgm or Run changes nothing on the brick: it remembers the click, and getButtons().clicked() returns the buttons clicked since the previous call as a mask of RcxButtons.VIEW, PRGM and RUN. The program decides what the display shows: getLcd().showText(...) writes up to four characters on the digits, and getLcd().clear() blanks them:

rcx.getLcd().clear();
while (true) {
    int clicked = rcx.getButtons().clicked();
    if ((clicked & RcxButtons.RUN) != 0) {
        rcx.getLcd().showText("RUN");
    }
    Delay.millis(200);
}