Piloter un bateau avec une application Android

Chez Spiria, les employés peuvent consacrer du temps de travail à des projets internes destinés à enrichir leurs compétences. L’un de ces projets, imaginé par le développeur Marc Tesson, était d’arriver à piloter un bateau radiocommandé avec une application Android. Une équipe de quatre développeurs, qui n’avait aucune expérience ni avec la plateforme Android ni avec l’ordinateur monocarte Raspberry Pi, s’est donc réunie pour mener à bien ce développement qui faisait appel à la fois à la programmation et l’électronique. Il leur aura fallu 8 sessions de 4 heures afin de terminer le projet. Marc vous raconte l’évolution de leur travail au cours de ces sessions, les écueils rencontrés et partage le code au cas où vous seriez tenté vous aussi de piloter un bateau (ou autre jouet radiocommandé) avec une application mobile.

Session 1, “Hello World”

Nous étions partis au départ avec l’idée de prendre un sous-marin, mais le fait que les signaux Wi-Fi et Bluetooth voyagent très mal dans l’eau ajoutait une complexité dont la résolution aurait demandé un temps excessif, sans apporter de réelle plus-value en regard des objectifs principaux du projet : se familiariser avec Android et les petits ordinateurs monocartes. Nous nous sommes donc rabattu sur un bateau, en l’occurrence ce superbe Power Venom (UDI 001) qui peut atteindre une vitesse de 20 km/h grâce à son puissant moteur et une coque en V :

Power Venom boat.

Power Venom (UDI 001). © Ripmax Ltd.

Pour réaliser notre projet, nous remplacerons la partie électronique du bateau par un Raspberry Pi. Cet ordinateur propose de nombreuses broches (GPIO) qui peuvent être utilisées pour “lire” et “écrire” afin de piloter des périphériques. Dans notre cas, il s’agit de contrôler le moteur et d’autres systèmes.

Après une installation simple et rapide, le mini-ordinateur est prêt à l’usage avec son interface graphique. Avec quelques lignes de code en Python, et en utilisant la libraire GPIO pour écrire sur les broches, il est très simple de faire allumer une DEL ; elle a simplement besoin d’une alimentation (et d’une résistance) pour s’allumer :

# 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

On peut aussi en connecter plusieurs et les faire clignoter :

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)

Et il n’est pas plus compliqué de faire tourner le moteur 5V DC qui est fourni avec notre kit de démarrage :

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)

Contrôler un servomoteur est un peu plus compliqué :

Starter kit servo.

Un servo fonctionne avec la modulation de largeur d’impulsions (Pulse Width Modulation, PWM). La position du servo dépend du rapport cyclique (duty-cycle) du signal qu’on lui envoie :

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)

Parallèlement, les deux développeurs affectés au côté Android du projet se sont attelés à l’installation d’Android Studio. Créer une application vide est relativement facile. Pour installer cette nouvelle application sur le téléphone, il faut d’abord brancher ce dernier à l’ordinateur, puis activer le mode “développeur”. Une fois le mode “développeur” activé, il est possible de transférer d’un simple clic le package de l’application d’Android Studio vers le téléphone. Après quelques autres clics, ils ont réussi à ajouter un bouton et du texte à l’interface de l’application. Un événement “click” sur le bouton change le texte affiché.

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");
        }
    };
}

À la fin de la session, l’équipe a jugé la prise en main du Raspberry Pi très facile. Il ne s’agit ni plus ni moins que d’un ordinateur après tout. Le langage Python associé à la libraire GPIO permet de piloter des périphériques simples en un tour de main. Du côté d’Android, la création et le déploiement d’une application simple sont aussi faciles : “click and drag” de widgets dans la maquette. Le plus laborieux est peut-être de positionner correctement les widgets les uns par rapport aux autres.

Session 2, communiquer

Pour utiliser le téléphone comme une radiocommande, il nous faut une connexion entre le serveur (le Raspberry Pi) et le client (le téléphone). Pour ce faire, nous allons utiliser le port Bluetooth de nos deux appareils.

Sur le Raspberry Pi

Activer le Bluetooth au niveau du système :

// Reset the Bluetooth adaptor
sudo hciconfig hci0 reset

// Restart de Bluetooth service
sudo service bluetooth restart

// Make the RaspberryPi discoverable
sudo hciconfig hci0 piscan

Utiliser le Bluetooth depuis notre script Python :

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

Maintenant, nous pouvons créer notre serveur en 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")

Sur le téléphone

Pour avoir Bluetooth dans notre application, il nous faut ajouter quelques lignes dans le manifeste pour autoriser l’utilisation des ressources Bluetooth :

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

Création du client :

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");
        }
    }
}

Réaliser une connexion unidirectionnelle via Bluetooth s’avère en fin de compte assez aisé. Mais notre Raspberry Pi demandait systématiquement l’autorisation de pairage chaque fois que le téléphone initialisait la connexion. Plus de recherche sur ce point sera nécessaire, car l’objectif est de ne plus avoir “d’interaction humaine” avec le Raspberry Pi une fois qu’il sera à bord du bateau.

Session 3, contrôler le moteur et le servo

Pour contrôler le moteur et le servo depuis le téléphone, nous avons établi un “protocole” de communication simple :

"throttle = [valeur décimale]"
  0 : arrêt du moteur
  ]0, 1] : moteur en marche avant
  [-1, 0[ : moteur en marche arrière
“steering = [valeur décimale]”
  0 : servo à 90
  ]0, 1] : servo à 0 : tourner à gauche
  [-1, 0[ : servo à 180 : tourner à droite

Code sur le 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 sur le télé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;
        }
    };
}

Le contrôle du servo et du moteur à partir du téléphone a fonctionné. Mais pour le moment, il n’est pas possible de faire varier la vitesse du moteur ni de le faire tourner en sens inverse (les pins IO du Raspberry Pi ne fournissent que 0 ou +Vcc).

Session 4, contrôler le moteur

Cette session a été consacrée à tenter de faire fonctionner correctement le moteur du bateau, mais en plus de nécessiter du 7V, soit plus que les 5V du moteur provenant du kit de démarrage que nous avions utilisé lors de nos précédents essais, il requiert un ampérage bien plus important que le Raspberry Pi ne peut fournir. Nous avons parallèlement amélioré l’interface utilisateur de l’application.

Session 5, contrôler le moteur, prise 2

La première solution serait d’utiliser un relais pour commander le moteur. Un relais se compose de deux circuits électriques indépendants : un circuit de commande, qui sera branché au Raspberry Pi, et un circuit de puissance, qui sera branché au moteur et à la batterie du bateau. Lorsqu’une tension est appliquée au circuit de commande, le circuit de puissance devient fermé et le courant peut donc circuler. Nous sommes alors bien capables de piloter le moteur avec le Raspberry Pi, mais il y a toujours des problèmes : pas de vitesse variable ni de marche arrière.

La seconde solution, c’est d’utiliser deux relais. Avec deux relais branchés plus ou moins en parallèle, chacun étant contrôlé par une broche du Raspberry Pi, nous sommes capables de faire fonctionner le moteur en mode marche avant et arrière. Mais il reste encore un problème : pas de vitesse variable !

Alors que le moteur est maintenant à peu près fonctionnel, nous avons eu un accident de parcours : après une mauvaise manipulation, un fil a touché un mauvais contact et Raspberry Pi est… mort.

Session 6, plan B

Utiliser un autre Raspberry Pi aurait été la solution facile, mais, après avoir eu entre les mains le Raspberry Pi et le bateau, une chose était sûre : jamais l’ordinateur n’aurait pu rentrer dedans sans modifications. Nous avons donc décidé un changement technique : utiliser un Arduino Mini à la place.

Arduino Mini.

L’Arduino Mini n’est pas un ordinateur comme le Raspberry Pi, mais un microcontrôleur. Il ne fait qu’une seule chose, celle qu’on lui a programmée. Autre changement : plus de Python, mais un langage apparenté au C.

Pour programmer l’Arduino Mini, il faut utiliser l’IDE Arduino sur un ordinateur. Après avoir édité son programme dans l’IDE, ce dernier doit faire la compilation avant qu’il soit possible de le transférer vers l’Arduino Mini. Bien entendu, pour pouvoir transférer vers l’Arduino Mini, celui-ci doit être connecté à l’ordinateur. L’Arduino Mini étant très mini comme son nom l’indique, il n’offre aucun connecteur pour se brancher directement. Mais en contrepartie, il a des broches Tx et Rx pour pouvoir réaliser une connexion série.

En utilisant un module USB-to-TTL, connecté à l’Arduino (Tx-Rx, Rx-Tx), nous pouvons brancher l’Arduino Mini à l’ordinateur.

USB-to-TTL.

Arduino, “Hello World”

Faire clignoter la DEL intégrée à la carte :

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);
}

Faire tourner le moteur :

#define kMotorPin 10

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

    delay(2000);

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

    delay(2000);
}

À ce point-là, nous avons le même problème de courant pour le moteur du bateau qu’avec le Rasperry Pi. Il est possible d’utiliser deux relais pour avant, arrêt et arrière, mais nous n’aurions toujours pas de vitesse variable.

Cependant, nous avons découvert que contrôler un servo est encore plus facile qu’avec le Raspberry Pi, car l’Arduino contient une librairie spécifique pour les servos ! Plus besoin de calculer le rapport cyclique pour avoir le bon 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);
    }
}

Maintenant, pour être au même niveau qu’avec le Raspberry Pi, il nous manque la communication Bluetooth. Comme avec le module USB-to-Serial, nous allons utiliser un module HC-05.

HC-05.

Le module HC-05 sera connecté au Arduino Mini grâce aux broches Tx et Rx, et nous fournira un émetteur/récepteur Bluetooth. Mais avant de pouvoir l’utiliser, nous allons devoir le configurer.

Pour ce faire, nous allons brancher le HC-05 sur l’USB-to-TTL (Tx-Rx, Rx-Tx). Ensuite, en maintenant appuyé le bouton du HC-05, nous connectons l’USB-to-TTL à l’ordinateur.

Le HC-05 s’allume en étant en mode “AT Command”, qui va nous permettre de voir et modifier la configuration du module. Avec l’outil de connexion “Serial Monitor” de l’IDE Arduino, il est possible de se connecter au module et d’exécuter quelques commandes. Les plus intéressantes sont :

    // 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

Si la configuration série du module est modifiée (AT+UART), alors il faudra aussi changer la configuration de “Serial Monitor” pour une utilisation suivante.

À ce stade, nous devrions être capable de mettre en place le “serveur Bluetooth” sur l’Arduino Mini et de se connecter avec l’application Android déjà réalisée (après une mise à jour de l’adresse MAC).

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);
  }
}

On branche l’Arduino Mini sur l’USB-to-TTL, l’USB-to-TTL à l’ordinateur, on compile et on transfère sur l’Arduino Mini. On débranche l’Arduino Mini du USB-to-TTL, on le rebranche sur le HC-05 et sur une alimentation, et on attend…

Sur le téléphone, on lance notre application et (normalement) la DEL du Arduino Mini s’allume ! On a une connexion, mais peut être que les données que l’on reçoit ne sont pas bonnes (vitesse, parité…). On verra ça une autre fois !

Bilan : la programmation Arduino est très simple. Le Arduino Mini étant très sobre, cela nous fait jongler un peu plus, en ayant à brancher/débrancher l’USB-to-TTL et le HC-05 à longueur de temps.

Session 7, valider la connexion

Maintenant que nous sommes capables de contrôler le servo et que nous avons une connexion Bluetooth, il est temps de faire fonctionner le servo depuis le téléphone.

Avant toute chose, nous allons changer le protocole défini lors de la troisième session. L’Arduino Mini étant bien moins performant que le Raspberry Pi (8/16 MHz vs 1,4 GHz), plutôt que d’envoyer une chaîne de caractères (“throttle = [value]”), nous allons envoyer un simple octet !

8e bit : 0 = vitesse, 1 = direction
7e bit : 0 = valeur positive, 1 = valeur négative

Ce qui nous donne :

x0111111 valeur positive maximum
x0000000 zéro
x1000000 également zéro
x1111111 valeur négative maximum

Avec 6 bits, nous avons un éventail de 63 valeurs pour le positif et le négatif, ce qui sera largement suffisant pour notre usage.

Encodage du côté du téléphone :

    // 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));
    }

Décodage du côté du Arduino Mini :

#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);
        }
    }
}

Après avoir transféré le programme sur l’Arduino Mini, on branche le servo et le module Bluetooth dessus et on démarre. Si tout va bien, on est maintenant en mesure de faire tourner le servo depuis le téléphone. Si cela ne marche pas, il faut vérifier les paramètres du module Bluetooth et recommencer. On peut aussi utiliser la DEL pour valider les statuts de isThrottle et isNegative.

Session 8, obtenir la vitesse variable

On sait déjà que l’on peut faire tourner le moteur de façon bidirectionnelle (en utilisant deux relais). Pour être capable d’ajouter la fonction de vitesse variable, nous aurions probablement pu ajouter des MOSFETs. À la place, afin de ne pas avoir à jouer plus dans l’électronique que notre mission de base ne laissait l’entendre, nous allons utiliser un ESC (Electronic speed control), un contrôleur de vitesse électronique.

Electronic speed control.

Malheureusement, l’ESC est arrivé sans aucune documentation. Après quelques recherches, il en ressort que, très vulgairement, l’ESC offre d’un côté un circuit de puissance, sur lequel nous brancherons notre batterie et notre moteur, et de l’autre coté, un circuit de commande, qui recevra l’Arduino Mini, le HC-05 et le servo. Le circuit de commande sera alimenté par l’ESC en 5V, ce qui est parfait pour nos modules ! De plus, le contrôle de l’ESC se fait avec de la modulation d’impulsion (PWM), comme notre servo. Cela devrait donc être assez facile à utiliser.

#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);
    }
    ...
}

Une fois l’Arduino Mini programmé, je m’empresse de tester. Hummm, les lumières de l’ESC s’allument quand des commandes throttle sont envoyées, mais le moteur ne tourne pas ! Après encore quelques recherches, je découvre que l’ESC nécessite d’être “activé” par une séquence.

void setup() {
    ...

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

Ahaha ! Avec ceci, on arrive à contrôler le moteur avec vitesse variable en marche avant “et” en marche arrière. Il demeure un problème : la marche arrière fonctionne correctement tant qu’on ne passe pas en marche avant. Une fois la marche avant engagée, plus capable de passer en marche arrière ! Quelques recherches plus tard, il s’avère que certains ESCs ont un mode de protection pour éviter de briser les moteurs ou autres parties mécaniques (comme les engrenages) en passant brutalement de la marche avant à la marche arrière.

#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);
    }
    ...
}

Finalement, nous sommes maintenant en mesure d’aller de la marche avant à la marche arrière ! Il aura fallu de nombreux essais-erreurs pour trouver “une” séquence valide et le “nombre magique”.

En conclusion

Du côté d’Android, avec une interface aussi simple que la nôtre, le développement a été relativement simple. Le plus compliqué a peut-être été d’avoir l’interface utilisateur désirée. Pour le reste, comme l’activation du Bluetooth et la création de la connexion, cela fut relativement aisé à mettre sur pied grâce à une bonne documentation.

Pour ce qui est du Raspberry Pi, pas trop de soucis là aussi. S’il avait survécu jusqu’au bout de notre réalisation, il aurait probablement été surdimensionné en regard de nos besoins. Et nous aurions peut-être eu du fil à retordre pour le configurer correctement afin que seule notre application soit exécutée au démarrage.

Et pour finir, l’Arduino Mini : mis à part son manque d’interface qui nous a forcés à jongler un peu pour le programmer et le déboguer, il s’est avéré très simple d’accomplir avec lui nos objectifs. La raison en est que microcontrôleur est pensé avant tout pour ce genre d’application : contrôler des lumières, des moteurs, des servos...

La bataille la plus difficile aura définitivement été d’obtenir de la documentation pour l’ESC.

Découvrez nos histoires
à succès.

Un projet de développement logiciel?
Contactez-nous!  

Cette entrée a été publiée dans Culture geek
par Marc Tesson.
Partager l’article
Articles récents