Пролог



Как я рассказывал тут, я начал постройку умного аквариума на основе платы 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


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


  • 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);

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



App.vue


<template>
  <div id="app">
    <b-navbar type="dark" variant="primary" class="rounded">
      <b-navbar-brand tag="h1" class="mb-0">Fish Tank</b-navbar-brand>
      <b-icon 
        icon="brightness-alt-high" 
        font-scale="3" 
        variant="light" 
        class="rounded bg-primary p-1"
      />
    </b-navbar>
    <list-of-range-controllers/>
  </div>
</template>

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

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

<style scoped>
  #app {
      margin: 50px 20px;
  }
</style>

Далее думал как организовать саму бизнес логику и отделить ее от шаблона. Решил попробовать полностью через 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


<template>
    <b-card 
        :title="header" 
    >
        <b-alert show>
            Change to : {{ controllerValue }} 
                        {{
                            name.match(/Water/gi) 
                            ? 'C\u00B0' : '%'
                        }}
        </b-alert>
        <b-form-input 
            type="range"
            :min="min"
            :max="max"
            v-model="controllerValue"
        />
        <b-button 
            variant="outline-primary" 
            size="sm"
            @click="$emit(`${buttonChangeName}Change`, controllerValue)"
        >
            {{ changeButton }}
        </b-button>
        <b-button 
            class="float-right"
            variant="outline-success" 
            size="sm"
            @click="$emit(`${buttonChangeName}Sync`)"
        >
            Sync value
        </b-button>
    </b-card>
</template>

<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


<template>
    <b-container class="bv-example-row mt-4 mb-4">
      <h1>Backlight</h1>
      <b-row>
        <b-col v-for="led in leds" :key="led.name">
          <range-controller
            :name="led.name"
            :value="led.value"
            :buttonChangeName="led.buttonName"
            v-on="{ 
              ledWhiteChange : ledWhiteChange,
              ledWhiteSync   : ledWhiteSync,
            }"
          />
        </b-col>

      </b-row>
      <h1>Temperature</h1>
      <b-row>
        <b-col>
          <range-controller
              name="Water"
              :value="waterTemperature"
              :min="20"
              :max="45"
              buttonChangeName="temperature"
              @temperatureChange="temperatureChange"
              @temperatureSync="temperatureSync"
          />
        </b-col>
      </b-row>

    </b-container>
</template>

<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>

На компе



На мобилке



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


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

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