====== Emetteur pour transmission par led ======
===== Principe =====
Comme vous l'avez sans doute lu [[https://wiki.ensfea.fr/doku.php?id=diy:projets:transmissiondonneesled|précédemment]], nous allons
chercher à créer un programme permettant de **contrôler le clignotement d'une led** pour **transmettre des données** aussi
rapidement que possible.
Pour réaliser cet émetteur, nous allons nous travailler à la création interface qui
va nous permettre de **contrôler le gpio** du raspberry et à la **gestion des
changements d'états de la led**.
----
===== Développement =====
==== Interface avec le gpio ====
Comme vous l'avez compris, on va avoir besoin d'interagir avec le gpio du raspberry afin de faire, plus tard,
clignoter une led.
En fait il est **extrêmement simple d'allumer ou d'éteindre une led**. Vous pouvez même vous y amusez directement
depuis un shell, sans passer par aucun programme (notez simplement dans un coin de votre tête que j'ai travaillé sous Raspbian Lite, noyau version 4.14).
Tout d'abord, vous aurez besoin d'**exporter** la led via le pin du gpio sur lequel elle est connectée.
En shell, vous aurez simplement à taper ceci:
echo numero_du_pin_de_la_led > /sys/class/gpio/export
Il vous faut maintenant **paramétrer la direction de la led**. Pour ce faire, il vous suffit de procéder ainsi:
echo "out" > /sys/class/gpio/gpioX/direction
où **X est le numéro du pin** où vous avez branché la led. Le principe simple et comme vous
le voyez ce n'est vraiment **pas compliqué** !
Vous êtes maitenant prêts à **allumer/éteindre la led**, et le principe est le même:
echo V > /sys/class/gpio/gpioX/value
avec **V** qui peut valoir **1 pour éteindre la led** ou **0 pour allumer la led** (**ceci dépend de la façon
dont vous avez branché la led au raspberry**, notre branchement est sans doute hasardeux) et **X** qui
est toujours le **numéro du pin de la led**.
Quand vous aurez fini de jouer, vous devrez dé-exportez la led. Là aussi, c'est très simple:
echo numero_du_pin_de_la_led > /sys/class/gpio/unexport
Et voilà, le pin de la led est de nouveau "inactif".
Tout va se passer de la même manière en C++ ! En effet, vous pouvez très bien **ouvrir ces fichiers dans votre code** pour y
**effectuer toutes les opérations** dont je viens de vous parler, c'est comme ça que je vous recommande de procéder.
J'ai personnellement choisi de **créer une classe** pour encapsuler ces opérations. Le code de cette classe est
disponible ci-dessous au cas où vous ne souhaiteriez pas
tout réécrire vous même.
**Je ne vais pas détailler** ligne par ligne le code de l'interface dans ce tutoriel étant donné qu'il est très simple de développer une
classe qui implémenterai ces opérations (à condition d'être familier avec le C++) et que **ce n'est pas cette
partie** de l'émetteur qui présente le plus d'intérêt.
Dans tous les cas **vous avez toutes les clés en main**, soit **pour comprendre** le code que je met à votre disposition,
soit **pour développer votre propre interface**, gardez simplement à l'esprit que tout va se passer de la même manière
qu'avec les commandes shell ci-dessus.
Pour comprendre le code qui va suivre, je vais vous détailler ci-dessous les fonctions que j'ai implémenté dans
mon interface que je vais utiliser pour la **gestion de la led**.
* //**LedController**//: le constructeur de notre classe, vous ne me verrez l'utiliser qu'avec un seul paramètre, qui sera le numéro du pin de la led.
* //**turnOn**//: une méthode de la classe LedController qui allume la led, si cette dernière est déjà allumée, rien ne se passe (cette fonction ne prend aucun paramètre).
* //**turnOff**//: une autre méthode de LedController qui éteint la led, si cette dernière est déjà éteinte, rien ne se passe (cette fonction ne prend aucun paramètre)
Si vous êtes arrivés jusqu'ici, vous êtes prêt à attaquer **la partie amusante** !
==== Les changements d'états de la led ====
Dans cette partie, nous allons écrire un **main** qui va utiliser notre **API** pour allumer/éteindre la led
en fonction de la valeur de **chaque bit** de **chaque caractère** d'un message. Nous avons décidé d'utiliser des
**chaînes de caractères comme données à transmettre**, simplement parce qu'il nous sera plus facile (selon nous) de
visualiser les **erreurs de bits** sur des caractères que sur les valeurs des pixels d'une image ou tout autre
type de données qu'on aurait pu transmettre.
En concevant le code que je vais vous présenter, j'ai prévu **deux chaînes de caractères** particulières
qui **agirait comme des commandes**. Ces deux commandes sont //**/end**// pour terminer le programme
proprement et **libérer les ressources** //(dé-exporter le pin de la led)//, et //**/test**// qui envoie
une séquence particulière de 0 et de 1 (qui sont en fait des **"U"**), parce que cette alternance
cyclique de 0 et de 1 va nous permettre de visualiser d'éventuelles **erreurs de synchronisation**.
Voyons à quoi ressemble **la boucle de notre main**:
int main() {
// GPIO_PIN est définie via un define par la valeur du pin de la led
LedController lc(GPIO_PIN);
std::string message;
std::cout << "Waiting for a string to send: (\"/test\" to launch a sequence of 0 and 1, \"/end\" to end the program)" << std::endl;
// On récupère le premier message à envoyer, on n'utilise pas l'opérateur **>>** de std::cin pour conserver les espaces
std::getline(std::cin, message);
// Tant qu'on a pas saisi le message d'arrêt ("/end")
while(message != "/end") {
std::cout << "Waiting for a string to send: " << std::endl;
// On vérifie si on a reçu la séquence de test, si c'est le cas, on envoie un certains nombre de U
if(message == "/test") {
// Attention, suivant la fréquence que vous sélectionnerai pour les bits, cette séquence peut être très longue
transmit(lc, "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU");
}
else {
// Si on a un message lambda, on le trasnmet
transmit(lc, message);
}
std::cout << "Transfer complete.\n" << std::endl;
}
return ALL_GOOD;
}
Pour vous pouvez le voir, rien de bien compliqué là aussi. On récupère simplement des messages et on
les traite en conséquence. Notez que si vous souhaitez ajouter **plus de commandes**, vous devriez
utiliser un //**switch**// pour plus de clarté.
Passons maintenant au coeur du programme, la **conversion de bits en signaux lumineux**. Nous allons commencer
par créer la boucle nous permettant d'itérer sur les caractères d'un string. En **C++** et avec
**la norme C++14**, vous pouvez faire ça comme suit:
for(const char& c: message) {
// ...
}
Nous devons ensuite itérer //"sur les bits"// des caractères. Pour cela, nous allons réaliser un **ET**
bit à bit (via l'opérateur logique "**&**" en C++) avec tout les octets possibles où un seul bit serait
à un (ex: 10000000, 01000000, 001 ...). J'ai donc déclaré un tableau constant en variable globale
qui contiendra tout ces octets.
const unsigned char powers_of_two[8] = {128, 64, 32, 16, 8, 4, 2, 1};
Nous pouvons maintenant tester pour tout les caractères (en utilisant une boucle) de cette façon:
const bool lightUp = c & powers_of_two[i];
Au final, **votre code devrait ressembler a quelque chose comme ça**:
void transmit(LedController& lc, const std::string& message) {
// On initialise la variable timer comme le temps référence de notre transmission
auto timer = std::chrono::high_resolution_clock::now();
// On allume la led pour signaler au récepteur le début de la transmission
lc.turnOn();
// On parcourt le message
for(const char& c: message) {
// On parcourt les bits de chaque caractère
for(unsigned int i = 0; i < 8; i++) {
// On incrémente le timer d'un pas pré-calculé (dépendant le la fréquence de transmission choisie)
timer += time_step;
// On regarde avant la pause si on doit allumer ou non la led
const bool lightUp = c & powers_of_two[i];
// On attend
std::this_thread::sleep_until(timer);
// On positionne la led dans le bon état
lightUp ? lc.turnOn() : lc.turnOff();
}
}
std::this_thread::sleep_until(timer + time_step);
lc.turnOff();
// On attend suffisamment pour que le récepteur reçoive un octet de zéros pour marquer la fin de transmission
std::this_thread::sleep_for(2 * time_step);
}
En ce qui concerne **les pauses**, je vous invite simplement à consulter le code ci-dessus,
vous avez sans doute remarquer que le "schéma" est simple et ne nécessite pas d'explications
détaillées.
----
===== Sources du projet =====
==== La classe pour contrôler le gpio ====
#include "ledcontroller.hpp"
// Private method that export the choosen gpio
void LedController::export_gpio() {
std::ofstream exportFile("/sys/class/gpio/export");
if(!exportFile.is_open()) {
std::cerr << "ERROR: Could not export gpio " << this->numero << "." << std::endl;
throw ERR_EXPORT;
}
exportFile << this->numero;
exportFile.close();
}
// Private method that unexport the previously opened gpio
void LedController::unexport_gpio() {
std::ofstream unexportFile("/sys/class/gpio/unexport");
if(!unexportFile.is_open()) {
std::cerr << "ERROR: Could not unexport gpio " << this->numero << "." << std::endl;
throw ERR_UNEXPORT;
}
unexportFile << this->numero;
unexportFile.close();
}
// Constructor of our class:
// numero: numero of the gpio's pin we want to open
// dirOut: boolean indicating if the direction we want is out or not (as it is a LedController, it is set to true by default)
// litUp: boolean indicating if the led should be initially on or off
LedController::LedController(int numero, bool dirOut, bool litUp) {
this->numero = numero;
export_gpio();
// Adding a little delay to make sure that the files created when exporting exists before we start working on them
std::this_thread::sleep_for(std::chrono::milliseconds(EXPORT_DELAY));
std::ofstream directionFile(std::string("/sys/class/gpio/gpio" + std::to_string(numero) + "/direction").c_str());
if(!directionFile.is_open()) {
unexport_gpio();
std::cerr << "ERROR: Could not open \"direction\" for gpio " << numero << "." << std::endl;
throw ERR_EXPORT;
}
// Setting the direction according to the boolean dirOut
directionFile << (dirOut ? "out" : "in");
directionFile.close();
if(!litUp) {
turnOff();
}
else {
turnOn();
}
}
// Destructor that will unexport the gpio previously opened
LedController::~LedController() {
unexport_gpio();
}
// Public method lighting up the led
void LedController::turnOn() {
std::ofstream value;
value.open(std::string("/sys/class/gpio/gpio" + std::to_string(this->numero) + "/value").c_str());
if(!value.is_open()) {
unexport_gpio();
std::cerr << "ERROR: Could not open \"value\" for gpio " << this->numero << "." << std::endl;
throw ERR_VALUE;
}
value << "0";
value.close();
}
// Public method turning off the led
void LedController::turnOff() {
std::ofstream value;
value.open(std::string("/sys/class/gpio/gpio" + std::to_string(this->numero) + "/value").c_str());
if(!value.is_open()) {
unexport_gpio();
std::cerr << "ERROR: Could not open \"value\" for gpio " << this->numero << "." << std::endl;
throw ERR_VALUE;
}
value << "1";
value.close();
}
==== Son header ====
#ifndef LEDCONTROLLER_HPP
#define LEDCONTROLLER_HPP
#include
#include
#include
#include
#include
// Refer to the constructor source code for more info on this define
#define EXPORT_DELAY 400
// A few define to make errors handling clearer
#define ERR_EXPORT -2
#define ERR_UNEXPORT -3
#define ERR_VALUE -4
#define ERR_DIRECTION -5
class LedController {
public:
LedController(int numero, bool dirOut = true, bool litUp = false);
~LedController();
void turnOn();
void turnOff();
private:
void export_gpio();
void unexport_gpio();
int numero;
};
#endif
==== Le programme principal et la fonction qui gère la led ====
#include
#include
#include
#include
#include "ledcontroller.hpp"
#define GPIO_PIN 21
#define BITRATE 20
#define BITTIME static_cast(1e6/BITRATE)
#define ALL_GOOD 0
const auto time_step = std::chrono::microseconds(BITTIME);
// We are asuming here that a char is always an octet, and we will assume in every part of the code
const unsigned char powers_of_two[8] = {128, 64, 32, 16, 8, 4, 2, 1};
// Function that iterates over message and switch on and off the led of lc according to the bits
void transmit(LedController& lc, const std::string& message) {
// Initializing timer as the reference time
auto timer = std::chrono::high_resolution_clock::now();
// Turning on the light to tell the receptor we're starting to emit
lc.turnOn();
// Iterating over the message
for(const char& c: message) {
// Iterating over the bits of c (again, we're assuming that a char is an octet long)
for(unsigned int i = 0; i < 8; i++) {
// Incrementing the timer, computing wether we should lightUp the led or not, and sleeping until it's time to change the state of the led
timer += time_step;
const bool lightUp = c & powers_of_two[i];
std::this_thread::sleep_until(timer);
lightUp ? lc.turnOn() : lc.turnOff();
}
}
std::this_thread::sleep_until(timer + time_step);
lc.turnOff();
// Sleeping long enough to make sure that the receptor received at least a full octet of zeros (it will mark the end of transmission for it)
std::this_thread::sleep_for(2 * time_step);
}
// This main simply get the input from stdin and act in consequence
int main() {
LedController lc(GPIO_PIN);
std::string message;
// Getting the first string
std::cout << "Waiting for a string to send: (\"/test\" to launch a sequence of 0 and 1, \"/end\" to end the program)" << std::endl;
std::getline(std::cin, message);
// Sending message if it is different of "/end" and "/test"
// "/end" will end the loop and therefore, the execution
while(message != "/end") {
// I feel like it's ok to not use a switch because I don't plan to add more commands, but if you plan to, you should definitely use one
if(message == "/test") {
// The test sequence is a sequence of upper case "u" because in ASCII, an U is 01010101, and this makes a pretty good test sequence
transmit(lc, "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU");
}
else {
transmit(lc, message);
}
std::cout << "Transfer complete.\n" << std::endl;
std::cout << "Waiting for a string to send: " << std::endl;
std::getline(std::cin, message);
}
return ALL_GOOD;
}
----
//Auteur: S. Durand//