Nekada su sistemi kućne automatizacije ili „pametna kuća“ kako su ih često nazivali bili strašno skupi i samo su bogati mogli da ih priušte. Danas na tržištu možete pronaći prilično jeftine komplete sa senzorima, dugmadima/prekidačima i aktuatorima za upravljanje rasvjetom, utičnicama, ventilacijom, vodoopskrbom i drugim potrošačima. A čak i najiskvareniji DIY osoba može se uključiti u ljepotu i sastaviti uređaje za pametni dom po jeftinoj cijeni.

Tipično, predloženi uređaji su ili senzori ili aktuatori. Olakšavaju implementaciju scenarija poput „kada se aktivira senzor pokreta, upali svjetla“ ili „prekidač pored izlaza gasi svjetla u cijelom stanu“. Ali stvari s telemetrijom nekako nisu išle. U najboljem slučaju, to je grafikon temperature i vlažnosti ili trenutne snage na određenoj utičnici.
Nedavno sam instalirao vodomjere sa impulsnim izlazom. Za svaki litar koji prođe kroz mjerač, prekidač se aktivira i zatvara kontakt. Jedino što preostaje je da se uhvatite za žice i pokušate izvući korist od toga. Na primjer, analizirajte potrošnju vode po satu i danu u sedmici. Pa, ako u stanu postoji nekoliko uspona za vodu, onda je zgodnije vidjeti sve trenutne indikatore na jednom ekranu nego se penjati u teško dostupne niše s baterijskom lampom.
Ispod reza je moja verzija uređaja baziranog na ESP8266, koji broji impulse sa vodomjera i šalje očitanja preko MQTT-a na server pametne kuće. Programiraćemo u micropythonu koristeći uasyncio biblioteku. Prilikom kreiranja firmvera naišao sam na nekoliko zanimljivih poteškoća, o kojima ću također govoriti u ovom članku. Idi!
Shema

Srce cijelog kola je modul na mikrokontroleru ESP8266. Prvobitno je planiran ESP-12, ali se pokazalo da je moj neispravan. Morali smo biti zadovoljni modulom ESP-07, koji je bio dostupan. Na sreću, isti su i po pinovima i po funkcionalnosti, jedina razlika je u anteni - ESP-12 ima ugrađenu, dok ESP-07 ima eksternu. Međutim, čak i bez WiFi antene, signal u mom kupatilu se normalno prima.
Standardno ožičenje modula:
- dugme za resetovanje sa povlačenjem i kondenzatorom (iako su oba već unutar modula)
- Signal za omogućavanje (CH_PD) se povlači na napajanje
- GPIO15 je povučen na tlo. Ovo je potrebno samo na početku, ali još uvijek nemam šta da pričvrstim za ovu nogu, više mi ne treba
Da biste modul stavili u režim firmvera, potrebno je kratko spojiti GPIO2 na masu, a da bi bio praktičniji, dao sam dugme za pokretanje. U normalnom stanju, ovaj pin se povlači na napajanje.
Stanje GPIO2 linije se provjerava samo na početku rada - kada se uključi napajanje ili odmah nakon resetiranja. Dakle, modul se ili pokreće kao i obično ili ide u firmware mod. Kada se učita, ovaj pin se može koristiti kao običan GPIO. Pa, pošto tamo već postoji dugme, možete mu priložiti neku korisnu funkciju.
Za programiranje i otklanjanje grešaka koristiću UART, koji se izlazi na češalj. Kada je potrebno, jednostavno spojim USB-UART adapter tamo. Samo treba da zapamtite da se modul napaja od 3.3V. Ako zaboravite prebaciti adapter na ovaj napon i napajati 5V, modul će najvjerovatnije izgorjeti.
Nemam problema sa strujom u kupatilu - utičnica se nalazi na oko metar od brojila, tako da ću se napajati na 220V. Kao izvor napajanja imaću mali od Tenstar Robot. Lično, teško se nosim sa analognom i energetskom elektronikom, ali evo već gotovog napajanja u malom kućištu.
Za signalizaciju načina rada dao sam LED diodu povezanu na GPIO2. Međutim, nisam ga odlemio, jer... Modul ESP-07 već ima LED, a takođe je povezan na GPIO2. Ali neka bude na ploči, u slučaju da želim da izbacim ovu LED diodu na kućište.
Pređimo na najzanimljiviji dio. Vodomjeri nemaju logiku, ne možete od njih tražiti trenutna očitanja. Jedino što nam je dostupno su impulsi - zatvaranje kontakata reed prekidača na svaki litar. Moji izlazi reed prekidača su povezani na GPIO12/GPIO13. Programski ću omogućiti pull-up otpornik unutar modula.
U početku sam zaboravio dati otpornike R8 i R9 i moja verzija ploče ih nema. Ali pošto već objavljujem dijagram da ga svi vide, vrijedi ispraviti ovaj previd. Otpornici su potrebni da ne bi spalili port ako firmver pokvari i postavi pin na jedan, a reed prekidač skrati ovu liniju na masu (sa otpornikom će teći maksimalno 3.3V/1000Ohm = 3.3mA).
Vrijeme je da razmislite šta učiniti ako nestane struje. Prva opcija je da na početku zatražite početne vrijednosti brojača od servera. Ali to bi zahtijevalo značajnu komplikaciju protokola razmjene. Štaviše, performanse uređaja u ovom slučaju zavise od stanja servera. Ako se server nije pokrenuo nakon što je napajanje isključeno (ili pokrenuto kasnije), vodomjer ne bi mogao tražiti početne vrijednosti i ne bi ispravno radio.
Stoga sam odlučio implementirati spremanje vrijednosti brojača u memorijski čip povezan preko I2C. Nemam nikakve posebne zahtjeve za veličinu flash memorije - potrebno je samo sačuvati 2 broja (broj litara prema mjeračima tople i hladne vode). Čak će i najmanji modul moći. Ali morate obratiti pažnju na broj ciklusa snimanja. Za većinu modula to je 100 hiljada ciklusa, za neke i do milion.
Čini se da je milion mnogo. Ali za 4 godine života u svom stanu potrošio sam nešto više od 500 kubnih metara vode, to je 500 hiljada litara! I 500 hiljada zapisa u flashu. A to je samo hladna voda. Možete, naravno, prelemiti čip svakih nekoliko godina, ali ispostavilo se da postoje FRAM čipovi. Sa programske tačke gledišta, ovo je isti I2C EEPROM, samo sa veoma velikim brojem ciklusa ponovnog pisanja (stotine miliona). Samo još uvijek ne mogu doći do radnje sa takvim mikro krugovima, pa će za sada stajati uobičajeni 24LC512.
Štampana ploča
U početku sam planirao da napravim ploču kod kuće. Stoga je ploča dizajnirana kao jednostrana. Ali nakon sat vremena sa laserskom peglom i maskom za lemljenje (bez toga nekako nije comme il faut), ipak sam odlučio da naručim ploče od Kineza.

Skoro pre nego što sam naručio ploču, shvatio sam da pored čipa fleš memorije mogu da povežem još nešto korisno na I2C magistralu, kao što je ekran. Još uvijek je pitanje šta točno dati na njega, ali to treba usmjeriti na ploču. Pa, pošto sam planirao da naručim ploče iz fabrike, nije bilo smisla da se ograničavam na jednostranu ploču, tako da su I2C linije jedine na zadnjoj strani ploče.
Postojao je i jedan veliki problem sa jednosmjernim ožičenjem. Jer Ploča je nacrtana kao jednostrana, tako da je planirano da se staze i SMD komponente postave na jednu stranu, a izlazne komponente, konektori i napajanje na drugu. Kada sam primio ploče mjesec dana kasnije, zaboravio sam na originalni plan i zalemio sve komponente na prednjoj strani. I tek kada je došlo do lemljenja napajanja, pokazalo se da su plus i minus obrnuti. Morao sam da se bavim farmom sa skakačima. Na gornjoj slici sam već promenio ožičenje, ali se zemlja prenosi sa jednog dela ploče na drugi preko pinova dugmeta Boot (iako bi bilo moguće nacrtati stazu na drugom sloju).
Ispalo je ovako

Stanovanje
Sljedeći korak je tijelo. Ako imate 3D štampač, to nije problem. Nisam se previše trudio - samo sam nacrtao kutiju prave veličine i napravio izreze na pravim mjestima. Poklopac je pričvršćen za tijelo malim samoreznim vijcima.

Već sam spomenuo da se dugme za pokretanje može koristiti kao dugme opšte namene - tako da ćemo ga prikazati na prednjoj ploči. Da bih to uradio, nacrtao sam poseban "bunar" gde dugme živi.

Unutar kućišta se nalaze i klinovi na koje se postavlja ploča i učvršćuje jednim M3 vijkom (nije bilo više mjesta na ploči)
Odabrao sam ekran već kada sam odštampao prvu probnu verziju kućišta. Standardni dvolinijski čitač se nije uklapao u ovo kućište, ali na dnu je bio OLED ekran SSD1306 128×32. Malo je mali, ali ne moram da buljim u njega svaki dan – previše mi je.
Smišljajući na ovaj način i kako će žice biti izvučene iz njega, odlučio sam da ekran zalijepim u sredinu kućišta. Ergonomija je, naravno, ispod nivoa – dugme je na vrhu, ekran je na dnu. Ali već sam rekao da je ideja za pričvršćivanje ekrana došla prekasno i da sam bio previše lijen da ponovo ožičim ploču da pomjerim dugme.
Uređaj je montiran. Modul displeja je zalijepljen vrućim ljepilom za šmrk


Krajnji rezultat se može vidjeti na KDPV-u
Firmware
Pređimo na softverski dio. Za male zanate kao što je ovaj, stvarno volim koristiti Python () - ispada da je kod vrlo kompaktan i razumljiv. Srećom, nema potrebe da se spuštate na nivo registra da biste istisnuli mikrosekunde - sve se može uraditi iz Pythona.
Čini se da je sve jednostavno, ali ne baš jednostavno - uređaj ima nekoliko nezavisnih funkcija:
- Korisnik pritisne dugme i gleda u ekran
- Litara označite i ažurirajte vrijednosti u flash memoriji
- Modul prati WiFi signal i ponovo se povezuje ako je potrebno
- Pa, bez trepćuće sijalice je nemoguće
Ne možete pretpostaviti da jedna funkcija nije radila ako je druga iz nekog razloga zaglavljena. Već sam se nasitio kaktusa u drugim projektima i sada još uvijek vidim kvarove u stilu “propustio još jednu litru jer se ekran u tom trenutku ažurirao” ili “korisnik ne može ništa učiniti dok se modul povezuje na WiFi.” Naravno, neke stvari se mogu obaviti putem prekida, ali možete naići na ograničenja u trajanju, ugniježđenju poziva ili neatomskim promjenama varijabli. Pa, kod koji sve radi brzo se pretvara u kašu.
В Koristio sam klasični preventivni multitasking i FreeRTOS, ali u ovom slučaju se model pokazao mnogo prikladnijim . Štaviše, Python implementacija korutina je jednostavno nevjerovatna - sve se radi jednostavno i zgodno za programera. Samo napišite svoju logiku, samo mi recite na koja mjesta možete prelaziti između streamova.
Predlažem da se kao izborni predmet proučavaju razlike između preventivnog i kompetitivnog multitaskinga. Sada konačno pređimo na kod.
#####################################
# 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
Svakim brojačem rukuje instanca klase Counter. Prije svega, početna vrijednost brojača se oduzima od EEPROM-a (value_storage) - tako se ostvaruje oporavak nakon nestanka struje.
Pin se inicijalizira sa ugrađenim povlačenjem do napajanja: ako je reed prekidač zatvoren, linija je nula, ako je linija otvorena, povlači se do napajanja i kontroler očitava jedan.
Ovdje se također pokreće poseban zadatak, koji će ispitati pin. Svaki brojač će pokrenuti svoj zadatak. Evo njene šifre
""" 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)
Za filtriranje odbijanja kontakta potrebno je kašnjenje od 25 ms, a istovremeno reguliše koliko često se zadatak budi (dok ovaj zadatak spava, drugi zadaci su pokrenuti). Svakih 25ms funkcija se budi, provjerava pin i ako su kontakti reed prekidača zatvoreni, onda je još jedan litar prošao kroz mjerač i to treba obraditi.
def _another_litre_passed(self):
self._value += 1
self._value_changed = True
self._value_storage.write(self._value)
Obrada sljedeće litre je trivijalna - brojač se jednostavno povećava. Pa, bilo bi lijepo zapisati novu vrijednost na fleš disk.
Radi lakšeg korišćenja, obezbeđeni su „pristupnici“.
def value(self):
self._value_changed = False
return self._value
def set_value(self, value):
self._value = value
self._value_changed = False
Pa, hajde da sada iskoristimo prednosti Pythona i uasync biblioteke i napravimo counter objekat na čekanju (kako možemo ovo prevesti na ruski? Onaj koji možete očekivati?)
def __await__(self):
while not self._value_changed:
yield from asyncio.sleep(0)
return self.value()
__iter__ = __await__
Ovo je tako zgodna funkcija koja čeka dok se vrijednost brojača ne ažurira - funkcija se s vremena na vrijeme budi i provjerava _value_changed zastavicu. Dobra stvar kod ove funkcije je da pozivni kod može zaspati dok poziva ovu funkciju i spavati dok se ne primi nova vrijednost.
Šta je sa prekidima?Da, u ovom trenutku možete me trolati, rekavši da ste sami rekli o prekidima, ali u stvarnosti ste napravili glupu anketu. Zapravo, prekidi su prvo što sam pokušao. U ESP8266, možete organizirati ivični prekid, pa čak i napisati rukovatelj za ovaj prekid u Python-u. U ovom prekidu, vrijednost varijable se može ažurirati. Vjerovatno bi ovo bilo dovoljno da je brojač slave uređaj - onaj koji čeka dok se od njega ne zatraži ova vrijednost.
Nažalost (ili na sreću?) moj uređaj je aktivan, mora sam slati poruke putem MQTT protokola i pisati podatke u EEPROM. I tu na scenu stupaju ograničenja – ne možete dodijeliti memoriju u prekidima i koristiti veliki stek, što znači da možete zaboraviti na slanje poruka preko mreže. Postoje kolačići poput micropython.schedule() koji vam omogućavaju da pokrenete neku funkciju „što je prije moguće“, ali postavlja se pitanje „u čemu je svrha?“ Šta ako trenutno šaljemo neku vrstu poruke, a onda dođe prekid i pokvari vrijednosti varijabli. Ili, na primjer, nova vrijednost brojača je stigla sa servera dok staru još nismo zapisali. Općenito, trebate blokirati sinhronizaciju ili se izvući iz nje nekako drugačije.
I s vremena na vrijeme RuntimeError: zakažite pune padove steka i ko zna zašto?
Uz eksplicitno polling i uasync, u ovom slučaju nekako ispadne ljepše i pouzdanije
Donio sam rad sa EEPROM-om u mali razred
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)
U Pythonu je teško raditi direktno sa bajtovima, ali bajtovi se zapisuju u memoriju. Morao sam ograditi konverziju između cijelog broja i bajtova koristeći ustruct biblioteku.
Da ne bih svaki put prenosio I2C objekt i adresu memorijske ćelije, sve sam zamotao u mali i praktičan klasik
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)
Sam I2C objekat se kreira sa ovim parametrima
i2c = I2C(freq=400000, scl=Pin(5), sda=Pin(4))
Dolazimo do najzanimljivijeg dijela - implementacije komunikacije sa serverom putem MQTT-a. Pa, nema potrebe za implementacijom samog protokola - našao sam ga na internetu . Ovo je ono što ćemo koristiti.
Sve najzanimljivije stvari sakupljene su u klasi CounterMQTTClient koja je bazirana na biblioteci MQTTClient. Krenimo od periferije
#####################################
# 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))
Ovdje možete kreirati i konfigurirati igle i dugmad sijalica, kao i objekte mjerača hladne i tople vode.
Sa inicijalizacijom, nije sve tako trivijalno
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())
Za postavljanje radnih parametara biblioteke mqtt_as koristi se veliki rječnik različitih postavki - config. Većina zadanih postavki nam odgovara, ali mnoge postavke treba eksplicitno postaviti. Da ne bih pisao postavke direktno u kodu, spremam ih u tekstualnu datoteku config.txt. To vam omogućava da promijenite kod bez obzira na postavke, kao i da zakivate nekoliko identičnih uređaja s različitim parametrima.
Posljednji blok koda pokreće nekoliko korutina koje služe različitim funkcijama sistema. Na primjer, evo korutine koja servisira brojače
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))
Korutina u petlji čeka novu vrijednost brojača i, čim se pojavi, šalje poruku putem MQTT protokola. Prvi dio koda šalje početnu vrijednost čak i ako voda ne teče kroz brojač.
MQTTClient osnovne klase služi sam sebi, pokreće WiFi vezu i ponovo se povezuje kada se veza izgubi. Kada dođe do promjena u stanju WiFi veze, biblioteka nas obavještava pozivom 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)
Funkcija je iskreno kopirana iz primjera. U ovom slučaju, broji se broj prekida (internet_outages) i njihovo trajanje. Kada se veza uspostavi, serveru se šalje vrijeme mirovanja.
Inače, posljednje spavanje je potrebno samo da bi funkcija bila asinhrona – u biblioteci se poziva putem await, a mogu se pozvati samo funkcije čije tijelo sadrži drugi await.
Osim povezivanja na WiFi, potrebno je uspostaviti i vezu sa MQTT brokerom (serverom). Biblioteka to radi i mi imamo priliku da uradimo nešto korisno kada se veza uspostavi
async def mqtt_connection_handler(self, client):
await client.subscribe(self._mqtt_cold_water_theme)
await client.subscribe(self._mqtt_hot_water_theme)
Ovdje se pretplaćujemo na nekoliko poruka - server sada ima mogućnost postavljanja trenutnih vrijednosti brojača slanjem odgovarajuće poruke.
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))
Ova funkcija obrađuje dolazne poruke, a ovisno o temi (naslov poruke), ažuriraju se vrijednosti jednog od brojača
Nekoliko pomoćnih funkcija
# 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")
Ova funkcija šalje poruku ako je veza uspostavljena. Ako nema veze, poruka se ignoriše.
A ovo je samo zgodna funkcija koja generiše i šalje poruke za otklanjanje grešaka.
async def publish_debug_msg(self, subtopic, msg):
await self.publish_msg("{}/{}".format(self._mqtt_debug_water_theme, subtopic), str(msg))
Toliko teksta, a još nismo trepnuli LED diodom. Evo
# 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)
Omogućio sam 2 načina treptanja. Ako se veza izgubi (ili se tek uspostavlja), uređaj će brzo treptati. Ako je veza uspostavljena, uređaj treperi jednom svakih 5 sekundi. Ako je potrebno, ovdje se mogu implementirati drugi načini treptanja.
Ali LED je samo maženje. Ciljali smo i na displej.
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)
Ovo je ono o čemu sam govorio - kako je to jednostavno i zgodno sa korutinama. Ova mala funkcija opisuje CIJELO korisničko iskustvo. Korutina jednostavno čeka da se dugme pritisne i uključuje ekran na 3 sekunde. Displej prikazuje trenutna očitanja brojila.
Ostalo je još par sitnica. Evo funkcije koja (ponovno) pokreće čitavo ovo preduzeće. Glavna petlja samo šalje razne informacije za otklanjanje grešaka jednom u minuti. Uglavnom, citiram onako kako jeste - mislim da nema potrebe previše komentarisati
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)
Pa, još par postavki i konstanti da upotpunimo opis
#####################################
# 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)
Sve počinje ovako
client = CounterMQTTClient()
loop = asyncio.get_event_loop()
loop.run_until_complete(client.main())
Nešto mi se desilo sa sećanjem
Dakle, sav kod je tu. Uploadovao sam fajlove koristeći ampy uslužni program - on vam omogućava da ih otpremite na interni (onaj u samom ESP-07) fleš disk i zatim mu pristupite iz programa kao normalnim fajlovima. Tamo sam također uploadovao mqtt_as, uasyncio, ssd1306 i biblioteke kolekcija koje sam koristio (koristi se unutar mqtt_as).
Pokrećemo i... Dobijamo MemoryError. Štaviše, što sam više pokušavao da shvatim gde tačno memorija curi, što sam više ispisa za otklanjanje grešaka postavljao, to se ova greška ranije pojavljivala. Kratka pretraga na Guglu dovela me je do shvatanja da mikrokontroler u principu ima samo 30 kB memorije, u koje 65 kB koda (uključujući biblioteke) jednostavno ne može da stane.
Ali postoji izlaz. Ispostavilo se da micropython ne izvršava kod direktno iz .py datoteke - ovaj fajl se prvo kompajlira. Štaviše, kompajlira se direktno na mikrokontroleru, pretvara se u bajt kod, koji se zatim pohranjuje u memoriju. Pa, da bi kompajler radio, potrebna vam je i određena količina RAM-a.
Trik je u tome da se mikrokontroler spasi od kompilacije koja zahtijeva veliku količinu resursa. Možete kompajlirati fajlove na velikom računaru i učitati gotov bajt kod u mikrokontroler. Da biste to učinili, morate preuzeti mikropython firmware i izgraditi .
Nisam napisao Makefile, već sam ručno prošao i kompajlirao sve potrebne fajlove (uključujući biblioteke) nešto ovako
mpy-cross water_counter.py
Sve što ostaje je da otpremite datoteke sa ekstenzijom .mpy, ne zaboravljajući da prvo izbrišete odgovarajući .py iz sistema datoteka uređaja.
Radio sam sav razvoj u programu (IDE?) ESPlorer. Omogućava vam da otpremite skripte u mikrokontroler i odmah ih izvršite. U mom slučaju, sva logika i kreiranje svih objekata nalazi se u datoteci water_counter.py (.mpy). Ali da bi se sve ovo pokrenulo automatski, na početku mora postojati i datoteka koja se zove main.py. Štaviše, trebalo bi da bude tačno .py, a ne unapred kompajlirani .mpy. Evo njegovog trivijalnog sadržaja
import water_counter
Pokrećemo ga - sve radi. Ali slobodna memorija je alarmantno mala - oko 1 kb. Još uvijek imam planove za proširenje funkcionalnosti uređaja, a ovaj kilobajt mi očito nije dovoljan. Ali pokazalo se da i za ovaj slučaj postoji izlaz.
Evo u čemu je stvar. Iako su datoteke kompajlirane u bajtkod i nalaze se na internom sistemu datoteka, u stvarnosti se i dalje učitavaju u RAM i odatle izvršavaju. Ali ispostavilo se da micropython može izvršiti bajtkod direktno iz flash memorije, ali za to ga morate ugraditi direktno u firmver. Nije teško, iako je na mom netbook-u trebalo dosta vremena (samo tamo sam imao Linux).
Algoritam je ovakav:
- Preuzmite i instalirajte . Ova stvar sastavlja kompajler i biblioteke za programe za ESP8266. Sastavljeno prema uputama na glavnoj stranici projekta (odabrao sam postavku STANDALONE=yes)
- Preuzimanje
- Postavite potrebne biblioteke u ports/esp8266/modules unutar micropython stabla
- Sastavljamo firmver prema uputama u datoteci
- Učitavamo firmver na mikrokontroler (to radim na Windows-u koristeći program ESP8266Flasher ili Python esptool)
To je to, sada će 'import ssd1306' podići kod direktno iz firmvera i RAM se neće trošiti za ovo. Ovim trikom sam uploadovao samo kod biblioteke u firmver, dok se glavni programski kod izvršava iz sistema datoteka. Ovo vam omogućava da lako modificirate program bez ponovnog kompajliranja firmvera. Trenutno imam oko 8.5kb slobodne RAM memorije. To će nam omogućiti da implementiramo dosta različitih korisnih funkcionalnosti u budućnosti. Pa, ako uopće nema dovoljno memorije, onda možete ugurati glavni program u firmver.
Dakle, šta da radimo u vezi s tim sada?
Ok, hardver je zalemljen, firmver napisan, kutija je odštampana, uređaj zakačen na zid i veselo treperi sijalica. Ali za sada je sve to crna kutija (bukvalno i figurativno) i još uvijek je od male koristi. Vrijeme je da se nešto uradi sa MQTT porukama koje se šalju na server.
Moj "pametni dom" se okreće . MQTT modul ili dolazi iz kutije, ili se lako instalira sa tržišta dodataka - ne sjećam se odakle sam ga nabavio. MQTT nije samodovoljna stvar - potreban vam je tzv. broker - server koji prima, sortira i prosljeđuje MQTT poruke klijentima. Koristim mosquitto, koji (kao majordomo) radi na istom netbook-u.
Nakon što uređaj barem jednom pošalje poruku, vrijednost će se odmah pojaviti na listi.

Ove vrijednosti sada se mogu povezati sa sistemskim objektima, mogu se koristiti u skriptama za automatizaciju i podvrgnuti raznim analizama - sve to je izvan okvira ovog članka. Mogu preporučiti majordomo sistem svima koji su zainteresovani — prijatelj takođe gradi pametnu kuću i jasno govori o postavljanju sistema.
Pokazaću vam samo nekoliko grafikona. Ovo je jednostavan grafikon dnevnih vrijednosti

Vidi se da skoro niko nije koristio vodu noću. Par puta je neko otišao u toalet, a čini se da filter reverzne osmoze usisava par litara po noći. Ujutro se potrošnja značajno povećava. Obično koristim vodu iz bojlera, ali tada sam se htio okupati i privremeno prešao na gradsku toplu vodu - to je također jasno vidljivo na donjem grafikonu.
Iz ovog grafikona sam saznao da je za odlazak u toalet potrebno 6-7 litara vode, za tuširanje je potrebno 20-30 litara, za pranje sudova oko 20 litara, a za kupanje 160 litara. Moja porodica troši negdje oko 500-600 litara dnevno.
Za one koji su posebno radoznali, možete pogledati zapise za svaku pojedinačnu vrijednost

Odavde sam saznao da kada je slavina otvorena, voda teče brzinom od otprilike 1 litra u 5 s.
Ali u ovom obliku statistike vjerovatno nisu baš zgodne za gledanje. Majordomo takođe ima mogućnost pregleda grafikona potrošnje po danu, sedmici i mjesecu. Evo, na primjer, grafikon potrošnje u stupcima

Za sada imam podatke samo za nedelju dana. Za mjesec dana ovaj grafikon će biti indikativniji - svaki dan će imati zasebnu kolonu. Sliku su malo pokvarila podešavanja vrijednosti koje unosim ručno (najveća kolona). I još nije jasno jesam li pogrešno postavio prve vrijednosti, skoro kocku manje, ili je ovo greška u firmveru i nisu izbrojane sve litre. Treba mi više vremena.
Samim grafovima je još potrebna magija, krečenje, farbanje. Možda ću napraviti i graf potrošnje memorije za potrebe otklanjanja grešaka - u slučaju da tamo nešto curi. Možda ću nekako prikazati periode kada nije bilo interneta. Za sada je sve to na nivou ideja.
zaključak
Danas je moj stan postao malo pametniji. S tako malim uređajem bit će mi zgodnije pratiti potrošnju vode u kući. Ako sam ranije bio ogorčen na “opet smo potrošili puno vode za mjesec dana”, sada mogu pronaći izvor ove potrošnje.
Nekima će možda biti čudno gledati očitanja na ekranu ako je metar udaljen od samog mjerača. Ali u ne tako dalekoj budućnosti planiram da se preselim u drugi stan, gdje će biti nekoliko vodostaja, a sami mjerači će se najvjerovatnije nalaziti na podestu. Stoga će uređaj za daljinsko čitanje biti vrlo koristan.
Također planiram proširiti funkcionalnost uređaja. Već gledam motorne ventile. Sada, da prebacim bojler na gradsku vodu, trebam okrenuti 3 slavine u teško dostupnoj niši. Bilo bi mnogo zgodnije to učiniti jednim dugmetom sa odgovarajućom indikacijom. Pa, naravno, vrijedi implementirati zaštitu od curenja.
U članku sam opisao svoju verziju uređaja baziranog na ESP8266. Po mom mišljenju, došao sam do vrlo interesantne verzije mikropython firmware-a koristeći korutine - jednostavno i lijepo. Pokušao sam da opišem mnoge nijanse i nedostatke na koje sam naišao tokom kampanje. Možda sam sve isuviše detaljno opisao; lično, kao čitaocu, lakše mi je da preskočim nepotrebne stvari nego da kasnije razmišljam šta je ostalo nedorečeno.
Kao i uvek, otvoren sam za konstruktivnu kritiku.
izvor: www.habr.com
