How I did a smart aquarium (frontend)

Prologue





As I described here , I started building a smart aquarium based on the board NodeMCU. On it, I used the firmware from micropython, set up a web server and made an API for manipulating all peripheral devices and sensors. Since my version of the smart aquarium was originally planned as a stand-alone one, I wanted to make some one UIto track all the processes and for manual adjustments. Each time, use routes of the type: http://192.168.1.70/led_controller?impulse=4000&level=200&ledName=whiteit was very dreary and inconvenient. Especially when you have already gone to bed and only your phone is at hand. And again, I wanted to get levelup in development and do something fun.



UI Vue.js. , .. " " WI-FI . , . , , . , , SPA(" ": "single page application"), , . backend — LED- . , vue-cli:



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




, :



  • vue-bootstrap — .
  • axiosbackend 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>


Then I thought about how to organize the business logic itself and separate it from the template. I decided to try it completely through the VuexVyuks itself did not split, but did everything in one file. For the LED level, I use the from scale 0 - 100 %, while the backendlight level itself is set from 0 - 1024units. Having rounded up, I thought that I would simply multiply by 10 when the data is sent by a POST request, or divide by 10 when the data is received by a GET request.



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;
        },
    }
})




Next, I make a universal component where the current value will be displayed, a scale for changing the value and a couple of buttons for synchronizing and actually changing.

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 "", .






All Articles