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