Transfert de données via Lora vers JEEDOM dans le cadre d’une ruche connectée
18 juin 2023Nous sommes une équipe d’étudiants en deuxième année d’école d’ingénieur à Grenoble-INP Ense3, nous avons comme projet de créer une ruche connecté. Notre système nous renvoie l’humidité, la température ainsi que la masse de la ruche.
Afin de pouvoir transmettre ces données à l’apiculteur, nous avons choisit d’utiliser le réseau LoRa qui consomme très peu et qui est open source. Dans notre système nous avons une carte pycom Lopy4 qui permet de traiter les données et de les envoyer en LoRa vers The Things Network puis JEEDOM.
Fonctionnement Carte Lopy4
Pour utiliser la carte, il faut au préalable installer le logiciel ATOM, pour cela il faut aller sur https://atom.io/, et ensuite une fois ATOM installé, cliquer sur « Install a Package ».
Puis il faut cliquer dans Open Installer et installer le package « pymakr », une fois fait on peut utiliser Atom (de Savoie).
Vous pouvez vous créer un folder où vous mettrez les codes de votre projet. A noter qu’ATOM et la carte lopy 4 se codent en langage python.
Vous devez vous connecter à votre carte par le réseau, normalement une fois alimenté vous allez voir dans vos réseau wifi disponible sur votre ordinateur, le réseau de votre carte pycom (lopy-wlan-xxxx). Vous vous connectez avec le mot de passe www.pycom.io . Sur Atom vous allez dans l’onglet connect et sélectionnez votre carte.
Vous voilà connecté à votre carte ! (Attention une fois connecté vous n’avez plus accès à internet)
Capteur de Température et d’Humidité
Le capteur DHT22 est un capteur pouvant renvoyer l’humidité et la température, nous avons donc utiliser celui là. Si vous voulez vous informer dessus et connaître ses caractéristiques (https://passionelectronique.fr/tutorial-dht22/).
Afin de le faire fonctionner sur Atom, voici notre code :
import pycom
import time
from machine import Pin
from machine import enable_irq, disable_irq
#################### Define DHT22 ####################
def getval(pin):
ms = [1]*700 # needs long sample size to grab all the bits from the DHT
time.sleep(1)
pin(0)
time.sleep_us(10000)
pin(1)
irqf = disable_irq()
for i in range(len(ms)):
ms[i] = pin() ## sample input and store value
enable_irq(irqf)
# for i in range(len(ms)): #print debug for checking raw data
# print (ms[i])
return ms
def decode(inp):
res= [0]*5
bits=[]
ix = 0
try:
#if inp[0] == 1 : ix = inp.index(0, ix) ## skip to first 0 # ignore first '1' as probably sample of start signal. *But* code seems to be missing the start signal, so jump this line to ensure response signal is identified in next two lines.
ix = inp.index(1,ix) ## skip first 0's to next 1 # ignore first '10' bits as probably the response signal.
ix = inp.index(0,ix) ## skip first 1's to next 0
while len(bits) < len(res)*8 : ##need 5 * 8 bits :
ix = inp.index(1,ix) ## index of next 1
ie = inp.index(0,ix) ## nr of 1's = ie-ix
# print ('ie-ix:',ie-ix)
bits.append(ie-ix)
ix = ie
except:
print('6: decode error')
print('length:')
print(len(inp), len(bits))
return([0xff,0xff,0xff,0xff])
# print('bits:', bits)
for i in range(len(res)):
for v in bits[i*8:(i+1)*8]: #process next 8 bit
res[i] = res[i]<<1 ##shift byte one place to left
if v > 5: # less than 5 '1's is a zero, more than 5 1's in the sequence is a one
res[i] = res[i]+1 ##and add 1 if lsb is 1
# print ('res', i, res[i])
if (res[0]+res[1]+res[2]+res[3])&0xff != res[4] : ##parity error!
print("Checksum Error")
print (res[0:4])
# res= [0xff,0xff,0xff,0xff]
#print ('res:', res[0:4])
return(res[0:4])
def DHT22(pin):
res = decode(getval(pin))
hum = res[0]*256+res[1]
temp = res[2]*256 + res[3]
if (temp > 0x7fff):
temp = 0x8000 - temp
return temp, hum
###########################Affichage_des_données#############################
def go_DHT():
dht_pin=Pin('PXX', Pin.OPEN_DRAIN) # connect DHT22 sensor data line to pin
dht_pin(1)
while True:
temp, hum = DHT22(dht_pin)
print(temp//10,'°C')
print(hum//10,'%')
pycom.heartbeat(False)
go_DHT()
Normalement Atom vous renvoie bien l’humidité et la température de la pièce.
Capteur de masse
Pour notre balance nous avons utilisé des capteurs de forces en série avec des amplificateurs HX711, on s’est inspiré de ce projet (https://www.robotique.tech/tutoriel/balance-de-pesee-utilisant-hx711-et-arduino-pour-la-surveillance-du-poids-a-distance-par-bluetooth/). Cependant étant sur Python nous n’avons pas pu utiliser leurs codes nous avons récupéré des codes sur (https://github.com/geda/hx711-lopy/commit/a1a71a015b7edf530266025ec40d3714e9631fb5).
Sur ATOM en faisant tourner :
from machine import Pin
class HX711:
def __init__(self, dout, pd_sck, gain=128):
self.pSCK = Pin(pd_sck , mode=Pin.OUT)
self.pOUT = Pin(dout, mode=Pin.IN, pull=Pin.PULL_DOWN)
self.GAIN = 0
self.OFFSET = 0
self.SCALE = 1
self.lastVal = 0
self.allTrue = False
self.set_gain(gain);
def createBoolList(size=8):
ret = []
for i in range(8):
ret.append(False)
return ret
def is_ready(self):
return self.pOUT() == 0
def set_gain(self, gain):
if gain is 128:
self.GAIN = 1
elif gain is 64:
self.GAIN = 3
elif gain is 32:
self.GAIN = 2
self.pSCK.value(False)
self.read()
print('Gain setted')
def read(self):
dataBits = [self.createBoolList(), self.createBoolList(), self.createBoolList()]
while not self.is_ready():
pass
for j in range(2, -1, -1):
for i in range(7, -1, -1):
self.pSCK.value(True)
dataBits[j][i] = self.pOUT()
self.pSCK.value(False)
#set channel and gain factor for next reading
for i in range(self.GAIN):
self.pSCK.value(True)
self.pSCK.value(False)
#check for all 1
if all(item == True for item in dataBits[0]):
print('all true')
self.allTrue=True
return self.lastVal
self.allTrue=False
readbits = ""
for j in range(2, -1, -1):
for i in range(7, -1, -1):
if dataBits[j][i] == True:
readbits= readbits +'1'
else:
readbits= readbits+'0'
self.lastVal = int(readbits, 2)
return self.lastVal
def read_average(self, times=3):
sum = 0
effectiveTimes = 0
readed = 0
for i in range(times):
readed = self.read()
if self.allTrue == False:
sum += readed
effectiveTimes+=1
if effectiveTimes == 0:
return 0
return sum / effectiveTimes
def get_value(self, times=3):
return self.read_average(times) - self.OFFSET
def get_units(self, times=3):
return self.get_value(times) / self.SCALE
def tare(self, times=15):
sum = self.read_average(times)
self.set_offset(sum)
def set_scale(self, scale):
self.SCALE = scale
def set_offset(self, offset):
self.OFFSET = offset
def power_down(self):
self.pSCK.value(False)
self.pSCK.value(True)
def power_up(self):
self.pSCK.value(False)
################################## Main Program ############################
scale1 = HX711('PX1','PX2',128) #Attention bien connecter les 2 bons pins
calibration_factor = -7340
scale1.set_scale(calibration_factor)
scale1.tare()
while True:
masse = round(scale1.get_units(3))
print (masse,'kg')
A noter qu’il faut faire plusieurs essais et jouer sur le calibration factor pour avoir la bonne échelle de masse.
Connection Atom/The Things Network
Maintenant que l’on arrive à avoir des données de masse, humidité et température, on souhaite les envoyer via le réseau Lora pour l’apiculteur. Pour cela, on utilise le réseau LoRa, pour cela on doit aller sur le site The Things Network, (TTN). Tout d’abord, il faut se créer un compte, ensuite aller dans l’onglet console. Puis sélectionner le cluster Europe 1(si vous êtes en Europe évidemment).
Ensuite il faut créer une application, en remplissant une application ID (par exemple « myLoraSensorXX« ), et cliquer sur « + Add end Device ».
Ensuite on arrive à cette page là :
On clique sur
Sélectionnez le mode manuel, et remplissez les trois premières cases comme ceci :
Ensuite on clique ces 3 cases on clique sur Generate, fill with zeros et Generate :
Maintenant, on clique sur :
On obtient quelque chose comme ça :
Maintenant on garde notre page internet ouverte et on retourne sur Atom, en amont du code, recopie ceci
from network import LoRa
import socket
import binascii
import struct
import pycom
import time
from machine import Pin
from machine import enable_irq, disable_irq
#################### Define LoraWan ########################
# for EU868
LORA_FREQUENCY = 868100000
LORA_GW_DR = "SF7BW125" #DR_5
LORA_NODE_DR = 5
# initialize LoRa in LORAWAN mode.
# Please pick the region that matches where you are using the device:
# Asia = LoRa.AS923
# Australia = LoRa.AU915
# Europe = LoRa.EU868
# United States = LoRa.US915
lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868)
# create an OTA authentication params
dev_eui = binascii.unhexlify('XXXXXXXXXXXXX')
app_eui = binascii.unhexlify('0000000000000000')
app_key = binascii.unhexlify('XXXXXXXXXXXXXXXXXXXXX')
# set the 3 default channels to the same frequency (must be before sending the OTAA join request)
lora.add_channel(0, frequency=LORA_FREQUENCY, dr_min=0, dr_max=5)
lora.add_channel(1, frequency=LORA_FREQUENCY, dr_min=0, dr_max=5)
lora.add_channel(2, frequency=LORA_FREQUENCY, dr_min=0, dr_max=5)
# join a network using OTAA
lora.join(activation=LoRa.OTAA, auth=(dev_eui, app_eui, app_key), timeout=0,
dr=LORA_NODE_DR)
# wait until the module has joined the network
while not lora.has_joined():
time.sleep(5)
print('Not joined yet...')
print("Joined")
# remove all the non-default channels
for i in range(3, 16):
lora.remove_channel(i)
# create a LoRa socket
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
# set the LoRaWAN data rate
s.setsockopt(socket.SOL_LORA, socket.SO_DR, LORA_NODE_DR)
# make the socket blocking
s.setblocking(True)
time.sleep(5.0)
#################### End of define LoraWan ###########
Attention, il faut bien rentrer son DevEui,son AppEui et son AppKey que l’on peut copier via TTN.
Encodage des données
Maintenant qu’Atom est relié à TTN, il faut faire passer nos données de l’un vers l’autre, cependant les nombres décimaux sont trop « gros » à faire passer donc on va convertir toutes nos données comme une grosse chaîne de caractère.
Comme on ne pourra pas avoir de valeurs négatives ni de nombre décimaux on décide de convertir nos valeurs en entiers naturels via additions et multiplications tel quel :
while True:
temp, hum = DHT22(dht_pin)
temp = round(temp//10) +40
hum=round(hum//10)
masse = round(scale1.get_units(3)*10)
str_temp=str(temp)
str_hum=str(hum)
str_masse=str(masse)
encoded=(2-len(str_temp)*'0'+str_temp
encoded+=(2-len(str_hum)*'0'+str_hum
encoded+=(3-len(str_masse))*'0'+str_masse
print(encoded)
s.send(encoded.encode())
Afin de décoder les données que l’on envoie, on saisie un code également sur TTN dans uplink :
Bien se mettre dans le bon « Formatter type »
et saisir ce code :
function Decoder(bytes, port) {
var decoded = {};
var counter = 0;
decoded.temperature = 0;
for(i=0; i<2; i++){
decoded.temperature += Math.pow(10,1-i)*(parseInt(bytes[i])%16);
}
decoded.temperature = decoded.temperature-40;
counter +=1;
decoded.humidite = 0;
for(i=0; i<2; i++){
decoded.humidite += Math.pow(10,1-i)*(parseInt(bytes[2*counter+i])%16);
}
decoded.humidite = decoded.humidite;
counter +=1;
decoded.masse=0;
for (i=0; i<3; i++){
decoded.masse += Math.pow(10,2-i)*(parseInt(bytes[2*counter+i])%16);
}
decoded.masse=decoded.masse/10
return decoded;
}
Dans Atom on saisie le code final :
from network import LoRa
import socket
import binascii
import struct
import pycom
import time
from machine import Pin
from machine import enable_irq, disable_irq
#################### Define LoraWan ########################
# for EU868
LORA_FREQUENCY = 868100000
LORA_GW_DR = "SF7BW125" #DR_5
LORA_NODE_DR = 5
# initialize LoRa in LORAWAN mode.
# Please pick the region that matches where you are using the device:
# Asia = LoRa.AS923
# Australia = LoRa.AU915
# Europe = LoRa.EU868
# United States = LoRa.US915
lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868)
# create an OTA authentication params
dev_eui = binascii.unhexlify('XXXXXXXXXXXXX')
app_eui = binascii.unhexlify('0000000000000000')
app_key = binascii.unhexlify('XXXXXXXXXXXXXXXXXXXX')
# set the 3 default channels to the same frequency (must be before sending the OTAA join request)
lora.add_channel(0, frequency=LORA_FREQUENCY, dr_min=0, dr_max=5)
lora.add_channel(1, frequency=LORA_FREQUENCY, dr_min=0, dr_max=5)
lora.add_channel(2, frequency=LORA_FREQUENCY, dr_min=0, dr_max=5)
# join a network using OTAA
lora.join(activation=LoRa.OTAA, auth=(dev_eui, app_eui, app_key), timeout=0,
dr=LORA_NODE_DR)
# wait until the module has joined the network
while not lora.has_joined():
time.sleep(5)
print('Not joined yet...')
print("Joined")
# remove all the non-default channels
for i in range(3, 16):
lora.remove_channel(i)
# create a LoRa socket
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
# set the LoRaWAN data rate
s.setsockopt(socket.SOL_LORA, socket.SO_DR, LORA_NODE_DR)
# make the socket blocking
s.setblocking(True)
time.sleep(5.0)
#################### End of define LoraWan ###########
#################### Création de la clas HX711 ######
class HX711:
def __init__(self, dout, pd_sck, gain=128):
self.pSCK = Pin(pd_sck , mode=Pin.OUT)
self.pOUT = Pin(dout, mode=Pin.IN, pull=Pin.PULL_DOWN)
self.GAIN = 0
self.OFFSET = 0
self.SCALE = 1
self.lastVal = 0
self.allTrue = False
self.set_gain(gain);
def createBoolList(size=8):
ret = []
for i in range(8):
ret.append(False)
return ret
def is_ready(self):
return self.pOUT() == 0
def set_gain(self, gain):
if gain is 128:
self.GAIN = 1
elif gain is 64:
self.GAIN = 3
elif gain is 32:
self.GAIN = 2
self.pSCK.value(False)
self.read()
print('Gain setted')
def read(self):
dataBits = [self.createBoolList(), self.createBoolList(), self.createBoolList()]
while not self.is_ready():
pass
for j in range(2, -1, -1):
for i in range(7, -1, -1):
self.pSCK.value(True)
dataBits[j][i] = self.pOUT()
self.pSCK.value(False)
#set channel and gain factor for next reading
for i in range(self.GAIN):
self.pSCK.value(True)
self.pSCK.value(False)
#check for all 1
if all(item == True for item in dataBits[0]):
print('all true')
self.allTrue=True
return self.lastVal
self.allTrue=False
readbits = ""
for j in range(2, -1, -1):
for i in range(7, -1, -1):
if dataBits[j][i] == True:
readbits= readbits +'1'
else:
readbits= readbits+'0'
self.lastVal = int(readbits, 2)
return self.lastVal
def read_average(self, times=3):
sum = 0
effectiveTimes = 0
readed = 0
for i in range(times):
readed = self.read()
if self.allTrue == False:
sum += readed
effectiveTimes+=1
if effectiveTimes == 0:
return 0
return sum / effectiveTimes
def get_value(self, times=3):
return self.read_average(times) - self.OFFSET
def get_units(self, times=3):
return self.get_value(times) / self.SCALE
def tare(self, times=15):
sum = self.read_average(times)
self.set_offset(sum)
def set_scale(self, scale):
self.SCALE = scale
def set_offset(self, offset):
self.OFFSET = offset
def power_down(self):
self.pSCK.value(False)
self.pSCK.value(True)
def power_up(self):
self.pSCK.value(False)
#################### Main program #################
import pycom
import time
from machine import Pin
from machine import enable_irq, disable_irq
#################### Define DHT22 ####################
def getval(pin):
ms = [1]*700 # needs long sample size to grab all the bits from the DHT
time.sleep(1)
pin(0)
time.sleep_us(10000)
pin(1)
irqf = disable_irq()
for i in range(len(ms)):
ms[i] = pin() ## sample input and store value
enable_irq(irqf)
# for i in range(len(ms)): #print debug for checking raw data
# print (ms[i])
return ms
def decode(inp):
res= [0]*5
bits=[]
ix = 0
try:
#if inp[0] == 1 : ix = inp.index(0, ix) ## skip to first 0 # ignore first '1' as probably sample of start signal. *But* code seems to be missing the start signal, so jump this line to ensure response signal is identified in next two lines.
ix = inp.index(1,ix) ## skip first 0's to next 1 # ignore first '10' bits as probably the response signal.
ix = inp.index(0,ix) ## skip first 1's to next 0
while len(bits) < len(res)*8 : ##need 5 * 8 bits :
ix = inp.index(1,ix) ## index of next 1
ie = inp.index(0,ix) ## nr of 1's = ie-ix
# print ('ie-ix:',ie-ix)
bits.append(ie-ix)
ix = ie
except:
print('6: decode error')
print('length:')
print(len(inp), len(bits))
return([0xff,0xff,0xff,0xff])
# print('bits:', bits)
for i in range(len(res)):
for v in bits[i*8:(i+1)*8]: #process next 8 bit
res[i] = res[i]<<1 ##shift byte one place to left
if v > 5: # less than 5 '1's is a zero, more than 5 1's in the sequence is a one
res[i] = res[i]+1 ##and add 1 if lsb is 1
# print ('res', i, res[i])
if (res[0]+res[1]+res[2]+res[3])&0xff != res[4] : ##parity error!
print("Checksum Error")
print (res[0:4])
# res= [0xff,0xff,0xff,0xff]
#print ('res:', res[0:4])
return(res[0:4])
scale1 = HX711('P3','P2',128)
calibration_factor = -7340
scale1.set_scale(calibration_factor)
scale1.tare()
def DHT22(pin):
res = decode(getval(pin))
hum = res[0]*256+res[1]
temp = res[2]*256 + res[3]
if (temp > 0x7fff):
temp = 0x8000 - temp
return temp, hum
#################### End of define DHT22 ###########
#################### Main program #################
def go_DHT():
dht_pin=Pin('P23', Pin.OPEN_DRAIN) # connect DHT22 sensor data line to pin P9/G16 on the expansion board
dht_pin(1) # drive pin high to initiate data conversion on DHT sensor
while True:
pycom.rgbled(0x7f0000) # red
temp, hum = DHT22(dht_pin)
temp = round(temp//10) +40
hum=round(hum//10)
masse = round(scale1.get_units(3)*10)
pycom.rgbled(0)
#res=str(temp)+'/'+str(hum)+'/'+str(scale1.get_units(3))
#pkt = str(res).encode() # Encode the data
# pkt=bin(temp)
# print(temp)
# # print(pkt)
# hex_temp=hex(temp)
# hex_hum=hex(hum)
# hex_masse=hex(masse)
str_temp=str(temp)
print(str_temp)
str_hum=str(hum)
print(str_hum)
str_masse=str(masse)
#str_masse='526'
encoded=(2-len(str_temp)*'0'+str_temp
encoded+=(2-len(str_hum)*'0'str_hum
encoded+=(3-len(str_masse))*'0'+str_masse
print(encoded)
s.send(encoded.encode())
pycom.heartbeat(False)
go_DHT()
#################### End of Main program #################
#################### End of Main program #################
Normalement s’affiche sur TTN dans l’onglet live data les valeurs ici, de masse, température et d’humidité !
Bonjour,
J’envisage de fabriquer une balance connectée et si j’ai bien suivi votre article il faut en matériel :
– Une carte Lopy 4
– un capteur de température et d’humidité DHT22
– 4 capteurs de force avec amplificateurs HX711
Faut il d’autres choses ?
Merci d’avance
Bonjour, à priori c’est suffisant. Pour alimenter tout ce matériel nous avions choisi un panneau solaire et une batterie pour le rendre « autonome ». Mais on peut également l’alimenter sur secteur à priori.
Notre projet consistait à envoyer des données via le réseau LoRa Wan, donc il faut également une antenne émettrice et réceptrice pour le transfert de donnée de la ruche.
Une carte Arduino peut également remplacer la carte lopy4, il faut juste faire attention puisque ce n’est plus le même langage de programmation.
Si vous avez d’autres questions n’hésitez pas
Bonjour,
la prochaine version sera avec un arduino mkr1310 qui est plus facile d’utilisation car la Lopy 4 est plus capricieuse