Tilknyt vandmåleren til det smarte hjem

Engang var hjemmeautomatiseringssystemer, eller hvad de ofte kaldes "det smarte hjem", vanvittigt dyre, og kun de rige kunne have råd til dem. I dag kan man finde relativt budgetvenlige sæt på markedet med sensorer, knapper/afbrydere og aktuatorer til styring af belysning, stikkontakter, ventilation, vandforsyning og andre forbrugere. Selv den mest klodsede DIY-entusiast kan let komme ind i den vidunderlige verden og billig opsætte enheder til det smarte hjem.

Tilknyt vandmåleren til det smarte hjem

Som regel er de tilbudte enheder enten sensorer eller aktuatorer. De gør det nemt at implementere scenarier som "tænd lyset ved bevægelsessensorens aktivering" eller "afbryderen ved udgangen slukker lyset i hele lejligheden". Men telemetry har ikke rigtig fungeret. I bedste fald er det et diagram over temperatur og fugtighed, eller øjeblikkelig effekt i en specifik stikkontakt.

For nylig installerede jeg vandmålere med impulsudgang. Hver gang der løber en liter vand gennem tælleren aktiveres en reedkontakt, som lukker kredsløbet. Det er nu blot et spørgsmål om at tilslutte ledningerne og prøve at få noget ud af det. For eksempel at analysere vandforbruget time for time og dag for dag. Og hvis der er flere vandledninger i lejligheden, er det mere praktisk at se alle aktuelle målinger på én skærm, end at krabbe rundt i svært tilgængelige nicher med en lommelygte.

Under overskriften finder du mit forslag til enheder baseret på ESP8266, der tæller impulserne fra vandmålerne og sender målingerne til smart home-serveren via MQTT. Vi vil programmere i micropython med brug af uasyncio-biblioteket. Under oprettelsen af firmware stødte jeg på flere interessante udfordringer, som jeg også vil beskrive i denne artikel. Lad os komme i gang!

Diagram

Tilknyt vandmåleren til det smarte hjem

Kernen i hele systemet er modulet med mikrokontrolleren ESP8266. Oprindeligt var ESP-12 planlagt, men min viste sig at være defekt. Jeg måtte nøjes med modulet ESP-07, som var tilgængeligt. Heldigvis er de ens både hvad angår pinout og funktionalitet; forskellen er kun i antennen - ESP-12 har en indbygget antenne, mens ESP-07 har en ekstern. Dog fanges WiFi-signalet normalt i mit badeværelse, selv uden antenne.

Modulens opsætning er standard:

  • resetknap med pull-up og kondensator (selvom begge dele allerede findes inde i modulet)
  • Signal enable (CH_PD) trukket til strøm
  • GPIO15 trukket til jord. Dette er kun nødvendigt ved opstart, men jeg har ikke noget at tilslutte til denne pin mere.

For at sætte modulet i firmware-tilstand, skal GPIO2 forbindes til jord, og for nemheds skyld har jeg indarbejdet en Boot-knap. I normal tilstand trækkes denne pin til strøm.

Tilstanden af GPIO2 kontrolleres kun i starten af driften - når der tændes for strømmen eller lige efter reset. Så modulerne indlæses enten som normalt, eller går i firmware-tilstand. Efter indlæsning kan denne udgang bruges som almindelig GPIO. Og da der allerede er en knap, kan man tilføje en nyttig funktion.

Til programmering og fejlfinding vil jeg bruge UART, som jeg har udtrukket til stikket. Når det er nødvendigt, tilslutter jeg blot en USB-UART adapter. Man skal bare huske, at modulet drives med 3.3V. Hvis man glemmer at skifte adapteren til denne spænding og tilslutter 5V, vil modulet sandsynligvis brænde af.

Jeg har ikke problemer med elektricitet i badeværelset - stikkontakten ligger omkring en meter fra målerne, så jeg vil tilslutte fra 220V. Jeg har en lille blok HLK-PM03 fra Tenstar Robot. Jeg har personligt dårlige færdigheder med analog og kraft elektronik, og her er der en klar strømforsyning i et lille kabinet.

Til signalering af driftsmodi har jeg tilføjet en LED, der er tilsluttet GPIO2. Jeg har dog ikke loddet den, da modulen ESP-07 allerede har en LED, og den er tilsluttet samme GPIO2. Men jeg lader det være på PCB'en - man ved jo aldrig, hvornår jeg vil flytte denne LED til kabinettet.

Nu til det mest interessante. Vandmålerne har ikke noget logik, og man kan ikke spørge om de nuværende målinger. Det eneste tilgængelige for os er pulser - lukning af kontakterne fra reed kontakten for hver liter. Udgangene til reed kontakterne er tilsluttet GPIO12/GPIO13. Den pull-up modstand vil jeg aktivere programmæssigt inden i modulet.

Oprindeligt glemte jeg at inkludere modstandene R8 og R9, og i min version af PCB'en er de ikke til stede. Men da jeg allerede deler diagrammet til offentligheden, er det værd at rette denne fejltagelse. Modstandene er nødvendige for ikke at brænde porten, hvis firmware'en fejler og sætter en høj værdi på pin'en, mens reed kontakten kortslutter denne linje til jord (med modstanden vil der maksimalt flyde 3.3V/1000Ω = 3.3mA).

Det er på tide at tænke på, hvad man skal gøre, hvis strømmen forsvinder. Den første mulighed er at anmode serveren om de indledende værdier for tællerne fra starten. Men dette ville kræve en betydelig kompleksitet i udvekslingsprotokollen. Desuden afhænger enhedens funktionalitet i så fald af serverens tilstand. Hvis serveren ikke ville starte op efter strømafbrydelsen (eller først ville starte senere), ville vandtælleren ikke kunne anmode om de indledende værdier og ville fungere forkert.

Derfor besluttede jeg at implementere gemmning af tællerværdier i en hukommelseschip, der er tilsluttet via I2C. Jeg har ikke særlige krav til størrelsen af flashhukommelsen - det er nødvendigt at gemme blot 2 tal (antal liter for de varme og kolde vand tællere). Selv den mindste modul vil være tilstrækkelig. Men jeg skal være opmærksom på antallet af skrivecykler. For de fleste moduler er det 100.000 cykler, for nogle op til en million.

Det kan synes som en million er meget. Men i løbet af mine 4 år i min lejlighed har jeg forbrugt lidt over 500 kubikmeter vand, hvilket svarer til 500.000 liter! Og 500.000 registreringer i flashhukommelsen. Og det er kun koldt vand. Man kan selvfølgelig genlodde chippen hvert par år, men det viser sig, at der findes FRAM-chips. Set fra programmeringsperspektiv er det den samme I2C EEPROM, bare med et utroligt stort antal omskrivningscykler (hundrede millioner). Jeg er bare ikke nået til butikken med sådanne chips endnu, så indtil videre vil jeg bruge den almindelige 24LC512.

Printplade

Oprindeligt planlagde jeg at lave printpladen derhjemme. Derfor blev pladen designet som en enkeltlagsplade. Men efter at have kæmpet i en time med en laserjern og loddemaske (uden den er det ikke rigtig som det skal være), besluttede jeg mig for at bestille plader fra kineserne.

Tilknyt vandmåleren til det smarte hjem

Lige før jeg skulle bestille printpladerne, indså jeg, at jeg udover flashhukommelsen kunne tilslutte noget andet nyttigt til I2C-bussen, for eksempel en skærm. Hvad jeg præcist skal vise på den - er stadig et spørgsmål, men det skal tilsluttes på printpladen. Og da jeg nu planlægger at bestille printplader fra fabrikken, gav det ikke mening længere at begrænse mig til en enkeltlagsplade, så I2C-linjerne er de eneste på bagsiden af printpladen.

Der ensidige layout havde også en stor fejl. Da kortet blev designet ensidigt, var det planlagt, at sporene og SMD-komponenterne skulle placeres på den ene side, mens de udførende komponenter, stik og strømforsyning skulle være på den anden. Da jeg modtog kortene en måned senere, glemte jeg den oprindelige plan og lod alle komponenter sidde på forsiden. Først da det kom til at lodde strømforsyningen, fandt jeg ud af, at plus og minus var byttet om. Jeg måtte improvisere med ledninger. På billedet ovenfor har jeg allerede ændret layoutet, men jorden kører fra den ene del af kortet til den anden gennem tilslutningerne til Boot-knappen (selvom man kunne have lavet et spor på det andet lag).

Det blev sådan her

Tilknyt vandmåleren til det smarte hjem

Kasse

Næste skridt er kassen. Med en 3D-printer er det ikke et problem. Jeg har ikke gjort mig meget umage — jeg har bare tegnet en boks i den ønskede størrelse og lavet udskæringer de rigtige steder. Låget fastgøres til kassen med små skruer.

Tilknyt vandmåleren til det smarte hjem

Jeg har allerede nævnt, at Boot-knappen kan bruges som en generel knap — så lad os føre den til forsiden. For dette har jeg tegnet en speciel "brønd", hvor knappen befinder sig.

Tilknyt vandmåleren til det smarte hjem

Inde i kassen er der også stubbe, som kortet monteres på og fastgøres med en enkelt M3-skrue (der er ikke mere plads på kortet).

Jeg valgte displayet, da jeg allerede havde printet den første prototype af kassen. Den standard to-linjers skærm kunne ikke passe i kassen, men i gemmerne fandt jeg en OLED-skærm SSD1306 128×32. Den er lidt lille, men jeg skal ikke se på den hver dag — den fungerer.

Når jeg overvejede, hvordan ledningerne skulle trækkes fra den, besluttede jeg at placere displayet midt i kassen. Ergonomien er selvfølgelig ikke optimal — knappen er ovenfor, displayet nedenfor. Men jeg har allerede sagt, at idéen om at sætte displayet på kom for sent, og jeg havde ikke lyst til at redesigne kortet for at flytte knappen.

Enheden samlet. Displaymodulet er limet med varm lim.

Tilknyt vandmåleren til det smarte hjem

Tilknyt vandmåleren til det smarte hjem

Det endelige resultat kan ses på KDPV.

Firmware

Lad os gå videre til den softwaremæssige del. Til sådanne små projekter kan jeg rigtig godt lide at bruge programmeringssproget Python (micropython)- koden bliver meget kompakt og forståelig. Heldigvis er der ikke behov for at dykke ned til registerniveau for at presse mikrosekunder — alt kan gøres fra Python.

Det virker simpelt, men det er ikke helt sådan – enheden har flere uafhængige funktioner:

  • Brugeren trykker på knappen og ser på skærmen
  • Literne tikker og opdaterer værdierne i flashhukommelsen
  • Modulet overvåger WiFi-signalet og genopretter forbindelsen ved behov
  • Og uden den blinkende lampe kan man slet ikke være sikker

Det er vigtigt, at en funktion ikke fejler, hvis en anden på en eller anden måde går i stå. Jeg har fået nok af kaktusser i andre projekter, og nu ser jeg kun fejl som "forsvandt en liter, fordi skærmen blev opdateret på det tidspunkt" eller "brugeren kan ikke gøre noget, mens modulet opretter forbindelse til WiFi". Selvfølgelig kan nogle ting håndteres med afbrydelser, men man kan støde på begrænsninger ved varighed, dybde af opkald eller ikke-atomiske ændringer af variabler. Og kode, der prøver at gøre alt på én gang, bliver hurtigt en grød.

I et mere seriøst projekt jeg har brugt klassisk preemptive multitasking og FreeRTOS, men i dette tilfælde viste modellen af co-rutiner (coroutines) og uasync-biblioteket . Og Python-implementeringen af korutinerne er simpelthen fantastisk – for programmøren er alt gjort simpelt og bekvemt. Man skal bare skrive sin logik og angive, hvor imellem trådene man kan skifte.

Forskellene mellem preemptive og concurrent multitasking foreslår jeg at studere som et valg. Men nu må vi endelig gå videre til koden.

#####################################
# Counter class - implements a single water counter on specified pin
#####################################
class Counter():
    debounce_ms = const(25)
    
    def __init__(self, pin_num, value_storage):
        self._value_storage = value_storage
        
        self._value = self._value_storage.read()
        self._value_changed = False

        self._pin = Pin(pin_num, Pin.IN, Pin.PULL_UP)

        loop = asyncio.get_event_loop()
        loop.create_task(self._switchcheck())  # Thread runs forever

Hver tæller behandles af en instans af klassen Counter. Først læses den indledende værdi af tælleren fra EEPROM (value_storage) – dette muliggør gendannelse efter strømsvigt.

Pinnen initialiseres med en indbygget pull-up: hvis reed-kontakten er lukket, er linjen nul, hvis den er åben, trækkes linjen op til strømmen, og kontrolleren læser en et.

Der startes også en separat opgave, der vil stille pin-spørgsmål. Hver tæller vil starte sin egen opgave. Her er dens kode

    """ Tjek pin og øg værdien, når der er gået endnu en liter """
    async def _switchcheck(self):
        last_checked_pin_state = self._pin.value()  # Få den indledende tilstand

        # Spørg efter ændring i pin
        while True:
            state = self._pin.value()
            if state != last_checked_pin_state:
                # Tilstand er ændret: handle nu.
                last_checked_pin_state = state
                if state == 0:
                    self._another_litre_passed()

            # Ignorer yderligere tilstandsændringer, indtil kontakten har stabiliseret sig
            await asyncio.sleep_ms(Counter.debounce_ms)

En forsinkelse på 25 ms er nødvendig for at filtrere kontakter fra støj, og samtidig regulerer den, hvor ofte opgaven vækkes (mens denne opgave sover, udfører andre opgaver). Hver 25 ms vågner funktionen op, tjekker pin, og hvis kontakterne lukker, betyder det, at endnu en liter er passeret gennem tælleren, og det skal behandles.

    def _another_litre_passed(self):
        self._value += 1
        self._value_changed = True

        self._value_storage.write(self._value)

Behandlingen af den næste liter er triviel – tælleren øges simpelthen. Og den nye værdi burde også blive gemt på flash.

For brugervenlighed er der indført "adgangsmetoder".

    def value(self):
        self._value_changed = False
        return self._value

    def set_value(self, value):
        self._value = value
        self._value_changed = False

Nu vil vi benytte os af fordelene ved Python og biblioteket uasync og gøre tællerobjektet waitable (hvordan oversættes det til dansk? Det, man kan vente på?)

    def __await__(self):
        while not self._value_changed:
            yield from asyncio.sleep(0)

        return self.value()

    __iter__ = __await__  

Dette er en praktisk funktion, som venter, indtil tællerværdien er blevet opdateret – funktionen vågner op fra tid til anden og tjekker flaget _value_changed. Det interessante ved denne funktion er, at kaldende kode kan sove under kaldet til denne funktion og sove indtil den får den nye værdi.

Men hvad med afbrydelser?Ja, her kan du drille mig, fordi jeg selv sagde om afbrydelser, men i virkeligheden har jeg indført en simpel polling af pin. Faktisk var afbrydelser det første, jeg prøvede. I ESP8266 kan man organisere et afbrydelse ved stigningen og endda skrive en behandler for denne afbrydelse i Python. I denne afbrydelse kan man opdatere værdien af variablen. Det ville nok være tilstrækkeligt, hvis tælleren var en slavedel – en, der venter på at få sin værdi spurgt.

Desværre (eller heldigvis?) er min enhed aktiv, den skal selv sende beskeder over MQTT-protokollen og gemme data i EEPROM. Her træder begrænsningerne ind — der kan ikke allokeres hukommelse i afbrydelserne, og der kan ikke bruges meget stack, hvilket betyder, at vi må glemme at sende beskeder over netværket. Der er fordele som micropython.schedule(), der gør det muligt at køre en funktion "så snart som muligt”, men spørgsmålet er “hvad er pointen?”. Hvad nu hvis vi sender en besked lige nu, og så kommer der en afbrydelse, der ødelægger værdierne for variablerne. Eller for eksempel, hvis der fra serveren er kommet en ny værdi for tælleren, mens vi endnu ikke har skrevet den gamle ind. Så vi skal enten skabe synkronisering eller finde en anden løsning.

Og så kan der tid af og til opstå en RuntimeError: schedule stack full, og hvem ved hvorfor?

Med eksplicit polling og uasync virker det i dette tilfælde mere elegant og pålideligt.

Jeg har taget arbejdet med EEPROM ud i en lille klasse.

class EEPROM():
    i2c_addr = const(80)

    def __init__(self, i2c):
        self.i2c = i2c
        self.i2c_buf = bytearray(4) # Undgå oprettelse/sletning af bufferen ved hvert kald


    def read(self, eeprom_addr):
        self.i2c.readfrom_mem_into(self.i2c_addr, eeprom_addr, self.i2c_buf, addrsize=16)
        return ustruct.unpack_from("<I", self.i2c_buf)[0]    
        
    
    def write(self, eeprom_addr, value):
        ustruct.pack_into("<I", self.i2c_buf, 0, value)
        self.i2c.writeto_mem(self.i2c_addr, eeprom_addr, self.i2c_buf, addrsize=16)

Det er lidt besværligt at arbejde direkte med bytes i Python, men det er netop bytes, der skrives til hukommelsen. Jeg måtte derfor lave en konvertering mellem heltal og bytes med ustruct biblioteket.

For ikke hver gang at skulle videregive I2C-objektet og hukommelsens adresse, har jeg indkapslet alt dette i en lille og praktisk klasse.

class EEPROMValue():
    def __init__(self, i2c, eeprom_addr):
        self._eeprom = EEPROM(i2c)
        self._eeprom_addr = eeprom_addr
        

    def read(self):
        return self._eeprom.read(self._eeprom_addr)


    def write(self, value):
        self._eeprom.write(self._eeprom_addr, value)

I2C-objektet oprettes med disse parametre.

i2c = I2C(freq=400000, scl=Pin(5), sda=Pin(4))

Nu kommer vi til det mest interessante — implementeringen af kommunikation med serveren over MQTT. Selve protokollen skal ikke implementeres — der findes en færdig asynkron implementering. Og den vil vi bruge.

Det mest interessante er samlet i klassen CounterMQTTClient, der bygger på biblioteks-MQTTClient. Lad os starte med periferien.

#####################################
# Class handles both counters and sends their status to MQTT
#####################################
class CounterMQTTClient(MQTTClient):

    blue_led = Pin(2, Pin.OUT, value = 1)
    button = Pin(0, Pin.IN)

    hot_counter = Counter(12, EEPROMValue(i2c, EEPROM_ADDR_HOT_VALUE))
    cold_counter = Counter(13, EEPROMValue(i2c, EEPROM_ADDR_COLD_VALUE))

Her oprettes og konfigureres stifterne til lampen og knappen, samt objekterne til tællerne for varmt og koldt vand.

Det er ikke så ligetil med initialisering.

    def __init__(self):
        self.internet_outage = True
        self.internet_outages = 0
        self.internet_outage_start = ticks_ms()

        with open("config.txt") as config_file:
            config['ssid'] = config_file.readline().rstrip()
            config['wifi_pw'] = config_file.readline().rstrip()
            config['server'] = config_file.readline().rstrip()
            config['client_id'] = config_file.readline().rstrip()
            self._mqtt_cold_water_theme = config_file.readline().rstrip()
            self._mqtt_hot_water_theme = config_file.readline().rstrip()
            self._mqtt_debug_water_theme = config_file.readline().rstrip()

        config['subs_cb'] = self.mqtt_msg_handler
        config['wifi_coro'] = self.wifi_connection_handler
        config['connect_coro'] = self.mqtt_connection_handler
        config['clean'] = False
        config['clean_init'] = False
        super().__init__(config)

        loop = asyncio.get_event_loop()
        loop.create_task(self._heartbeat())
        loop.create_task(self._counter_coro(self.cold_counter, self._mqtt_cold_water_theme))
        loop.create_task(self._counter_coro(self.hot_counter, self._mqtt_hot_water_theme))
        loop.create_task(self._display_coro())

For at angive parametrene for mqtt_as-bibliotekets arbejde bruges en stor ordbog med forskellige indstillinger — config. De fleste standardindstillinger passer os, men mange indstillinger skal angives eksplicit. For at undgå at skrive indstillinger direkte i koden gemmer jeg dem i en tekstfil, config.txt. Dette gør det muligt at ændre koden uafhængigt af indstillingerne og også hurtigt at lave flere ens enheder med forskellige parametre.

Den sidste blok kode starter flere korotiner for at understøtte forskellige systemfunktioner. Her er for eksempel en koroutine, der håndterer tællerne.

    async def _counter_coro(self, counter, topic):
        # Publicer initialværdi
        value = counter.value()
        await self.publish(topic, str(value))

        # Publicer hver ny værdi
        while True:
            value = await counter
            await self.publish_msg(topic, str(value))

Koroutinen venter i en løkke på den nye tælleværdi, og så snart den er tilgængelig, sendes der et besked via MQTT-protokollen. Den første del af koden sender startværdien, selvom der ikke strømmer vand gennem tælleren.

Den grundlæggende klasse MQTTClient varetager sig selv, initierer forbindelsen via WiFi og genopretter forbindelsen, når den falder. Biblioteket informerer os om ændringer i WiFi-forbindelsens tilstand ved at kalde wifi_connection_handler.

    async def wifi_connection_handler(self, state):
        self.internet_outage = not state
        if state:
            self.dprint('WiFi er oppe.')
            duration = ticks_diff(ticks_ms(), self.internet_outage_start) / 1000
            await self.publish_debug_msg('GenoprettetEfter', duration)
        else:
            self.internet_outages += 1
            self.internet_outage_start = ticks_ms()
            self.dprint('WiFi er nede.')
            
        await asyncio.sleep(0)

Funktionen er ærligt taget fra eksempler. I dette tilfælde tæller den antallet af internetafbrud og deres varighed. Når forbindelsen genoprettes, sendes nedetidens tid til serveren.

Den sidste sleep er forresten kun nødvendig for at gøre funktionen asynkron - i biblioteket kaldes den med await, og ellers kan der kun kaldes funktioner, der har en anden await inde i sig.

Udover forbindelsen med WiFi skal der også etableres en forbindelse til MQTT-broker (server). Det gør biblioteket også, og vi får mulighed for at lave noget nyttigt, når forbindelsen er etableret.

    async def mqtt_connection_handler(self, client):
        await client.subscribe(self._mqtt_cold_water_theme)
        await client.subscribe(self._mqtt_hot_water_theme)

Her abonnerer vi på flere beskeder - serveren har nu mulighed for at angive de nuværende værdier for tællerne ved at sende en tilsvarende besked.

    def mqtt_msg_handler(self, topic, msg):
        topicstr = str(topic, 'utf8')
        self.dprint("Modtaget MQTT-besked topic={}, msg={}".format(topicstr, msg))

        if topicstr == self._mqtt_cold_water_theme:
            self.cold_counter.set_value(int(msg))

        if topicstr == self._mqtt_hot_water_theme:
            self.hot_counter.set_value(int(msg))

Denne funktion behandler de modtagne beskeder, og i henhold til emnet (navnet på beskeden) opdateres værdierne for en af tællerne.

Et par hjælpefunktioner.

    # Publish a message if WiFi and broker is up, else discard
    async def publish_msg(self, topic, msg):
        self.dprint("Publishing message on topic {}: {}".format(topic, msg))
        if not self.internet_outage:
            await self.publish(topic, msg)
        else:
            self.dprint("Message was not published - no internet connection")

Denne funktion sender en besked, hvis forbindelsen er etableret. Hvis der ikke er forbindelse, ignoreres beskeden.

Og dette er bare en praktisk funktion, der danner og sender fejlfindingmeddelelser.

    async def publish_debug_msg(self, subtopic, msg):
        await self.publish_msg("{}\/{}".format(self._mqtt_debug_water_theme, subtopic), str(msg))

Så meget tekst, og vi har stadig ikke blinket med LED'en. Her er det.

    # Blink flash LED if WiFi down
    async def _heartbeat(self):
        while True:
            if self.internet_outage:
                self.blue_led(not self.blue_led()) # Fast blinking if no connection
                await asyncio.sleep_ms(200) 
            else:
                self.blue_led(0) # Rare blinking when connected
                await asyncio.sleep_ms(50)
                self.blue_led(1)
                await asyncio.sleep_ms(5000)

Jeg har forudset 2 blinkemodes. Hvis forbindelsen går tabt (eller den netop etableres), vil enheden blinke hurtigt. Hvis forbindelsen er etableret, blinker enheden hvert 5. sekund. Hvis det er nødvendigt, kan der implementeres andre blinkemodes her.

Men LED'en er bare en sjov detalje. Vi har jo også kastet os ud i skærmen.

    async def _display_coro(self):
        display = SSD1306_I2C(128,32, i2c)
    
        while True:
            display.poweron()
            display.fill(0)
            display.text("KOLD: {:.3f}".format(self.cold_counter.value() / 1000), 16, 4)
            display.text("VARM:  {:.3f}".format(self.hot_counter.value() / 1000), 16, 20)
            display.show()
            await asyncio.sleep(3)
            display.poweroff()

            while self.button():
                await asyncio.sleep_ms(20)

Dette er det, jeg talte om — hvor nemt og praktisk det er med korutiner. Denne lille funktion beskriver ALT brugerinteraktion. Korutinen venter simpelthen på et knaptryk og tænder skærmen i 3 sekunder. Aktuelle værdier fra tællerne vises på skærmen.

Der er stadig et par småting tilbage. Her er funktionen, der (gen)starter det hele. Hovedcyklussen sender blot forskellig debug-information hvert minut. Kort sagt, jeg viser det som det er — jeg tror ikke, der er behov for at kommentere det nærmere.

   async def main(self):
        while True:
            try:
                await self._connect_to_WiFi()
                await self._run_main_loop()
                    
            except Exception as e:
                self.dprint('Global kommunikationsfejl: ', e)
                await asyncio.sleep(20)

    async def _connect_to_WiFi(self):
        self.dprint('Forbinder til WiFi og MQTT')
        sta_if = network.WLAN(network.STA_IF)
        sta_if.connect(config['ssid'], config['wifi_pw'])
        
        conn = False
        while not conn:
            await self.connect()
            conn = True

        self.dprint('Forbundet!')
        self.internet_outage = False

    async def _run_main_loop(self):
        # Løkke for evigt
        mins = 0
        while True:
            gc.collect()  # Til RAM-statistik.
            mem_free = gc.mem_free()
            mem_alloc = gc.mem_alloc()

            try:
                await self.publish_debug_msg("Uptime", mins)
                await self.publish_debug_msg("Repubs", self.REPUB_COUNT)
                await self.publish_debug_msg("Outages", self.internet_outages)
                await self.publish_debug_msg("MemFree", mem_free)
                await self.publish_debug_msg("MemAlloc", mem_alloc)
            except Exception as e:
                self.dprint("Der opstod en undtagelse: ", e)
            mins += 1

            await asyncio.sleep(60)

Nå, et par flere indstillinger og kontanter for fuldstændighedens skyld.

#####################################
# Constants and configuration
#####################################


config['keepalive'] = 60
config['clean'] = False
config['will'] = ('/ESP/Wemos/Water/LastWill', 'Goodbye cruel world!', False, 0)

MQTTClient.DEBUG = True

EEPROM_ADDR_HOT_VALUE = const(0)
EEPROM_ADDR_COLD_VALUE = const(4)

Alt sammen startes på denne måde.

client = CounterMQTTClient()
loop = asyncio.get_event_loop()
loop.run_until_complete(client.main())

Noget er der galt med min hukommelse.

Så hele koden er der. Jeg uploadede filerne ved hjælp af værktøjet ampy — det tillader mig at uploade dem til den interne (den der er i selve ESP-07) flash og derefter tilgå dem i programmet som almindelige filer. Jeg uploaded også de biblioteker, jeg bruger: mqtt_as, uasyncio, ssd1306 og collections (brugt inde i mqtt_as).

Vi starter og… får MemoryError. Jo mere jeg forsøgte at forstå, hvor hukommelsen egentlig lækker, jo tidligere opstod denne fejl, jo flere debug prints jeg tilføjede. En kort googlesøgning førte mig til at forstå, at mikrocontrolleren i bund og grund har 30 kB hukommelse, hvor 65 kB kode (inklusive biblioteker) overhovedet ikke kan passe.

Men der er en løsning. Det viser sig, at micropython ikke udfører kode direkte fra .py-filen — denne fil kompileres først. Og den kompileres direkte på mikrokontrolleren, hvor den omdannes til bytekode, som derefter opbevares i hukommelsen. Der kræves også et vist mængde RAM til kompilatoren.

Tricket er at frigøre mikrokontrolleren fra ressourcekrævende kompilering. Man kan kompilere filer på en stor computer og derefter uploade den allerede færdige bytekode til mikrokontrolleren. For dette skal man hente micropython-firmware og bygge værktøjet mpy-cross..

Jeg valgte ikke at skrive en Makefile, men gik manuelt igennem og kompilerede alle de nødvendige filer (inklusive biblioteker) cirka således

mpy-cross water_counter.py

Det er kun tilbage at uploade filerne med .mpy-udvidelsen, idet man husker at fjerne de tilsvarende .py-filer fra enhedens filsystem først.

Jeg udførte al udvikling i programmet (IDE?) ESPlorer. Det gør det muligt at uploade scripts til mikrokontrolleren og straks udføre dem. I mit tilfælde findes al logik og oprettelse af alle objekter i filen water_counter.py (.mpy). Men for at dette kan køre automatisk ved start, skal der også være en fil med navnet main.py. Og det skal netop være en .py, ikke en forkompileret .mpy. Her er dens trivielle indhold

import water_counter

Vi kører det - alt fungerer. Men der er truende lidt fri hukommelse - omkring 1 kB. Jeg har stadig planer om at udvide enhedens funktionalitet, og denne kilobyte vil åbenbart ikke være tilstrækkelig. Men der ser også ud til at være en løsning til dette tilfælde.

Sagen er den, at selvom filerne er kompilert til bytekode og befinder sig på det interne filsystem, bliver de alligevel indlæst i RAM og udført derfra. Men det viser sig, at micropython kan udføre bytekode direkte fra flashhukommelsen, men for at gøre det, skal det integreres direkte i firmwaren. Det er ikke svært, selvom det tog en betydelig tid på min netbook (for der havde jeg Linux).

Algoritmen er sådan:

  • Hent og installer ESP Open SDK. Dette værktøj opbygger kompilatoren og bibliotekerne til programmer til ESP8266. Det samles i henhold til instruktionerne på projektets hovedside (jeg valgte installationen STANDALONE=yes)
  • Download kilderne til micropython.
  • De nødvendige biblioteker skal placeres i ports/esp8266/modules indenfor micropython-træet.
  • Vi bygger firmwaren i henhold til instrukserne i filen ports/esp8266/README.md
  • Vi uploader firmwaren til mikrocontrollere (jeg gør dette på Windows med programmerne ESP8266Flasher eller Python-værktøjet esptool)

Det er det, nu vil 'import ssd1306' hente koden direkte fra firmwaren, og den RAM, der bruges til dette, vil ikke blive opbrugt. Med det trick har jeg kun uploaded kodebibliotekerne til firmwaren, mens hovedprogrammets kode kører fra filsystemet. Dette gør det nemt at ændre programmet uden at recompilere firmwaren. I øjeblikket har jeg omkring 8,5 kB RAM ledigt. Det vil give mulighed for at implementere en del nyttige funktioner i fremtiden. Hvis der ikke er nok hukommelse, kan jeg også putte hovedprogrammet ind i firmwaren.

Og hvad skal man så gøre med det nu?

Okay, hardwaren er samlet, firmwaren er skrevet, boksen er printet, enheden er fastgjort til væggen og blinker muntert med lys. Men indtil videre er dette alt sammen en sort boks (i både bogstavelig og overført betydning), og der er ikke meget praktisk anvendelse af det endnu. Det er tid til at gøre noget med MQTT-beskederne, der sendes til serveren.

Mit "smarte hjem" kører på Majordomo systemet. MQTT-modulet er muligvis inkluderet fra starten eller let kan installeres fra tilføjelsesmarkedet - jeg kan ikke huske, hvor det kom fra. MQTT er ikke selvforsyndende - en såkaldt broker er nødvendig - en server, der modtager, sorterer og videresender MQTT-beskeder til klienterne. Jeg bruger mosquitto, som (ligesom majordomo) også kører på den samme netbook.

Når enheden har sendt en besked én gang, vil værdien straks vises på listen.

Tilknyt vandmåleren til det smarte hjem

Disse værdier kan nu knyttes til systemobjekter, de kan bruges i automatiseringsscenarier og underkastes forskellige analyser - alt dette ligger uden for denne artikels ramme. Hvis nogen er interesseret i majordomo-systemet, kan jeg anbefale kanalen Elektronik I Fokuseret — en ven, som også bygger et smart hjem og tydeligt forklarer, hvordan man konfigurerer systemet.

Jeg vil kun vise et par grafer. Dette er en simpel graf over værdierne for et døgn.

Tilknyt vandmåleren til det smarte hjem
Det er tydeligt, at der næsten ikke blev brugt vand om natten. Et par gange gik nogen på toilet, og det ser ud til, at omvendt osmosefilter suger et par liter om natten. Om morgenen stiger forbruget betydeligt. Normalt bruger jeg varmt vand fra kedlen, men her ønskede jeg at tage et bad og skiftede midlertidigt til kommunalt varmt vand - det er også tydeligt i den nederste graf.

Fra denne graf kan jeg se, at det at gå på toilettet kræver 6-7 liter vand, at tage et brusebad kræver 20-30 liter, at vaske op tager omkring 20 liter, og at tage et bad kræver 160 liter. I løbet af en dag forbruger min familie omkring 500-600 liter.

For de særligt nysgerrige er der mulighed for at kigge ind i posterne for hver enkelt værdi.

Tilknyt vandmåleren til det smarte hjem

Her fik jeg at vide, at vandet strømmer fra hanen med en hastighed på cirka 1 liter hver 5. sekund.

Men i den form er statistikken nok ikke særlig nem at se på. I majordomo er der også muligheder for at se forbrugsgrafer pr. dag, uge og måned. Her er for eksempel forbrugsdiagrammet i søjler.

Tilknyt vandmåleren til det smarte hjem

Indtil videre har jeg kun data for en uge. Om en måned vil denne graf være mere repræsentativ, da hver dag vil have sin egen søjle. Det ødelægger lidt billedet, at de værdier, jeg manuelt indtaster, bliver justeret (den største søjle). Og det er stadig uklart, om jeg har indtastet de allerførste værdier næsten en kubik meter for lavt, eller om det er en fejl i firmware, og ikke alle liter er medtaget. Jeg har brug for mere tid.

Der skal også arbejdes lidt mere på selve graferne, male dem og gøre dem pæne. Måske vil jeg også til debug-formål opbygge en graf over hukommelsesforbrug – måske siver der noget. Og jeg overvejer også at vise perioder, hvor internettet har været fraværende. Indtil videre forbliver alt dette på idé-niveau.

Konklusion

I dag er min lejlighed blevet lidt smartere. Med denne lille enhed bliver det nemmere for mig at følge med i vandforbruget i huset. Hvis jeg tidligere brokkede mig over, at vi igen havde forbrugt for meget vand i måneden, kan jeg nu finde kilden til dette forbrug.

Nogen vil måske synes, det er mærkeligt at se aflæsningerne på en skærm, når den er en meter fra tælleren. Men i en ikke så fjern fremtid planlægger jeg at flytte til en anden lejlighed, hvor der vil være flere vandrør, og tællerne sandsynligvis vil være placeret på trappen. Så en enhed til fjernaflæsning vil være meget praktisk.

Jeg planlægger også at udvide enhedens funktionalitet. Jeg kigger allerede på motoriserede ventiler. Lige nu skal jeg dreje tre haner i et svært tilgængeligt hjørne for at skifte mellem varmt vand og byvand. Det ville være meget mere bekvemt at gøre det med én knap med tilsvarende indikation. Og selvfølgelig er det værd at implementere lækagesikkerhed.

I denne artikel deler jeg min version af en enhed baseret på ESP8266. Efter min mening har jeg lavet en ret interessant firmware tilbage med micropython ved hjælp af koroutiner — simpelt og pænt. Jeg har forsøgt at beskrive mange nuancer og problemer, jeg stødte på undervejs. Måske har jeg beskrevet alt for detaljeret; for mig som læser er det nemmere at spole forbi det unødvendige end at tænke på det, der ikke blev sagt.

Som altid er jeg åben for konstruktiv kritik.

Kildekode
Skema og kredsløb
Model af kabinet

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster