image


Пролог


Работая программистом в одной из больших и успешных компаний Москвы, я не переставал совершенствовать свои навыки программирования и проходил различные курсы на платформе Udemy.
Конечно просто смотря курс и повторяя все за автором было скучновато, да и были моменты которые я не понимал ввиду своей некомпетентности на тот момент. Нужно делать свои проекты, основываясь на том, что дает автор курса — подумал я, и был конечно же прав. Только настоящие трудности и их разрешение дает вам бесценный опыт, это и есть настоящая обучение.


Обучался я в основном web программированию, поскольку и работал на том же направлении. Охватывал Full-stack разработку, поскольку решил, что нужно разбираться как в серверной части, так и во фронтовой. Учил JavaScript и различные фреймворки для бека это были Express, Appolo GraphQL (поскольку на работе был именно такой стек, да и в целом хотелось попробовать что-то отличное от REST подхода), на фронте это был все тот же Apollo GQL и Vue.


И вот немного окрепнув в этой связке, завершая один из учебных проектов, я задумался, что бы сделать интересного, где можно было бы задействовать мои знания. Эта мысль меня уже давно беспокоила, конечно многие мне советовали, для того что бы получить опыт, можно сделать с нуля свой "В контакте", "The Facebook", "Instagram" и т.д. и они были правы, действительно это бы прибавило мне опыта, но такие большие проекты я боялся не вывести в одиночку и забросить его.


Как то раз мой друг решил купить себе аквариум, тогда я думал, что это глупое занятие, что толку пялится на рыб? Подумал я тогда...


Но случилось то что должно было, друг купил себе аквариум на 15 литров и тройку рыбешек по 3см каждая. Спустя неделю друг понял, что хочет больше и конечно же свой старый он подсунул именно мне со словами "Да ладно он не большой, тебе понравится, просто поставь, пусть стоит рядом с кроватью". Но тех рыб он оставил себе. Я же пошел в ближайшей магазин и купил себе "Петушка" — Кто не в курсе погуглите, вкратце скажу, что это бойцовская рыбка, которую всегда селят в отдельном аквариуме.


Спустя время аквариумистика мне понравилась и прочитав кучу инфы, понял что мне хочется морской аквариум. Во-первых это красиво, настоящее море и кораллы у тебя дома. Во-вторых это сложно и бросает тебе вызов. Будет интересно подумал я.


И так почитав разные статейки я пришел к тому, что я хочу для начала попробовать так называемый "Нано риф" — маленький морской аквариум от 20-50 литров, в который даже "Самп" необязательно ставить. Выяснилось, что мне нужно освещение в разных спектрах (белый, синий, зеленый, красный), при всем этом светодиоды должны светить одновременно, поэтом RGB светодиоды мне не подойдут. Освещение должно само меняться, создавая иллюзию дня и ночи. Нужен был рассвет и закат. Нужно следить за температурой. Нужно следить за химической составляющей воды. Делать ее подмены. И еще много всего. И тут я подумал, что можно какую то часть из этого автоматизировать, по крайней мере что касаемо освещения и температуры.


Еще несколько лет назад я делал несложные приспособления на платформе Arduino. Было весело и интересно, было решено сделать умный аквариум на базе этой платформы, позже я поменял саму платформу.


Не переплачивай — делай сам


Выбор был сделан в пользу платы NodeMCU — это такая "Ардуина" с уже встроенным WI-FI на борту (ESP 8266), что мне и нужно было, хотя у меня и была в закромах Arduino nano и сам wi-fi модуль в отдельном исполнении, но его надо было прошивать для этого и т.д. Заказал на известном сайте в поднебесной новенькую NodeMCU, пришла быстро и работа закипела.



По старинке скачал Arduino IDE настроил ее на работу с этой платой (на Windows 10, сам драйвер встал по умолчанию), и открыл пример для подключения к WI-FI. Не знаю почему, долго я мучил ее но так и не влетело мое подключение, начал гуглить. Почитав несколько статей, узнал, что на платку можно залить прошивку для некоего языка Lua(потратив усилия и время потом уже вычитал, что еще есть Micro python, но об этом позже)


Прошив плату и открыв документацию вместе с форумом, увидел примеры кода, которые шокировали даже меня, человека который нормально относится к C++ в Arduino и знающего JS и Python(немного). И там был синтаксис которого я не хотел:


init.lua


print ( "Waiting ...")
tmr.register (0, 5000, tmr.ALARM_SINGLE, function (t) tmr.unregister (0); print ( "Starting ..."); dofile ( "main.lua") end)
tmr.start (0)

main.lua


--WiFi Settup
wifi.setmode(wifi.STATION)
local cfg={}
cfg.ssid="wifi_point_name"
cfg.pwd="point_pass"
wifi.sta.config(cfg)
cfg = nil
collectgarbage()

=wifi.ap.getip()

Пример для подключения к WI-FI был менее ужасным, но тем не менее, я не хотел разбираться в этих begin и end. Почитав документацию к esp-8266 узнал, что есть на ней некая прошивка с Питоном на борту. Точнее это было не просто обычный Питон, это было Micro python, некая обрезанная версия его, но все же это было лучше чем lua ИМХО. И я полез искать прошивки и инструменты


Скачиваем с сайта прошивку под нужную плату и прошиваем при помощи esptool прошиваем


pip install -g esptool

esptool.py --port COM3 --baud 460800 write_flash --flash_size=detect 0 esp8266-20191220-v1.12.bin

скачиваем специальную IDE EsPy для работы с "Микропитоном" и понеслась.


Для начала прочитал что на плату надо залить обязательных два файла, без которых ничего не поедет, а именно:


  • boot.py — скрипт, который загружается первым при включении платы. Обычно в него вставляют функции для инициализации модулей, подключения к Wi-Fi и запуска WebREPL;
  • main.py — основной скрипт, который запускается сразу после выполнения boot.py, в него записывается основная программа.

Далее читаю как все используют интерфейс взаимодействия WebREPL — очень схоже с тем как используют ssh подключение для работы с raspberri, но он был мне не нужен, поскольку EsPy умел общаться с платой и можно было также закачивать свеженаписанный скрипт на плату без использования esptool, хотя и там не было ничего сложного. Первый шаг — подключение к WI-FI — засунул в скрипт самый базовый и легкий код.


boot.py


import network
import time

''' Код подключения к WiFi '''
wlan_id = "my_point"
wlan_pass = "strong_pass"

wlan = network.WLAN(network.STA_IF)
wlan.active(True)

if wlan.isconnected() == False:
    wlan.connect(wlan_id, wlan_pass)
    while wlan.isconnected() == False:
        time.sleep(3)
        print("Connection Fail...")
print('Device IP:', wlan.ifconfig()[0])

В файлик main.py решил добавить запуск самого приложения, код получился простым и лаконичным, в дальнейшем добавил туда создание таски для asyncio, что бы было все асинхронно(хоть и питоновские библиотеки были сыроваты для этого).


main.py


from app import app_start

app_start()

Далее наткнулся на библиотеку MicroPyServer которая была не большой (всего один файлик), и позволял на базовом уровне создать веб-сервер с роутами(библиотека была построена на сокетах). Ну что ж напишем Хэлсчек для проверки и зальем все на плату.


app.py


from micropyserver import MicroPyServer

import ujson

server = MicroPyServer()

def send(self, **kwargs):
        '''
            Отправляем ответ
        '''
        server.send(
            ujson.dumps(kwargs),
            content_type="Content-Type: application/json",
            # Добавляем заголовки для CORS политики
            extra_headers=["Access-Control-Allow-Origin: *"]
        )
        gc.collect()

def healthcheck():
    send(success=1, healthcheck='green')

server.add_route("/healthcheck", healthcheck)

def app_start():
    server.start()

Со структурой (иерархией) проекта решил не выпентриваться, все таки памяти маловато и размещать все в отдельных папках боялся, а вдруг она закончится в самый не походящий момент. Залил все файлики прям в корень платы.


  • main.py
  • boot.py
  • app.py
  • micropyserver.py

Запускаем, видим что сервак локально поднялся и предлагает сходить на http://192.168.1.70/ и проверить. Идем в браузер, делаем запрос http://192.168.1.70/healthcheck, и о чудо — все работает, сервер отвечает



// Json from chrome browser
{
    "success": 1,
    "healthcheck": "green",
}

Далее ждал когда приедут нужные мне hardware запчасти (датчик температуры, LED-матрицы, LCD-дисплей и т.д. ), пока ждал, принялся писать бекенд дальше, все разбил на классы и начал писать. Для начала решил написать Помошника для сервера, который бы включал в себя парсер запросов и функцию отправки, и добавил Класс для работы с датчиком температуры.


app.py


from heater import Heater

# ... пропущен код

class HttpHelper:
    """
        Простой парсер GET запросов
    """
    def __init__(self):
        pass

    def parse(self, request):
        '''
            парсер строки ответа сервера
        '''
        lines = request.split("\r\n")

        result = {
            'lines': lines,
            'method': ure.search("^([A-Z]+)", lines[0]).group(1),
            'path': ure.search(
                "^[A-Z]+\\s+(/[-a-zA-Z0-9_.]*)", lines[0]
            ).group(1),
        }

        param_split = ure.sub("\/([a-z]+_?)+?\?", '', lines[0].split(" ")[1])
        result['params'] = self.get_params(param_split.split("&"))

        return result

    def get_params(self, params_as_array):
        '''
            создает словарь для query параметров
        '''
        params = {}

        for element in params_as_array:
            splited = element.split("=")

            params[splited[0]] = splited[1]

        return params

    def send(self, **kwargs):
        '''
            Отправляем ответ
        '''
        server.send(
            ujson.dumps(kwargs),
            content_type="Content-Type: application/json",
            extra_headers=["Access-Control-Allow-Origin: *"]
        )
        gc.collect()

# ... пропущен код

def get_water_temperature_C(request):
    '''
        Отдает температуру в Цельсиях
    '''
    try:
        water_heater.get_water_tmp_C()

        http_helper.send(
            success=1,
            water_temperature_c=water_heater.water_tmp
        )

    except Exception as e:
        print(e)
        http_helper.send(success=0, error=e)

# ... пропущен код

server.add_route("/healthcheck", healthcheck)
server.add_route("/get_water_tmp", get_water_temperature_C)

# ... пропущен код

heater.py


import machine
import onewire
import ds18x20

HEATER_PIN = 2

class Heater:
    def __init__(self):
        self.water_tmp = 0.00
        self.heater_pin = machine.Pin(HEATER_PIN)
        self.sensor = ds18x20.DS18X20(onewire.OneWire(self.heater_pin))

    def get_water_tmp_C(self):
        rows = self.sensor.scan()

        self.sensor.convert_temp()

        for rom in rows:
            self.water_tmp = self.sensor.read_temp(rom)

        return self.water_tmp

LED я ждал больше всего и хотелось поскорей уже их прикрутить к проекту и все проверить. Основу для размещения я взял достаточно большой корпус от видеокарты, с алюминиевым радиатором. По моим расчетам туда встанут 5 матриц:



  • белый 2 штуки
  • зеленый 1 штука
  • красный 1 штука
  • синий 1 штука

Все они питались от 32 вольт, взял блок питания для термотринтеров (24V) и повышающий преобразователь. Посоветовавшись со знающим другом, принял решение подключать все их через микросхему, которая называется в простонародье "составной транзистор Дарлингтона", понравилось, что все на одной небольшой микросхеме и не нужно париться с радиаторами охлаждения, если бы я выбрал биполярные транзисторы. Все приехало, все спаял и прикрутил к радиатору. Светодиоды не стал промазывать термо-пастой, так как знал, что на все 100% они не будут гореть и тепло будет хорошо отводится и так. Получилось все конечно не на продажу, но в целом меня внешний вид пока устраивал.


Написал класс для них, и добавил роуты для работы через API.


Прицепил еще LCD дисплей, что бы видеть на каком IP стартанул сервер, поскольку на тот момент еще не было реализовано статичный ip для этого. Ну и после этого пошел писать frontend для того, что бы можно было с любого устройства смотреть инфу и управлять аквариумом.



Эпилог


В целом получил хороший опыт в разработке IOT устройства, работа над которым еще не завершена и будет продолжаться. В процессе пришлось немного подкорректировать работу библиотеки MicroPyServer, а именно объем передаваемых данных в сокете. Поскольку я пришел к тому, что при нескольких запросах в течении небольшого промежутка, память просто заканчивалась и контроллер падал с ошибкой, пока не придумал как ее можно отловить в try except, но это надо будет сделать.


micropyserver.py


# ... пропущенный код

def _get_request(self):
        """ Return request body """
        # было выставлено 4096 , при этом быстро возникает MemoryError
        return str(self._connect.recv(1024), "utf8")

В планах:


  • Автоматизировать работу с освещение, что бы он сам проверял саму освещенность в комнате + создавал эффект заката и рассвета, а ночью — лунный свет.
  • Автоматизировать анализ воды
  • Автоматизировать нагрев воды
  • Автоматизировать помпу течения
  • Автокормушка
  • Автодолив