Un tempo i sistemi domotici, o “casa intelligente”, come venivano spesso chiamati, erano terribilmente costosi e solo i ricchi potevano permetterseli. Oggi sul mercato si possono trovare kit abbastanza economici con sensori, pulsanti/interruttori e attuatori per il controllo dell'illuminazione, delle prese, della ventilazione, dell'approvvigionamento idrico e di altri consumatori. E anche il fai-da-te più disonesto può dedicarsi alla bellezza e assemblare dispositivi per una casa intelligente a un prezzo conveniente.

Tipicamente, i dispositivi proposti sono sensori o attuatori. Semplificano l’implementazione di scenari come “quando viene attivato un sensore di movimento, accendi le luci” o “l’interruttore vicino all’uscita spegne le luci dell’intero appartamento”. Ma in qualche modo le cose non hanno funzionato con la telemetria. Nella migliore delle ipotesi, è un grafico di temperatura e umidità o potenza istantanea su una presa specifica.
Recentemente ho installato contatori dell'acqua con uscita a impulsi. Per ogni litro che passa nel contatore l'interruttore reed si attiva e chiude il contatto. L’unica cosa che resta da fare è aggrapparsi ai fili e cercare di trarne beneficio. Ad esempio, analizza il consumo di acqua per ora e giorno della settimana. Bene, se nell'appartamento ci sono diversi montanti d'acqua, è più conveniente vedere tutti gli indicatori attuali su uno schermo piuttosto che arrampicarsi in nicchie difficili da raggiungere con una torcia.
Sotto il taglio c'è la mia versione del dispositivo basato su ESP8266, che conta gli impulsi dei contatori dell'acqua e invia le letture tramite MQTT al server della casa intelligente. Programmeremo in micropython utilizzando la libreria uasyncio. Durante la creazione del firmware, mi sono imbattuto in diverse difficoltà interessanti, di cui parlerò anche in questo articolo. Andare!
Guida

Il cuore dell'intero circuito è un modulo sul microcontrollore ESP8266. Inizialmente era previsto l'ESP-12, ma il mio si è rivelato difettoso. Dovevamo accontentarci del modulo ESP-07, che era disponibile. Fortunatamente, sono gli stessi sia in termini di pin che di funzionalità, l'unica differenza è nell'antenna: l'ESP-12 ne ha una integrata, mentre l'ESP-07 ne ha una esterna. Tuttavia, anche senza antenna WiFi, il segnale nel mio bagno viene ricevuto normalmente.
Cablaggio del modulo standard:
- pulsante di reset con pull-up e condensatore (anche se entrambi sono già all'interno del modulo)
- Il segnale di abilitazione (CH_PD) viene collegato all'alimentazione
- GPIO15 viene tirato a terra. Questo serve solo all'inizio, ma non ho ancora niente da attaccare a questa gamba, non mi serve più
Per mettere il modulo in modalità firmware, è necessario cortocircuitare il GPIO2 a terra e, per renderlo più comodo, ho fornito un pulsante di avvio. In condizioni normali, questo pin viene alimentato.
Lo stato della linea GPIO2 viene controllato solo all'inizio del funzionamento, quando viene collegata l'alimentazione o immediatamente dopo un ripristino. Quindi il modulo si avvia normalmente o entra in modalità firmware. Una volta caricato, questo pin può essere utilizzato come un normale GPIO. Bene, poiché c'è già un pulsante lì, puoi allegargli qualche funzione utile.
Per la programmazione e il debug utilizzerò l'UART, che viene inviato a un pettine. Se necessario, collego semplicemente lì un adattatore USB-UART. Devi solo ricordare che il modulo è alimentato a 3.3 V. Se si dimentica di commutare l'adattatore su questa tensione e di fornire 5 V, molto probabilmente il modulo si brucerà.
Non ho problemi con la corrente elettrica in bagno: la presa si trova a circa un metro dai contatori, quindi sarò alimentato a 220V. Come fonte di energia avrò un piccolo di Tenstar Robot. Personalmente, ho difficoltà con l'elettronica analogica e di potenza, ma ecco un alimentatore già pronto in una piccola custodia.
Per segnalare le modalità operative ho previsto un LED collegato al GPIO2. Tuttavia non l'ho dissaldato perché... Il modulo ESP-07 ha già un LED ed è anche collegato a GPIO2. Ma lascia che sia sulla scheda, nel caso in cui volessi inserire questo LED nel case.
Passiamo alla parte più interessante. I contatori dell'acqua non hanno logica; non puoi chiedere loro la lettura attuale. L'unica cosa a nostra disposizione sono gli impulsi: chiudere i contatti dell'interruttore reed ogni litro. Le uscite del mio interruttore reed sono collegate a GPIO12/GPIO13. Abiliterò la resistenza pull-up a livello di codice all'interno del modulo.
Inizialmente mi ero dimenticato di fornire i resistori R8 e R9 e la mia versione della scheda non li ha. Ma poiché sto già pubblicando il diagramma affinché tutti possano vederlo, vale la pena correggere questa svista. I resistori sono necessari per non bruciare la porta se il firmware presenta problemi e imposta il pin su uno, e l'interruttore reed cortocircuita questa linea a terra (con il resistore scorrerà un massimo di 3.3 V/1000 Ohm = 3.3 mA).
È tempo di pensare a cosa fare se si interrompe l'elettricità. La prima opzione è richiedere all'avvio i valori dei contatori iniziali al server. Ma ciò richiederebbe una notevole complicazione del protocollo di scambio. Inoltre, le prestazioni del dispositivo in questo caso dipendono dallo stato del server. Se il server non si avviasse dopo lo spegnimento (o si avviasse successivamente), il contatore dell'acqua non sarebbe in grado di richiedere i valori iniziali e non funzionerebbe correttamente.
Pertanto, ho deciso di implementare il salvataggio dei valori dei contatori in un chip di memoria collegato tramite I2C. Non ho requisiti speciali per la dimensione della memoria flash: devi solo salvare 2 numeri (il numero di litri secondo i contatori dell'acqua calda e fredda). Anche il modulo più piccolo andrà bene. Ma devi prestare attenzione al numero di cicli di registrazione. Per la maggior parte dei moduli si tratta di 100mila cicli, per alcuni fino a un milione.
Sembrerebbe che un milione sia tanto. Ma durante i 4 anni di vita nel mio appartamento ho consumato poco più di 500 metri cubi d’acqua, ovvero 500mila litri! E 500mila record in flash. E questa è solo acqua fredda. Ovviamente puoi risaldare il chip ogni due anni, ma risulta che ci sono chip FRAM. Dal punto di vista della programmazione, questa è la stessa EEPROM I2C, solo con un numero molto elevato di cicli di riscrittura (centinaia di milioni). È solo che non riesco ancora ad arrivare al negozio con tali microcircuiti, quindi per ora rimarrà il solito 24LC512.
Scheda a circuito stampato
Inizialmente avevo pensato di realizzare la tavola a casa. Pertanto, il tabellone è stato progettato come unilaterale. Ma dopo aver passato un'ora con un ferro laser e una maschera per saldatura (in qualche modo senza di essa non è comme il faut), ho comunque deciso di ordinare le schede dai cinesi.

Quasi prima di ordinare la scheda, mi sono reso conto che oltre al chip di memoria flash, avrei potuto collegare qualcos'altro di utile al bus I2C, come ad esempio un display. Cosa esattamente produrre è ancora una questione, ma deve essere instradato sulla scheda. Ebbene, dal momento che avrei ordinato le schede dalla fabbrica, non aveva senso limitarmi a una scheda a lato singolo, quindi le linee I2C sono le uniche sul retro della scheda.
C'era anche un grosso problema con il cablaggio unidirezionale. Perché La scheda è stata disegnata come unilaterale, quindi le piste e i componenti SMD dovevano essere posizionati su un lato e i componenti di uscita, connettori e alimentazione sull'altro. Quando ho ricevuto le schede un mese dopo, ho dimenticato il progetto originale e ho saldato tutti i componenti sul lato anteriore. E solo quando si è trattato di saldare l'alimentatore si è scoperto che il più e il meno erano cablati al contrario. Ho dovuto coltivare con i ponticelli. Nella foto sopra ho già cambiato il cablaggio, ma la massa viene trasferita da una parte all'altra della scheda tramite i pin del pulsante Boot (anche se sarebbe possibile disegnare una traccia sul secondo strato).
È andata così

alloggiamento
Il passo successivo è il corpo. Se hai una stampante 3D, questo non è un problema. Non mi sono preoccupato troppo: ho semplicemente disegnato una scatola della giusta dimensione e ho realizzato dei ritagli nei punti giusti. Il coperchio è fissato al corpo con piccole viti autofilettanti.

Ho già detto che il pulsante Boot può essere utilizzato come pulsante generico, quindi lo visualizzeremo sul pannello frontale. Per fare questo, ho disegnato un "pozzo" speciale in cui vive il pulsante.

All'interno del case sono presenti anche dei prigionieri su cui viene installata la scheda e fissata con un'unica vite M3 (non c'era più spazio sulla scheda)
Ho selezionato il display già quando ho stampato la prima versione campione della custodia. Un lettore standard a due righe non entrava in questo case, ma nella parte inferiore c'era un display OLED SSD1306 128×32. È un po’ piccolo, ma non devo fissarlo tutti i giorni: è troppo per me.
Avendo capito in questo modo e come sarebbero stati instradati i cavi, ho deciso di attaccare il display al centro del case. L'ergonomia, ovviamente, è al di sotto della media: il pulsante è in alto, il display è in basso. Ma ho già detto che l'idea di collegare il display è arrivata troppo tardi ed ero troppo pigro per ricablare la scheda per spostare il pulsante.
Il dispositivo è assemblato. Il modulo display è incollato al moccio con colla a caldo


Il risultato finale può essere visto su KDPV
inserimento
Passiamo alla parte software. Per piccoli lavoretti come questo, mi piace molto usare Python () - il codice risulta essere molto compatto e comprensibile. Fortunatamente, non è necessario scendere al livello del registro per spremere microsecondi: tutto può essere fatto da Python.
Sembra che tutto sia semplice, ma non molto semplice: il dispositivo ha diverse funzioni indipendenti:
- L'utente preme il pulsante e guarda il display
- I litri spuntano e aggiornano i valori nella memoria flash
- Il modulo monitora il segnale WiFi e si riconnette se necessario
- Ebbene, senza una lampadina lampeggiante è impossibile
Non si può presumere che una funzione non abbia funzionato se un'altra è bloccata per qualche motivo. Mi sono già riempito di cactus in altri progetti e ora vedo ancora problemi del tipo "mancato un altro litro perché il display si stava aggiornando in quel momento" o "l'utente non può fare nulla mentre il modulo si connette a Wifi." Naturalmente, alcune cose possono essere eseguite tramite interruzioni, ma è possibile che si verifichino limitazioni sulla durata, sull'annidamento delle chiamate o su modifiche non atomiche alle variabili. Ebbene, il codice che fa tutto si trasforma rapidamente in poltiglia.
В Ho utilizzato il classico multitasking con preemptive e FreeRTOS, ma in questo caso il modello si è rivelato molto più adatto . Inoltre, l'implementazione delle coroutine in Python è semplicemente sorprendente: tutto viene fatto in modo semplice e conveniente per il programmatore. Scrivi semplicemente la tua logica, dimmi solo in quali posti puoi passare da un flusso all'altro.
Suggerisco di studiare le differenze tra multitasking preventivo e competitivo come materia facoltativa. Ora passiamo finalmente al codice.
#####################################
# 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
Ogni contatore è gestito da un'istanza della classe Counter. Innanzitutto il valore iniziale del contatore viene sottratto dalla EEPROM (value_storage): in questo modo viene realizzato il ripristino dopo un'interruzione di corrente.
Il pin viene inizializzato con un pull-up incorporato all'alimentatore: se il reed è chiuso la linea è zero, se la linea è aperta viene tirato fino all'alimentatore e il controller legge uno.
Qui viene avviata anche un'attività separata, che effettuerà il polling del pin. Ogni contatore eseguirà la propria attività. Ecco il suo codice
""" 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)
È necessario un ritardo di 25 ms per filtrare il rimbalzo dei contatti e allo stesso tempo regolare la frequenza con cui l'attività si riattiva (mentre questa attività è in stato di stop, le altre attività sono in esecuzione). Ogni 25ms la funzione si attiva, controlla il pin e se i contatti dell'interruttore reed sono chiusi, significa che un altro litro è passato attraverso il contatore e questo deve essere elaborato.
def _another_litre_passed(self):
self._value += 1
self._value_changed = True
self._value_storage.write(self._value)
Elaborare il litro successivo è banale: il contatore aumenta semplicemente. Bene, sarebbe carino scrivere il nuovo valore su una chiavetta USB.
Per facilità d'uso, vengono forniti "accessori".
def value(self):
self._value_changed = False
return self._value
def set_value(self, value):
self._value = value
self._value_changed = False
Bene, ora approfittiamo delle delizie di Python e della libreria uasync e creiamo un oggetto contatore attendibile (come possiamo tradurlo in russo? Quello che puoi aspettarti?)
def __await__(self):
while not self._value_changed:
yield from asyncio.sleep(0)
return self.value()
__iter__ = __await__
Questa è una funzione così comoda che attende fino all'aggiornamento del valore del contatore: la funzione si sveglia di tanto in tanto e controlla il flag _value_changed. La cosa bella di questa funzione è che il codice chiamante può addormentarsi mentre chiama questa funzione e dormire finché non viene ricevuto un nuovo valore.
E le interruzioni?Sì, a questo punto puoi trollarmi dicendo che tu stesso hai parlato di interruzioni, ma in realtà hai fatto uno stupido sondaggio. In realtà gli interrupt sono la prima cosa che ho provato. Nell'ESP8266 puoi organizzare un edge interrupt e persino scrivere un gestore per questo interrupt in Python. In questo interrupt è possibile aggiornare il valore di una variabile. Probabilmente questo sarebbe sufficiente se il contatore fosse un dispositivo slave, che attende finché non gli viene richiesto questo valore.
Purtroppo (o per fortuna?) il mio dispositivo è attivo, deve esso stesso inviare messaggi tramite il protocollo MQTT e scrivere dati su EEPROM. E qui entrano in gioco le restrizioni: non puoi allocare memoria negli interrupt e utilizzare uno stack di grandi dimensioni, il che significa che puoi dimenticarti di inviare messaggi sulla rete. Esistono funzionalità come micropython.schedule() che ti consentono di eseguire alcune funzioni "il più presto possibile", ma sorge la domanda: "qual è il punto?" Cosa succede se stiamo inviando qualche tipo di messaggio in questo momento, e poi arriva un'interruzione e rovina i valori delle variabili. Oppure, ad esempio, è arrivato un nuovo controvalore dal server mentre non avevamo ancora trascritto quello vecchio. In generale, è necessario bloccare la sincronizzazione o uscirne in qualche modo in modo diverso.
E di tanto in tanto RuntimeError: schedule stack full si blocca e chissà perché?
Con il polling esplicito e uasync, in questo caso risulta in qualche modo più bello e affidabile
Ho portato il lavoro con EEPROM in una piccola classe
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)
In Python è difficile lavorare direttamente con i byte, ma sono i byte che vengono scritti in memoria. Ho dovuto recintare la conversione tra numeri interi e byte utilizzando la libreria ustruct.
Per non trasferire ogni volta l'oggetto I2C e l'indirizzo della cella di memoria, ho racchiuso il tutto in un piccolo e comodo classico
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)
L'oggetto I2C stesso viene creato con questi parametri
i2c = I2C(freq=400000, scl=Pin(5), sda=Pin(4))
Veniamo alla parte più interessante: l'implementazione della comunicazione con il server tramite MQTT. Bene, non è necessario implementare il protocollo stesso: l'ho trovato su Internet . Questo è ciò che useremo.
Tutte le cose più interessanti sono raccolte nella classe CounterMQTTClient, che si basa sulla libreria MQTTClient. Partiamo dalla 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))
Qui puoi creare e configurare spille e pulsanti delle lampadine, nonché oggetti contatore dell'acqua fredda e calda.
Con l'inizializzazione, non tutto è così banale
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())
Per impostare i parametri operativi della libreria mqtt_as, viene utilizzato un ampio dizionario di diverse impostazioni: config. La maggior parte delle impostazioni predefinite per noi vanno bene, ma molte impostazioni devono essere impostate in modo esplicito. Per non scrivere le impostazioni direttamente nel codice, le memorizzo nel file di testo config.txt. Ciò consente di modificare il codice indipendentemente dalle impostazioni, nonché di rivettare diversi dispositivi identici con parametri diversi.
L'ultimo blocco di codice avvia diverse coroutine per servire varie funzioni del sistema. Ad esempio, ecco una coroutine che serve i contatori
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))
La coroutine attende in loop un nuovo valore del contatore e, non appena appare, invia un messaggio tramite il protocollo MQTT. Il primo pezzo di codice invia il valore iniziale anche se non scorre acqua attraverso il contatore.
La classe base MQTTClient si serve da sola, avvia una connessione WiFi e si riconnette quando la connessione viene persa. Quando ci sono cambiamenti nello stato della connessione WiFi, la libreria ci informa chiamando 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 funzione è copiata onestamente dagli esempi. In questo caso conta il numero di interruzioni (internet_outages) e la loro durata. Quando la connessione viene ripristinata, viene inviato un tempo di inattività al server.
A proposito, l'ultimo sleep è necessario solo per rendere la funzione asincrona: nella libreria viene chiamata tramite wait e possono essere chiamate solo le funzioni il cui corpo contiene un altro wait.
Oltre alla connessione al WiFi, è necessario stabilire anche una connessione al broker MQTT (server). Anche la biblioteca fa questo e abbiamo l'opportunità di fare qualcosa di utile quando viene stabilita la connessione
async def mqtt_connection_handler(self, client):
await client.subscribe(self._mqtt_cold_water_theme)
await client.subscribe(self._mqtt_hot_water_theme)
Qui sottoscriviamo diversi messaggi: il server ora ha la possibilità di impostare i valori correnti del contatore inviando il messaggio corrispondente.
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))
Questa funzione elabora i messaggi in arrivo e, a seconda dell'argomento (titolo del messaggio), vengono aggiornati i valori di uno dei contatori
Un paio di funzioni di aiuto
# 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")
Questa funzione invia un messaggio se la connessione viene stabilita. Se non c'è connessione, il messaggio viene ignorato.
E questa è solo una comoda funzione che genera e invia messaggi di debug.
async def publish_debug_msg(self, subtopic, msg):
await self.publish_msg("{}/{}".format(self._mqtt_debug_water_theme, subtopic), str(msg))
Così tanto testo e non abbiamo ancora lampeggiato un LED. Qui
# 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)
Ho fornito 2 modalità di lampeggiamento. Se la connessione viene persa (o viene appena stabilita), il dispositivo lampeggerà rapidamente. Se la connessione è stabilita, il dispositivo lampeggia una volta ogni 5 secondi. Se necessario, qui possono essere implementate altre modalità di lampeggiamento.
Ma il LED è solo una coccola. Abbiamo puntato anche sul display.
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)
Questo è ciò di cui stavo parlando: quanto sia semplice e conveniente con le coroutine. Questa piccola funzione descrive l'INTERA esperienza dell'utente. La coroutine attende semplicemente la pressione del pulsante e accende il display per 3 secondi. Il display mostra le letture attuali del contatore.
Rimangono ancora un paio di piccole cose. Ecco la funzione che (ri)fa partire tutta questa impresa. Il ciclo principale invia semplicemente varie informazioni di debug una volta al minuto. In generale, lo cito così com'è: non credo che ci sia bisogno di commentare troppo
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)
Bene, ancora un paio di impostazioni e costanti per completare la descrizione
#####################################
# 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)
Tutto inizia così
client = CounterMQTTClient()
loop = asyncio.get_event_loop()
loop.run_until_complete(client.main())
Qualcosa è successo alla mia memoria
Quindi, tutto il codice è lì. Ho caricato i file utilizzando l'utility ampy: ti consente di caricarli sull'unità flash interna (quella nell'ESP-07 stesso) e quindi accedervi dal programma come file normali. Lì ho anche caricato le librerie mqtt_as, uasyncio, ssd1306 e le raccolte che ho usato (usate all'interno di mqtt_as).
Lanciamo e... Otteniamo un MemoryError. Inoltre, più cercavo di capire dove esattamente perdeva memoria, più stampe di debug posizionavo, prima appariva questo errore. Una breve ricerca su Google mi ha portato a capire che il microcontrollore ha, in linea di principio, solo 30 kB di memoria, nella quale semplicemente non possono entrare 65 kB di codice (comprese le librerie).
Ma c'è una via d'uscita. Si scopre che micropython non esegue il codice direttamente da un file .py: questo file viene compilato per primo. Inoltre, viene compilato direttamente sul microcontrollore, trasformato in bytecode, che viene poi archiviato in memoria. Ebbene, affinché il compilatore funzioni, è necessaria anche una certa quantità di RAM.
Il trucco è salvare il microcontrollore dalla compilazione ad alta intensità di risorse. Puoi compilare i file su un computer di grandi dimensioni e caricare il bytecode già pronto nel microcontrollore. Per fare ciò, è necessario scaricare il firmware micropython e creare .
Non ho scritto un Makefile, ma ho esaminato e compilato manualmente tutti i file necessari (comprese le librerie) in qualcosa del genere
mpy-cross water_counter.py
Non resta che caricare i file con estensione .mpy, senza dimenticare di eliminare prima il corrispondente .py dal file system del dispositivo.
Ho fatto tutto lo sviluppo del programma (IDE?) ESPlorer. Ti consente di caricare script sul microcontrollore e di eseguirli immediatamente. Nel mio caso, tutta la logica e la creazione di tutti gli oggetti si trova nel file water_counter.py (.mpy). Ma affinché tutto questo si avvii automaticamente, all'inizio deve esserci anche un file chiamato main.py. Inoltre, dovrebbe essere esattamente .py e non .mpy precompilato. Ecco i suoi banali contenuti
import water_counter
Lo lanciamo: tutto funziona. Ma la memoria libera è allarmantemente piccola: circa 1kb. Ho ancora in programma di espandere le funzionalità del dispositivo e questo kilobyte chiaramente non mi basta. Ma si è scoperto che anche in questo caso esiste una via d'uscita.
Ecco il punto. Anche se i file sono compilati in bytecode e risiedono nel file system interno, in realtà vengono comunque caricati nella RAM ed eseguiti da lì. Ma si scopre che micropython può eseguire il bytecode direttamente dalla memoria flash, ma per questo è necessario integrarlo direttamente nel firmware. Non è difficile, anche se sul mio netbook ci è voluto un po' di tempo (solo lì avevo Linux).
L'algoritmo è il seguente:
- качать и установить . Questa cosa assembla un compilatore e librerie per programmi per ESP8266. Assemblato secondo le istruzioni presenti nella pagina principale del progetto (ho scelto l'impostazione STANDALONE=yes)
- scaricare
- Posiziona le librerie richieste in ports/esp8266/modules all'interno dell'albero micropython
- Montiamo il firmware secondo le istruzioni nel file
- Carichiamo il firmware sul microcontrollore (lo faccio su Windows utilizzando i programmi ESP8266Flasher o Python esptool)
Questo è tutto, ora 'import ssd1306' recupererà il codice direttamente dal firmware e la RAM non verrà consumata per questo. Con questo trucco ho caricato nel firmware solo il codice della libreria, mentre il codice del programma principale viene eseguito dal file system. Ciò consente di modificare facilmente il programma senza ricompilare il firmware. Al momento ho circa 8.5kb di RAM libera. Ciò ci consentirà di implementare molte funzionalità utili diverse in futuro. Bene, se non c'è abbastanza memoria, puoi inserire il programma principale nel firmware.
Quindi cosa dovremmo fare adesso?
Ok, l'hardware è saldato, il firmware è scritto, la scatola è stampata, il dispositivo è attaccato al muro e lampeggia allegramente una lampadina. Ma per ora è tutta una scatola nera (letteralmente e figuratamente) ed è ancora di scarsa utilità. È ora di fare qualcosa con i messaggi MQTT inviati al server.
La mia “casa intelligente” gira . Il modulo MQTT è pronto all'uso o può essere facilmente installato dal mercato dei componenti aggiuntivi: non ricordo da dove l'ho preso. MQTT non è una cosa autosufficiente: è necessario un cosiddetto. broker: un server che riceve, ordina e inoltra i messaggi MQTT ai client. Uso mosquitto, che (come majordomo) funziona sullo stesso netbook.
Dopo che il dispositivo ha inviato un messaggio almeno una volta, il valore apparirà immediatamente nell'elenco.

Questi valori possono ora essere associati a oggetti di sistema, possono essere utilizzati negli script di automazione e sottoposti a varie analisi, il che va oltre lo scopo di questo articolo. Posso consigliare il sistema majordomo a chiunque sia interessato — Anche un amico sta costruendo una casa intelligente e parla chiaramente di configurare il sistema.
Ti mostrerò solo un paio di grafici. Questo è un semplice grafico dei valori giornalieri

Si vede che quasi nessuno utilizzava l'acqua durante la notte. Un paio di volte qualcuno è andato in bagno e sembra che il filtro ad osmosi inversa aspiri un paio di litri a notte. Al mattino il consumo aumenta notevolmente. Di solito utilizzo l'acqua di una caldaia, ma poi volevo fare un bagno e sono passato temporaneamente all'acqua calda cittadina - questo è chiaramente visibile anche nel grafico in basso.
Da questo grafico ho appreso che per andare in bagno sono necessari 6-7 litri d'acqua, per fare la doccia sono necessari 20-30 litri, per lavare i piatti sono necessari circa 20 litri e per fare il bagno sono necessari 160 litri. La mia famiglia consuma circa 500-600 litri al giorno.
Per chi è particolarmente curioso è possibile consultare i record per ogni singolo valore

Da qui ho appreso che quando il rubinetto è aperto l'acqua scorre ad una velocità di circa 1 litro ogni 5 s.
Ma in questa forma le statistiche probabilmente non sono molto comode da guardare. Majordomo ha anche la possibilità di visualizzare i grafici dei consumi per giorno, settimana e mese. Ecco, ad esempio, un grafico dei consumi in barre

Finora ho solo dati per una settimana. Tra un mese, questo grafico sarà più indicativo: ogni giorno avrà una colonna separata. L'immagine è leggermente rovinata dalle modifiche ai valori che inserisco manualmente (la colonna più grande). E non è ancora chiaro se ho impostato erroneamente i primissimi valori, quasi un cubo in meno, oppure se si tratta di un bug del firmware e non sono stati contati tutti i litri. Ho bisogno di più tempo.
I grafici stessi hanno ancora bisogno di un po' di magia, di imbiancatura, di verniciatura. Forse creerò anche un grafico del consumo di memoria a scopo di debug, nel caso in cui si verifichino delle perdite. Forse in qualche modo mostrerò periodi in cui non c'era Internet. Per ora tutto questo è a livello di idee.
conclusione
Oggi il mio appartamento è diventato un po’ più smart. Con un dispositivo così piccolo mi sarà più comodo monitorare il consumo di acqua in casa. Se prima ero indignato per "di nuovo, abbiamo consumato molta acqua in un mese", ora posso trovare la fonte di questo consumo.
Alcuni potrebbero trovare strano guardare le letture sullo schermo se è a un metro di distanza dal contatore stesso. Ma in un futuro non molto lontano, ho intenzione di trasferirmi in un altro appartamento, dove ci saranno diverse colonne montanti l'acqua, e molto probabilmente i contatori stessi saranno posizionati sul pianerottolo. Quindi un dispositivo di lettura remota sarà molto utile.
Ho anche intenzione di espandere le funzionalità del dispositivo. Sto già guardando le valvole motorizzate. Ora, per commutare la caldaia sull'acqua di città, devo aprire 3 rubinetti in una nicchia difficile da raggiungere. Sarebbe molto più conveniente farlo con un pulsante con l'indicazione corrispondente. Bene, ovviamente vale la pena implementare la protezione contro le perdite.
Nell'articolo ho descritto la mia versione di un dispositivo basato su ESP8266. Secondo me, ho creato una versione molto interessante del firmware Micropython utilizzando le coroutine: semplice e carina. Ho provato a descrivere molte delle sfumature e delle carenze che ho riscontrato durante la campagna. Forse ho descritto tutto in modo troppo dettagliato; personalmente, come lettore, è più facile per me tralasciare le cose inutili piuttosto che riflettere poi su ciò che non è stato detto.
Come sempre, sono aperto alle critiche costruttive.
Fonte: habr.com
