さて、Advent Calendarですよ!
めりーくりすまーす!!!
※時間がないのでWIFI-TNGとESP-WROOM-02で始めるWIFI Arduino使い回しですw
今度は何?
たぶん皆さん同様、年末進行中!そんな中!!
「明日までに何か展示するものを作らなきゃならない!!!」
まじかよ!
ということが、前日の夕方に決定!アホか!
ということが、先週ありました。(いつもそんな感じではあるw)
- 営業目的の社内展示会用。私は技術サポートの仕事をやってるので出来る感をアピールする必要がある!
- 時間がない!徹夜しちゃったら翌日説明員する体力ない!!
- 手元にCHIRIMENがある!昨日までにLチカは出来た。というか、別の場所でCHIRIMENを使った気合いの展示も行われている。よし!コバンザメ作戦だ!
- 前後左右のブースではVRとかドローンとか技術力を駆使した展示が盛りだくさん!違いを出さなければ!
→CHIRIMENを使った「何か」を作ることにしました。
CHIRIMENとは?
CHIRIMENは、MozOpenHardプロジェクトで開発が進められている、Firefox OS搭載のボードです。以前KDDIがイベントとかで配ってたOpen Web Boardと似ていますが、豊富なGPIOピンが搭載されているのが特徴です。
JavaScriptでGPIO制御できる、こうしたボードは結構あります。Tesselや、konashiが有名ですね。
CHIRIMENの特徴は何と言っても「中でブラウザが動いてる」ということ。
JavaScriptだけでなく、HTML5の機能がフルで使えるんですね!
詳細はMozOpenHard CHIRIMENを見てください。
Lチカ(3時間の前に終わってたこと)
Lチカは、GPIOポートにLEDを繋いでHIGHとLOWを繰り返し出力するとできます。
Arduinoとかと一緒です。
CHIRIMENのアプリは、HTML/CSS/JavaScriptで作るWebアプリなので、LチカもJavaScriptからやる感じになります。
下記を参考にしました。
IOポートの情報
CHIRIMENのIOポート情報は下記にありました。
こちらのリポジトリ整理中らしいので、URL変わるかもです。
今回は、下記ピンを使うことにしました。
ピン番号 | GPIOポート番号 |
---|---|
CH1-10 | 199 |
CH1-11 | 244 |
CH1-12 | 243 |
CH1-13 | 246 |
CH1-14 | 245 |
Lチカプログラム
というわけで書いてみました。
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Blink</title>
</head>
<body>
<div>Lチカ</div>
<script src="app.js"></script>
</body>
</html>
var pnums = [199,244,243,246,245];
for(var i= 0;i< pnums.length; i++){
navigator.mozGpio.export(pnums[i]);
var start = new Date();
while((new Date()-start)<100);
}
var val,c = 0;
setInterval(function(){
val ^= 1;
navigator.mozGpio.setValue(pnums[c],val);
if(val == 0){
c=(c+1)%pnums.length;
}
},100);
スクリーンセイバーへの道
最初はあまりの時間のなさにLチカを展示しようと思いましたが、、、、さすがにそれは。
せっかくHTML5が動くボードなので、画面を見せたい。
しかし!時間がない!
クリスマスシーズンなので、サンタとか出てくればいいだろ!
というわけで、クリスマスっぽいスクリーンセイバーを作ることにしました。
こんなの!
Lチカ付き!(←あまりコラボ感はないw)
CHIRIMENに操作デバイスを作る暇はないので、スマホと連携して操作する感じにします。
最終的につくるのは下記3つになります。
- スクリーンセイバー(CHIRIMENにインストールするWebアプリ)
- WebSocket 中継サーバー(node.jsで作る)
- コントローラー(スマホからアクセスするWebサイト)
作ったコードはこちら!tadfmac/screensaver
さて、頑張ります。
1. スクリーンセイバー
画像を集める!
クリスマスっぽさは、画像で!
"サンタ イラスト フリー" とかでググって、背景透明っぽいのを収集しまくります。
※著作権とかはご注意を!
画像を動かす!
画像を動かせば、スクリーンセイバーの出来上がり!
Pixi.JSを使って動かしました。
公開からずいぶん時間が経ってしまいましたが、Pixi.JSに興味のある方は下記も参考にしてください。
上記記事のような感じで画像を動かしました。
手抜きリモコン対応
WebSocketに対応する部分を作ります。
時間が無いので、慣れたpoorwsを使います。
これで、ws.onmessage()
で受け取ったメッセージにより、画像を出したりするようにします。
ソース
こんな感じになりました。
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>XMAS Screen Saver</title>
<style>
*{
padding:0px;
margin:0px;
}
#status{
position:fixed;
bottom:0px;
right:0px;
font-size:18px;
background-color:black;
color:white;
}
</style>
</head>
<body>
<div id="status">Connecting...</div>
<div id="pixiview"></div>
<script src="pixi.min.js"></script>
<script src="poorws.js"></script>
<script src="jquery-2.0.3.min.js"></script>
<script src="app.js"></script>
</body>
</html>
JSはこんな感じで。
(毎度の事ですが、勢いで書いてるので関数化すら出来ていませんw)
// ws
var host = "ws://xxx.net:xxxx";
var ws = new poorws(host);
var status = 0;
ws.onStatusChange = function(sts){
status = sts;
if(sts == 0){
$("#status").text("Connecting...");
}else if(sts == 1){
$("#status").text("Connected!");
}else if(sts == 2){
$("#status").text("Disconnecting...");
}else if(sts == 3){
$("#status").text("Re-Connecting...");
}
};
ws.onOpen = function(e){
ws.send("master");
};
ws.onMessage = function(mes){
if(mes.data == "master ack"){
console.log("master ack received");
}else{
if(mes.data == "clear"){
clearSprites();
}else if(mes.data == "snow"){
if(snow == true){
removeSnow();
}else{
addSnow();
}
}else{
addSprite(getRandomInt(0,fruitsnames.length));
}
}
};
function getRandomInt(min, max) {
return Math.floor( Math.random() * (max - min + 1) ) + min;
}
// Animation
var stage = new PIXI.Stage(0x000000);
var width = window.innerWidth;
var height = window.innerHeight;
var renderer = PIXI.autoDetectRenderer(width, height, {autoResize: true});
document.getElementById("pixiview").appendChild(renderer.view);
function initWall(){
var wall = PIXI.Texture.fromImage('./img/wall.jpg');
var wsprite = new PIXI.Sprite(wall);
wsprite.position.x = width / 2;
wsprite.position.y = height / 2;
wsprite.anchor.x = 0.5;
wsprite.anchor.y = 0.5;
stage.addChild(wsprite);
}
initWall();
var xmascontainer = new PIXI.Container();
stage.addChild(xmascontainer);
// 画像からスプライトオブジェクトを作る
var sprites = [];
var fruitsnames = [
'./img/bell1.png',
'./img/santa1.png',
'./img/santa2.png',
'./img/santa3.png',
'./img/tonakai1.png',
'./img/tonakai2.png',
'./img/tree1.png',
'./img/tree2.png'
];
function addSprite(cnt){
var ftex = PIXI.Texture.fromImage(fruitsnames[cnt%fruitsnames.length]);
var fsprite = new PIXI.Sprite(ftex);
fsprite.position.x = Math.random() * width;
fsprite.position.y = Math.random() * height;
fsprite.anchor.x = 0.5;
fsprite.anchor.y = 0.5;
fsprite.speed = Math.random();
sprites.push(fsprite);
xmascontainer.addChild(fsprite);
blinkStart();
}
function clearSprites(){
for(var i=0;i < sprites.length; i++){
xmascontainer.removeChild(sprites[i]);
}
}
var snow = false;
requestAnimationFrame(animate);
function animate(){
requestAnimationFrame(animate);
for(var cnt=0;cnt <sprites.length;cnt++){
sprites[cnt].rotation += (sprites[cnt].speed / 10);
sprites[cnt].position.x += (sprites[cnt].speed * 10);
if(sprites[cnt].position.x > (width + 100)){
sprites[cnt].position.x = -200;
sprites[cnt].position.y = Math.random() * height;
sprites[cnt].speed = Math.random();
}
}
if(snow == true){
for(cnt=0;cnt <MAX_SNOW;cnt++){
var scale = snowimgs[cnt].scale.x;
snowimgs[cnt].position.x += scale * (Math.random() - 0.5) * 4;
snowimgs[cnt].position.y += (scale * 3) + 1;
if(snowimgs[cnt].position.y > 1024){
snowimgs[cnt].position.y = -10;
}
}
}
renderer.render(stage);
}
// snow
var texture = PIXI.Texture.fromImage('img/snow2.png');
var MAX_SNOW = 300;
var snowimgs = [];
for(var cnt=0;cnt < MAX_SNOW;cnt ++){
snowimgs.push(new PIXI.Sprite(texture));
snowimgs[cnt].position.x = Math.random() * width;
snowimgs[cnt].position.y = Math.random() * height;
snowimgs[cnt].anchor.x = 0.5;
snowimgs[cnt].anchor.y = 0.5;
var base = Math.random();
snowimgs[cnt].alpha = (base/2) + 0.4;
snowimgs[cnt].scale.x = base/2;
snowimgs[cnt].scale.y = base/2;
}
function addSnow(){
snow = true;
for(var cnt=0;cnt < MAX_SNOW;cnt ++){
stage.addChild(snowimgs[cnt]);
}
}
function removeSnow(){
snow = false;
for(var cnt=0;cnt < MAX_SNOW;cnt ++){
stage.removeChild(snowimgs[cnt]);
}
}
// resizeing
var resizeTimer = false;
$(window).resize(function() {
if (resizeTimer !== false) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(function() {
width = window.innerWidth;
height = window.innerHeight;
renderer.resize(width, height);
}, 200);
});
// Lチカ (For CHIRIMEN)
if(navigator.mozGpio){
var pnums = [199,244,243,246,245];
for(var i= 0;i< pnums.length; i++){
navigator.mozGpio.export(pnums[i]);
delay(100);
}
var val,c = 0;
var timer = null;
function blinkStart(){
if(timer){
clearInterval(timer);
}
timer = setInterval(function(){
val ^= 1;
navigator.mozGpio.setValue(pnums[c],val);
if(val == 0){
c=(c+1)%pnums.length;
}
},100);
}
function blinkStop(){
if(timer){
clearInterval(timer);
}
timer = null;
}
function delay(millisec){
var start = new Date();
while((new Date()-start)<millisec);
}
function toggle(){
if(timer){
blinkStop();
}else{
blinkStart();
}
}
blinkStart();
}
2. WebSocket 中継サーバー
WenSocketを中継するサーバーです。
websockets/wsを使って、下記サーバーを立ち上げておきます。
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({port: xxxx});
var connections = [];
var masterconn = null;
wss.on('connection', function (ws) {
connections.push(ws);
ws.on('close', function () {
connections = connections.filter(function (conn, i) {
return (conn === ws) ? false : true;
});
if(ws == masterconn){
masterconn = null;
}
});
ws.on('message', function (message,flags) {
if(flags.binary){
for(var cnt1=0;cnt1 < message.length; cnt1++){
console.dir("bynarydata_"+cnt1+":"+message[cnt1]);
}
if((masterconn != null)&&(masterconn.readyState == 1)){
masterconn.send(message);
}
}else{
console.log("message received: "+message);
if(message == "ping"){
ws.send("ack");
}else if(message == "master"){
masterconn = ws;
ws.send("master ack");
}else{
if((masterconn != null)&&(masterconn.readyState == 1)){
masterconn.send(message);
}
}
}
});
// polling
setInterval(function(){
connections.forEach(function (con, i) {
if(con.readyState == 1){
con.ping("p");
}else{
console.log("ping failed:"+i);
}
});
},5000);
});
仕組み
スクリーンセイバーアプリ(CHIRIMEN)から、"master"
というメッセージを送っておきます。
WebSocketサーバーでは、他のコネクションから送られたメッセージを"master"
に転送します。
これだけ!!
3. リモコン
最後はリモコンです。リモコンもWebアプリ。
ボタン押したらWebSocketを送るだけ!
こちらも、poorwsを使います。
<!DOCTYPE html>
<html>
<head>
<title>Controller</title>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8;"/>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
</head>
<body>
<button id="send">PUSH!</button>
<button id="clear">clear</button>
<button id="snow">snow</button>
<div id="status">Connecting...</div>
<script src='jquery-2.0.3.min.js'></script>
<script src='poorws.js'></script>
<script>
var host = "ws://xxx.net:xxxx";
var ws = new poorws(host);
var status = 0;
ws.onStatusChange = function(sts){
status = sts;
if(sts == 0){
$("#status").text("Connecting...");
}else if(sts == 1){
$("#status").text("Connected!");
}else if(sts == 2){
$("#status").text("Disconnecting...");
}else if(sts == 3){
$("#status").text("Re-Connecting...");
}
};
$(function(){
$("#send").bind({
"touchstart mousedown":function(e){
ws.send("test");
}
});
$("#clear").bind({
"touchstart mousedown":function(e){
ws.send("clear");
}
});
$("#snow").bind({
"touchstart mousedown":function(e){
ws.send("snow");
}
});
});
</script>
</body>
</html>
簡単!
まとめ
展示会というと、まだ出したことの無い人は下記のように考えがちです。
私もそうでした。ちょっと前までは。
- 展示会なんてのに出すのは、凄く頑張ってる凄い人ばかり。自分は違う
- モノを作ってるけど、まだ途中。まだ完成してない。出さない方がいいのでは?
しかし!実際は違います!
だいたい、展示会とかに出してる人は下記のように考えてます。
- やっつけ仕事もネタに出来ればOK!
- 完成?そのうちにね!
展示会での反応を貰って、どんどん良くするのです。
なので、何でもいいから出しましょう。叩かれましょう!ネタにしましょう!
楽しいですよ!! ←これがいいたい