さて、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!
- 完成?そのうちにね!
展示会での反応を貰って、どんどん良くするのです。
なので、何でもいいから出しましょう。叩かれましょう!ネタにしましょう!
楽しいですよ!! ←これがいいたい




