Ansluta en vattenmätare till ett smart hem

En gång i tiden var hemautomationssystem, eller "smarta hem" som de ofta kallades, fruktansvärt dyra och bara de rika hade råd med dem. Idag på marknaden kan man hitta ganska billiga kit med sensorer, knappar/brytare och ställdon för styrning av belysning, uttag, ventilation, vattenförsörjning och andra konsumenter. Och även den mest sneda gör-det-själv-personen kan engagera sig i skönhet och montera enheter för ett smart hem till ett billigt pris.

Ansluta en vattenmätare till ett smart hem

Typiskt är de föreslagna anordningarna antingen sensorer eller ställdon. De gör det enkelt att implementera scenarier som "när en rörelsesensor utlöses, slå på belysningen" eller "strömbrytaren nära utgången släcker ljuset i hela lägenheten." Men på något sätt fungerade det inte med telemetri. I bästa fall är det en graf över temperatur och luftfuktighet, eller momentan effekt vid ett specifikt uttag.

Jag har nyligen installerat vattenmätare med pulsutgång. För varje liter som passerar genom mätaren aktiveras reedomkopplaren och stänger kontakten. Det enda som återstår är att hålla fast vid trådarna och försöka få nytta av det. Analysera till exempel vattenförbrukningen efter timme och veckodag. Tja, om det finns flera vattenstigare i lägenheten, är det bekvämare att se alla nuvarande indikatorer på en skärm än att klättra in i svåråtkomliga nischer med en ficklampa.

Nedanför snittet finns min version av en enhet baserad på ESP8266, som räknar pulser från vattenmätare och skickar avläsningar via MQTT till den smarta hemservern. Vi kommer att programmera i micropython med hjälp av uasyncio-biblioteket. När jag skapade firmwaren stötte jag på flera intressanta svårigheter, som jag också kommer att diskutera i den här artikeln. Gå!

Schemat

Ansluta en vattenmätare till ett smart hem

Hjärtat i hela kretsen är en modul på ESP8266 mikrokontroller. ESP-12 var ursprungligen planerad, men min visade sig vara defekt. Vi fick nöja oss med ESP-07-modulen, som fanns tillgänglig. Som tur är är de lika både vad gäller stift och funktionalitet, den enda skillnaden ligger i antennen – ESP-12 har en inbyggd, medan ESP-07 har en extern. Men även utan WiFi-antenn tas signalen i mitt badrum emot normalt.

Standard modulledningar:

  • återställningsknapp med pull-up och kondensator (även om båda redan finns inne i modulen)
  • Aktiveringssignalen (CH_PD) dras upp till ström
  • GPIO15 dras till marken. Detta behövs bara i början, men jag har fortfarande inget att fästa på det här benet, jag behöver det inte längre

För att sätta modulen i firmware-läge måste du kortsluta GPIO2 till jord, och för att göra det mer bekvämt tillhandahöll jag en Boot-knapp. I normalt tillstånd dras denna stift till ström.

Tillståndet för GPIO2-linjen kontrolleras endast i början av driften - när strömmen slås på eller omedelbart efter en återställning. Så modulen startar antingen som vanligt eller går in i firmware-läge. När det är laddat kan detta stift användas som en vanlig GPIO. Tja, eftersom det redan finns en knapp där kan du bifoga någon användbar funktion till den.

För programmering och felsökning kommer jag att använda UART, som matas ut till en kam. Vid behov ansluter jag helt enkelt en USB-UART-adapter dit. Du behöver bara komma ihåg att modulen drivs av 3.3V. Om du glömmer att byta adaptern till denna spänning och mata 5V kommer modulen med största sannolikhet att brinna ut.

Jag har inga problem med el i badrummet - uttaget ligger ungefär en meter från mätarna, så jag kommer att få 220V ström. Som strömkälla kommer jag att ha en liten block HLK-PM03 av Tenstar Robot. Själv har jag svårt för analog och kraftelektronik, men här finns ett färdigt nätaggregat i ett litet fodral.

För att signalera driftlägen tillhandahöll jag en lysdiod ansluten till GPIO2. Men jag lödde inte upp det, eftersom... ESP-07-modulen har redan en lysdiod, och den är även ansluten till GPIO2. Men låt det vara på tavlan, ifall jag vill mata ut denna lysdiod till fodralet.

Låt oss gå vidare till den mest intressanta delen. Vattenmätare har ingen logik, du kan inte be dem om aktuella avläsningar. Det enda som är tillgängligt för oss är impulser - att stänga kontakterna på reed switchen varje liter. Mina reed switch-utgångar är anslutna till GPIO12/GPIO13. Jag kommer att aktivera pull-up-motståndet programmatiskt inuti modulen.

Till en början glömde jag att tillhandahålla motstånden R8 och R9 och min version av kortet har inte dem. Men eftersom jag redan publicerar diagrammet för alla att se, är det värt att korrigera denna förbiseende. Motstånd behövs för att inte bränna porten om firmwaren missar och ställer in stiftet på ett, och reed-omkopplaren kortsluter denna linje till jord (med motståndet kommer maximalt 3.3V/1000Ohm = 3.3mA att flöda).

Det är dags att fundera på vad man ska göra om elen går ur. Det första alternativet är att begära initiala räknarvärden från servern vid starten. Men detta skulle kräva en betydande komplikation av utbytesprotokollet. Dessutom beror enhetens prestanda i detta fall på serverns tillstånd. Om servern inte startade efter att strömmen stängdes av (eller startade senare), skulle vattenmätaren inte kunna begära initiala värden och skulle inte fungera korrekt.

Därför bestämde jag mig för att implementera att spara räknarvärden i ett minneschip kopplat via I2C. Jag har inga speciella krav på storleken på flashminnet - du behöver bara spara 2 siffror (antal liter enligt varm- och kallvattenmätarna). Även den minsta modulen duger. Men du måste vara uppmärksam på antalet inspelningscykler. För de flesta moduler är detta 100 tusen cykler, för vissa upp till en miljon.

Det verkar som att en miljon är mycket. Men under de fyra åren jag bodde i min lägenhet förbrukade jag lite mer än 4 kubikmeter vatten, det är 500 tusen liter! Och 500 tusen rekord i blixt. Och det är bara kallt vatten. Du kan naturligtvis löda om chippet vartannat år, men det visar sig att det finns FRAM-chips. Ur programmeringssynpunkt är detta samma I500C EEPROM, bara med ett mycket stort antal omskrivningscykler (hundratals miljoner). Det är bara det att jag fortfarande inte kan ta mig till affären med sådana mikrokretsar, så för tillfället står den vanliga 2LC24.

Tryckt kretskort

Från början tänkte jag göra tavlan hemma. Därför utformades tavlan som ensidig. Men efter att ha tillbringat en timme med ett laserjärn och en lödmask (det är på något sätt inte comme il faut utan det), bestämde jag mig ändå för att beställa brädor från kineserna.

Ansluta en vattenmätare till ett smart hem

Nästan innan jag beställde brädet insåg jag att jag förutom flashminneschippet kunde ansluta något annat användbart till I2C-bussen, till exempel en display. Vad exakt som ska matas ut till det är fortfarande en fråga, men det måste dirigeras på kortet. Jo, eftersom jag skulle beställa brädor från fabriken var det ingen idé att begränsa mig till en enkelsidig bräda, så I2C-linjerna är de enda på baksidan av brädan.

Det fanns också ett stort problem med enkelriktad kabeldragning. Därför att Kortet ritades som ensidigt, så spåren och SMD-komponenterna var planerade att placeras på ena sidan, och utgångskomponenterna, kontakter och strömförsörjning på den andra. När jag fick brädorna en månad senare glömde jag originalplanen och lödde fast alla komponenter på framsidan. Och först när det kom till att löda strömförsörjningen visade det sig att plus och minus var kopplade i omvänd riktning. Jag var tvungen att odla med hoppare. På bilden ovan har jag redan bytt ledningar, men marken överförs från en del av kortet till en annan genom stiften på Boot-knappen (även om det skulle vara möjligt att rita ett spår på det andra lagret).

Det blev så här

Ansluta en vattenmätare till ett smart hem

hölje

Nästa steg är kroppen. Om du har en 3D-skrivare är detta inget problem. Jag brydde mig inte för mycket - jag ritade bara en låda i rätt storlek och gjorde utskärningar på rätt ställen. Locket fästs på kroppen med små självgängande skruvar.

Ansluta en vattenmätare till ett smart hem

Jag har redan nämnt att Boot-knappen kan användas som en allmän knapp - så vi kommer att visa den på frontpanelen. För att göra detta ritade jag en speciell "brunn" där knappen bor.

Ansluta en vattenmätare till ett smart hem

Inuti höljet finns det också dubbar på vilka brädan är installerad och säkrad med en enda M3-skruv (det fanns inget mer utrymme på brädan)

Jag valde displayen redan när jag skrev ut den första provversionen av fodralet. En vanlig tvåradsläsare fick inte plats i detta fodral, men i botten fanns en OLED-skärm SSD1306 128×32. Den är lite liten, men jag behöver inte stirra på den varje dag – det är för mycket för mig.

När jag kom på det här sättet och hur kablarna skulle dras från den, bestämde jag mig för att sätta fast displayen i mitten av fodralet. Ergonomi är naturligtvis under pari - knappen är på toppen, displayen är på botten. Men jag har redan sagt att idén att fästa displayen kom för sent och jag var för lat för att koppla om kortet för att flytta knappen.

Enheten är monterad. Displaymodulen limmas på snoppen med varmt lim

Ansluta en vattenmätare till ett smart hem

Ansluta en vattenmätare till ett smart hem

Slutresultatet kan ses på KDPV

införing

Låt oss gå vidare till mjukvarudelen. För små hantverk som detta gillar jag verkligen att använda Python (mikropyton) - koden visar sig vara mycket kompakt och begriplig. Som tur är behöver man inte gå ner till registernivån för att klämma ut mikrosekunder – allt går att göra från Python.

Det verkar som att allt är enkelt, men inte särskilt enkelt - enheten har flera oberoende funktioner:

  • Användaren petar på knappen och tittar på displayen
  • Liter tickar och uppdaterar värden i flashminnet
  • Modulen övervakar WiFi-signalen och återansluter vid behov
  • Tja, utan en blinkande glödlampa är det omöjligt

Du kan inte anta att en funktion inte fungerade om en annan har fastnat av någon anledning. Jag har redan blivit mätt på kaktusar i andra projekt och nu ser jag fortfarande fel i stil med "missade ytterligare en liter eftersom displayen uppdaterades just nu" eller "användaren kan inte göra någonting medan modulen ansluter till WiFi.” Visst kan vissa saker göras genom avbrott, men du kan stöta på begränsningar av varaktighet, kapsling av samtal eller icke-atomära förändringar av variabler. Tja, koden som gör allt förvandlas snabbt till mos.

В mer seriöst projekt Jag använde klassisk förebyggande multitasking och FreeRTOS, men i det här fallet visade sig modellen vara mycket mer lämplig coroutines och uasync-bibliotek . Dessutom är Python-implementeringen av coroutines helt enkelt fantastisk - allt görs enkelt och bekvämt för programmeraren. Skriv bara din egen logik, berätta bara på vilka ställen du kan växla mellan strömmar.

Jag föreslår att du studerar skillnaderna mellan förebyggande och konkurrenskraftig multitasking som ett valfritt ämne. Låt oss äntligen gå vidare till 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

Varje räknare hanteras av en instans av klassen Counter. Först och främst subtraheras det initiala räknarvärdet från EEPROM (value_storage) - detta är hur återställning efter ett strömavbrott implementeras.

Stiftet initieras med en inbyggd pull-up till strömförsörjningen: om reed-omkopplaren är stängd är linjen noll, om ledningen är öppen dras den upp till strömförsörjningen och styrenheten läser av en.

En separat uppgift lanseras också här, som kommer att polla stiftet. Varje räknare kommer att köra sin egen uppgift. Här är hennes kod

    """ Poll pin and advance value when another litre passed """
    async def _switchcheck(self):
        last_checked_pin_state = self._pin.value()  # Get initial state

        # Poll for a pin change
        while True:
            state = self._pin.value()
            if state != last_checked_pin_state:
                # State has changed: act on it now.
                last_checked_pin_state = state
                if state == 0:
                    self._another_litre_passed()

            # Ignore further state changes until switch has settled
            await asyncio.sleep_ms(Counter.debounce_ms)

En fördröjning på 25ms behövs för att filtrera kontaktstuds, och samtidigt reglerar den hur ofta uppgiften vaknar (medan den här uppgiften sover körs andra uppgifter). Var 25:e ms vaknar funktionen, kontrollerar stiftet och om reedkontaktens kontakter är stängda så har ytterligare en liter passerat genom mätaren och detta måste bearbetas.

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

        self._value_storage.write(self._value)

Att bearbeta nästa liter är trivialt - räknaren ökar helt enkelt. Tja, det skulle vara trevligt att skriva det nya värdet på en flash-enhet.

För enkel användning tillhandahålls "tillbehör".

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

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

Nåväl, låt oss nu dra nytta av Python och uasync-biblioteket och skapa ett väntat motobjekt (hur kan vi översätta detta till ryska? Det du kan förvänta dig?)

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

        return self.value()

    __iter__ = __await__  

Detta är en så bekväm funktion som väntar tills räknarvärdet uppdateras - funktionen vaknar då och då och kontrollerar flaggan _value_changed. Det coola med den här funktionen är att anropskoden kan somna när den anropas och vila tills ett nytt värde tas emot.

Hur är det med avbrott?Ja, vid det här laget kan du trolla mig och säga att du själv sa om avbrott, men i verkligheten gjorde du en dum pin-undersökning. Avbrott är faktiskt det första jag försökte. I ESP8266 kan du organisera ett kantavbrott och till och med skriva en hanterare för detta avbrott i Python. I detta avbrott kan värdet på en variabel uppdateras. Förmodligen skulle detta vara tillräckligt om räknaren var en slavenhet - en som väntar tills den tillfrågas om detta värde.

Tyvärr (eller lyckligtvis?) är min enhet aktiv, den måste själv skicka meddelanden via MQTT-protokollet och skriva data till EEPROM. Och här spelar begränsningar in - du kan inte allokera minne i avbrott och använda en stor stack, vilket gör att du kan glömma att skicka meddelanden över nätverket. Det finns bullar som micropython.schedule() som låter dig köra någon funktion "så snart som möjligt", men frågan uppstår, "vad är poängen?" Tänk om vi skickar något slags meddelande just nu, och sedan kommer ett avbrott in och förstör variablernas värden. Eller till exempel så kom ett nytt räknarvärde från servern medan vi ännu inte hade skrivit ner det gamla. I allmänhet måste du blockera synkronisering eller komma ur den på något annat sätt.

Och då och då RuntimeError: schemalägg hela stacken kraschar och vem vet varför?

Med explicit polling och uasync blir det i det här fallet på något sätt vackrare och pålitligare

Jag tog med mig arbete med EEPROM till en liten klass

class EEPROM():
    i2c_addr = const(80)

    def __init__(self, i2c):
        self.i2c = i2c
        self.i2c_buf = bytearray(4) # Avoid creation/destruction of the buffer on each call


    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)

I Python är det svårt att arbeta direkt med bytes, men det är byten som skrivs till minnet. Jag var tvungen att inhägna omvandlingen mellan heltal och byte med hjälp av ustruct-biblioteket.

För att inte överföra I2C-objektet och adressen till minnescellen varje gång, slog jag in det hela i en liten och bekväm klassiker

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)

Själva I2C-objektet skapas med dessa parametrar

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

Vi kommer till den mest intressanta delen - implementeringen av kommunikation med servern via MQTT. Tja, det finns inget behov av att implementera själva protokollet - jag hittade det på Internet färdig asynkron implementering. Detta är vad vi kommer att använda.

Alla de mest intressanta sakerna är samlade i klassen CounterMQTTClient, som är baserad på biblioteket MQTTClient. Låt oss börja från periferin

#####################################
# 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))

Här kan du skapa och konfigurera glödlampsstift och -knappar, samt kall- och varmvattenmätarobjekt.

Med initiering är inte allt så trivialt

    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())

För att ställa in driftsparametrarna för biblioteket mqtt_as används en stor ordbok med olika inställningar - config. De flesta av standardinställningarna är bra för oss, men många inställningar måste ställas in explicit. För att inte skriva inställningarna direkt i koden lagrar jag dem i textfilen config.txt. Detta gör att du kan ändra koden oavsett inställningarna, samt nita flera identiska enheter med olika parametrar.

Det sista kodblocket startar flera koroutiner för att tjäna olika funktioner i systemet. Till exempel, här är en coroutine som servar räknare

    async def _counter_coro(self, counter, topic):
        # Publish initial value
        value = counter.value()
        await self.publish(topic, str(value))

        # Publish each new value
        while True:
            value = await counter
            await self.publish_msg(topic, str(value))

Koroutinen väntar i en slinga på ett nytt räknarvärde och skickar, så snart det dyker upp, ett meddelande via MQTT-protokollet. Den första koden skickar startvärdet även om inget vatten rinner genom räknaren.

Basklassen MQTTClient betjänar sig själv, initierar en WiFi-anslutning och återansluter när anslutningen bryts. När det sker förändringar i tillståndet för WiFi-anslutningen, informerar biblioteket oss genom att ringa wifi_connection_handler

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

Funktionen är ärligt kopierad från exempel. I det här fallet räknar den antalet avbrott (internet_outages) och deras varaktighet. När anslutningen återställs skickas en vilotid till servern.

Den sista sömnen behövs förresten bara för att göra funktionen asynkron - i biblioteket kallas den via await, och bara funktioner vars kropp innehåller en annan await kan anropas.

Förutom att ansluta till WiFi måste du också upprätta en anslutning till MQTT-mäklaren (servern). Biblioteket gör detta också och vi får möjlighet att göra något nyttigt när förbindelsen är etablerad

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

Här prenumererar vi på flera meddelanden - servern har nu möjlighet att ställa in de aktuella räknarvärdena genom att skicka motsvarande meddelande.

    def mqtt_msg_handler(self, topic, msg):
        topicstr = str(topic, 'utf8')
        self.dprint("Received MQTT message 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))

Den här funktionen behandlar inkommande meddelanden, och beroende på ämnet (meddelandets titel) uppdateras värdena för en av räknarna

Ett par hjälpfunktioner

    # 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")

Denna funktion skickar ett meddelande om anslutningen upprättas. Om det inte finns någon anslutning ignoreras meddelandet.

Och detta är bara en bekväm funktion som genererar och skickar felsökningsmeddelanden.

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

Så mycket text, och vi har inte blinkat en lysdiod än. Här

    # 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)

Jag har tillhandahållit 2 blinkande lägen. Om anslutningen bryts (eller den håller på att upprättas) blinkar enheten snabbt. Om anslutningen upprättas blinkar enheten en gång var 5:e sekund. Vid behov kan andra blinkande lägen implementeras här.

Men lysdioden är bara bortskämd. Vi siktade också på displayen.

    async def _display_coro(self):
        display = SSD1306_I2C(128,32, i2c)
    
        while True:
            display.poweron()
            display.fill(0)
            display.text("COLD: {:.3f}".format(self.cold_counter.value() / 1000), 16, 4)
            display.text("HOT:  {:.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)

Det här är vad jag pratade om - hur enkelt och bekvämt det är med koroutiner. Denna lilla funktion beskriver HELA användarupplevelsen. Coroutinen väntar helt enkelt på att knappen ska tryckas in och sätter på displayen i 3 sekunder. Displayen visar aktuella mätarvärden.

Det finns fortfarande ett par småsaker kvar. Här är funktionen som (om)startar hela företaget. Huvudslingan skickar bara olika felsökningsinformation en gång i minuten. I allmänhet citerar jag det som det är - jag tror inte att det finns något behov av att kommentera för mycket

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

    async def _connect_to_WiFi(self):
        self.dprint('Connecting to WiFi and 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('Connected!')
        self.internet_outage = False

    async def _run_main_loop(self):
        # Loop forever
        mins = 0
        while True:
            gc.collect()  # For RAM stats.
            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("Exception occurred: ", e)
            mins += 1

            await asyncio.sleep(60)

Nåväl, ett par inställningar och konstanter till för att slutföra beskrivningen

#####################################
# 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)

Allt börjar så här

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

Något hände med mitt minne

Så, all kod finns där. Jag laddade upp filerna med hjälp av ampy-verktyget - det låter dig ladda upp dem till den interna (den i själva ESP-07) flashenheten och sedan komma åt den från programmet som vanliga filer. Där laddade jag också upp mqtt_as, uasyncio, ssd1306 och samlingsbiblioteken som jag använde (används inuti mqtt_as).

Vi startar och... Vi får ett minnesfel. Dessutom, ju mer jag försökte förstå var exakt minnet läckte, desto fler felsökningsutskrifter jag placerade, desto tidigare dök detta fel upp. En kort Google-sökning ledde mig till insikten att mikrokontrollern i princip bara har 30 kB minne, i vilket 65 kB kod (inklusive bibliotek) helt enkelt inte får plats.

Men det finns en väg ut. Det visar sig att micropython inte kör kod direkt från en .py-fil - den här filen kompileras först. Dessutom kompileras den direkt på mikrokontrollern, omvandlas till bytekod, som sedan lagras i minnet. Jo, för att kompilatorn ska fungera behöver du också en viss mängd RAM.

Tricket är att rädda mikrokontrollern från resurskrävande kompilering. Du kan kompilera filerna på en stor dator och ladda upp den färdiga bytekoden till mikrokontrollern. För att göra detta måste du ladda ner micropython-firmware och bygga mpy-cross-verktyget.

Jag skrev inte en Makefile, utan gick igenom och kompilerade manuellt alla nödvändiga filer (inklusive bibliotek) ungefär så här

mpy-cross water_counter.py

Allt som återstår är att ladda upp filer med filtillägget .mpy, utan att glömma att först radera motsvarande .py från enhetens filsystem.

Jag gjorde all utveckling i programmet (IDE?) ESPlorer. Det låter dig ladda upp skript till mikrokontrollern och omedelbart exekvera dem. I mitt fall finns all logik och skapande av alla objekt i filen water_counter.py (.mpy). Men för att allt detta ska starta automatiskt måste det också finnas en fil som heter main.py i början. Dessutom bör det vara exakt .py och inte förkompilerad .mpy. Här är dess triviala innehåll

import water_counter

Vi lanserar det - allt fungerar. Men ledigt minne är oroväckande litet - cirka 1 kb. Jag har fortfarande planer på att utöka enhetens funktionalitet, och denna kilobyte räcker helt klart inte för mig. Men det visade sig att det finns en utväg även för det här fallet.

Så här är det. Även om filerna är kompilerade till bytekod och finns i det interna filsystemet, laddas de i verkligheten fortfarande in i RAM och exekveras därifrån. Men det visar sig att micropython kan exekvera bytekod direkt från flashminnet, men för detta måste du bygga in den direkt i firmware. Det är inte svårt, även om det tog ganska lång tid på min netbook (endast där råkade jag ha Linux).

Algoritmen är denna:

  • ladda ner och installera ESP Open SDK. Den här saken sammanställer en kompilator och bibliotek för program för ESP8266. Monteras enligt instruktionerna på projektets huvudsida (jag valde inställningen STANDALONE=ja)
  • nedladdning micropython sorterar
  • Placera de nödvändiga biblioteken i ports/esp8266/modules inuti micropython-trädet
  • Vi monterar den fasta programvaran enligt instruktionerna i filen ports/esp8266/README.md
  • Vi laddar upp firmware till mikrokontrollern (jag gör detta på Windows med ESP8266Flasher-program eller Python esptool)

Det är allt, nu kommer 'import ssd1306' att lyfta koden direkt från firmware och RAM kommer inte att förbrukas för detta. Med det här tricket laddade jag bara upp bibliotekskoden till firmwaren, medan huvudprogramkoden exekveras från filsystemet. Detta gör att du enkelt kan modifiera programmet utan att kompilera om den fasta programvaran. För tillfället har jag cirka 8.5 kb ledigt RAM-minne. Detta kommer att tillåta oss att implementera en hel del olika användbara funktioner i framtiden. Tja, om det inte finns tillräckligt med minne alls, kan du trycka in huvudprogrammet i firmware.

Så vad ska vi göra åt det nu?

Ok, hårdvaran är lödd, firmware är skriven, lådan är utskriven, enheten har fastnat på väggen och glatt blinkar en glödlampa. Men för tillfället är allt en svart låda (bokstavligen och bildligt talat) och det är fortfarande till liten nytta. Det är dags att göra något med MQTT-meddelandena som skickas till servern.

Mitt "smarta hem" snurrar på Majordomo system. MQTT-modulen kommer antingen ur lådan eller är lätt att installera från tilläggsmarknaden - jag kommer inte ihåg var jag fick den ifrån. MQTT är inte en självförsörjande sak – du behöver en sk. broker - en server som tar emot, sorterar och vidarebefordrar MQTT-meddelanden till klienter. Jag använder mygga, som (som majordomo) körs på samma netbook.

Efter att enheten har skickat ett meddelande minst en gång, kommer värdet omedelbart att visas i listan.

Ansluta en vattenmätare till ett smart hem

Dessa värden kan nu associeras med systemobjekt, de kan användas i automatiseringsskript och utsättas för olika analyser - som alla ligger utanför ramen för denna artikel. Jag kan rekommendera majordomo-systemet till alla intresserade kanal Electronics In Lens — en vän bygger också ett smart hem och pratar tydligt om att sätta upp systemet.

Jag ska bara visa dig ett par grafer. Detta är en enkel graf över dagliga värden

Ansluta en vattenmätare till ett smart hem
Man kan se att nästan ingen använde vattnet på natten. Ett par gånger gick någon på toaletten, och det verkar som att omvänd osmosfiltret suger ett par liter per natt. På morgonen ökar konsumtionen avsevärt. Jag brukar använda vatten från en panna, men då ville jag ta ett bad och gick tillfälligt över till stadsvarmvatten - det syns också tydligt i den nedersta grafen.

Från den här grafen lärde jag mig att det krävs 6-7 liter vatten för att gå på toaletten, att duscha kräver 20-30 liter, att diska kräver cirka 20 liter och att ta ett bad kräver 160 liter. Min familj förbrukar någonstans runt 500-600 liter per dag.

För den som är särskilt nyfiken kan man titta på journalerna för varje enskilt värde

Ansluta en vattenmätare till ett smart hem

Härifrån lärde jag mig att när kranen är öppen rinner vattnet med en hastighet av cirka 1 liter per 5 s.

Men i denna form är statistiken förmodligen inte särskilt bekväm att titta på. Majordomo har också möjlighet att se konsumtionsdiagram per dag, vecka och månad. Här är till exempel en förbrukningsgraf i staplar

Ansluta en vattenmätare till ett smart hem

Än så länge har jag bara data för en vecka. Om en månad kommer denna graf att vara mer vägledande - varje dag kommer att ha en separat kolumn. Bilden är något bortskämd av justeringarna av värdena som jag anger manuellt (den största kolumnen). Och det är ännu inte klart om jag ställde in de allra första värdena felaktigt, nästan en kub mindre, eller om detta är en bugg i firmware och inte alla liter räknades. Behöver mer tid.

Själva graferna behöver fortfarande lite magi, vitkalkning, målning. Kanske kommer jag också att bygga en graf över minnesförbrukningen i felsökningssyfte - ifall något läcker där. Kanske kommer jag på något sätt att visa perioder då det inte fanns något internet. För närvarande är allt detta på idénivå.

Slutsats

Idag har min lägenhet blivit lite smartare. Med en så liten enhet blir det bekvämare för mig att övervaka vattenförbrukningen i huset. Om jag tidigare var indignerad över att "igen, vi konsumerade mycket vatten på en månad", nu kan jag hitta källan till denna konsumtion.

Vissa kan tycka att det är konstigt att titta på avläsningarna på skärmen om den är en meter bort från själva mätaren. Men inom en inte särskilt avlägsen framtid planerar jag att flytta till en annan lägenhet, där det kommer att finnas flera vattenstigare, och själva mätarna kommer med största sannolikhet att vara placerade på trappavsatsen. Så en fjärravläsningsenhet kommer att vara mycket användbar.

Jag planerar också att utöka enhetens funktionalitet. Jag tittar redan på motoriserade ventiler. Nu, för att byta pannan till stadsvatten, måste jag vända 3 kranar i en svåråtkomlig nisch. Det skulle vara mycket bekvämare att göra detta med en knapp med motsvarande indikation. Tja, naturligtvis är det värt att implementera skydd mot läckor.

I artikeln beskrev jag min version av en enhet baserad på ESP8266. Enligt min åsikt kom jag på en mycket intressant version av micropython-firmware med hjälp av coroutines - enkelt och trevligt. Jag försökte beskriva många av de nyanser och brister som jag stötte på under kampanjen. Jag kanske beskrev allt för mycket detaljerat; personligen, som läsare, är det lättare för mig att hoppa över det onödiga än att senare fundera ut vad som lämnades osagt.

Som alltid är jag öppen för konstruktiv kritik.

Källkod
Krets och bräda
Fallmodell

Källa: will.com

Lägg en kommentar