空中生物カヤックラゲ見たか? 癒やしの空中生物を!
面白法人カヤック『空中生物カヤックラゲ』

面白法人カヤック

見たか? 癒やしの空中生物を!
面白法人カヤック『空中生物カヤックラゲ』

Play!

「Intel® Edison」× 日本屈指の表現者たちーー両者の融合から始まる、かつてない “未来体験” へ、ようこそ!
8/1(土)、2(日)の2日間にわたって開催された、最先端の “ものづくり” の祭典『Maker Faire Tokyo 2015』(MFT 2015)。その一角、Intelブースの目玉企画のひとつとして、最前線の作り手たちによる展示企画『Artist Showcase』の作品が、今年もその全貌を現した。
過去3回にわたって5組の参加アーティストたちに密着し、発想のヒントや完成へと至る道のりをお届けしてきたこのレポート。
ついにお披露目を迎えた彼らの作品は、MFT 2015の会場でどんな光を放ったのか。彼らのチャレンジ精神と最新テクノロジーにかける想いを、システム構成やソースコードとともに解き明かしていこう……!

「クラゲが立った! 空中に!」
面白電子生物シリーズ、
ここに爆誕

MFTの会場に、毎年必ず登場する最先端のドローンたち。
“空を飛ぶ” っていいよね。でも、あのブィーンという音はちょっぴり怖い。クラゲのようにゆったりほっこり、空中を漂う “電子生物” がいたら、逆に心癒やされるはず……!
そう語ったのは、面白法人カヤックのエンジニア、中野 稔(なかの・みのる)と新美太基(にいみ・たいき)。そんな彼らの 想が、ついに現実化してしまった! MFT 2015のIntelブースの頭上にぷかぷかと浮かぶ、『空中生物カヤックラゲ』。
小型軽量にしてパワフルというEdisonの特性を活用し、バルーンの浮力で漂いながら、内蔵センサーで高度を検知。モーター制御のプロペラで一定の高さをキープし続ける姿は、まさにクラゲそのもの。
その姿を傍らで “クラゲ師” のように見守る中野 稔に、現在の心境を聞いてみた。

中野 稔
一言でいえば、「浮かんだ!」という感じ。会場でもすごく話しかけられますね。制作中は、いくらEdisonが軽いとはいえ、モーターや配線、バルーンの結束材などが増えれば増えるほど重くなって、「重力おそるべし……」状態だったわけですが、最終的にEdisonとリポ電池、モーターをフリスクケース内に収めつつ、気圧計を備えて浮かせることに成功しました。気を付けたのは、「ただ風船を束ねただけだとゴミに見える」ということ。集光板をレーザーカッターで切り抜き、透明パーツを作成することで、見た目的にも軽さを演出しています。機能としては、気圧計で海抜標高を測定し、スタート地点から高度3メートルの高さを維持するだけ。ぷかぷかと会場の空調に流されていってしまうなど、手がかかるところも含めて、「電子生物できた!」と実感しています。

“癒やし” をくれるEdison内蔵クラゲーー
『空中生物カヤックラゲ』の仕組みを大公開!

Edison に接続されたのは温湿度・気圧センサーが一つとなった、わずか10mm x 13mm の小型で軽量なモジュールだ(センサー自体は、なんと 2.5mm x 2.5 という大きさ!)。このモジュールから常時 Edison にデータが送られる。温度と気圧のデータは Altitude.cpp スクリプトによって処理され、デバイスの高度を計算する。
デバイスの高度はソケット通信を通して Node.js アプリ(app.js と関連した子プロセス)に送られ、それが現在の高度を保つためにモーターを制御する。 モーターのスピード自体は一定だが、時間と回転方向が、距離に応じて変化する 。

またモーターは Wi-Fi 5Ghz を介してオフにすることが可能だ。ここに Edison 搭載のWi-Fi無線とスマートフォンのアプリが使用されている。

材料

ソースコード

すべてのコードは GitHub からご覧いただきたい。
ここでは、モーターの電源オンオフや回転方向を判断する Node.js ファイルをご紹介する。

Learn More on GitHub

app.js
                'use strict'

/*
--------------------------------------------------------------------------------
    Module
--------------------------------------------------------------------------------
*/
var altimeter  = require('./altimeter')
  , controller = require('./controller')
  , motor      = require('./motor')
  , exec       = require('child_process').exec
  , dgram      = require('dgram')
  ;

/*
--------------------------------------------------------------------------------
    Constant
--------------------------------------------------------------------------------
*/
var UDP_HOST = '127.0.0.1';
var UDP_PORT = 6789;
var AUTO_MODE = (process.argv[2] !== 'no');
console.log('AUTO MODE:', AUTO_MODE)

/*
--------------------------------------------------------------------------------
    Variable
--------------------------------------------------------------------------------
*/
var socket
  ;

/*
--------------------------------------------------------------------------------
    Initialize
--------------------------------------------------------------------------------
*/
exec('./bmeReader');

socket = dgram.createSocket('udp4', receive);
socket.bind(UDP_PORT, UDP_HOST);
motor.update(0);
altimeter.init();

if (AUTO_MODE === true)
{
    altimeter.addUpdateHandler(controller.update);
}

process.on('SIGINT', close);
process.on('SIGHUP', close);
process.on('SIGTERM', close);

function close()
{
    motor.update(0);
    process.exit(1);
}

/*
--------------------------------------------------------------------------------
    Private
--------------------------------------------------------------------------------
*/
function receive(message, rinfo)
{
    var msg = message.toString();

    if (isNaN(msg) === false)
    {
        motor.update(msg);
    }
    else if (msg === 'stop')
    {
        controller.deActive();
    }
}
                
              
controller.js
                'use strict'

/*
--------------------------------------------------------------------------------
    Module
--------------------------------------------------------------------------------
*/
var motor = require('./motor')
  ;

/*
--------------------------------------------------------------------------------
    Constant
--------------------------------------------------------------------------------
*/
var SAMPLING_RATE  = 10
  , TARGET_HEIGHT  = 1
  , MOTOR_POWER    = 40
  , MOTOR_DURATION = 1000
  ;

/*
--------------------------------------------------------------------------------
    Variable
--------------------------------------------------------------------------------
*/
var isActive     = false
  , samplingNum  = SAMPLING_RATE
  , targetHeight = 0
  , currentTime  = Date.now()
  , idolTime     = currentTime
  , idolTimer    = null
  ;


/*
--------------------------------------------------------------------------------
    Public
--------------------------------------------------------------------------------
*/
/**
 * Altimeter update
 * @param {number} Current altitude
 */
module.exports.update = function(currentHeight)
{
    currentTime = Date.now();

    if (samplingNum > 0)
    {
        targetHeight += currentHeight;
        console.log('sampling ', samplingNum);
        --samplingNum;
    }

    if (isActive === false && samplingNum === 0)
    {
        targetHeight = (targetHeight / SAMPLING_RATE) + TARGET_HEIGHT;
        isActive = true;
        samplingNum = false;
        console.log('Target height: ' + targetHeight);
    }

    if (isActive === true && isMotorIdoling())
    {
        if (currentHeight < targetHeight)
        {
            idolTime = drive(MOTOR_POWER, (targetHeight - currentHeight) * MOTOR_DURATION);
        }
        else
        {
            console.log('negative')
        }
    }
}

/**
 * Mode
 */
module.exports.deActive = function()
{
    isActive = false;
}

/**
 * Mode
 */
module.exports.active = function()
{
    isActive = true;
}

/*
--------------------------------------------------------------------------------
    Private
--------------------------------------------------------------------------------
*/
//
function isMotorIdoling()
{
    return currentTime > idolTime;
}

function drive(power, time)
{
    clearInterval(idolTimer);
    motor.update(power);

    console.log(power, time);

    idolTimer = setTimeout(function ()
    {
        motor.update(0);
    }, time);

    return currentTime + time;
}
                
              
motor.js
                'use strict'

/*
--------------------------------------------------------------------------------
    Module
--------------------------------------------------------------------------------
*/
var mraa  = require('mraa')
  ;

/*
--------------------------------------------------------------------------------
    Constant
--------------------------------------------------------------------------------
*/
var ADDRESS   = 0x64;
var MAX_VALUE = 40;

/*
--------------------------------------------------------------------------------
    Variable
--------------------------------------------------------------------------------
*/
var driver
  , command
  ;

driver = new mraa.I2c(0);
driver.address(ADDRESS);

command = new Buffer(2);
command[0] = 0x00;
command[1] = 0x00;

/*
--------------------------------------------------------------------------------
    Public
--------------------------------------------------------------------------------
*/
/**
 * Motor control.
 * @param {number} Power. Controlled by the integer and negative.
 */
module.exports.update = function(val)
{
    var direc = 0;

    if (val > 0)
    {
        direc = 1;
    }
    else if (val < 0)
    {
        direc = 2;
        val *= -1;
    }

    if (val > MAX_VALUE)
    {
        val = MAX_VALUE;
    }

    command[1] = val << 2 | direc;
    driver.write(command);
}

/*
--------------------------------------------------------------------------------
    Private
--------------------------------------------------------------------------------
*/
                
              
altimeter.js
                'use strict'

/*
--------------------------------------------------------------------------------
    Module
--------------------------------------------------------------------------------
*/
var dgram = require('dgram')
  ;

/*
--------------------------------------------------------------------------------
    Constant
--------------------------------------------------------------------------------
*/
var UDP_HOST = '127.0.0.1';
var UDP_PORT = 12345;

/*
--------------------------------------------------------------------------------
    Variable
--------------------------------------------------------------------------------
*/
var socket
  , value = 0
  , handlers = []
  ;

/*
--------------------------------------------------------------------------------
    Public
--------------------------------------------------------------------------------
*/
/**
 * Initialize.
 */
module.exports.init = function()
{
    socket = dgram.createSocket('udp4', receive);
    socket.bind(UDP_PORT, UDP_HOST);
}

/**
 * Altimeter value.
 * @return {number}
 */
module.exports.getValue = function()
{
    return value;
}

/**
 * Update handler.
 * @param {function}
 */
module.exports.addUpdateHandler = function(handler)
{
    handlers.push(handler);
}

/*
--------------------------------------------------------------------------------
    Private
--------------------------------------------------------------------------------
*/
function receive(message, rinfo)
{
    var val = Number(message.toString());

    if (value !== val)
    {
        value = val;

        for (var i = 0, n = handlers.length; i < n; i++)
        {
            handlers[i](value);
        }
    }
}
                
              

「機械であるEdisonを使いながら、ナマモノっぽいものを作れたら面白い」(中農)
「世話が焼けるところも含めて、生物っぽく見えたら成功だと思います」(新美太基)
(※いずれもこれまでの『Artist Showcase』密着記事より)
まさに “面白法人” ならではの発想力と、Intel® Edisonというテクノロジーが融合し、
21世紀の日本に出現した、“面白電子生物シリーズ” 第1号。
「ここからどんどん進化させていきます。第2世代はプロペラじゃなく、ヒレでふわふわ〜っと漂うようにさせたいです」
彼らの夢とロマン、そして面白さを注ぎ込んで爆誕した『カヤックラゲ』。
進化したその勇姿が上空を漂う日も、そう遠くはないはずだ……!

8月1日、MFT 2015内 Intel『Artist Showcase』会場にて

ARTIST PROFILE

面白法人カヤック

業務内容に「日本的面白コンテンツ事業」を掲げ、古都鎌倉から新しい技術と面白いサービスを次々にリリースする会社。「つくる人を増やす」を経営理念とし、Webやアプリを中心とする自社サービスを展開しつつ、サイコロを振って給料が変わる「サイコロ給」などのユニークな取り組みで注目を集め続ける。

http://www.kayac.com/
中農稔(Minoru Nakano)
新美 太基(Tacky Naomi)