Si vous avez suivi la première partie, vous avez dû comprendre que le mode que je m’apprête à vous présenter ici est mon petit préféré. Nous allons détailler le mode de fonctionnement dit “standalone” en programmant directement ce microcontrôleur qu’est l’ESP8266 ; et ce, dans le but de créer une sonde de température autonome et très basse consommation.

NodeMCU

Ah, enfin, nous l’abordons ! Au risque de me répeter : l’ESP8266 est un microcontrôleur. Et qui dit microcontrôleur, dit programme.

En vérité, lorsque nous utilisons le module avec son jeu de commandes AT, nous demandons au programme d’interpréter nos commandes pour faire telle ou telle chose. Alors pourquoi simplement ne pas remplacer ledit programme par le notre et ainsi supprimer l’Arduino que nous aurions par exemple dû utiliser précédement ?

Et bien parce qu’on perd un atout majeur de l’ESP8266 : sa simplicité de mise en oeuvre.

Pour corriger ce problème, des gens brillants se sont mis à travailler sur des firmware alternatifs, le plus connu d’entre eux étant NodeMCU.

Concrètement, NodeMCU permettra à l’ESP8266 d’embarquer un petit système de fichier ainsi qu’un interpreteur LUA (car oui, il va falloir se mettre au LUA) ; tout cela pour nous permettre d’utiliser les capacités Wi-Fi de l’ESP et de jouer avec les entrées/sorties du microcontrôleur.

L’exemple que je développerai ici consiste en une sonde de temperature autonome capable d’envoyer toutes les heures la temperature sur un serveur. Mais avant ça, il va falloir préparer le terrain en flashant notre ESP avec ce cher NodeMCU firmware.

Flasher l’ESP8266

Tout d’abord, nous allons devoir récupérer la dernière version stable du firmware sur le GitHub de NodeMCU. Pensez à choisir une version float si vous ne voulez pas chercher des heures pourquoi il est impossible de manipuler des nombres à virgules flottantes… Et je parle d’expérience !

Nous allons devoir modifier légèrement notre schéma en reliant GPIO0 à la masse (ici représenté en bleu) :


ESPlorer

Ensuite, pour ceux qui sont sous Linux ou OS X, installez esptool :

1
2
3
$ git clone https://github.com/themadinventor/esptool.git
$ cd esptool
$ python setup.py install

Puis flashez votre module à l’aide de la commande suivante :

1
2
3
4
5
6
$ python esptool.py -p /dev/tty.usbserial write_flash 0x000000 ./nodemcu_float_0.9.6.bin
Connecting...
Erasing flash...
Writing at 0x00070c00... (100 %)

Leaving...

Pour les autres installez un bon Linux téléchargez puis exécutez NodeMCU flasher. Rendez-vous dans l’onglet “Config” afin de sélectionner le firmware fraîchement téléchargé, puis revenez sur l’onglet “Opération” pour sélectionner votre port série avant de cliquer sur “Flash”.

Une fois la procédure terminée, vous pouvez revenir au schéma de base en supprimant le lien entre GPIO0 et la masse.

Petit détour du côté de chez LUA

Je ne compte pas vous faire un cours sur le sujet, pour la simple et bonne raison que j’ai fais mes premiers pas sur ce langage il y a quelques jours à peine. Je me contenterai de vous présenter de loin les choses à avoir à l’esprit en faisant un parrallèle avec Javascript.

LUA est à l’instar de Javascript un langage interprété. Son interpréteur, écris en C ANSI, est réputé pour être très léger et facilement portable ; ce qui en fait un langage de choix pour les petites applications embarquées.

Voyons ce qu’il faut avoir à l’esprit avant de commencer …

Des commentaires notés -- :

1
2
3
4
-- Ceci est un commentaire simple.

--[[ Et ceci est un
commentaire multilignes. ]]

Concaténation à l’aide de l’opérateur .. :

1
print("Lorem" .. "Ipsum")
1
2
$ lua test.lua
LoremIpsum

Typage dynamique :

1
2
3
4
example = "Toto"
print(type(example) .. " -> " .. example)
example = 3.14
print(type(example) .. " -> " .. example)
1
2
3
$ lua test.lua
string -> Toto
number -> 3.14

Pas d’accolades, mais des do … end :

1
2
3
for i = 0, 5, 2 do
print("Boucle: " .. i)
end
1
2
3
4
$ lua test.lua
Boucle: 0
Boucle: 2
Boucle: 4

Support des fonctions anonymes :

1
2
3
4
5
print(
(function()
print("Tata")
end)()
)
1
2
$ lua test.lua
Tata

N’importe quoi peut indexer une table :

1
2
3
4
5
6
7
8
9
10
11
12
example_table = { id1 = "Titi" }
example_table[666] = "Tutu"
example_function = function()
return 42
end
example_table[example_function] = "Haha" -- Oui oui, des fonctions aussi.

for key, value in pairs(example_table) do
print("Key type: " .. type(key))
print("Value: " .. example_table[key])
print(" ----- ")
end
1
2
3
4
5
6
7
8
9
10
$ lua test.lua
-----
Key type: number
Value: Tutu
-----
Key type: string
Value: Titi
-----
Key type: function
Value: Haha

Support des closures :

1
2
3
4
5
6
7
8
9
appender = function(element)
return function(str)
return str .. element
end
end

celsius_appender = appender(" °C")

print(celsius_appender(24))
1
2
$ lua test.lua
24 °C

Voilà pour nos exemples. Si vous souhaitez en voir plus, je vous invite à vous rendre sur le site de LUA.

Découverte de ESPlorer

Vous aimez les gros IDE lourds et boutonneux qui sentent bon le Java ? ESPlorer est fait pour vous !


ESPlorer

Je vous avais prévenus, c’est pas l’interface la plus accueillante qu’il soit. Bon, je taquine, mais ESPlorer fait très bien son travail et même si un petit lifting ne lui ferait pas de mal, on ne peux qu’applaudir le travail des développeurs.

Notre ESP8266 étant désormais flashé, nous n’aurons plus à nous occuper de l’onglet AT v0.2.0. À vrai dire, le seul qui nous intéressera ici est l’onglet “NodeMCU + MicroPython, et plus précisement sa sous-partie Script puisque c’est ici que nous écrirons notre programme.

Concernant le volet de droite, je vous invite à selectionner votre port série ainsi que le baudrate de votre ESP. Une fois ces choix effectués, cliquez sur Open, puis mettez sous tension votre montage. Celui-ci devrait déjà être bavard :


ESPlorer

Tout ceci ne semble pas bien encourrageant, mais rassurez-vous, l’autodétection du firmware ne m’a pas posé de problème jursqu’à maintenant.

En ce qui concerne l’inscription “cannot open init.lua”, souvenez-vous de ce que je vous disais tout à l’heure : NodeMCU firmware consiste en un interpreteur LUA et un petit système de fichiers. Pour écrire nos programmes, nous créerons un fichier nommé init.lua, puis nous le déplacerons dans notre ESP. Au boot, ce dernier ira lire ce fichier et l’exécutera à l’aide de son interpréteur.

Cette ligne n’est donc pas une erreur, dans le sens où nous n’avons pas encore commencé à travailler.

L’API NodeMCU

NodeMCU fournit une API très complète et diablement bien documentée. Nous nous intéresserons dans un premier temps à la méthode wifi.sta.getap() qui nous permettra comme tout à l’heure d’afficher une liste des points d’accès à portée.

Le code se limitera à ceci :

1
2
3
4
5
6
7
function print_ap(aps)
for ssid, infos in pairs(aps) do
print(ssid)
end
end

wifi.sta.getap(print_ap)

On commence ici par déclarer la fonction print_ap() qui prend en paramètre une table, puis la parcourt pour en afficher chaque clé, la clé correspondant au SSID du réseau.

En cliquant sur Send to ESP, vous devriez voir apparaître une liste dans la fenêtre de gauche :


ESPlorer getap()

Cliquez maintenant sur Save to ESP, puis nommez votre fichier init.lua. Éteignez votre ESP, puis rallumez le (sans fermer la connexion série). Si tout se passe bien, il devrait démarrer puis afficher la liste des réseaux.

Et voilà, c’est tout ! En quelques lignes, nous avons créé un programme capable d’être embarqué dans l’ESP et pouvant fonctionner sans aide extérieure.

Exemple concret

Passons à la réalisation de notre sonde de température autonome. Pour cela, j’utiliserai ici le capteur I2C Atmel AT30TS74, mais n’importe quel autre capteur peut faire l’affaire.

Lecture de la temperature :

Voyons rapidement la partie qui permettra de lire la température en provenance du capteur :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
i2c_id = 0
sensor_address = 0x48

i2c.setup(i2c_id, 4, 3, i2c.SLOW)

function humanize(str)
h, l = string.byte(str, 1, 2)
l = bit.rshift(l, 5)
return h + l / 10
end

function read_sensor_bytes()
i2c.start(i2c_id)
i2c.address(i2c_id, sensor_address, i2c.TRANSMITTER)
-- 0x01: Basculement sur le registre de configuration
-- 0x02: Temperature sur 12 bits
i2c.write(i2c_id, 0x01, 0xFF, 0x02)
i2c.stop(i2c_id)

i2c.start(i2c_id)
i2c.address(i2c_id, sensor_address, i2c.TRANSMITTER)
-- 0x00: Basculement sur le registre de temperature
i2c.write(i2c_id, 0x00)
i2c.stop(i2c_id)


i2c.start(i2c_id)
i2c.address(i2c_id, sensor_address, i2c.RECEIVER)
c = i2c.read(i2c_id, 2)
i2c.stop(i2c_id)

return c
end

Ce code est propre au capteur que j’utilise. Si vous en avez un autre, à vous de mettre le nez dans sa doc car celui-ci risque malheuresement de ne pas vous être utile.

Connexion au point d’accès :

Nous aurons bien sûr besoin de nous connecter à un réseau Wi-Fi. Voici une façon de procéder :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
wifi.setmode(wifi.STATION) -- => AT+CWMODE=1
wifi.sta.config("NC-AP2", "votrepass", 0)
wifi.sta.connect() -- => AT+CWJAP="NC-AP2","votrepass"

-- On lance une fonction toutes les secondes :
tmr.alarm(1, 1000, 1, function()
-- tant que le statut est différent de 5 (= STATION_GOT_IP) :
if wifi.sta.status() ~= 5 then
print("En attente de connexion ...")
else
-- On a desormais une IP, signe de connexion réussie :
print("Connexion OK avec l'IP: " .. wifi.sta.getip())
-- On stoppe le timer pour passer à la suite :
tmr.stop(1)
main()
end
end)

Les amateurs de beau code risquent de se sentir mal à la vue de ce fichu timer… Mais pour le moment, nul autre choix. Je dis bien pour le moment, puisque les développeurs de NodeMCU firmware travaillent actuellement sur la gestion des évènements de connexion / déconnexion. Patience !

Boucle principale :

Nous sommes desormais connectés. Il nous faut maintenant lire la temperature à intervalles régulières, puis l’envoyer au serveur :

1
2
3
4
5
6
function main()
tmr.alarm(2, 10000, 1, function()
temp = humanize(read_sensor_bytes())
publish(temp)
end)
end

Le timer execute maintenant la fonction toutes les 10 secondes pour aller lire la température et la passer à la méthode publish().

Publication sur le serveur :

Nous allons devoir construire la requête HTTP à la main avant de pouvoir l’envoyer. C’est le rôle de la méthode build_post_request() que voici :

1
2
3
4
5
6
7
8
9
10
function build_post_request(path, key, value)
payload = key .. "=" .. value

return "POST " .. path .. " HTTP/1.1\r\n" ..
"Host: " .. server_ip .. "\r\n" ..
"Connection: close\r\n" ..
"Content-Type: application/x-www-form-urlencoded\r\n" ..
"Content-Length: " .. string.len(payload) .. "\r\n" ..
"\r\n" .. payload
end

Et nous n’avons plus qu’à ouvrir un socket TCP pour l’envoyer :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function send_temperature(value)
block = true
sk = net.createConnection(net.TCP, 0)

sk:on("connection", function(sck)
request = build_post_request("/temperature", "temperature_value", value)
sk:send(request, function()
sk:close()
block = false
end)
end)

sk:connect(server_port, server_ip)
end

Nous nous accrochons ici à l’évènement connexion pour ne déclencher l’envoi qu’une fois connecté. La variable block permet d’éviter un second envoi si une requête est encore en suspend. Notez également que nous prenons soin de fermer le socket une fois la requête envoyée.

Le code au complet :

Nous touchons au but :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
server_ip = "172.22.12.10"
server_port = 4242
i2c_id = 0
sensor_address = 0x48
block = false

i2c.setup(i2c_id, 4, 3, i2c.SLOW)

wifi.setmode(wifi.STATION)
wifi.sta.config("NC-AP2", "votrepass", 0)
wifi.sta.connect()

function humanize(str)
h, l = string.byte(str, 1, 2)
l = bit.rshift(l, 5)
return h + l / 10
end

function read_sensor_bytes()
i2c.start(i2c_id)
i2c.address(i2c_id, sensor_address, i2c.TRANSMITTER)
-- 0x01: Basculement sur le registre de configuration
-- 0x02: Temperature sur 12 bits
i2c.write(i2c_id, 0x01, 0xFF, 0x02)
i2c.stop(i2c_id)

i2c.start(i2c_id)
i2c.address(i2c_id, sensor_address, i2c.TRANSMITTER)
-- 0x00: Basculement sur le registre de temperature
i2c.write(i2c_id, 0x00)
i2c.stop(i2c_id)

i2c.start(i2c_id)
i2c.address(i2c_id, sensor_address, i2c.RECEIVER)
c = i2c.read(i2c_id, 2)
i2c.stop(i2c_id)

return c
end

function build_post_request(path, key, value)
payload = key .. "=" .. tostring(value)

return "POST " .. path .. " HTTP/1.1\r\n" ..
"Host: " .. server_ip .. "\r\n" ..
"Connection: close\r\n" ..
"Content-Type: application/x-www-form-urlencoded\r\n" ..
"Content-Length: " .. string.len(payload) .. "\r\n" ..
"\r\n" .. payload
end

function send_temperature(value)
block = true
sk = net.createConnection(net.TCP, 0)

sk:on("connection", function(sck)
request = build_post_request("/temperature", "temperature_value", value)
sk:send(request, function()
sk:close()
block = false
end)
end)

sk:connect(server_port, server_ip)
end

function main()
tmr.alarm(2, 10000, 1, function()
if block == false then
temp = humanize(read_sensor_bytes())
send_temperature(temp)
end
end)
end

tmr.alarm(1, 1000, 1, function()
if wifi.sta.status() ~= 5 then
print("En attente de connexion ...")
else
print("Connexion OK avec l'IP: " .. wifi.sta.getip())
main()
tmr.stop(1)
end
end)

Le pseudo-serveur

Puisque ça n’est pas le sujet ici, nous allons faire très simple en quelques lignes de Javascript :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var express = require('express');
var bodyParser = require('body-parser');
var app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));

app.post('/temperature', function(req, res) {
console.log("Temperature reçue ! -> ", req.body.temperature_value);
});

app.listen(4242);

Nous utilisons ici Express.js pour intercepter les requête POST, et bodyParser pour retrouver temperature_value=XX. Place au test !

Le grand test

Puisqu’il est temps de tester, testons !

1
2
3
4
5
6
$ node server.js
Temperature reçue ! -> 29.6
Temperature reçue ! -> 30.2
Temperature reçue ! -> 30.4
Temperature reçue ! -> 30.7
^C

Pour citer un collègue : “Et en plus, ça marche !”

Aller plus loin

Vous imaginez bien qu’on ne va pas s’arrêter en si bon chemin ! Dans un prochain article, nous parlerons du mode deep sleep et nous ferons un pas de plus vers la domotique en remplaçant ce pseudo serveur par un broker MQTT. Ça vous dit ?

En attendant, si vous souhaitez reproduire ces étapes chez vous, le code et les schémas sont disponibles sur GitHub.

Si cet article vous a plu, n’hésitez pas à le partager. Et encore une fois, si vous avez des questions, je me ferai une joie d’y répondre ; que ce soit sur Twitter ou dans les commentaires un peu plus bas.

À très bientôt, et restez dans le coin !