1 この記事は
リモコンがないサーキュレータを、100V AC電源を直接ON/OFFにする動作を決まった時間にスケジュール実行したり、好きな時間にスマホから遠隔操作することで、利便性を上げたいと思った。
2 必要な部材
下記記載の部材をそろえてください。
・ソリッド・ステート・リレー(SSR)キット(S108T02)
・Raspberry Pi zero WH
・Raspberry Pi4(可能であれば)
・配線
ソリッド・ステート・リレー(SSR)キット(S108T02)を説明書に従い組み立ててください。かつ下記の写真のようにケースに収納が必須と思います。
Raspberry Pi zero WHは、ケースやUSB電源等、必要に応じで準備してください。
3 どうやって行うの?
3-1 まずはRaspberry Pi zeroの初期設定を行います。
micro SDカードにOSを書き込む作業やwifi設定、SSH設定等が必要ですが、多くの記事がWeb上にありますので、説明は省略します。ワンポイントコメントとして、Raspberry Piに公式のイメージ書き込みツール「Raspberry Pi Imager」はとても便利かと思います。
3-2 配線図を検討します。
ソリッド・ステート・リレー(SSR)の左側端子はAC電源に接続し、右側の3つの端子でAC電源を通過させるか、遮断させるかを制御しています。ソリッド・ステート・リレー(SSR)の左側端子へ5V電源の印可が必要ですので、Raspberry Pi zero WHの5V電源端子とGND端子を接続します。また、Raspberry Pi zero WHの任意のGPIOピンをSSRに接続します。Raspberry Pi zero WHのGPIOピンからH信号(3.3V)がSSRに印可されたとき、100V AC電源はソリッド・ステート・リレー(SSR)を導通し、制御したい電化製品に印可されます。一方、Raspberry Pi zero WHのGPIOピンからL信号(0V)がSSRに印可されたとき、100V AC電源はソリッド・ステート・リレー(SSR)にて遮断され、制御したい電化製品には電源は供給されません。Raspi zeroピンアサイン表
3-3 システム図を考えます。
Raspberry Pi4を別用途で使用していた経緯より、Raspberry Pi4がフロントエンドを担い、Raspberry Pi zeroがバックエンドを担うことにしました。フロントエンドであるRaspberry Pi4にはVue-cliを稼働させ、インターフェースを実現します。フロントエンドのWebページにて、「ON」「OFF」がクリックされれば、Raspberry Pi4からRaspberry Pi zeroに信号が伝達され、Raspberry Pi ZeroのGPIOピンの出力電圧をH or Lに設定するpythonコードが動作します。Vue-cliの動作は比較的パワーが必要なのでRaspberry Pi4を割り当て、バックエンドは単にH信号、もしくはL信号をソリッド・ステート・リレー(SSR)に与えるのみで処理が軽いのでRaspberry Pi zeroのアサインが適していると思われます。
ここでは、フロントエンドはRaspberry Pi4に担当させ、バックエンドはRaspberry Pi zeroに担当させましたが、Rapberry Pi zeroしかない場合は、フロントエンド、バックエンドどちらともRaspberry Pi zero上で実現することも可能です。
3-4 コーディング
下記説明は、フロントエンドはRaspberry Pi4に担当させ、バックエンドはRaspberry Pi zeroに担当させる前提で説明を行います。下記のリンク先に記載された情報をもとにRaspberry Pi4にVue-cliをインストールします。またRaspberry Pi zeroにpython-shellのインストールを行います。
Vue-cliをインストールを完了したRaspberry Pi4の第3階層までのディレクトリは下記の図のとおりになっていると思われますので、下記図記載のとおり第4階層に「Kaden.vue」と「index.js」のファイルを作成します。
また、Raspberry Pi zeroにおいても下記の図のとおりに「index.js」「conton.py」「contoff.py」のファイルを準備します。
それぞれのファイルのコードを下記に記載しております。コードをご覧いただき動作を理解いただければここで紹介したシステムを組むことが可能になると思います。
<template>
<div class="page">
<h1>家電制御アプリ</h1>
<h4> サーキュレータ制御 </h4>
<p><input type="button" value="ON" class="btn-gradient-radius" @click="ciron()"></p>
<p><input type="button" value="OFF" class="btn-gradient-radius" @click="ciroff()"></p>
</div>
</template>
<script>
// eslint-disable-next-line
/* eslint-disable */
import Vue from 'vue'
const urlz='192.168.xx.xx' //Raspberry Pi zero(バックエンド)のIPアドレス
export default {
name: 'kaden',
methods:{
//GPIO出力をH信号に設定するpythonコードにアクセスする。
ciron: function(){
this.$axios.get('http://' + urlz +'/on')
.then(function(response){
}.bind(this)) //Promise処理を行う場合は.bind(this)が必要
.catch(function(error){ //バックエンドからエラーが返却された場合に行う処理について
}.bind(this))
},
//GPIO出力をL信号に設定するpythonコードにアクセスする。
ciroff:function(){
//再生したい番組の日にちと時間のデータをバックエンドに転送する。
this.$axios.get('http://' + urlz +'/off')
.then(function(response){
}.bind(this)) //Promise処理を行う場合は.bind(this)が必要
.catch(function(error){ //バックエンドからエラーが返却された場合に行う処理について
}.bind(this))
},
},
data: function(){
return {
}
}
}
</script>
<style>
.page {
width: auto;
max-width: 800px;
margin: 0 auto;
border: 5px solid #ccc;
padding: 1em;
background: white;
border-radius: 1.5em;
}
#overlay{
/* 要素を重ねた時の順番 */
z-index:1;
/* 画面全体を覆う設定 */
position:fixed;
top:0;
left:0;
width:100%;
height:100%;
background-color:rgba(0,0,0,0.2); /* rgb(0,0,0)は黒, a=0.2は透明度 */
/* 画面の中央に要素を表示させる設定 */
display: flex;
align-items: center;
justify-content: center;
}
#content{
z-index:2;
width:66%; /* overlay表示時の白画面が占める大きさ */
padding: 1em;
background:#fff;
}
h4 {
background: #b0dcfa; /*背景色*/
padding: 0.5em;/*文字周りの余白*/
color: white;/*文字を白に*/
border-radius: 0.5em;/*角の丸み*/
}
/* ボタン */
.btn-gradient-radius {
display: inline-block;
padding: 7px 20px;
border-radius: 25px;
text-decoration: none;
color: #FFF;
background-image: linear-gradient(45deg, #FFC107 0%, #ff8b5f 100%);
transition: .4s;
}
.btn-gradient-radius:hover {
background-image: linear-gradient(45deg, #FFC107 0%, #f76a35 100%);
}
</style>
フロントエンドにアクセスを許可するポートを設定する。
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')
module.exports = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
},
dev: {
env: require('./dev.env'),
host:'0.0.0.0',
port: 91,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false
}
}
// eslint-disable-next-line
/* eslint-disable */
import Vue from 'vue'
import Router from 'vue-router'
import Kaden from '@/components/Kaden'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/kaden/',
name: 'kaden',
component: Kaden
},
]
})
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
// eslint-disable-next-line
/* eslint-disable */
import Kaden from '/home/techan-vue/src/components/Kaden.vue'
export default {
name: 'app',
components:{
Top
},
data() {
return {
mes: 'Hello Vue3!!'
}
}
}
</script>
<style>
# app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
// eslint-disable-next-line
/* eslint-disable */
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import axios from 'axios'
import store from './store'
Vue.config.productionTip = false
Vue.prototype.$axios = axios
//export const eventBus = new Vue();
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
template: '<App/>',
components: { App },
})
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json())
//CORSポリシーを無効にしている。
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(3000, () => {
console.log(`Example app listening at http://localhost:3000`)
})
//サーキュレータON
app.get('/on', function(req, res) {
var {PythonShell} = require('python-shell');
var pyshell = new PythonShell('/home/pi/cirq/code/conton.py');
//pythonコード実施後にpythonから本コードにデータが引き渡される。
pyshell.on('message', function (data) {
res.send({
message: "OK" //pythonで実施した演算結果をフロントエンドに返している。
})
})
})
//サーキュレータOFF
app.get('/off', function(req, res) {
var {PythonShell} = require('python-shell');
var pyshell = new PythonShell('/home/pi/cirq/code/contoff.py');
//pythonコード実施後にpythonから本コードにデータが引き渡される。
pyshell.on('message', function (data) {
res.send({
message: "OK" //pythonで実施した演算結果をフロントエンドに返している。
})
})
})
# GPIO26の出力電圧をHにする。
import RPi.GPIO as GPIO
import time
PNO = 26 # 抵抗に繋いだ側の、GPIOポート番号
GPIO.setmode(GPIO.BCM)
GPIO.setup(PNO, GPIO.OUT)
GPIO.output(PNO, GPIO.HIGH) # 点灯
# GPIO26の出力電圧をLにする。
import RPi.GPIO as GPIO
import time
PNO = 26 # 抵抗に繋いだ側の、GPIOポート番号
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(PNO, GPIO.OUT)
GPIO.output(PNO, GPIO.LOW) # 消灯
GPIO.cleanup()