Conectamos o contador de auga á casa intelixente

Érase unha vez, os sistemas de domótica, ou "casa intelixente", como se lles chamaba a miúdo, eran terriblemente caros e só os ricos podían pagalos. Hoxe no mercado pódense atopar kits bastante económicos con sensores, botóns/interruptores e actuadores para controlar a iluminación, enchufes, ventilación, abastecemento de auga e outros consumidores. E ata a persoa de bricolaxe máis torta pode involucrarse na beleza e montar dispositivos para unha casa intelixente a un prezo económico.

Conectamos o contador de auga á casa intelixente

Normalmente, os dispositivos propostos son sensores ou actuadores. Facilitan a implementación de escenarios como "cando se activa un sensor de movemento, acende as luces" ou "o interruptor preto da saída apaga as luces de todo o apartamento". Pero dalgunha maneira as cousas non funcionaron coa telemetría. No mellor dos casos, é un gráfico de temperatura e humidade, ou potencia instantánea nunha toma específica.

Hai pouco instalei contadores de auga con saída de pulso. Por cada litro que pasa polo contador, o interruptor de lengüeta está activado e pecha o contacto. O único que queda por facer é aferrarse aos fíos e tratar de sacar proveito del. Por exemplo, analice o consumo de auga por hora e día da semana. Ben, se hai varios elevadores de auga no apartamento, entón é máis conveniente ver todos os indicadores actuais nunha soa pantalla que subir a nichos de difícil acceso cunha lanterna.

Debaixo do corte está a miña versión dun dispositivo baseado en ESP8266, que conta os pulsos dos contadores de auga e envía lecturas a través de MQTT ao servidor da casa intelixente. Programaremos en micropython utilizando a biblioteca uasyncio. Ao crear o firmware, atopeime con varias dificultades interesantes, que tamén comentarei neste artigo. Vaia!

O esquema

Conectamos o contador de auga á casa intelixente

O corazón de todo o circuíto é un módulo no microcontrolador ESP8266. O ESP-12 foi orixinalmente planeado, pero o meu resultou ser defectuoso. Tivemos que conformarnos co módulo ESP-07, que estaba dispoñible. Afortunadamente, son iguais tanto en pinos como en funcións, a única diferenza está na antena: o ESP-12 ten un integrado, mentres que o ESP-07 ten un externo. Non obstante, aínda sen unha antena WiFi, o sinal no meu baño recíbese normalmente.

Cableado do módulo estándar:

  • botón de reinicio con pull-up e capacitor (aínda que ambos xa están dentro do módulo)
  • O sinal de activación (CH_PD) está encendido
  • GPIO15 é tirado ao chan. Isto só é necesario ao principio, pero aínda non teño nada que unir a esta perna; xa non o necesito

Para poñer o módulo en modo de firmware, cómpre curtocircuitar GPIO2 a terra e, para facelo máis cómodo, proporcionei un botón de arranque. En condicións normais, este pin está tirado ao poder.

O estado da liña GPIO2 compróbase só ao comezo da operación, cando se aplica a alimentación ou inmediatamente despois dun reinicio. Polo tanto, o módulo arranca como de costume ou pasa ao modo de firmware. Unha vez cargado, este pin pódese usar como un GPIO normal. Ben, como xa hai un botón alí, podes engadirlle algunha función útil.

Para programar e depurar usarei o UART, que se saia a un pente. Cando sexa necesario, só conecto alí un adaptador USB-UART. Só tes que lembrar que o módulo está alimentado por 3.3 V. Se se esquece de cambiar o adaptador a esta tensión e subministrar 5 V, o módulo probablemente se queime.

Non teño problemas coa electricidade no baño: a toma está situada a un metro dos contadores, polo que me alimentarei a 220V. Como fonte de enerxía terei un pequeno bloque HLK-PM03 por Tenstar Robot. Persoalmente, teño dificultades coa electrónica analóxica e de potencia, pero aquí tes unha fonte de alimentación preparada nun pequeno caso.

Para sinalar os modos de funcionamento, proporcionei un LED conectado a GPIO2. Non obstante, non o dessoldei, porque... O módulo ESP-07 xa ten un LED e tamén está conectado a GPIO2. Pero déixao estar no taboleiro, no caso de que quero emitir este LED ao caso.

Pasemos á parte máis interesante. Os contadores de auga non teñen lóxica, non se lles pode pedir lecturas actuais. O único dispoñible para nós son os impulsos: pechando os contactos do interruptor de lengüeta cada litro. As miñas saídas de interruptor de láminas están conectadas a GPIO12/GPIO13. Activarei a resistencia pull-up mediante programación dentro do módulo.

Inicialmente, esquecín proporcionar resistencias R8 e R9 e a miña versión da placa non as ten. Pero como xa estou colgando o diagrama para que todos o vexan, paga a pena corrixir este descoido. Necesítanse resistencias para non queimar o porto se o firmware falla e establece o pin nun, e o interruptor de lengüeta corta esta liña a terra (coa resistencia fluirá un máximo de 3.3 V/1000 Ohm = 3.3 mA).

É hora de pensar que facer se a electricidade se apaga. A primeira opción é solicitar os valores iniciais do contador do servidor ao inicio. Pero isto requiriría unha complicación importante do protocolo de intercambio. Ademais, o rendemento do dispositivo neste caso depende do estado do servidor. Se o servidor non se iniciase despois de apagar a enerxía (ou iniciarse máis tarde), o contador de auga non podería solicitar os valores iniciais e non funcionaría correctamente.

Polo tanto, decidín implementar o aforro de valores de contador nun chip de memoria conectado a través de I2C. Non teño requisitos especiais para o tamaño da memoria flash: só tes que gardar 2 números (o número de litros segundo os contadores de auga quente e fría). Incluso o módulo máis pequeno servirá. Pero cómpre prestar atención ao número de ciclos de gravación. Para a maioría dos módulos son 100 mil ciclos, para algúns ata un millón.

Parece que un millón é moito. Pero durante os 4 anos de vida no meu apartamento, consumín algo máis de 500 metros cúbicos de auga, é dicir, 500 mil litros! E 500 mil rexistros en flash. E iso é só auga fría. Por suposto, podes volver a soldar o chip cada dous anos, pero resulta que hai chips FRAM. Desde o punto de vista da programación, esta é a mesma EEPROM I2C, só cun número moi grande de ciclos de reescritura (centos de millóns). É que aínda non podo chegar á tenda con tales microcircuítos, polo que de momento o habitual 24LC512 estará en pé.

Placa de circuíto impreso

Inicialmente, pensaba facer o taboleiro na casa. Polo tanto, o taboleiro foi deseñado como unilateral. Pero despois de pasar unha hora cun ferro láser e unha máscara de soldadura (non é de algunha maneira comme il faut sen ela), aínda decidín encargarlles as placas aos chineses.

Conectamos o contador de auga á casa intelixente

Case antes de pedir a placa, decateime de que ademais do chip de memoria flash, podía conectar algo máis útil ao bus I2C, como unha pantalla. Que é exactamente o que sacarlle aínda é unha pregunta, pero hai que encamiñalo no taboleiro. Ben, xa que ía pedir placas de fábrica, non tiña sentido limitarme a unha placa dunha soa cara, polo que as liñas I2C son as únicas na parte traseira da placa.

Tamén houbo un gran problema co cableado unidireccional. Porque O taboleiro foi debuxado como unilateral, polo que se planificou colocar as pistas e os compoñentes SMD nun lado e os compoñentes de saída, conectores e fonte de alimentación polo outro. Cando recibín as placas un mes despois, esquecín o plan orixinal e soldei todos os compoñentes na parte frontal. E só cando se trataba de soldar a fonte de alimentación resultou que o máis e o menos estaban conectados ao revés. Tiven que cultivar con saltadores. Na imaxe superior, xa cambiei o cableado, pero o chan transfírese dunha parte do taboleiro a outra a través dos pinos do botón de arranque (aínda que sería posible debuxar unha pista na segunda capa).

Resultou así

Conectamos o contador de auga á casa intelixente

Vivenda

O seguinte paso é o corpo. Se tes unha impresora 3D, isto non é un problema. Non me molestei demasiado: só debuxei unha caixa do tamaño correcto e fixen recortes nos lugares correctos. A tapa está unida ao corpo con pequenos parafusos autorroscantes.

Conectamos o contador de auga á casa intelixente

Xa mencionei que o botón de arranque pódese usar como un botón de propósito xeral, polo que o mostraremos no panel frontal. Para iso, debuxei un "pozo" especial onde vive o botón.

Conectamos o contador de auga á casa intelixente

Dentro da caixa tamén hai cravos nos que se instala o taboleiro e se fixa cun só parafuso M3 (non había máis espazo no taboleiro)

Xa seleccionei a pantalla cando imprimín a primeira versión de mostra do caso. Un lector estándar de dúas liñas non encaixaba neste caso, pero na parte inferior había unha pantalla OLED SSD1306 128×32. É un pouco pequeno, pero non teño que miralo todos os días, é demasiado para min.

Descubrindo deste xeito e como se encamiñarían os cables a partir del, decidín pegar a pantalla no medio da caixa. A ergonomía, por suposto, está por debaixo da media: o botón está na parte superior, a pantalla está na parte inferior. Pero xa dixen que a idea de conectar a pantalla chegou demasiado tarde e que me daba preguiza para volver a cablear o taboleiro para mover o botón.

O dispositivo está montado. O módulo de visualización está pegado ao moco con cola quente

Conectamos o contador de auga á casa intelixente

Conectamos o contador de auga á casa intelixente

O resultado final pódese ver en KDPV

Firmware

Pasemos á parte do software. Para pequenas manualidades como esta, gústame moito usar Python (micropitón) - o código resulta ser moi compacto e comprensible. Afortunadamente, non hai necesidade de baixar ao nivel de rexistro para espremer microsegundos: todo se pode facer desde Python.

Parece que todo é sinxelo, pero non moi sinxelo: o dispositivo ten varias funcións independentes:

  • O usuario preme o botón e mira a pantalla
  • Os litros marcan e actualizan os valores na memoria flash
  • O módulo supervisa o sinal WiFi e volve conectarse se é necesario
  • Ben, sen unha lámpada piscando é imposible

Non pode asumir que unha función non funcionou se outra está bloqueada por algún motivo. Xa me enchei de cactus noutros proxectos e agora aínda vexo fallos do estilo de "perdín outro litro porque a pantalla estaba actualizando nese momento" ou "o usuario non pode facer nada mentres o módulo se conecta a Wifi." Por suposto, algunhas cousas pódense facer mediante interrupcións, pero é posible que teñas limitacións de duración, aniñación de chamadas ou cambios non atómicos nas variables. Ben, o código que fai todo rapidamente convértese en papilla.

В proxecto máis serio Usei a clásica multitarefa preventiva e FreeRTOS, pero neste caso o modelo resultou moito máis axeitado coroutines e bibliotecas uasync . Ademais, a implementación de coroutines en Python é simplemente sorprendente: todo faise de forma sinxela e cómoda para o programador. Simplemente escribe a túa propia lóxica, só dime en que lugares podes cambiar entre fluxos.

Suxiro estudar as diferenzas entre multitarefa preventiva e competitiva como materia optativa. Agora imos finalmente pasar ao código.

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

Cada contador é xestionado por unha instancia da clase Counter. Primeiro de todo, o valor do contador inicial réstase da EEPROM (valor_almacenamento) - así é como se realiza a recuperación despois dunha falla de enerxía.

O pin inicialízase cun pull-up incorporado na fonte de alimentación: se o interruptor reed está pechado, a liña é cero, se a liña está aberta, lévase á fonte de alimentación e o controlador le un.

Aquí tamén se inicia unha tarefa separada, que sondeará o PIN. Cada contador executará a súa propia tarefa. Aquí está o seu código

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

É necesario un atraso de 25 ms para filtrar o rebote de contactos e, ao mesmo tempo, regula a frecuencia con que se esperta a tarefa (mentres esta tarefa está durmindo, outras tarefas están en execución). Cada 25 ms a función esperta, comproba o pin e se os contactos do interruptor de lengüetas están pechados, entón pasou outro litro polo medidor e isto debe ser procesado.

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

        self._value_storage.write(self._value)

Procesar o seguinte litro é trivial: o contador simplemente aumenta. Ben, sería bo escribir o novo valor nunha unidade flash.

Para facilitar o seu uso, ofrécense "accesorios".

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

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

Ben, agora aproveitemos as delicias de Python e a biblioteca uasync e fagamos un obxecto contador esperable (como podemos traducir isto ao ruso? O que podes esperar?)

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

        return self.value()

    __iter__ = __await__  

Esta é unha función tan conveniente que agarda ata que se actualice o valor do contador: a función esperta de cando en vez e verifica o indicador _value_changed. O interesante desta función é que o código de chamada pode quedar durmido mentres chama a esta función e durmir ata que se reciba un novo valor.

E as interrupcións?Si, a estas alturas podes trolearme, dicindo que ti mesmo dixeches de interrupcións, pero en realidade fixeches unha estúpida enquisa de pin. En realidade, as interrupcións son o primeiro que intentei. No ESP8266, pode organizar unha interrupción de borde e incluso escribir un controlador para esta interrupción en Python. Nesta interrupción pódese actualizar o valor dunha variable. Probablemente, isto sería suficiente se o contador fose un dispositivo escravo, un que espera ata que se lle solicite este valor.

Por desgraza (ou por sorte?) o meu dispositivo está activo, debe enviar mensaxes a través do protocolo MQTT e escribir datos na EEPROM. E aquí entran en xogo as restricións: non pode asignar memoria en interrupcións e usar unha pila grande, o que significa que pode esquecerse de enviar mensaxes pola rede. Hai bollos como micropython.schedule() que che permiten executar algunha función "canto antes", pero xorde a pregunta "para que serve?" E se estamos enviando algún tipo de mensaxe agora mesmo, e entón aparece unha interrupción que estraga os valores das variables. Ou, por exemplo, un novo valor de contador chegou do servidor mentres aínda non tiñamos anotado o antigo. En xeral, cómpre bloquear a sincronización ou saír dela dun xeito diferente.

E de cando en vez RuntimeError: programa bloqueos completos da pila e quen sabe por que?

Con sondaxes explícitos e uasync, neste caso resulta dalgunha maneira máis fermoso e fiable

Trouxei traballo con EEPROM a unha clase pequena

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)

En Python, é difícil traballar directamente con bytes, pero son os bytes os que se escriben na memoria. Tiven que limitar a conversión entre enteiros e bytes usando a biblioteca ustruct.

Para non transferir o obxecto I2C e o enderezo da cela de memoria cada vez, envolvín todo nun pequeno e cómodo clásico

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)

O propio obxecto I2C créase con estes parámetros

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

Chegamos á parte máis interesante: a implementación da comunicación co servidor a través de MQTT. Ben, non hai necesidade de implementar o protocolo en si, atopeino en Internet implementación asincrónica preparada. Isto é o que usaremos.

Todas as cousas máis interesantes recóllense na clase CounterMQTTClient, que está baseada na biblioteca MQTTClient. Comecemos pola periferia

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

Aquí podes crear e configurar alfinetes e botóns de lámpadas, así como obxectos de contadores de auga fría e quente.

Coa inicialización, non todo é tan trivial

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

Para establecer os parámetros de funcionamento da biblioteca mqtt_as, utilízase un gran dicionario de configuracións diferentes: config. A maioría dos axustes predeterminados están ben para nós, pero hai que establecer moitos axustes de forma explícita. Para non escribir a configuración directamente no código, gárdoas no ficheiro de texto config.txt. Isto permítelle cambiar o código independentemente da configuración, así como remachar varios dispositivos idénticos con diferentes parámetros.

O último bloque de código inicia varias corrutinas para servir a varias funcións do sistema. Por exemplo, aquí tes unha corrutina que conta os servizos

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

A corrutina agarda nun bucle un novo valor de contador e, en canto aparece, envía unha mensaxe a través do protocolo MQTT. A primeira peza de código envía o valor inicial aínda que non pase auga polo contador.

A clase base MQTTClient serve a si mesma, inicia unha conexión WiFi e volve conectarse cando se perde a conexión. Cando hai cambios no estado da conexión WiFi, a biblioteca infórmao chamando a 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)

A función cópiase honestamente de exemplos. Neste caso, conta o número de interrupcións (internet_outages) e a súa duración. Cando se restablece a conexión, envíase un tempo de inactividade ao servidor.

Por certo, o último soño só é necesario para que a función sexa asíncrona: na biblioteca chámase a través de await e só se poden chamar as funcións cuxo corpo contén outro await.

Ademais de conectarse a WiFi, tamén cómpre establecer unha conexión co corredor MQTT (servidor). A biblioteca tamén fai isto, e temos a oportunidade de facer algo útil cando se establece a conexión

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

Aquí subscribimos varias mensaxes: o servidor agora ten a capacidade de establecer os valores actuais do contador enviando a mensaxe correspondente.

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

Esta función procesa as mensaxes entrantes e, dependendo do tema (título da mensaxe), os valores dun dos contadores actualízanse

Un par de funcións auxiliares

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

Esta función envía unha mensaxe se se establece a conexión. Se non hai conexión, ignorarase a mensaxe.

E esta é só unha función conveniente que xera e envía mensaxes de depuración.

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

Tanto texto e aínda non acendemos un LED. Aquí

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

Proporcionei 2 modos de parpadeo. Se se perde a conexión (ou se acaba de establecer), o dispositivo parpadeará rapidamente. Se se establece a conexión, o dispositivo parpadea unha vez cada 5 segundos. Se é necesario, pódense implementar outros modos de parpadeo aquí.

Pero o LED é só mimos. Tamén apuntamos á exhibición.

    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)

Isto é do que falaba: o sinxelo e cómodo que é coas corrutinas. Esta pequena función describe toda a experiencia do usuario. A corrutina simplemente espera a que se prema o botón e acende a pantalla durante 3 segundos. A pantalla mostra as lecturas actuais do contador.

Aínda quedan un par de cousiñas. Aquí está a función que (re)inicia toda esta empresa. O bucle principal só envía varias informacións de depuración unha vez por minuto. En xeral, citoo tal e como está: non creo que haxa que comentar demasiado

   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)

Ben, un par de configuracións e constantes máis para completar a descrición

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

Todo comeza así

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

Algo pasou coa miña memoria

Entón, todo o código está aí. Carguei os ficheiros mediante a utilidade ampy, que permite cargalos na unidade flash interna (a do propio ESP-07) e despois acceder a ela desde o programa como ficheiros normais. Alí subín tamén as bibliotecas mqtt_as, uasyncio, ssd1306 e coleccións que usei (utilizadas dentro de mqtt_as).

Lanzamos e... Recibimos un MemoryError. Ademais, canto máis tentaba comprender onde se escapaba exactamente a memoria, máis impresións de depuración colocaba, máis cedo apareceu este erro. Unha pequena busca en Google levoume a entender que o microcontrolador ten, en principio, só 30 kB de memoria, nos que simplemente non caben 65 kB de código (incluíndo as bibliotecas).

Pero hai unha saída. Resulta que micropython non executa código directamente desde un ficheiro .py; este ficheiro compílase primeiro. Ademais, compílase directamente no microcontrolador, convertido en bytecode, que despois se almacena na memoria. Ben, para que o compilador funcione, tamén necesitas unha certa cantidade de RAM.

O truco é salvar o microcontrolador da compilación de recursos intensivos. Podes compilar os ficheiros nun ordenador grande e cargar o bytecode preparado no microcontrolador. Para iso, cómpre descargar o firmware de micropython e compilalo utilidade mpy-cross.

Non escribín un Makefile, pero repasei e compilei manualmente todos os ficheiros necesarios (incluídas as bibliotecas) algo así

mpy-cross water_counter.py

Só resta cargar ficheiros coa extensión .mpy, sen esquecer primeiro eliminar o .py correspondente do sistema de ficheiros do dispositivo.

Fixen todo o desenvolvemento do programa (IDE?) ESPlorer. Permítelle cargar scripts ao microcontrolador e executalos inmediatamente. No meu caso, toda a lóxica e creación de todos os obxectos atópase no ficheiro water_counter.py (.mpy). Pero para que todo isto se inicie automaticamente, tamén debe haber un ficheiro chamado main.py ao comezo. Ademais, debería ser exactamente .py, e non .mpy previamente compilado. Velaquí os seus contidos triviais

import water_counter

Lanzámolo - todo funciona. Pero a memoria libre é alarmantemente pequena: preto de 1 kb. Aínda teño plans para ampliar a funcionalidade do dispositivo, e este kilobyte claramente non é suficiente para min. Pero resultou que tamén hai unha saída para este caso.

Aquí está a cousa. Aínda que os ficheiros están compilados en bytecode e residen no sistema de ficheiros interno, en realidade aínda se cargan na RAM e execútanse desde alí. Pero resulta que micropython pode executar bytecode directamente desde a memoria flash, pero para iso cómpre construílo directamente no firmware. Non é difícil, aínda que levaba bastante tempo no meu netbook (só alí tiña Linux).

O algoritmo é o seguinte:

  • Descarga e instala ESP Open SDK. Esta cousa ensambla un compilador e bibliotecas para programas para o ESP8266. Montado segundo as instrucións da páxina principal do proxecto (escollín a opción AUTÓNOMO=si)
  • Descargar tipos de micropython
  • Coloque as bibliotecas necesarias en ports/esp8266/modules dentro da árbore de micropython
  • Montamos o firmware segundo as instrucións do ficheiro ports/esp8266/README.md
  • Subimos o firmware ao microcontrolador (fago isto en Windows usando programas ESP8266Flasher ou esptool Python)

Isto é todo, agora 'import ssd1306' levantará o código directamente do firmware e non se consumirá RAM para iso. Con este truco, carguei só o código da biblioteca no firmware, mentres que o código do programa principal execútase desde o sistema de ficheiros. Isto permítelle modificar facilmente o programa sen recompilar o firmware. Polo momento teño uns 8.5 kb de RAM libre. Isto permitiranos implementar moitas funcionalidades útiles diferentes no futuro. Ben, se non hai memoria suficiente, podes colocar o programa principal no firmware.

Entón, que debemos facer agora?

Ok, o hardware está soldado, o firmware está escrito, a caixa está impresa, o dispositivo está pegado na parede e acende felizmente unha lámpada. Pero de momento é todo unha caixa negra (literalmente e figuradamente) e aínda non serve de nada. É hora de facer algo coas mensaxes MQTT que se envían ao servidor.

A miña "casa intelixente" está xirando Sistema Majordomo. O módulo MQTT sae da caixa ou instálase facilmente desde o mercado de complementos; non lembro de onde o saquei. MQTT non é unha cousa autosuficiente - necesitas un chamado. broker: un servidor que recibe, ordena e reenvía mensaxes MQTT aos clientes. Eu uso mosquitto, que (como o majordomo) funciona no mesmo netbook.

Despois de que o dispositivo envíe unha mensaxe polo menos unha vez, o valor aparecerá inmediatamente na lista.

Conectamos o contador de auga á casa intelixente

Estes valores agora pódense asociar a obxectos do sistema, poden usarse en scripts de automatización e someterse a varias análises, todas elas fóra do alcance deste artigo. Podo recomendar o sistema majordomo a calquera interesado canle Electronics In Lens — un amigo tamén está a construír unha casa intelixente e fala claramente sobre a configuración do sistema.

Vouche mostrar só un par de gráficos. Este é un gráfico sinxelo de valores diarios

Conectamos o contador de auga á casa intelixente
Pódese ver que case ninguén usaba a auga pola noite. Un par de veces alguén foi ao baño, e parece que o filtro de ósmose inversa chupa un par de litros por noite. Pola mañá, o consumo aumenta significativamente. Normalmente uso auga dunha caldeira, pero entón quería darme un baño e cambiei temporalmente á auga quente da cidade; isto tamén se ve claramente no gráfico inferior.

Desta gráfica aprendín que para ir ao baño precisa de 6-7 litros de auga, para ducharse 20-30 litros, para lavar a louza uns 20 litros e para bañarse 160 litros. A miña familia consome uns 500-600 litros ao día.

Para aqueles que teñan especial curiosidade, poden consultar os rexistros de cada valor individual

Conectamos o contador de auga á casa intelixente

De aquí aprendín que cando a billa está aberta, a auga flúe a unha velocidade aproximada de 1 litro cada 5 s.

Pero nesta forma as estatísticas probablemente non sexan moi convenientes de mirar. Majordomo tamén ten a posibilidade de ver gráficos de consumo por día, semana e mes. Aquí, por exemplo, tes un gráfico de consumo en barras

Conectamos o contador de auga á casa intelixente

Polo de agora só teño datos dunha semana. Nun mes, este gráfico será máis indicativo: cada día terá unha columna separada. A imaxe está lixeiramente estragada polos axustes dos valores que introduzo manualmente (a columna máis grande). E aínda non está claro se fixen incorrectamente os primeiros valores, case un cubo menos, ou se se trata dun erro no firmware e non se contaron todos os litros. Necesita máis tempo.

Os propios gráficos aínda precisan algo de maxia, encalado, pintura. Quizais tamén construírei un gráfico do consumo de memoria para fins de depuración, por se se filtra algo alí. Quizais, dalgún xeito, mostre períodos nos que non había Internet. Polo de agora, todo isto está a nivel de ideas.

Conclusión

Hoxe o meu apartamento volveuse un pouco máis intelixente. Con un dispositivo tan pequeno, será máis cómodo para min controlar o consumo de auga na casa. Se antes me indignaba "de novo, consumimos moita auga nun mes", agora podo atopar a orixe deste consumo.

Algúns poden considerar estraño mirar as lecturas na pantalla se está a un metro de distancia do propio contador. Pero nun futuro non moi afastado, planeo mudarme a outro apartamento, onde haberá varias subidas de auga, e os propios medidores probablemente estarán situados no descanso. Polo tanto, un dispositivo de lectura remota será moi útil.

Tamén penso ampliar a funcionalidade do dispositivo. Xa estou mirando as válvulas motorizadas. Agora, para cambiar a caldeira á auga da cidade, teño que abrir 3 billas nun nicho de difícil acceso. Sería moito máis cómodo facelo cun botón coa indicación correspondente. Ben, por suposto, paga a pena implementar protección contra fugas.

No artigo describín a miña versión dun dispositivo baseado en ESP8266. Na miña opinión, ocorréuseme unha versión moi interesante do firmware micropython usando coroutines, sinxela e agradable. Tentei describir moitos dos matices e carencias que atopei durante a campaña. Quizais describín todo con demasiado detalle; persoalmente, como lector, é máis fácil para min saltarme as cousas innecesarias que pensar máis tarde no que non se dixo.

Como sempre, estou aberto á crítica construtiva.

Código fonte
Circuíto e placa
Modelo de caso

Fonte: www.habr.com

Engadir un comentario