Una vez que los sistemas de automatización del hogar, o como a menudo se les llama "hogar inteligente", eran terriblemente caros y solo las personas ricas podían pagarlos. Hoy en el mercado puede encontrar kits bastante económicos con sensores, botones / interruptores y actuadores para controlar la iluminación, los enchufes, la ventilación, el suministro de agua y otros consumidores. E incluso el bricolaje
shr más krivoruky puede unirse a los hermosos y coleccionar dispositivos para un hogar inteligente a bajo costo.

Como regla general, los dispositivos propuestos son sensores o actuadores. Facilitan la implementación de escenarios como "encender la luz cuando se activa el sensor de movimiento" o "el interruptor en la salida apaga la luz en todo el apartamento". Pero la telemetría de alguna manera no funcionó. En el mejor de los casos, este es un gráfico de temperatura y humedad, o potencia instantánea en una salida particular.
Recientemente instalé un medidor de agua con salida de pulso. Se activa un interruptor de láminas a través de cada litro a través del medidor y cierra el contacto. Lo único que queda es aferrarse a los cables e intentar obtener ganancias de él. Por ejemplo, analice el consumo de agua por hora y día de la semana. Bueno, si hay varios tubos ascendentes para el agua en el apartamento, entonces es más conveniente ver todos los indicadores actuales en una pantalla que escalar nichos difíciles de alcanzar con una linterna.
Bajo el corte, mi versión del dispositivo se basa en el ESP8266, que cuenta los pulsos de los medidores de agua y envía lecturas al servidor doméstico inteligente a través de MQTT. Programaremos en micropython usando la biblioteca uasyncio. Al crear el firmware, me encontré con varias dificultades interesantes, que también discutiré en este artículo. Vamos!
Esquema

El corazón de todo el circuito es el módulo en el microcontrolador ESP8266. ESP-12 fue originalmente planeado, pero el mío resultó ser defectuoso. Tenía que estar contento con el módulo ESP-07, que estaba disponible. Afortunadamente, son iguales tanto en conclusiones como en funcionalidad, la diferencia está solo en la antena: el ESP-12 lo tiene incorporado y el ESP-07 tiene uno externo. Sin embargo, incluso sin una antena WiFi, la señal en mi baño se capta normalmente.
El enlace del módulo es estándar:
- botón de reinicio con un tirante y un condensador (aunque ambos ya están dentro del módulo)
- Señal de habilitación (CH_PD) activada
- GPIO15 tirado al suelo. Esto solo es necesario al principio, pero todavía no tengo nada a lo que aferrarme más.
Para poner el módulo en modo firmware, debe cerrar el GPIO2 a tierra, y para que sea más conveniente, he proporcionado el botón de inicio. En condiciones normales, este pin se tira al poder.
El estado de la línea GPIO2 se verifica solo al comienzo del trabajo, cuando se aplica energía o inmediatamente después de un reinicio. Por lo tanto, el módulo se carga como de costumbre o entra en modo firmware. Después de cargar, esta salida se puede usar como GPIO normal. Bueno, dado que ya hay un botón allí, puedes ponerle alguna función útil.
Para la programación y la depuración, usaré el UART, que trajo al peine. Cuando sea necesario, solo conecto un adaptador USB-UART. Solo necesita recordar que el módulo está alimentado por 3.3V. Si olvida cambiar el adaptador a este voltaje y aplica 5V, lo más probable es que el módulo se queme.
No tengo problemas con la electricidad en el baño: el tomacorriente se encuentra a aproximadamente un metro de los medidores, por lo que lo alimentaré desde 220V. Como fuente de energía, trabajaré en un pequeño
bloque de HLK-PM03 de Tenstar Robot. Personalmente, estoy familiarizado con la electrónica analógica y de potencia, pero aquí hay una fuente de alimentación terminada en una caja pequeña.
Para señalar los modos de funcionamiento, proporcioné un LED conectado a GPIO2. Sin embargo, no comencé a soldarlo, porque el módulo ESP-07 ya tiene un LED, además, conectado al mismo GPIO2. Pero déjelo estar en el tablero; de repente, quiero llevar este LED a la carcasa.
Pasamos a lo más interesante. Los medidores de agua no tienen lógica; no se les puede pedir lecturas actuales. Lo único que está disponible para nosotros son los pulsos: cerrar los contactos del interruptor de láminas cada litro. Las conclusiones de los interruptores de láminas se configuran en GPIO12 / GPIO13. Encenderé la resistencia pull-up mediante programación dentro del módulo.
Inicialmente, olvidé proporcionar resistencias R8 y R9, y en mi versión de la placa no lo son. Pero como ya estoy poniendo el esquema en exhibición pública, vale la pena corregir este descuido. Se necesitan resistencias para no quemar el puerto si el firmware tiene errores y pone la unidad en el pin, y el interruptor de láminas corta esta línea a tierra (un máximo de 3.3V / 1000Ohm = 3.3mA fluirá con la resistencia).
Es hora de pensar qué hacer si se corta la electricidad. La primera opción es solicitar al servidor los contadores iniciales al inicio. Pero esto requeriría una complicación significativa del protocolo de intercambio. Además, la operatividad del dispositivo en este caso depende del estado del servidor. Si, después de apagar la luz, el servidor no se inicia (o comienza más tarde), el medidor de agua no podrá solicitar los valores iniciales y funcionará incorrectamente.
Por lo tanto, decidí implementar el almacenamiento de valores de contador en un chip de memoria conectado a través de I2C. No tengo requisitos especiales para el tamaño de la memoria flash: necesito guardar solo 2 números (la cantidad de litros por medidor de agua fría y caliente). Incluso el módulo más pequeño servirá. Pero en el número de ciclos de grabación, debe prestar atención. Para la mayoría de los módulos, esto es 100 mil ciclos, para algunos hasta un millón.
Parecería que un millón es mucho. Pero durante 4 años viviendo en mi departamento, consumí un poco más de 500 metros cúbicos de agua, ¡eso es 500 mil litros! Y 500 mil entradas en el flash. Y esto es solo agua fría. Por supuesto, puede soldar el chip cada dos años, pero resultó que hay chips FRAM. Desde el punto de vista de la programación, esta es la misma EEPROM I2C, pero con una gran cantidad de ciclos de reescritura (cientos de millones). Eso es solo hasta que todo llegue a la tienda con tales chips de alguna manera, por lo que por ahora el 24LC512 habitual se mantendrá.
Placa de circuito
Inicialmente, planeaba hacer una tarifa en casa. Por lo tanto, el tablero fue diseñado como unilateral. Pero después de una larga hora con una plancha de láser y una máscara de soldadura (sin que de alguna manera se convirtiera en algo falso), todavía decidí pedir tablas a los chinos.

Casi antes de ordenar la placa, me di cuenta de que, además del chip de memoria flash en el bus I2C, puede recoger algo más útil, como una pantalla. Todavía es una pregunta qué dar exactamente a conocer, pero debe reproducirse en el tablero. Bueno, ya que iba a pedir las tablas en la fábrica, no tenía sentido limitarme a una tabla de un solo lado, por lo que las líneas en I2C son las únicas en la parte posterior de la placa.
Una jamba grande también se asoció con el cableado unilateral. Porque la placa se dibujó en un lado, las pistas y los componentes SMD se planearon para colocar en un lado, y los componentes de salida, los conectores y la fuente de alimentación en el otro. Cuando recibí las placas en un mes, me olvidé del plan inicial y descomprimí todos los componentes en la parte frontal. Y solo cuando se trataba de soldar la fuente de alimentación, resultó que los más y menos estaban divorciados por el contrario. Tuve que explotar granjas colectivas con puentes. En la imagen de arriba, ya cambié el cableado, pero el suelo se lanza de una parte de la placa a la otra a través de las salidas del botón Boot (aunque sería posible dibujar una pista en la segunda capa).
Resultó así

Vivienda
El siguiente paso es la vivienda. Con una impresora 3D, esto no es un problema. No me molesté mucho: simplemente dibujé una caja del tamaño correcto e hice recortes en los lugares correctos. La cubierta está unida al cuerpo con pequeños tornillos.

Ya mencioné que el botón de inicio se puede usar como un botón de uso general; aquí lo traemos al panel frontal. Para hacer esto, dibujé un "pozo" especial donde vive el botón.

Dentro de la caja también hay tocones en los que la placa se instala y se fija con un solo tornillo M3 (no había más espacio en la placa)
La pantalla se seleccionó cuando imprimí la primera versión de la carcasa. El estándar de dos líneas no encajaba en este caso, pero se encontró una pantalla OLED SSD1306 de 128x32 en el cardán. Es pequeño, pero no tengo que mirarlo todos los días: rodará.
Estimando en ambos sentidos y cómo se colocarán los cables de él, decidí pegar la pantalla en el medio del estuche. Ergonomía, por supuesto, debajo de la placa base: un botón en la parte superior, una pantalla en la parte inferior. Pero ya dije que la idea de arruinar la pantalla llegó demasiado tarde y era demasiado flojo reorganizar el tablero para mover el botón.
El dispositivo está completo. El módulo de visualización está pegado a las boquillas de fusión en caliente.


El resultado final se puede ver en KDPV
Firmware
Pasemos a la parte del software. Para estas manualidades pequeñas, me gusta mucho usar el lenguaje Python (
micropython ): el código es muy compacto y comprensible. Afortunadamente, no hay necesidad de bajar al nivel de registros para exprimir microsegundos: todo se puede hacer desde Python.
Parece ser todo simple, pero no muy: varias funciones independientes se describen en el dispositivo:
- El usuario presiona un botón y mira la pantalla
- Los litros marcan y actualizan los valores en la memoria flash
- El módulo monitorea la señal WiFi y se vuelve a conectar si es necesario.
- Bueno, sin una luz parpadeante, no puedes hacerlo
Es imposible suponer que una función no funcionó si la otra es estúpida por alguna razón. Ya comí cactus en otros proyectos y ahora veo fallas en el estilo de "perdí otro litro, porque en ese momento la pantalla se actualizó" o "el usuario no puede hacer nada mientras el módulo se conecta a WiFi". Por supuesto, algunas cosas se pueden hacer a través de interrupciones, pero puede encontrar una limitación en la duración, anidamiento de llamadas o cambio no atómico de variables. Bueno, el código que hace todo e inmediatamente se convierte en un desastre.
En el
proyecto, utilicé la multitarea preventiva clásica y FreeRTOS con más seriedad, pero en este caso el modelo de
corutinas y la biblioteca uasync resultaron ser mucho más adecuados. Además, la implementación de la corutina en Pitonovskiy es solo una bomba: para el programador, todo se hace de manera simple y conveniente. Simplemente escribe tu propia lógica, solo dime en qué lugares puedes cambiar entre hilos.
Sugiero explorar las diferencias entre el desplazamiento y la multitarea competitiva opcionalmente. Ahora, finalmente pasemos al código.
Cada contador es procesado por una instancia de la clase Counter. En primer lugar, el valor inicial del contador se resta de la EEPROM (value_storage): así es como se implementa la recuperación de una falla de energía.
El pin se inicializa con un pull-up incorporado a la alimentación: si el interruptor de láminas está cerrado, es cero en la línea, si la línea está abierta, la línea se conecta a la alimentación y el controlador lee una.
Además, aquí se inicia una tarea separada, que sondeará el pin. Cada contador ejecutará su propia tarea. Aquí está su código
""" Poll pin and advance value when another litre passed """ async def _switchcheck(self): last_checked_pin_state = self._pin.value()
Se necesita un retraso de 25 ms para filtrar el rebote de contacto y, al mismo tiempo, regula la frecuencia con la que la tarea se despierta (mientras esta tarea está inactiva, otras tareas funcionan). Cada 25 ms, la función se activa, verifica el pin y si los contactos del interruptor de láminas están cerrados, entonces otro litro ha pasado por el contador y esto debe procesarse.
def _another_litre_passed(self): self._value += 1 self._value_changed = True self._value_storage.write(self._value)
Procesar el siguiente litro es trivial: el contador simplemente aumenta. Bueno, un nuevo valor sería bueno para escribir en una unidad flash.
Para facilitar su uso, se proporcionan "accesorios".
def value(self): self._value_changed = False return self._value def set_value(self, value): self._value = value self._value_changed = False
Bueno, ahora aprovecharemos los placeres de Python y la biblioteca uasync y haremos que el objeto del contador sea esperable (¿cómo se puede traducir esto al ruso? ¿El que se puede esperar?)
def __await__(self): while not self._value_changed: yield from asyncio.sleep(0) return self.value() __iter__ = __await__
Esta es una función tan conveniente que espera hasta que se actualice el valor del contador: la función se activa de vez en cuando y comprueba el indicador _value_changed. La broma de esta función es que el código de llamada puede quedarse dormido en una llamada a esta función y dormir hasta que se reciba un nuevo valor.
¿Pero qué hay de las interrupciones?Sí, en este lugar puedes trollearme, diciendo que él mismo dijo acerca de las interrupciones, pero en realidad organizó una estúpida encuesta sobre el alfiler. De hecho, las interrupciones son lo primero que probé. En ESP8266, puede organizar una interrupción en el borde e incluso escribir un controlador para esta interrupción en Python. En esta interrupción, puede actualizar el valor de una variable. Probablemente, esto sería suficiente si el contador fuera un dispositivo esclavo, uno que espera hasta que se le solicite este valor.
Desafortunadamente (¿o afortunadamente?) Mi dispositivo está activo, debe enviar mensajes utilizando el protocolo MQTT y escribir datos en la EEPROM. Y aquí ya entran las restricciones: no puede asignar memoria y usar una gran pila en las interrupciones, lo que significa que puede olvidarse de enviar mensajes a través de la red. Hay bollos como micropython.schedule () que le permiten ejecutar algún tipo de función "tan pronto como sea posible", pero la pregunta es "¿cuál es el punto?". De repente, estamos enviando algún tipo de mensaje en este momento, y aquí la interrupción se rompe y estropea los valores de las variables. O, por ejemplo, un nuevo valor de contador llegó del servidor mientras todavía no escribimos el anterior. En general, necesita cercar la sincronización o salir de alguna manera diferente.
Y de vez en cuando, RuntimeError se bloquea: la pila de programación está llena y ¿quién sabe por qué?
Con una encuesta explícita y uasync, en este caso es de alguna manera más hermosa y más confiable.
Salí a trabajar con EEPROM en una clase pequeña
class EEPROM(): i2c_addr = const(80) def __init__(self, i2c): self.i2c = i2c self.i2c_buf = bytearray(4)
En python, trabajar con bytes directamente es difícil, pero son los bytes que se escriben en la memoria. Tuve que arreglar la conversión entre entero y bytes usando la biblioteca ustruct.
Para no transferir el objeto I2C y la dirección de la celda de memoria cada vez, lo envolví todo en un clásico pequeño y conveniente
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)
El objeto I2C mismo se crea con dichos parámetros.
i2c = I2C(freq=400000, scl=Pin(5), sda=Pin(4))
Nos acercamos a lo más interesante: la implementación de la comunicación con el servidor a través de MQTT. Bueno, el protocolo en sí no necesita ser implementado: se encontró una
implementación asincrónica lista para usar en Internet. Aquí lo usaremos.
Todo lo más interesante se recopila en la clase CounterMQTTClient, que se basa en la biblioteca MQTTClient. Empecemos desde la periferia
Aquí, se crean y configuran pines de bombillas y botones, así como objetos de medidores de agua fría y caliente.
Con la inicialización, no todo es 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 los parámetros de la biblioteca mqtt_as, se utiliza un gran diccionario de diferentes configuraciones: config. La mayoría de las configuraciones predeterminadas nos convienen, pero muchas configuraciones deben establecerse explícitamente. Para no registrar la configuración directamente en el código, la almaceno en el archivo de texto config.txt. Esto le permite cambiar el código independientemente de la configuración, así como remachar varios dispositivos idénticos con diferentes parámetros.
El último bloque de código ejecuta varias corutinas para cumplir diversas funciones del sistema. Aquí hay un ejemplo de rutina que sirve para mostradores
async def _counter_coro(self, counter, topic):
En un ciclo, Corutin espera un nuevo valor de contador y tan pronto como aparece, envía un mensaje utilizando el protocolo MQTT. El primer fragmento de código envía el valor inicial incluso si el agua no fluye a través del contador.
La clase base MQTTClient se sirve sola, inicia una conexión WiFi y se vuelve a conectar cuando se pierde la conexión. Cuando el estado de la conexión WiFi cambia, la biblioteca nos informa llamando 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)
La función es honestamente lamida de los ejemplos. En este caso, considera el número de desconexiones (internet_outage) y su duración. Cuando se restaura una conexión, el tiempo de inactividad se envía al servidor.
Por cierto, el último sueño solo se necesita para que la función se vuelva asíncrona: en la biblioteca se llama vía de espera, y solo se pueden llamar las funciones en el cuerpo del que hay otra espera.
Además de conectarse a WiFi, también debe establecer una conexión con el agente MQTT (servidor). La biblioteca también hace esto, pero tenemos la oportunidad de hacer algo útil cuando se establece la 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í nos suscribimos a varios mensajes: el servidor ahora tiene la capacidad de establecer los valores de contador actuales enviando el mensaje apropiado.
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 los mensajes entrantes y, según el tema (nombre del mensaje), los valores de uno de los contadores se actualizan
Un par de funciones auxiliares
Esta función envía mensajes si se establece una conexión. Si no hay conexión, el mensaje se ignora.
Y esta es solo una función conveniente que genera y envía mensajes de depuración.
async def publish_debug_msg(self, subtopic, msg): await self.publish_msg("{}/{}".format(self._mqtt_debug_water_theme, subtopic), str(msg))
Mucho texto, pero no hemos parpadeado un LED. Aqui
He proporcionado 2 modos de parpadeo. Si se pierde la conexión (o se está estableciendo), el dispositivo parpadeará rápidamente. Si se establece la conexión, el dispositivo parpadea una vez cada 5 segundos. Si es necesario, aquí puede implementar otros modos de parpadeo.
Pero el LED es tan mimador. Todavía saludamos a la pantalla.
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)
De esto es de lo que hablé: cuán simple y conveniente con las corutinas. Esta pequeña función describe TODA la interacción del usuario. Corutin solo espera que presione un botón y enciende la pantalla durante 3 segundos. La pantalla muestra las lecturas actuales del medidor.
Todavía hay un par de pequeñas cosas. Aquí está la función que ejecuta toda la granja (re). El ciclo principal solo trata con el envío de información de depuración una vez por minuto. En general, lo cito tal como está, especialmente el comentario, creo que no es necesario
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):
Bueno, un par de configuraciones y constantes más para completar la descripción
Comienza todo este camino
client = CounterMQTTClient() loop = asyncio.get_event_loop() loop.run_until_complete(client.main())
Algo con mi memoria se ha convertido
Entonces, todo el código está ahí. Subí los archivos usando la utilidad ampy: permite que se carguen en la unidad flash interna (la que está en ESP-07) y luego se accede desde el programa como archivos normales. Allí cargué las bibliotecas mqtt_as, uasyncio, ssd1306 y colecciones (usadas dentro de mqtt_as) que uso.
Comenzamos y ... Recibimos MemoryError. Además, cuanto más intentaba entender exactamente dónde estaba perdiendo la memoria, más arreglaba la depuración de las impresiones, antes se producía este error. Un breve gugelezh me llevó a comprender que, en principio, el microcontrolador solo tiene 30 kb de memoria en la que 65 kb de código (junto con las bibliotecas) no se ajusta de ninguna manera.
Pero hay un camino. Resulta que micropython no ejecuta código directamente desde el archivo .py; este archivo se compila primero. Y se compila directamente en el microcontrolador, se convierte en bytecode, que luego se almacena en la memoria. Bueno, para que el compilador funcione, también necesitas una cierta cantidad de RAM.
El truco consiste en salvar el microcontrolador de la compilación intensiva en recursos. Puede compilar archivos en una computadora grande y completar el código de bytes terminado en el microcontrolador. Para hacer esto, descargue el firmware de micropython y
cree la utilidad mpy-cross .
No escribí un Makefile, pero revisé y compilé manualmente todos los archivos necesarios (incluidas las bibliotecas) como este
mpy-cross water_counter.py
Solo queda cargar archivos con la extensión .mpy, sin olvidar eliminar primero el correspondiente .py del sistema de archivos del dispositivo.
Realicé todo el desarrollo en el programa (IDE?) ESPlorer. Le permite cargar scripts en el microcontrolador e inmediatamente ejecutarlos. En mi caso, toda la lógica y la creación de todos los objetos se encuentran en el archivo water_counter.py (.mpy). Pero para que todo esto se inicie automáticamente, al principio debe haber otro archivo llamado main.py. Y debe ser exactamente .py, y no un .mpy precompilado. Aquí está su contenido trivial
import water_counter
Comenzamos, todo funciona. Pero hay peligrosamente poca memoria libre, aproximadamente 1kb. Todavía tengo planes para expandir la funcionalidad del dispositivo, y este kilobyte obviamente no será suficiente para mí. Pero resultó que hay una salida para este caso.
Aquí está la cosa. Aunque los archivos se compilan en bytecode y se encuentran en el sistema de archivos interno, de hecho todavía se cargan en la RAM y se ejecutan desde allí. Pero resulta que micropython es capaz de ejecutar bytecode directamente desde la memoria flash, pero para esto necesita incrustarlo directamente en el firmware. Esto no es difícil, aunque me llevó una cantidad de tiempo decente en mi netbook (solo allí obtuve Linux).
El algoritmo es el siguiente:
- Descargue e instale el ESP Open SDK . Esto crea un compilador y bibliotecas para programas bajo ESP8266. Se ensambla de acuerdo con las instrucciones de la página principal del proyecto (elegí la configuración STANDALONE = yes)
- Descargar Micropython Sorts
- Coloque las bibliotecas necesarias en los puertos / esp8266 / modules dentro del árbol de micropython
- Ensamblamos el firmware de acuerdo con las instrucciones en el archivo ports / esp8266 / README.md
- Vierta el firmware en el microcontrolador (hago esto en Windows con los programas ESP8266Flasher o Python esptool)
Eso es todo, ahora 'import ssd1306' elevará el código directamente desde el firmware y no se gastará RAM para esto. Con este truco, descargué solo el código de la biblioteca en el firmware, mientras que el código del programa principal se ejecuta desde el sistema de archivos. Esto le permite modificar fácilmente el programa sin volver a compilar el firmware. Por el momento tengo alrededor de 8.5kb de RAM libre. Esto permitirá implementar muchas funcionalidades útiles diferentes en el futuro. Bueno, si no hay suficiente memoria, entonces también puede insertar el programa principal en el firmware.
¿Y qué hacer con eso ahora?
Ok, el hardware está soldado, el firmware está escrito, la caja está impresa, el dispositivo está pegado a la pared y parpadea alegremente con una bombilla. Pero si bien todo esto es una caja negra (en el sentido literal y figurado) y el sentido aún no es suficiente. Es hora de hacer algo con los mensajes MQTT que se envían al servidor.
Mi "casa inteligente" está girando en
el sistema Majordomo . El módulo MQTT está listo para usar o se puede instalar fácilmente desde el mercado de complementos; ya no recuerdo de dónde vino. La cosa MQTT no es autosuficiente: la llamada broker: un servidor que recibe, clasifica y redirige los mensajes MQTT a los clientes. Yo uso mosquitto, que (como el mayordomo) se ejecuta todo en el mismo netbook.
Después de que el dispositivo envíe un mensaje al menos una vez, el valor aparecerá inmediatamente en la lista.

Estos valores ahora pueden asociarse con objetos del sistema, pueden usarse en scripts de automatización y someterse a diversos análisis; todo esto está fuera del alcance de este artículo. Quien esté interesado en el sistema majordomo, puedo recomendar
el canal Electronics In Lens : un amigo también construye una casa inteligente y habla de manera inteligible sobre la configuración del sistema.
Solo mostraré un par de gráficos. Este es un gráfico simple de valores diarios.

Se puede ver que casi nadie usa agua por la noche. Un par de veces alguien fue al baño, y parece que un filtro de ósmosis inversa absorbe un par de litros por noche. Por la mañana, el consumo aumenta significativamente. Por lo general, uso agua de una caldera, pero luego quería bañarme y cambiar temporalmente a agua caliente de la ciudad; esto también es claramente visible en el gráfico inferior.
De este cronograma, aprendí que ir al baño es de 6 a 7 litros de agua, ducharse, 20-30 litros, lavar los platos unos 20 litros y bañarse, se necesitan 160 litros. Por un día, mi familia consume alrededor de 500-600l.
Para los más curiosos, puede mirar las entradas para cada valor individual

Desde aquí aprendí que con el grifo abierto, el agua fluye a una velocidad de aproximadamente 1 litro en 5 segundos.
Pero de esta forma, las estadísticas probablemente no sean muy convenientes de ver. En majordomo todavía existe la oportunidad de ver gráficos de consumo por día, semana y mes. Por ejemplo, un gráfico de consumo en columnas

Hasta ahora solo tengo datos para una semana. En un mes, este cuadro será más indicativo: una columna separada corresponderá a cada día. Los ajustes a los valores que ingreso manualmente estropean un poco la imagen (la columna más grande). Y aún no está claro si configuré incorrectamente los primeros valores casi un cubo menos, o si esto es un error en el firmware y no todos los litros entraron en la compensación. Toma más tiempo
Todavía es necesario conjurar sobre los propios gráficos, blanquear, colorear. Quizás también construya un gráfico de consumo de memoria con fines de depuración; de repente, algo se filtra por ahí. Quizás de alguna manera mostraré períodos en los que Internet no estaba disponible. Si bien todo esto gira a nivel de ideas.
Conclusión
Hoy, mi apartamento se ha vuelto un poco más inteligente. Con un dispositivo tan pequeño, será más conveniente para mí controlar el consumo de agua en la casa. Si antes estaba indignado "nuevamente consumieron mucha agua en un mes", ahora puedo encontrar la fuente de este consumo.
A alguien le parecería extraño ver las lecturas en la pantalla si está a un metro del medidor. Pero en un futuro no muy lejano, planeo mudarme a otro departamento, donde habrá varios elevadores, y los medidores probablemente estarán ubicados en el rellano. Por lo tanto, un dispositivo de lectura remota sería muy útil.
También planeo expandir la funcionalidad del dispositivo. Ya estoy mirando las válvulas motorizadas. Ahora, para cambiar el agua de la ciudad de la caldera, necesito girar 3 grifos en un nicho inaccesible. Sería mucho más conveniente hacer esto con un botón con la indicación adecuada. Bueno, por supuesto, vale la pena implementar protección contra fugas.
En el artículo le dije a mi versión del dispositivo basado en ESP8266. En mi opinión, obtuve una versión muy interesante del firmware de micropython usando coroutine, simple y bonita.
Traté de describir los muchos matices y escuelas que encontré con la campaña. Quizás describí todo con demasiado detalle, para mí personalmente, como lector, es más fácil derrochar demasiado que pensar en lo que no se dijo.Como siempre, estoy abierto a la crítica constructiva. Circuito decódigo fuentey modelo de caja de placa