Transfert de données via Lora vers JEEDOM dans le cadre d’une ruche connectée

Transfert de données via Lora vers JEEDOM dans le cadre d’une ruche connectée

18 juin 2023 3 Par VictorM

Nous 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é !