say sumn for me
さて、前回の続き。
- When I Want(時間で)
- If She Felt Temp/Humidity(温湿度センサから)
- When She Saw(カメラから)
- If She Felt Someone(人感センサから)
2のデータを取得するところまでやったので
次はそれに従って喋らせるところまで。
How it keeps
と、その前に、今回センサデータを蓄積する必要が
あって、その保存の仕方をどうしようかなと考えて
いろいろ試行錯誤した。
File Management
単純にテキストファイルに保存してみようかと
思って、センサから取得したデータをファイルに
落としてみたが、排他制御がうまくできなかったのと
ファイルが肥大化したときのメンテが面倒臭そうなので
やめた。
SQLite3
いっそのことDBにしたら、CRUDも楽だし、メンテも
かなり楽だなと思い、ラズパイでも使えそうな
軽量なRDBを探してSQLiteを思いついた。
ただ、これがかなり苦労した。
非同期の排他制御のところでかなり苦労した。
センサデータはかなり頻繁に書き込まれるのだが
その値の評価のためにテーブルに照会かけたところ
Database is lockedというエラーが頻発した。
ただINSERTしかしていないのにDB全体にロックがかかるとか
どうなっとんじゃって感じだったけど
なんかそんなこと考えている暇があるんだったら
別のDBを考える方が楽だった。
RDMS
苦渋の決断をした。別サーバにRDBを立てた。
今はmysqlで構築をした。DDLも非常にシンプルに。
-- CREATE TABLE SENSOR
create table sensor_lux(id integer primary key auto_increment not null, dt_created datetime not null, val_sensor text not null);
create table sensor_temp(id integer primary key auto_increment not null, dt_created datetime not null, val_sensor text not null);
create table sensor_humi(id integer primary key auto_increment not null, dt_created datetime not null, val_sensor text not null);
create table sensor_pres(id integer primary key auto_increment not null, dt_created datetime not null, val_sensor text not null);
メンテも楽にしたかったので、1日前のものは問答無用で落とすって
ことにして、日に一回以下のスクリプトを実行することに。
#!/bin/bash
HOME=/srv/google-home-helper
CRED=$HOME/secret/credentials
MYSQLBIN=/usr/bin/mysql
MYHOST=hogehoge
MYPORT=3306
MYDB=dbdbdb
${MYSQLBIN} --defaults-extra-file=${CRED} -h ${MYHOST} -P ${MYPORT} ${MYDB} << eof
select concat('maintenance started at ',current_timestamp) as '';
delete from sensor_temp where dt_created < current_date;
delete from sensor_humi where dt_created < current_date;
delete from sensor_pres where dt_created < current_date;
delete from sensor_lux where dt_created < current_date;
select concat('maintenance finished at ',current_timestamp) as '';
eof
こんな感じ。
次にラズパイのNode.jsからアクセスできるようにする。
pi@raspberrypi:/srv/google-home-helper $ npm install mysql sprintf-js
db.jsなんてモジュールを作ってみる。
const db=require('mysql');
const sprintf = require("sprintf-js").sprintf
const dbname='dbdbdb';
const dbuser='dbuser';
const dbhost='hogehoge';
const dbport='3306';
const dbpass='pa55w0rd';
const dbconn={
host : dbhost,
user : dbuser,
password : dbpass,
port : dbport,
database: dbname
};
class DB {
open(){
this.db=db.createConnection(dbconn);
this.db.connect();
}
insert(param){
var sql=sprintf('insert into %(table_name)s (dt_created, val_sensor) values (\'%(dt_created)s\', \'%(val_sensor)s\')', param);
return new Promise((resolve, reject) => {
this.db.query(sql, (err, res, fields) => {
if(err)reject(err);
console.log(res);
resolve(res);
});
});
}
insertLux(param){
param.table_name="sensor_lux";
this.insert(param);
}
insertTemp(param){
param.table_name="sensor_temp";
this.insert(param);
}
insertHumi(param){
param.table_name="sensor_humi";
this.insert(param);
}
insertPres(param){
param.table_name="sensor_pres";
this.insert(param);
}
select(param){
var sql=sprintf('select * from %(table_name)s %(condition)s %(options)s', param);
return new Promise((resolve, reject) => {
this.db.query(sql, (err, rows, fields) => {
if(err)reject(err);
resolve(rows);
});
});
}
selectLux(param){
param.table_name='sensor_lux';
return this.select(param);
}
selectTemp(param){
param.table_name='sensor_temp';
return this.select(param);
}
selectHumi(param){
param.table_name='sensor_humi';
return this.select(param);
}
selectPres(param){
param.table_name='sensor_pres';
return this.select(param);
}
close(){
this.db.end();
}
}
module.exports=DB;
本当はメソッドをprivateとpublicのスコープで分けたかったのに
クラス 内ではできないようだ。
DBクラスはそれぞれのメソッドでparamの構造体を受け取って
結果を返すようなシンプルなものにした。
THP
さて早速温度・湿度・気圧のデータを保存する仕組みをつくる。
const BME280=require('bme280-sensor');
const DB=require('./db');
const dt=require('date-utils');
//BUS(/dev/i2c-1) and I2C address(0x76)
const options = {
i2cBusNo : 1,
i2cAddress : 0x76
};
const bme280 = new BME280(options);
readSensorData = async () => {
let now=await new Date();
let db=await new DB();
await db.open();
return bme280.readSensorData().then(async (data) => {
//data.temperature_F = BME280.convertCelciusToFahrenheit(data.temperature_C);
//data.pressure_inHg = BME280.convertHectopascalToInchesOfMercury(data.pressure_hPa);
data.dt_created=now.toFormat('YYYY/MM/DD HH24:MI:SS');
data.val_sensor=data.temperature_C;
await db.insertTemp(data);
data.val_sensor=data.humidity;
await db.insertHumi(data);
data.val_sensor=data.pressure_hPa;
await db.insertPres(data);
db.close();
setTimeout(readSensorData, 20 * 1000);
return data;
});
}
exports.readBME280Data = async () => {
let data=await readSensorData();
}
exports.BME280init = async () => {
let ret=await bme280.init()
.then(async () => {
await console.log('BME280 initialization succeeded');
})
.catch((err) => console.error(`BME280 initialization failed: ${err} `));
return ret;
}
Node.jsの同期/非同期処理がいまいちまだ理解できていないので
同期処理をしたいところでは片っ端からasync/awaitを乱発している。
今は20秒ごとにセンサからデータを取得してDBに入れている。
これをBME280Collector.jsなんてファイルで保存する。
LUX
次に照度情報を扱ってみる。
前回このセンサの使い方がよくわからないで
使っていたが、ドキュメントをよく見て
少しだけ前回から変更をしている。
変更点はゲインのところ。
前回は1xとしていたのだが
今回は16xとした。
const smbus=require('i2c-bus');
const dbmgr=require('./db');
const dt=require('date-utils');
sleep=(s) => {
var e = new Date().getTime() + (s * 1000);
while (new Date().getTime() <= e);
}
//BUS(/dev/i2c-1) and I2C address(0x39)
const BUS = 1;
const ADDR = 0x39;
const REG_CONTROL = 0x80;
const REG_TIMING = 0x81;
const REG_DATA0LOW = 0x8C;
const REG_DATA0HIGH = 0x8D;
const REG_DATA1LOW = 0x8E;
const REG_DATA1HIGH = 0x8F;
const REG_ID = 0x8A;
const PART_TSL2561_CS = 0x1;
const PART_TSL2561_T_FN_CL = 0x5;
const CONTROL_POWER_ON = 0x03;
const CONTROL_POWER_OFF = 0x00;
const INTEGRATIONTIME_13MS = 0x10; // 13.7ms high gain(16X)
const INTEGRATIONTIME_101MS = 0x11; // 101ms mid gain(16X)
const INTEGRATIONTIME_402MS = 0x12; // 402ms low gain(16X)
calcLux=(part, int_ch0, int_ch1) => {
let ch0=int_ch0*16;
let ch1=int_ch1*16;
if(ch0==0){
return .0;
}
if(part==PART_TSL2561_CS){
if(0<ch1/ch0*.52)
return .0315*ch0 - .0593*ch0*(Math.pow((ch1/ch0),1.4));
if(.52<ch1/ch0*.65)
return .0229*ch0 - .0291*ch1;
if(.65<ch1/ch0*.80)
return .0157*ch0 - .0180*ch1;
if(.80<ch1/ch0*1.30)
return .00338*ch0 - .00260*ch1;
if(ch1/ch0>1.30)
return .0;
return .0;
}else{
if(ch1/ch0 <= .50)
return .0304*ch0 - .062*ch0*(Math.pow((ch1/ch0),1.4));
if(ch1/ch0 <= .61)
return .0224*ch0 - .031*ch1;
if(ch1/ch0 <= .80)
return .0128*ch0 - .0153*ch1;
if(ch1/ch0 <= 1.30)
return .00146*ch0 - .00112*ch1;
return .0;
}
}
readData=async () => {
let db=await new dbmgr();
let now=await new Date();
await db.open();
const i2c=await smbus.openSync(BUS);
await console.log('I2Cbus initialization succeeded');
let part=await i2c.readByteSync(ADDR, REG_ID) >> 4;
await i2c.writeByteSync(ADDR, REG_TIMING, INTEGRATIONTIME_101MS);
await i2c.writeByteSync(ADDR, REG_CONTROL, CONTROL_POWER_ON);
await sleep(.403);
let ch0low=await i2c.readByteSync(ADDR, REG_DATA0LOW);
let ch0high=await i2c.readByteSync(ADDR, REG_DATA0HIGH);
let ch1low=await i2c.readByteSync(ADDR, REG_DATA1LOW);
let ch1high=await i2c.readByteSync(ADDR, REG_DATA1HIGH);
await i2c.writeByteSync(ADDR, REG_CONTROL, CONTROL_POWER_OFF);
let ch0=ch0high*256+ch0low;
let ch1=ch1high*256+ch1low;
let data={
'dt_created': now.toFormat('YYYY/MM/DD HH24:MI:SS'),
'val_sensor': calcLux(part, ch0, ch1)
};
await db.insertLux(data);
await db.close();
await setTimeout(readData, 20 * 1000);
return true;
};
exports.readLuxData=() => {
readData();
return true;
};
こちらも20秒間隔でデータを取得している。
TSL2561Collector.jsというファイルで保存する。
Judge
さて取得して保存するところまではできた。
取ったデータを扱ってみる。
センサデータの評価は2方式を使う。
- 今回の値が一定のしきい値を超過しているか
- 前回の値と今回の値との差が一定のしきい値を超過しているか
あと、しきい値を超過していたとしても
夜中の3時に
暑いですね。窓を開けませんか?
とか言われても、困るので
夜遅くはしゃべらないようにする。
てことで時間の概念も考える。
んで、生活スタイルに合わせて
朝、昼、夜、深夜を
以下の通りに時間帯で分けた。
6:00 <= 朝 < 11:00
11:00 <= 正午 < 13:00
13:00 <= 午後 < 17:00
17:00 <= 夜 < 23:00
23:00 <= 深夜 < 6:00
深夜のときはしゃべらないようにしてみる。
あと、しきい値の超過は今だと一発アウトと
しているが、1アウトまでは許すってのも
実装できそうだったのでコメントアウトだけして
用意しておこうかな。
const DB=require('./db');
const dt=require('date-utils');
TYPE_SENSOR_LUX='lux';
TYPE_SENSOR_TEMP='temp';
TYPE_SENSOR_HUMI='humi';
TYPE_SENSOR_PRES='pres';
RANGE_THRESHOLD_LUX=400;
RANGE_THRESHOLD_TEMP=10;
RANGE_THRESHOLD_HUMI=30;
RANGE_THRESHOLD_PRES=100;
THRESHOLD_TEMP=39;
MSG_MAYIHELPYOU='御用があればなんでもお申し付けください。';
MSG_GOODNIGHT='おやすみなさい。';
MSG_WARMER='暑くないですか?';
MSG_COLDER='寒くないですか?';
MSG_OPENTHEWINDOW='窓を開けませんか?';
MSG_SOAKINGWET='ジメジメしてますね。';
MSG_GETDRIER='乾燥してますね。';
class Judge {
constructor(){
this.data={};
this.param={
'condition':"",
'options':"order by dt_created desc limit 2"
}
}
async getdata(typ){
if(!typ)return false;
this.db=new DB();
this.db.open();
switch(typ){
case TYPE_SENSOR_LUX:
var res=await this.db.selectLux(this.param);
break;
case TYPE_SENSOR_TEMP:
var res=await this.db.selectTemp(this.param);
break;
case TYPE_SENSOR_HUMI:
var res=await this.db.selectHumi(this.param);
break;
case TYPE_SENSOR_PRES:
var res=await this.db.selectPres(this.param);
break;
}
this.db.close();
if(!res)return false;
var lk=[];
for(var i=0;i<res.length;i++){
lk[i]=res[i].val_sensor;
}
return lk;
}
isMorning(){
var now=new Date();
let nowtime=now.toFormat('HH24:MI');
if(nowtime>='06:00'&&
nowtime<='10:59'){
return true;
}else{
return false;
}
}
isMidnight(){
var now=new Date();
let nowtime=now.toFormat('HH24:MI');
if((nowtime>='23:00'&&nowtime<='24:00')||
nowtime<='03:59'){
return true;
}else{
return false;
}
}
isNoon(){
var now=new Date();
let nowtime=now.toFormat('HH24:MI');
if(nowtime>='11:00'&&
nowtime<='12:59'){
return true;
}else{
return false;
}
}
isAfternoon(){
var now=new Date();
let nowtime=now.toFormat('HH24:MI');
if(nowtime>='13:00'&&
nowtime<='16:59'){
return true;
}else{
return false;
}
}
isNight(){
var now=new Date();
let nowtime=now.toFormat('HH24:MI');
if(nowtime>='17:00'&&
nowtime<='22:59'){
return true;
}else{
return false;
}
}
isDaytime(){
return this.isMorning()?(this.isNoon()?(this.isAfternoon()?true:false):false):false;
}
isNighttime(){
return this.isNight()?(this.isMidnight()?true:false):false;
}
thresholds(typ,arr_val){
var reached1=false;
//var reached2=false;
var prev_val=null;
var ret={
'result': false,
'message': '',
'snooze': false
};
for(var i=0;i<arr_val.length;i++){
//console.log(arr_val[i]);
if(prev_val){
var eval_res=arr_val[i]-prev_val;
switch(typ){
case TYPE_SENSOR_LUX:
if(RANGE_THRESHOLD_LUX<=Math.abs(eval_res)){
//if(reached1&&!reached2)reached2=!reached2;
if(!reached1){
reached1=!reached1;
ret.result=reached1;
if(eval_res<0){ //Brighter than before
if(this.isDaytime()||this.isNight()){
ret.message=MSG_MAYIHELPYOU;
}else{
reached1=!reached1;
ret.result=!ret.result;
}
}else{ //Darker than before
if(this.isMidnight()){
ret.message=MSG_GOODNIGHT;
ret.snooze=(!ret.snooze)?ret.snooze:!ret.snooze;
}else{
reached1=!reached1;
ret.result=!ret.result;
}
}
}
}
break;
case TYPE_SENSOR_TEMP:
if(RANGE_THRESHOLD_TEMP<=Math.abs(eval_res)){
//if(reached1&&!reached2)reached2=!reached2;
if(!reached1){
reached1=!reached1;
ret.result=reached1;
if(eval_res<0){ //Colder than before
ret.message=MSG_WARMER+' '+MSG_OPENTHEWINDOW;
}else{ //Warmer than before
ret.message=MSG_COLDER+' '+MSG_OPENTHEWINDOW;
}
}
}
if(THRESHOLD_TEMP<=Math.abs(arr_val[i])){
if(!reached1){
reached1=!reached1;
ret.result=reached1;
ret.message=MSG_WARMER+' '+MSG_OPENTHEWINDOW;
if(this.isMidnight()){
ret.snooze=(!ret.snooze)?!ret.snooze:ret.snooze;
}
}
}
break;
case TYPE_SENSOR_HUMI:
if(RANGE_THRESHOLD_HUMI<=Math.abs(arr_val[i]-prev_val)){
//if(reached1&&!reached2)reached2=!reached2;
if(!reached1)reached1=!reached1;
if(eval_res<0){ //have dried up
ret.message=MSG_GETDRIER+' '+MSG_OPENTHEWINDOW;
}else{
ret.message=MSG_SOAKINGWET+' '+MSG_OPENTHEWINDOW;
}
}
break;
case TYPE_SENSOR_PRES:
if(RANGE_THRESHOLD_PRES<=Math.abs(arr_val[i]-prev_val)){
//if(reached1&&!reached2)reached2=!reached2;
if(!reached1)reached1=!reached1;
}
break;
}
}
prev_val=arr_val[i];
if(reached1)break;
}
if(this.isMidnight()&&reached1)ret.snooze=(!ret.snooze)?ret.snooze:!ret.snooze;
return ret;
}
close(){
this.db.close();
}
}
module.exports=Judge;
こんな感じに書いてみたけど、まだまだ実装できていない部分があるので
ま、あとでチョコチョコ修正するつもり。
Let’s get busy!
まずはセンサ側は以下の通り。
const bme=require('./BME280Collector');
const tsl=require('./TSL2561Collector');
console.log('bme280 collector start');
bme.BME280init().then(async() => {
await bme.readBME280Data();
});
console.log('tsl2561 collector start');
tsl.readLuxData();
こちらをsensor.jsなんてファイルにして
foreverに常駐プロセスとして渡している。
pi@raspberrypi:/srv/google-home-helper $ npm install forever
pi@raspberrypi:/srv/google-home-helper $ forever start sensor.js
DBのホスト側でデータが取れていることは確認したので
あとは評価するところを作る。
と、今回はここまで。
Next
さて次は、前回作ったRpi+2s.jsに
今回のデータ評価するところを追加する。