Spiria logo.

Piloting a boat with an Android application

August 29, 2019.

Spiria employees are encouraged to spend some time at work on pet projects that support skills development. One of these projects, dreamt up by developer Marc Tesson, was to pilot a radio-controlled boat through an Android application. A team of four developers with no experience with either the Android platform or the single-board Raspberry Pi mini-computer, got together to develop this application that required knowledge in both programming and electronics. It took eight four-hour sessions to finish the project. Marc tells us about the progress of their work over the eight sessions and the hazards they encountered. He also shares their code, in case you’re also interested in piloting a boat (or any radio-controlled toy) with a mobile app:

Session #1, “Hello World”

At first, we thought we’d pilot a submarine, but the fact that Wi-Fi and Bluetooth signals travel poorly in water added a layer of complexity that would have made it too time-consuming without adding any significant value with regards to the main goals of the project: to familiarize ourselves with Android and single-board mini-computers. We therefore fell back on a boat, specifically this superb Power Venom (UDI 001) with a cruising speed of 20km/h thanks to its powerful motor and V-shaped hull:

Power Venom boat.

Power Venom (UDI 001). © Ripmax Ltd.

To carry out our project, we will replace the electronic part of the boat with a Raspberry Pi. This computer has many pins (GPIO) that can be used to “read” and “write” to drive devices. In our case, it is about controlling the motor and other systems.

After a simple and quick installation, the minicomputer was ready for use with its graphical interface. With a few lines of code in Python, and using the GPIO library to write on the pins, it was very simple to switch on an LED; it simply needs a power supply (and a resistor) to switch on:

# Import the GPIO library
import RPi.GPIO as GPIO

# Set the GPIO mode : define the pin numbering we are going to use (BCM vs BOARD)
GPIO.setmode(GPIO.BCM)

# Initialize the pin #23 (BCM mode) as an output : we are going to write to it
GPIO.setup(23,GPIO.OUT)

# Write a HIGH value to pin #23 : it will output a positive voltage
GPIO.output(23,GPIO.HIGH)
# The LED connected to pin #23 is now ON

# Write a LOW value to pin #23 : it will output no voltage (or close to none)
GPIO.output(23,GPIO.LOW)
# The LED connected to pin #23 is now OFF

You can also connect several of them and make them blink:

import RPi.GPIO as GPIO
import time

kRedPin = 23
kYellowPin = 24
kGreenPin = 25

GPIO.setmode(GPIO.BCM)
GPIO.setup(kRedPin,GPIO.OUT)
GPIO.setup(kYellowPin,GPIO.OUT)
GPIO.setup(kGreenPin,GPIO.OUT)

for i in range (1,3):
    GPIO.output(kRedPin,GPIO.HIGH)
    time.sleep(1)
    GPIO.output(kRedPin,GPIO.LOW)

    GPIO.output(kYellowPin,GPIO.HIGH)
    time.sleep(1)
    GPIO.output(kYellowPin,GPIO.LOW)

    GPIO.output(kGreenPin,GPIO.HIGH)
    time.sleep(1)ArduinoMini
    GPIO.output(kGreenPin,GPIO.LOW)

And it is just as easy to simply run the 5V DC motor that is supplied with our starter kit:

Starter kit motor.

import RPi.GPIO as GPIO

kMotorPin = 13

GPIO.setmode(GPIO.BCM)
GPIO.setup(kMotorPin,GPIO.OUT)

GPIO.output(kMotorPin,GPIO.HIGH)
time.sleep(5)
GPIO.output(kMotorPin,GPIO.LOW)

Controlling a servomotor is a little more complicated:

Starter kit servo.

A servo works with Pulse Width Modulation (PWM). The position of the servo depends on the duty cycle of the signal being sent to it:

import RPi.GPIO as GPIO
import time

kServo = 12

GPIO.setmode(GPIO.BOARD)
GPIO.setup(kServo, GPIO.OUT)

# Create a PWM instance on the servo pin for a 50Hz frequency
myServo = GPIO.PWM(kServo, 50)
myServo.start(7.5)

# turn towards 90 degree
myServo.ChangeDutyCycle(7.5)
time.sleep(1)

# turn towards 0 degree
myServo.ChangeDutyCycle(2.5)
time.sleep(1)

# turn towards 180 degree
myServo.ChangeDutyCycle(12.5)
time.sleep(1)

Meanwhile, the two developers assigned to the Android side of the project had been working on installing Android Studio. Creating an empty application is relatively easy. To install this new application on the phone, you must first connect the phone to the computer and then activate the “developer mode”. Once this mode is activated, you can transfer the package from the Android Studio application to the phone with a single click. After a few more clicks, our developers managed to add a button and text to the application interface. A “click event” on the button changes the displayed text.

public class MainActivity extends AppCompatActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        Button myButton = (Button)findViewById(R.id.myButton);
        myButton.setOnClickListener(onMyButtonClicked);
        ...
    }

    private OnClickListener onMyButtonClicked = new OnClickListener() {
        public void onClick(View v) {
            TextView myText = (TextView) findViewById(R.id.myText);
            myText.setText("myButton has been clicked");
        }
    };
}

At the end of the session, the team found it very easy to get started with the Raspberry Pi: it is nothing more or less than a computer, after all. The Python language combined with the GPIO library allows simple devices to be controlled in no time at all. On the Android side, creating and deploying a simple application is also easy: “click and drag” widgets in the layout. Perhaps the most difficult thing is to position widgets correctly in relation to each other.

Session #2, communicating

To use the phone as a remote control, we needed a connection between the server (the Raspberry Pi) and the client (the phone). To do this, we used the Bluetooth port of both our devices.

On the Raspberry Pi

Enable Bluetooth at the system level:

// Reset the Bluetooth adaptor
sudo hciconfig hci0 reset

// Restart de Bluetooth service
sudo service bluetooth restart

// Make the RaspberryPi discoverable
sudo hciconfig hci0 piscan

Use Bluetooth from our Python script:

// Install the bluez package
sudo apt-get install bluez bluez-firmware

Now we can create our server in Python:

import bluetooth

server_sock=bluetooth.BluetoothSocket(bluetooth.RFCOMM)
server_sock.bind(("", bluetooth.PORT_ANY))
server_sock.listen(1)

port = server_sock.getsockname()[1]

uuid = "94f39d29-7d6d-437d-973b-fba39e49d4ee"

bluetooth.advertise_service(
    server_sock,
    "SampleServer",
    service_id = uuid,
    service_classes = [ uuid, bluetooth.SERIAL_PORT_CLASS ],
    profiles = [ bluetooth.SERIAL_PORT_PROFILE ], 
)
                   
print("Waiting for connection on RFCOMM channel %d" % port)

client_sock, client_info = server_sock.accept()
print("Accepted connection from ", client_info)

try:
    while True:
        data = client_sock.recv(1024)
        if len(data) == 0: break
        print("received [%s]" % data)
except IOError:
    pass

print("disconnected")

client_sock.close()
server_sock.close()
print("all done")

On the phone

To have Bluetooth in our application, we needed to add a few lines in the manifest to allow the use of Bluetooth resources:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

Client creation:

public class MainActivity extends AppCompatActivity {
    private BluetoothAdapter m_btAdapter = null;
    private BluetoothDevice m_btDevice = null;
    private BluetoothSocket m_btSocket = null;
    private OutputStream m_btOutStream = null;
    static int kRequestEnableBT = 12;
    static String kAddressBT = "00:00:00:00:00:00";  // MAC address of the Raspberry Pi Bluetooth device
    static final UUID kUUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); // dummy, but valid, uuid

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        // Init the bluetooth adapter 
        m_btAdapter = BluetoothAdapter.getDefaultAdapter();
        if (m_btAdapter != null && m_btAdapter.isEnabled() == false) {
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBtIntent, kRequestEnableBT);
        }
        startBT();
    }

    @Override
    protected void onDestroy() {
        stopBT();
        super.onDestroy();
    }

    // Print some debug info on our myText field
    private void debugBT(String msg) {
        TextView myText = findViewById(R.id.myText);
        myText.setText(msg);
    }

    // Init the Bluetooth connection
    private void startBT() {
        if (m_btAdapter == null) {
            debugBT("no adapter");
            return;
        }

        debugBT("get remote device...");
        m_btAdapter.cancelDiscovery();
        m_btDevice = m_btAdapter.getRemoteDevice(kAddressBT);
        if (m_btDevice == null) {
            debugBT("no device");
            return;
        }

        debugBT("create socket...");
        try {
            m_btSocket = m_btDevice.createInsecureRfcommSocketToServiceRecord(kUUID);
        }
        catch (Exception e) {
            m_btSocket = null;
            debugBT( e.getMessage());
            return;
        }

        debugBT("connect...");
        try {
            m_btSocket.connect();
        }
        catch (Exception e) {
            m_btSocket = null;
            debugBT( e.getMessage());
            return;
        }

        try {
            m_btOutStream = m_btSocket.getOutputStream();
            debugBT("connected");
        }
        catch (Exception e) {
            m_btSocket = null;
            debugBT( e.getMessage());
        }
    }

    // Terminate the Bluetooth connection
    private void stopBT() {
        if (m_btOutStream != null) {
            try {
                m_btOutStream.flush();
            }
            catch (Exception e) {
            }
            m_btOutStream = null;
        }
        if (m_btSocket != null) {
            try {
                m_btSocket.close();
            }
            catch (Exception e) {
            }
            m_btSocket = null;
        }
        debugBT("disconnected");
    }

    // Send a message via the Bluetooth socket
    private void sendMessage(String msg) {
        if(m_btOutStream == null) {
            return;
        }

        try {
            m_btOutStream.write(msg.getBytes());
        }
        catch(Exception e) {
        }
    }

    // Listen to the button and send a message over Bluetooth
    private OnClickListener onMyButtonClicked = new OnClickListener() {
        public void onClick(View v) {
            sendMessage("myButton has been clicked");
        }
    }
}

Setting up a unidirectional connection via Bluetooth is ultimately quite easy. But our Raspberry Pi systematically requested pairing authorization every time the phone initialized the connection. More research on this point was needed, as the objective was to no longer have “human interaction” with the Raspberry Pi once it was onboard the boat.

Session #3, controlling the motor and servo

To control the motor and servo from the phone, we established a simple “communication protocol”:

"throttle = [decimal value]"
  0 : motor stop
  ]0, 1] : forward
  [-1, 0[ : reverse
“steering = [decimal value]”
  0 : servo at 90
  ]0, 1] : servo at 0: turn left
  [-1, 0[ : servo at 180: turn right

Code on the Raspberry Pi:

// Init GPIO for servo and motor
// Init Bluetooth socket
...

def setThrottle(speed):
    // For now we only support 2 speeds: stop and max forward
    if speed > 0:
        GPIO.output(kMotorPin,GPIO.HIGH)
    else:
        GPIO.output(kMotorPin,GPIO.LOW)

def setSteerring(angle):
    // For now we only support 3 directions: full left, straight or full right
    if angle == 0:
        myServo.ChangeDutyCycle(2.5)
    else if angle < 0:
        myServo.ChangeDutyCycle(12.5)
    else:
        myServo.ChangeDutyCycle(7.5)

try:
    while True:
        data = client_sock.recv(1024)
        if len(data) == 0: break
        [key, value] = data.split('=')
        key = key.strip()
        value = float(value.strip())

        if (key == "throttle") {
            setThrottle(value)
        }
        if (key == "steering") {
            setSteerring(value)
        }

except IOError:
    pass

...

Android app.

Code on the phone:

public class MainActivity extends AppCompatActivity {
    // Bluetooth variables
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        // Init the Bluetooth adapter 
        ...

        // dpadButton is a float action button with a directionnal pad as image
        FloatingActionButton dpadButton = findViewById(R.id.dpadButton);
        dpadButton.setOnTouchListener(handleDPadTouch);
    }

    @Override
    public void onStop() {
        super.onStop();
        stopMotion();
    }

    // onDestroy
    // debugBT
    // startBT
    // stopBT
    // sendMessage

    private void sendMotion(float speed, float angle) {
        sendMessage("throttle = " + speed);
        sendMessage("steering = " angle);
    }

    private void stopMotion(float speed) {
        sendMotion(0.f, 0.f);
    }

    private static float clampValue(float value, float max, float min) {
        return Math.min( max, Math.max( value, min) );
    }

    private static float mapValue(float max, float value) {
        return clampValue( 1.f - (value-max)/max, 1.f, -1.f);
    }

    private View.OnTouchListener handleDPadTouch = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
                stopMotion();
                return true;
            }
            // From the touch position, get the vector from the center of the
            // dpadButton and retrieve the speed and angle
            float x = view.getWidth() / 2.f;
            float y = view.getHeight() / 2.f;
            float radius = Math.min(view.getWidth(), view.getHeight()) / 2.f;

            float speed = mapValue(radius, y + event.getY());
            float angle = - mapValue(radius, x + event.getX());
            sendMotion(speed, angle);
            return true;
        }
    };
}

Servo and motor control from the phone worked, but at this point, it was not possible to vary the speed of the motor or to put it in reverse, since the IO pins of the Raspberry Pi only provide 0 or +Vcc.

Session #4, controlling the motor

This session was devoted to trying to get the boat's motor to work properly, but in addition to requiring 7V, more than the 5V of the motor from the starter kit we had used in our previous tests, it requires a much higher amperage than the Raspberry Pi can provide. In the meantime, we improved the user interface of the application.

Session #5, controlling the motor, take 2

The first solution would have been to use a relay to control the motor. A relay consists of two independent electrical circuits: a control circuit, connected to the Raspberry Pi, and a power circuit, connected to the boat’s motor and battery. When voltage is applied to the control circuit, the power circuit becomes closed, allowing the electricity to flow. We could then drive the motor with the Raspberry Pi, but the problem remained: no variable speed or reverse functions.

The second solution would be to use two relays. With two relays, more or less connected in parallel, and each controlled by a pin on the Raspberry Pi, the motor could be operated in forward and reverse mode. But there is still one problem: no variable speed!

When the motor was about to work just fine, we had a little accident: after some improper handling, a wire touched the wrong contact and the Raspberry Pi... died.

Session #6, plan B

Using another Raspberry Pi would have been the easy solution, but after holding the Raspberry Pi and the boat in our hands, one thing became clear: the computer could never have been inserted in the boat without modifications. So we decided on a technical change: using an Arduino Mini instead.

Arduino Mini.

The Arduino Mini is not a computer like the Raspberry Pi, but a microcontroller. It does only one thing, i.e. what we program it to do. Another change: no more Python; we used instead a language similar to C.

To program the Arduino Mini, you must use the Arduino IDE on a computer. Once the program is edited in the IDE, it must compiled by the IDE before it can be transferred to the Arduino Mini. Of course, in order for it to transfer, it must be connected to the computer. As its name suggests, the Arduino Mini is very small. So small, in fact, that it does not have any connectors to connect directly; but it does have Tx and Rx pins to make a serial connection.

Using a USB-to-TTL module connected to the Arduino (Tx-Rx, Rx-Tx), you can connect the Arduino Mini to the computer.

USB-to-TTL.

Arduino, “Hello World”

Blinking the integrated LED on the card:

void loop() {
    // loop() is a system function that will be called repeatedly

    // send some voltage to the built-in LED
    digitalWrite(LED_BUILTIN, HIGH);
    // the builtin led in now ON

    delay(500);

    // send no voltage to the built-in LED
    digitalWrite(LED_BUILTIN, LOW);
    // the builtin led in now OFF

    delay(500);
}

Running the motor:

#define kMotorPin 10

void loop() {
    digitalWrite(kMotorPin, HIGH);
    // the motor now turns forward

    delay(2000);

    digitalWrite(kMotorPin, LOW);
    // the motor now stops

    delay(2000);
}

At this point, we had the same power problem for the boat’s motor as we did with the Rasperry Pi. We could use two relays for forward, stop and reverse, but we still wouldn’t have variable speed.

However, we discovered that controlling a servo is even easier than with the Raspberry Pi, because the Arduino contains a specific library for servos! No need to calculate the duty cycle to get the right angle!

#include <Servo.h>

#define kServoPin 11

Servo myServo;

void setup() {
    // setup() is a system function that will be called only once,
    // when the Arduino is powered ON

    // Attach the servo to our desired pin
    myServo.attach(kServoPin);
    // Rotate the servo in neutral position
    myServo.write(90);
}

void loop() {
    // slowly rotate the servo from 90˚ (neutral) to 0˚ (full left)
    for (int angle = 90; angle != 0; angle-=5) {
        myServo.write(angle);
        delay(100);
    }

    // slowly rotate the servo from 0˚ (full left) to 180˚ (full right)
    for (int angle = 0; angle <= 180; angle+=5) {
        myServo.write(angle);
        delay(100);
    }

    // slowly rotate the servo from 180˚ (full right) to 90˚ (neutral)
    for (int angle = 180; angle >= 90; angle-=5) {
        myServo.write(angle);
        delay(100);
    }
}

Now, to be on par with the Raspberry Pi, we needed Bluetooth communication. As with the USB-to-Serial module, we used an HC-05 module.

HC-05.

The HC-05 module connects to the Arduino Mini via the Tx and Rx pins and provides us with a Bluetooth transmitter/receiver. But before we could use it, we had to configure it.

To do this, we connected the HC-05 to the USB-to-TTL (Tx-Rx, Rx-Tx). Then, we connected the USB-to-TTL to the computer by holding down the HC-05 button.

The HC-05 lit up in “AT Command” mode, which allowed us to see and modify the module configuration. With the “Serial Monitor” connection tool of the Arduino IDE, we were able to connect to the module and execute some commands. The most interesting were:

    // check that module and AT mode are ok
    AT
    // should return "OK"

    // get the current module name
    AT+NAME?

    // set the module name
    AT+NAME=myName

    // get the mac address of the module
    AT+ADDR?

    // get the current PIN code
    AT+PSWD?

    // set the PIN CODE
    AT+PSWD=1234

    // get the current serial settings
    AT+UART?

    // set the serial setting to baud rate = 38400, stop bit = 1, parity = 0
    AT+UART=38400,1,0

If you modify the serial configuration of the module (AT+UART), you also need to change the configuration of “Serial Monitor” for subsequent use.

At this point, we should be able to set up the “Bluetooth server” on the Arduino Mini and connect with the Android application already created (after updating the MAC address).

void setup() {
    // initialize the serial connection between the ArduinoMini and the HC-05 module
    // baud rate 38400 as we configured the HC-05
    // SERIAL_8N1 which correspond to 8 data bits, no parity and on stop bit (as the HC-05)
    Serial.begin(38400, SERIAL_8N1);
    Serial.setTimeout(100);

    // turn off the built-in LED
    digitalWrite(LED_BUILTIN, LOW);
}

void loop() {
  if(Serial.available() > 0) {
      // we received something !!!
      // let turn the built-in LED on
      digitalWrite(LED_BUILTIN, HIGH);
  }
}

We connected the Arduino Mini to the USB-to-TTL and the USB-to-TTL to the computer, compiled, and transferred to the Arduino Mini. We disconnected the Arduino Mini from the USB-to-TTL, plugged it back into the HC-05 and a power supply, and waited...

We launched our application on the phone and, as we hoped, the Arduino Mini’s LED lit up! We had a connection, but was the data we received correct (speed, parity...)? We’ll see about that another time!

In short: Arduino programming is very simple. The Arduino Mini being very minimalistic, it made us juggle a little more, constantly plugging and unplugging the USB-to-TTL and HC-05.

Session #7, validating the connection

Since we were now able to control the servo and have a Bluetooth connection, it was time to operate the servo from the phone.

First of all, we changed the protocol defined during the third session. The Arduino Mini being much less powerful than the Raspberry Pi (8/16 MHz vs 1.4 GHz), rather than sending a string of characters ("throttle =[value]"), we’ll send a single byte.

8th byte: 0 = speed, 1 = direction
7th byte: 0 = positive value, 1 = negative value

Which gave us:

x0111111 maximum positive value
x0000000 zero
x1000000 also zero
x1111111 maximum negative value

With 6 bytes, we have a range of 63 values for positive and negative, which is more than enough for our purposes.

Encoding on the phone side:

    // We want to send a byte value, but the outstream interface to send a byte
    // takes an int ! -- the 24 high-order bits are ignored.
    // So we are going to work with int only
    private void sendByte(int value) {
        if(m_btOutStream == null) {
            return;
        }

        try {
            m_btOutStream.write(value);
        }
        catch(Exception e) {
        }
    }

    static int kThrottleMask = 0x80;
    static int kNegativeMask = 0x40;
    static int kValueMask    = 0x3F;

    // Convert a float value to 7bits
    private static int convertToByte(float value) {
        // 7th bit indicative a negative value
        // 011 1111 : positive max
        // 000 0000 : zero
        // 100 0000 : also zero
        // 111 1111 : negative max

        // the input value is expected to be in the range [-1, 1]
        int intValue = (int)(value * (float)kValueMask);
        boolean isNegative = intValue < 0;
        if (isNegative) {
            intValue = -intValue;
            intValue |= kNegativeMask;
        }
        return intValue;
    }

    private void sendMotion(float speed, float angle) {
        sendByte(kThrottleMask | convertToByte(speed));
        sendByte(convertToByte(angle));
    }

Decoding on the Arduino Mini side:

#define kThrottleMask 0x80
#define kNegativeMask 0x40
#define kValueMask    0x3F

#define kSteeringPin      11
#define kSteeringLeftMax  45    // angle in degrees
#define kSteeringNeutral  90    // angle in degrees
#define kSteeringRightMax 135   // angle in degrees
// LeftMax and RightMax angles can be adjusted

Servo mySteeringServo;

void setup() {
    Serial.begin(38400, SERIAL_8N1);
    Serial.setTimeout(100);

    mySteeringServo.attach(kSteeringPin);
    mySteeringServo.write(kSteeringNeutral);
}

void loop() {
    if (Serial.available() > 0) {
        // Serial.read return a byte as an int
        int value = Serial.read();

        bool isThrottle = (value & kThrottleMask) == kThrottleMask;
        bool isNegative = (value & kNegativeMask) == kNegativeMask;
        value &= kValueMask;

        if (isThrottle) {
            // Do nothing for now
        }
        else {
            int maxValue = isNegative ? kSteeringLeftMax : kSteeringRightMax;
            // convert value from input range [0, kValueMask] to steering range [kSteeringNeutral, maxValue]
            value = map(value, 0, kValueMask, kSteeringNeutral, maxValue);
            mySteeringServo.write(value);
        }
    }
}

After transferring the program to the Arduino Mini, connect the servo and Bluetooth module to it and hit play. If all goes well, you should be able to run the servo from the phone. If this does not work, check the Bluetooth module settings and start again. The LED can also be used to validate the status of isThrottle and isNegative.

Session #8, getting the variable speed

We already know that the motor can be driven bidirectionally (using two relays). To be able to add the variable speed function, we could probably have added MOSFETs. Instead, to avoid messing around with electronics more than our basic mission required, we used an ESC (Electronic speed control).

Electronic speed control.

Unfortunately, the ESC arrived without any documentation. After some research, we found out that basically, the ESC has a power circuit, to which we connected our battery and motor, and a control circuit, to which we connected the Arduino Mini, the HC-05 and the servo. The control circuit is powered by the ESC in 5V, which is perfect for our modules! In addition, ESC control is done with pulse-code modulation (PWM), like our servo. So it should be quite easy to use.

#define kThrottlePin         12
#define kThrottleReverseMax  1000   // micro seconds
#define kThrottleNeutral     1500   // micro seconds
#define kThrottleForwardMax  2000   // micro seconds
// Throttle values comes from Servo.writeMicroseconds() documentation
// may need to be adjusted depending on ESC

Servo myThrottleServo;

void setup() {
    ...

    myThrottleServo.attach(kThrottlePin);
    myThrottleServo.write(kThrottleNeutral);
}

void loop() {
    ...
    if (isThrottle) {
        int maxValue = isNegative ? kThrottleReverseMax : kThrottleForwardMax;
        // convert value from input range [0, kValueMask] to throttle range [kThrottleNeutral, maxValue]
        value = map(value, 0, kValueMask, kThrottleNeutral, maxValue);
        myThrottleServo.writeMicroseconds(value);
    }
    ...
}

Once the Arduino Mini was programmed, I hurried to test it. Uhhhh, the ESC lights came on when throttle commands were sent, but the motor didn’t run! After some more research, I discovered that the ESC needed to be “activated” by a sequence.

void setup() {
    ...

    myThrottleServo.attach(kThrottlePin);
    myThrottleServo.write(kThrottleForwardMax);
    delay(2000);
    myThrottleServo.write(kThrottleNeutral);
    delay(2000);
}

Aha! Now, the motor could be controlled at variable speeds in forward “and” in reverse. There was still a problem, though: reverse worked properly only until I switched to forward. Once forward was engaged, I could no longer shift into reverse! More research later, it appeared that some ESCs have a protection mode to avoid breaking motors or other mechanical parts (such as gears) by abruptly shifting from forward to reverse.

#define kThrottlePin         12
#define kThrottleReverseMax  1000   // micro seconds
#define kThrottleReverseMin  1450   // micro seconds
#define kThrottleNeutral     1500   // micro seconds
#define kThrottleForwardMin  1550   // micro seconds
#define kThrottleForwardMax  2000   // micro seconds
#define kThrottleMagicNumber 10

...

bool needReverseSequence = false;
void updateThrottle(int value) {
    if (value > kThrottleReverseMin && value < kThrottleForwardMin) {
        // value in the neutral zone
        value = kThrottleNeutral;
    }
    else if (value >= kThrottleForwardMin) {
        // next time we want do go in reverse we’ll need to do something!
        needReverseSequence = true;
    }
    else if (needReverseSequence) {
        // we were in forward motion before we need to do some black magic!
        myThrottleServo.writeMicroseconds(kThrottleReverseMin-kThrottleMagicNumber);
        delay(100);
        myThrottleServo.writeMicroseconds(kThrottleNeutral+kThrottleMagicNumber);
        delay(100);
        myThrottleServo.writeMicroseconds(kThrottleReverseMin-kThrottleMagicNumber);
        delay(100);
    }
    myThrottleServo.writeMicroseconds(value);
}

void loop() {
    ...
    if (isThrottle) {
        int maxValue = isNegative ? kThrottleReverseMax : kThrottleForwardMax;
        // convert value from input range [0, kValueMask] to throttle range [kThrottleNeutral, maxValue]
        value = map(value, 0, kValueMask, kThrottleNeutral, maxValue);
        updateThrottle(value);
    }
    ...
}

We were finally able to go from forward to reverse! It took much trial and error to find “one” valid sequence and the “magic number”.

In conclusion

On the Android side, with an interface as simple as ours, the development was relatively simple. Perhaps the most complicated thing was to have the desired user interface. For the rest, such as activating Bluetooth and creating the connection, it was relatively easy to set up, thanks to good documentation.

As for the Raspberry Pi, not too much to worry about here either. If it had survived to the end of our project, it would probably have been oversized in relation to our needs. And we might have had trouble configuring it correctly so that only our application was executed at startup.

And finally, the Arduino Mini: apart from its lack of interface that forced us to juggle with it a little bit to program and debug, it proved very simple to accomplish our objectives with it. The reason is that the microcontroller is designed primarily for this type of application: controlling lights, motors, servos...

The hardest battle was definitely to obtain documentation for the ESC.