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

от автора

Пролог

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

ссылка на оригинал статьи https://habr.com/ru/post/520878/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *