#夏ですね
連日暑い日が続きますね。
できればクーラーの効いた部屋の中でじっとしてやり過ごしたいものですが、
そういう訳にもいかず何かしら用をたしに外にでなければならない。
夏場はほんと外を歩くのが億劫になります。
#というわけで
少しでもこの暑い中出歩くしんどさを紛らわしてくれるものが作れないかという思いから、
Line thingsを利用した、日陰歩くゲーム?みたいなのを作ってみました。
#どういうやつか
単純です。
デバイスに接続したUVセンサから紫外線量を取得して、
それをUVインデックスという紫外線の強さの指標に変換します。
UVインデックスが「中程度」以上であればLIFF上のキャラクターのライフゲージみたいなのが減ってくみたいなのにしました。
UVインデックスとは紫外線が人体に及ぼす影響の度合いをわかりやすく示すために、紫外線の強さを指標化したもの
らしいです。
5段階になっています。
今回は、「できるだけ日陰を利用しよう」となっている中程度以上でゲージがへる、それ以下でゲージが回復するというかたちにしました。
# こんな感じです
すいません。。退屈ですね。
作ったタイミングで曇りになったので外歩いて試すとかできてないです。
デバイス
M5stack + Grove - I2C UV Sensor (VEML6070)
- M5stackのGroveポートに接続
- Adafruitが出しているライブラリを利用
- 取得した紫外線量をこちらの記事を参考にUVインデックスに変換
Seedの出しているライブラリを使えばUV Indexまで出してくれるのですが、なぜか常に0を返すので諦めて、Adafruitのライブラリで紫外線量を取得、自前でUV INDEXを返すというかたちにしました。
なぜSeedのライブラリだとうまくいかないのか、、ライブラリで設定されている抵抗の値?がM5Stackの抵抗の値と違うからなのか。。?integration timeって何ですか。。?知識不足でわからず。。
データシート読めたら分かるのかもしれないけどさっぱりです。
実装
長いので一部のみ載せます。
全文はこちら、
M5Stack側(一部)
//省略
bool deviceConnected = false;
bool oldDeviceConnected = false;
bool scanUV = false;
Adafruit_VEML6070 uv = Adafruit_VEML6070();
class serverCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};
class writeCallback: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *bleWriteCharacteristic) {
std::string value = bleWriteCharacteristic->getValue();
if ((char)value[0] == 1) {
//省略
scanUV = true;
} else if((char)value[0] == 0) {
//省略
scanUV = false;
}
}
};
void setup(){
//省略
// UV センサー初期化
uv.begin(VEML6070_1_T);
}
int getIndex(int uv_data){
int UVindex;
if (uv_data <= 560) {UVindex = 1; //Low
}else if (uv_data >= 561 && uv_data <= 1120) {UVindex = 2; //Moderate
}else if (uv_data >= 1121 && uv_data <= 1494) {UVindex = 3; //High
}else if (uv_data >= 1495 && uv_data <= 2054) {UVindex = 4; //Very High
}else if (uv_data >= 2055 ) {UVindex = 5; //Extreme
}
return UVindex;
}
void loop() {
// Disconnection
if (!deviceConnected && oldDeviceConnected) {
delay(500); // Wait for BLE Stack to be ready
thingsServer->startAdvertising(); // Restart advertising
oldDeviceConnected = deviceConnected;
M5.Lcd.clear(BLACK);
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(65, 10);
M5.Lcd.println("Ready to Connect");
}
// Connection
if (deviceConnected && !oldDeviceConnected) {
oldDeviceConnected = deviceConnected;
M5.Lcd.clear(BLACK);
M5.Lcd.setTextColor(GREEN);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(100, 10);
M5.Lcd.println("Connected");
}
if (deviceConnected && scanUV) {
scanUVStart();
}
delay(500);
}
void scanUVStart() {
int uv_data = uv.readUV();
char index[24];
char s_uv[24];
char result[100];
snprintf(s_uv, 24, "%d", uv_data);
snprintf(index, 24, "%d", getIndex(uv_data));
strcat(result, s_uv);
strcat(result, ",");
strcat(result, index);
notifyCharacteristic->setValue(result);
notifyCharacteristic->notify();
M5.Lcd.clear(WHITE);
M5.Lcd.setTextColor(BLACK);
M5.Lcd.setTextSize(4);
M5.Lcd.setCursor(120, 100);
M5.Lcd.println(result);
}
//省略
Liff側(一部)
const uiToggle = document.getElementById('toggle')
const uvLevelMap = ['弱い', '中程度', '強い', 'とても強い', '危険']
let callCounter = 0
class LifeGage {
constructor() {
this.container = document.getElementById('tab2')
this.bar = document.getElementById('js-life-bar')
this.state = 'default'
this.uvIndex = 0
this.timer = null
this.value = 0
}
setUvIndex(index) {
this.uvIndex = index
}
toggle(isStart) {
if (isStart) {
this.start()
} else {
this.stop()
}
}
start() {
this.loop()
}
stop() {
clearTimeout(this.timer)
}
loop() {
this.timer = setTimeout(() => {
if (this.uvIndex > 1) {
this.decrease()
} else {
this.increase()
}
this.loop()
}, 300)
}
increase() {
if (this.value === 0) return
if (this.value >= 0) {
this.value = 0
} else {
this.value = this.value + 3
}
this.moveBar()
}
decrease() {
if (this.value === -100) return
if (this.value <= -100) {
this.value = -100
} else {
--this.value
}
this.moveBar()
}
moveBar() {
this.bar.style.transform = `translateX(${this.value}%)`
this.switchStateIfNeed()
}
switchStateIfNeed() {
let state
if (this.value >= -50) {
state = 'default'
}
if (this.value < -50) {
state = 'danger'
}
if (this.state !== state) {
const cls = state === 'default' ? 'is-def' : 'is-danger'
const removeCls = this.state === 'default' ? 'is-def' : 'is-danger'
this.container.classList.remove(removeCls)
this.container.classList.add(cls)
}
this.state = state
}
}
class UvLevel {
constructor() {
this.panel = document.getElementById('tab1')
this.levelText = document.getElementById('js-ttl-level')
}
switchState(index) {
const cls = `is-level-${index}`
const levelText = uvLevelMap[--index]
if (this.current) {
this.panel.classList.remove(`is-level-${this.current}`)
}
this.panel.classList.add(cls)
this.levelText.textContent = levelText
this.current = index
}
}
class Tab {
constructor(tabList) {
this.triggers = [...tabList.querySelectorAll('.js-tab-btn')]
this.init()
}
init() {
this.triggers.forEach(trigger => {
const target = document.getElementById(trigger.dataset.tab)
const isActive = trigger.classList.contains('is-active')
trigger.dataset.tab = target
if (isActive) {
this.current = { trigger, target }
}
trigger.addEventListener('click', e => {
if (trigger === this.current.trigger) return
this.toggle(trigger, target)
})
})
}
toggle(trigger, target) {
trigger.classList.add('is-active')
target.classList.add('is-show')
this.deactiveCurrent()
this.current = { trigger, target }
}
deactiveCurrent() {
this.current.trigger.classList.remove('is-active')
this.current.target.classList.remove('is-show')
}
}
// -------------- //
// On window load //
// -------------- //
window.onload = () => {
initializeApp()
}
// -------------- //
// UI functions //
// -------------- //
uiToggle.addEventListener('click', uiToggleHandler)
new Tab(document.getElementById('js-tab-list'))
const lifeGage = new LifeGage()
const uvLevel = new UvLevel()
function makeErrorMsg(errorObj) {
if (typeof errorObj === 'string' || typeof errorObj === 'number')
return errorObj
return errorObj.toString()
}
function uiStatusError(message) {
uiError.innerText = message
}
function uiToggleDeviceConnected(connected) {
if (!connected) {
uiToggleUVScanToggle(false)
}
}
function uiUpdateValues(uvValues) {
const index = Number(uvValues[1])
lifeGage.setUvIndex(index)
uvLevel.switchState(index)
}
function uiToggleUVScanToggle(state) {
const label = state ? 'Stop' : 'Start'
uiToggle.textContent = label
}
function uiToggleHandler(e) {
const isStart = e.target.textContent === 'Start' ? true : false
uiToggleUVScanToggle(isStart)
lifeGage.toggle(isStart)
liffToggleState(isStart)
}
// -------------- //
// LIFF functions //
// -------------- //
function initializeApp() {
liff.init(() => initializeLiff(), error => uiStatusError(makeErrorMsg(error)))
}
function initializeLiff() {
liff
.initPlugins(['bluetooth'])
.then(() => {
liffCheckAvailablityAndDo(() => liffRequestDevice())
})
.catch(error => {
uiStatusError(makeErrorMsg(error))
})
}
function liffCheckAvailablityAndDo(callbackIfAvailable) {
// Check Bluetooth availability
liff.bluetooth
.getAvailability()
.then(isAvailable => {
if (isAvailable) {
uiToggleDeviceConnected(false)
callbackIfAvailable()
} else {
uiStatusError('Bluetooth not available')
setTimeout(() => liffCheckAvailablityAndDo(callbackIfAvailable), 10000)
}
})
.catch(error => {
uiStatusError(makeErrorMsg(error))
})
}
function liffRequestDevice() {
liff.bluetooth
.requestDevice()
.then(device => {
liffConnectToDevice(device)
})
.catch(error => {
uiStatusError(makeErrorMsg(error))
})
}
function liffConnectToDevice(device) {
device.gatt
.connect()
.then(() => {
// Show status connected
uiToggleDeviceConnected(true)
// Get service
device.gatt
.getPrimaryService(USER_SERVICE_UUID)
.then(service => {
liffGetUserService(service)
})
.catch(error => {
uiStatusError(makeErrorMsg(error), false)
})
device.gatt.getPrimaryService(PSDI_SERVICE_UUID).catch(error => {
uiStatusError(makeErrorMsg(error), false)
})
// Device disconnect callback
const disconnectCallback = () => {
// Show status disconnected
uiToggleDeviceConnected(false)
// Remove disconnect callback
device.removeEventListener('gattserverdisconnected', disconnectCallback)
// Try to reconnect
initializeLiff()
}
device.addEventListener('gattserverdisconnected', disconnectCallback)
})
.catch(error => {
uiStatusError(makeErrorMsg(error))
})
}
function liffGetUserService(service) {
// Button pressed state
service
.getCharacteristic(NOTIFY_CHARACTERISTIC_UUID)
.then(characteristic => {
liffGetReadCharacteristic(characteristic)
})
.catch(error => {
uiStatusError(makeErrorMsg(error))
})
// Toggle LED
service
.getCharacteristic(WRITE_CHARACTERISTIC_UUID)
.then(characteristic => {
window.writeCharacteristic = characteristic
})
.catch(error => {
uiStatusError(makeErrorMsg(error))
})
}
function liffGetReadCharacteristic(characteristic) {
// Add notification hook for button state
// (Get notified when button state changes)
characteristic
.startNotifications()
.then(() => {
characteristic.addEventListener(
'characteristicvaluechanged',
liffCharacteristicValueChanged
)
})
.catch(error => {
uiStatusError(makeErrorMsg(error), false)
})
}
function liffCharacteristicValueChanged(e) {
const buff = new Uint8Array(e.target.value.buffer)
try {
const val = new TextDecoder().decode(buff).split(',')
uiUpdateValues(val)
} catch (e) {
uiStatusError(e)
}
}
function liffToggleState(state) {
// on: 0x01
// off: 0x00
window.writeCharacteristic
.writeValue(state ? new Uint8Array([0x01]) : new Uint8Array([0x00]))
.catch(error => {
uiStatusError(makeErrorMsg(error), false)
})
}
まとめ
ちょうど作ったタイミングで曇りになって、外歩きながら試せなかったので晴れたら試したい。
「うわ!ここ日陰ないじゃんやばい死んじゃう!」みたいに一人で盛り上がれるかな。
暑いの少しは紛れるかな。多分無理。