Темы



Как я умный аквариум делал (frontend)

Пролог

d4c9e7fe6d3eda87c2ae0c8275cf62b8.png

Как я рассказывал тут, я начал постройку умного аквариума на основе платы NodeMCU. На ней я использовал прошивку с micropython, поднял веб сервер и сделал API для манипуляции всеми периферийными устройствами и датчиками. Поскольку мой вариант умного аквариума изначально планировался как автономный, я хотел сделать некий UI для отслеживания всех процессов ну и для ручных корректировок. Каждый раз обращаться по роутам типа: http://192.168.1.70/led_controller?impulse=4000&level=200&ledName=white было очень муторно и неудобно. Особенно когда ты уже лег спать и под рукой только телефон. Да и опять же, хотелось получить levelup в разработке и сделать что-то увлекательное.

За основу UI взял Vue.js. Авторизация как таковая не нужна, т.к. мой «умный друг» был только локально в пределах моего WI-FI окружения. Да и если бы его взломали, ничего страшного не случилось. Другое дело когда я буду делать умный дом, там уже безопасность на первом месте, но сейчас не об этом. Итак, никакой авторизации, только SPA («Одностраничное приложение»: «single page application»), никакого роутинга, все показатели и манипуляторы на одной странице. Из того что было сделано на backend — контроль за LED-матрицами и температурный датчик. Создаем новый проект на гите, делаем клон на рабочем месте и запускаем vue-cli:

$ vue ui
  Starting GUI...
  Ready on http://localhost:8000

sstxao0ik295zlqzyugb9szke6q.png

Создаем новый проект, добавляем туда все необходимые плагины:


  • vue-bootstrap — сам себе дизайнер.
  • axios — для работы с backend по API.
  • vuex — для отделения бизнес логики

Для axios настроил базовый url

plugin/axios.js

import Vue from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';

axios.defaults.baseURL = 'http://192.168.1.70';

Vue.use(VueAxios, axios);

Дизайн и выбор цветовых решений — дело сугубо индивидуальное, выбор пал на синий цвет шапки, потому, что аквариум и в целом вода у меня лично ассоциируется с этим цветом. И так накидал такую вот шапку и подключил основной компонент в котором будет список всех ручек и крутилок.

tfw1c8xjnetdd1bc-ugmsuuchli.png

App.vue



<script>
import ListOfRangeControllers from './components/ListOfRangeControllers';

export default {
    name: 'App',
    components: {
        ListOfRangeControllers
    }
}
</script>

Далее думал как организовать саму бизнес логику и отделить ее от шаблона. Решил попробовать полностью через Vuex Сам вьюкс не стал дробить, а сделал все в одном файлике. Для уровня LED я использую шкалу от 0 - 100 %, в то время когда на backend сам уровень света устанавливается от 0 - 1024 единиц. Округлив я подумал, что буду просто умножать на 10, когда данные будут уходить POST запросом или делить на 10, когда данные будут приходить GET запросом.

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        whiteLED         : 0,
        waterTemperature : 0,
    },

    mutations: {
        'SYNC_WHITE_LED' (state, level) {
            state.whiteLED = level;
        },
        'SYNC_WATER_TEMPERATURE' (state, level) {
            state.waterTemperature = level;
        },
        'SET_WHITE_LED' (state, level) {
            state.whiteLED = level;
        },
        'SET_HEATER_LEVEL' (state, level) {
            state.waterTemperature = level;
        }
    },

    actions: {
        async syncWhiteLED({commit}) {
            try {
                const response = await Vue.axios.get('/get_led_info?ledName=white');
                commit('SYNC_WHITE_LED', response.data['level']/10);
            }
            catch(error) {
                console.error(error);
            }
        },
        async syncWaterTemperature({commit}) {
            try {
                const response = await Vue.axios.get('/get_water_tmp');
                commit('SYNC_WATER_TEMPERATURE', response.data['water_temperature_c']);
            }
            catch(error) {
                console.error(error);
            }
        },
        async setWhiteLED({commit}, level) {
            try {
                await Vue.axios.get(`/led_controller?impulse=4000&level=${level*10}&ledName=white`);
                commit('SET_WHITE_LED', level);
            }
            catch(error) {
                console.error(error);
            }
        },
        async setWaterTemperature({commit}, level) {
            try {
                await Vue.axios.get(`/heater_control?params=${level}`);
                commit('SET_HEATER_LEVEL', level);
            }
            catch(error) {
                console.error(error);
            }
        },
    },

    getters: {
        whiteLED: state => {
            return state.whiteLED;
        },
        waterTemperature: state => {
          return state.waterTemperature;
        },
    }
})

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

components/ui/RangeController.vue



<script>
export default {
    props: {
        name: {
            type    : String,
            default : 'Header',
        },
        value: {
            type    : Number,
            default : 0,
        },
        buttonChangeName: {
            type    : String,
            default : 'Change'
        },
        min: {
            type    : Number,
            default : 0
        },
        max: {
            type    : Number,
            default : 100
        }
    },
    data() {
        return {
            controllerValue: this.min,
        }
    },
    computed: {
        header() {
            const isWater = this.name.match(/Water/gi);
            const postfix = isWater ? 'C\u00B0' : '%';

            const sufix = isWater ? 'Temperature' : this.name.match(/Pump/gi)? '' : 'LED';

            return `${this.name} ${sufix} is : ${this.value} ${postfix}`;
        },
        changeButton() {
            return `${this.buttonChangeName} change`;
        },
    }
}
</script>

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

components/ListOfRangeControllers.vue



<script>
import RangeController from './ui/RangeController';
import { mapActions, mapGetters } from 'vuex'

export default {
    components: {
        RangeController
    },

    methods: {
        ...mapActions([
            'syncWhiteLED',
            'syncWaterTemperature',
            'setWhiteLED',
        ]),

        ledWhiteChange(value) {
            this.setWhiteLED(value);
        },

        // не реализовано
        temperatureChange(value) {
            console.log('temp is changed!' + `${value}`);
        },

        ledWhiteSync() {
            this.syncWhiteLED();
        },
        async temperatureSync() {
            await this.syncWaterTemperature();

            console.log(this.waterTemperature);
        },
    },

    computed: {
        ...mapGetters([
            'waterTemperature',
            'whiteLED',
        ]),

        leds() {
            return [
                {
                    name: 'White',
                    value: this.$store.getters.whiteLED,
                    buttonName: 'ledWhite',
                },
            ]
        },
    },
}
</script>

На компе

7hga9gf-blybyrh5iw4mmfxnqoe.png

На мобилке

xxggxdvtahde7sakbi4rm2czs4a.jpeg

Вот я и получил UI для моего умного аквариума, где я мог получить информацию об освещенности и температуре, и в ручном режиме выставить нужный свет и его интенсивность. Пришло время все это запустить вместе, повесить над аквариумом и проверить. Vue приложение запустил на старом ноуте, лег на кровать и открыл браузер на телефоне… чтож верстка немного поехала на небольшом экране, но меня вполне устраивала, я знал, что все это еще будет переделываться и автоматизироваться. Но это была рабочая связка моего устройства на NodeMCU и Vue приложения. Я был рад и горд собой. В голове летали мысли о том, что же будет в конечном итоге, самое страшное для меня было реализация химического анализа воды. Ведь хороший анализ делается путем опускания в воду бумажных палочек, пропитанных определенным химическим составом. От чего она меняет цвет и уже по карте цветов можно определить есть ли каки либо отклонения от нормы. А анализ нужен не один, а именно, анализы на:


  • Аммоний
  • Нитриты
  • Нитраты
  • Фосфаты
  • Кислотно-щелочной баланс (Ph)
  • Карбонатная жесткость (kH)
  • Кальций
  • Магний
  • Силикаты

Пока нахожусь в поиске каких-то решений, поскольку натыкался в магазинах на электронные приборы, которые все это измеряют. Муляж ли это? кто знает. Кто ищет — то найдет. Предстоит еще много работы как на стороне моей NodeMCU так и на стороне «Клиента», но я не опускаю рук.

x8tkxjxaiqd7gl_ktaznmx2lrhs.png

© Habrahabr.ru