マインクラフトのTNTを現実世界から爆発させてみる(Wio Nodeを使います)

f:id:suki_yo_kirai_yo:20161215120221j:plain

どうも!スマートフォンアプリチームの岩田です!

IoTLT Advent Calendar 2016の15日目の記事です。

最近、社内に導入されているSlackの絵文字を追加することが小さな楽しみになっています。前回の記事で懸念していた健康診断は血圧が高めになりそうだということをこの場を借りてご報告させていただきます。

今回は少し業務から離れて、社内LT大会やIoTLTで発表した「Minecraft x IoT」の補足記事を書いていきます。

発表を見ていない方はSlideShareにて資料を共有していますので一読していただけると幸いです。

IoTを駆使してゲームの世界と現実の世界を組み合わせた新しいMinecraftの遊び方を紹介していきたいと思います。

使うもの

さっそく新しい遊び方を実現するために必要なものたちを紹介します。

  • Minecraft PC版
  • Wio Node
  • ボタン
  • LED
  • 1KΩ、4.7KΩの抵抗2個

構成

上記の「使うもの」を以下の図のように構成します。 f:id:g-editor:20161130162611p:plain

Minecraft

サンドボックス型ゲームでYouTubeでのプレイ動画もよく公開されています。そのため、遊んだことがない方でも、どこかしらで見たことがあるかもしれません。

今回はMOD(Modification)を利用するためにPC版を使用します。 MODとは拡張機能でゲーム内に新しい機能を追加することを可能にする機能です。このMODを使ってJavaで外部と通信をする機能を実装し、IoTデバイスとゲーム内の処理をやり取りできるようにします。

Wio Node

Wio NodeはSeeed Studio製の小型なIoTデバイスです。コネクタに様々なセンサを接続することが可能で、たくさんのセンサがモジュール化されているため、簡単にセンサを繋ぐことが可能です。

設定もスマホアプリで簡単に行うことができ、どのコネクタにどのモジュールを接続したのかをスマホアプリ上で視覚的に設定を行うだけで、難しい回路設計などは不要です。

設定を行うとURLが発行され、HTTP POSTやGETメソッドでアクセスすることでセンサデータの取得や出力の設定ができます。

誰でも非常にお手軽に使うことができるため、Webエンジニアやデザイナーの方など組込の世界がわからないけどもIoTやってみたい!という方におすすめです。

スペック

Wikiに載っているスペックをまとめると以下のとおりです。

項目 スペック
Wi-Fi規格 802.11b/g/n
Wi-Fi暗号化 WEP/TKIP/AES
外部インターフェース UART0/I2C0/D0, Analog/I2C1/D1(両方ともGroveコネクタ)
I/Oピンからの出力電流 12mA
入力電圧 マイクロUSBからは5V、バッテリからは3.4~4.2V
最大出力電流 1000mA
駆動電圧 3.3V
最大充電電流 500mA
フラッシュメモリ 4MByte(W25Q32B)
寸法 28mm x 28mm
CPUクロック 26MHz
CE/FCC/TELEC 認証 ESP-WROOM-02(only)

凄く小さくて500円玉ぐらいの大きさです。小さすぎてWioNodeを何処かに固定するための穴が開いていません。そのため作業環境は十分に広くて物が散乱していない場所を確保しましょう。

Minecraftと繋ぐ

それでは作り方の前に、実際にIoTの技術を利用したゲーム動画を紹介します。

レバーのON/OFFに合わせてLEDが点灯する


ゲーム内のレバーを操作することで現実世界のLEDの点灯・消灯を操作できます。

スイッチを押すと開く扉


突然の来客でもちゃんと対応できるようにしておきましょう。

ゲーム内のクロック回路でLEDを制御する


RED STONEを組み合わせることでクロック回路(周期的にON/OFFを繰り返す装置)を作成し、現実世界のLEDを制御します。

爆破スイッチ


現実世界のスイッチを押すとゲーム内のTNT爆弾が爆発します。

作り方

ハードウェア

ハードウェアといってもそんなに複雑な回路を作るというわけではありません。LEDとボタンを接続します。

回路図

LEDと抵抗(1kΩ、4.7kΩ)、タクトスイッチを使用します。ブレッドボード上で配線をした図です。 f:id:g-editor:20161130162612p:plain

LEDをバッテリコネクタに対して左側へ、タクトスイッチを反対側の端子に接続します。

f:id:g-editor:20161130170000j:plain

実際に配線した写真です。

もし、LEDやタクトスイッチが無い場合は、既に回路が実装済みのモジュールが市販されていますので、そちらを利用してください。

  1. LEDモジュール
  2. ボタンモジュール

WioNodeの設定

バッテリコネクタに対して左側の端子に「Generic Digital Input」を、反対側の端子に「Generic Digital Output」を配置します。配置ができたら忘れないように「Update Firmware」でWioNodeのファームウェアを更新します。

f:id:g-editor:20161130162613p:plain

これでWio Nodeは使用することができる状態になりました。アプリの右上のメニューから「View API」を選択し、Web APIのURLを確認します。また、このURLがMODがアクセスするURLになりますのでメモをしておきましょう。

ソフトウェア

MODを実装していきます。MODにはMinecraft Forge 1.10を利用します。

MODの開発の大まかな流れとしては以下のとおりです。

  1. HTTPS通信機
  2. ブロックの機能を実装
  3. ブロックの登録
  4. 動作確認

HTTPS通信機能の実装

HTTPS通信に今回はAndroidでもおなじみのRetrofit2を利用します。

build.gradle の dependencies{} の最後に

compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'

を追加して以下の様に変更します。

build.gradle

// 略
dependencies {
    // you may put jars on which you depend on in ./libs
    // or you may define them like so..
    //compile "some.group:artifact:version:classifier"
    //compile "some.group:artifact:version"

    // real examples
    //compile 'com.mod-buildcraft:buildcraft:6.0.8:dev'  // adds buildcraft to the dev env
    //compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env

    // the 'provided' configuration is for optional dependencies that exist at compile-time but might not at runtime.
    //provided 'com.mod-buildcraft:buildcraft:6.0.8:dev'

    // the deobf configurations:  'deobfCompile' and 'deobfProvided' are the same as the normal compile and provided,
    // except that these dependencies get remapped to your current MCP mappings
    //deobfCompile 'com.mod-buildcraft:buildcraft:6.0.8:dev'
    //deobfProvided 'com.mod-buildcraft:buildcraft:6.0.8:dev'

    // for more info...
    // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html
    // http://www.gradle.org/docs/current/userguide/dependency_management.html

    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
}
//略

WioNodeのWeb APIを実行するためのServiceを定義します。

WioNodeService.java

package jp.dip.iwatan;

import jp.dip.iwatan.models.GenericDigitalInput;
import jp.dip.iwatan.models.Result;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;

/**
 * Retrofitを利用してWioNodeと通信するサービス
 */
public interface WioNodeService {
    public interface GenericDigitalInputService {
        @GET("input")
        Call<GenericDigitalInput> get(@Query(value="access_token") String token);
    }

    public interface GenericDigitalOutputService {
        String HIGH = "1";
        String LOW = "0";

        @POST("onoff/{onoff}")
        Call<Result> post(
                @Path("onoff") String onoff,
                @Query(value="access_token") String token
        );
    }
}

HTTP通信を行う実体を作成します。

WioNode.java

package jp.dip.iwatan;

import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

import javax.net.ssl.*;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * WioNodeと通信するクラスを生成するクラス
 */
public class WioNode {
    /**
     * @return
     */
    private static OkHttpClient createClient() {
        OkHttpClient client = new OkHttpClient();
        try {
            X509TrustManager trustManager = new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    // 特に何もしない
                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    // 特に何もしない
                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            };
            final SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
            SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
            client = new OkHttpClient.Builder()
                    .sslSocketFactory(sslSocketFactory, trustManager)
                    .build();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
        return client;
    }

    /**
     * デジタル出力用クラスの生成
     */
    public static WioNodeService.GenericDigitalOutputService getGenericDigitalOutput() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://us.wio.seeed.io/v1/node/GenericDOutD1/")
                .addConverterFactory(GsonConverterFactory.create())
                .client(createClient())
                .build();
        return retrofit.create(WioNodeService.GenericDigitalOutputService.class);
    }

    /**
     * デジタル入力用クラスの生成
     */
    public static WioNodeService.GenericDigitalInputService getGenericDigitalInput() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://us.wio.seeed.io/v1/node/GenericDInD0/")
                .addConverterFactory(GsonConverterFactory.create())
                .client(createClient())
                .build();
        return retrofit.create(WioNodeService.GenericDigitalInputService.class);
    }
}

ブロックの機能を実装

上記で作ったHTTP通信を行うクラスを利用して、デジタル入力用とデジタル出力用のMinecraftのブロックを2個実装します。

デジタル出力用のブロックの実装は以下のとおりです。

Minecraftの世界はTickと呼ばれる周期で定期的に処理が実行されているので、その処理に合わせて、ブロックの周辺状況(レッドストーン入力があるかないか)を判断してWio NodeのWeb APIを呼び出しています。

デジタル出力の場合は、レッドストーン入力があればWio Nodeに対して出力をONにするように、レッドストーン入力がなければOFFにするような処理を行っています。

WioDigitalOutputBlock.java

package com.example.examplemod;

import jp.dip.iwatan.WioNode;
import jp.dip.iwatan.WioNodeService;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

import javax.annotation.Nullable;
import java.util.Random;

import static jp.dip.iwatan.WioNodeService.GenericDigitalOutputService.HIGH;
import static jp.dip.iwatan.WioNodeService.GenericDigitalOutputService.LOW;

/**
 * レッドストーンの入力に応じてWioNodeの汎用デジタル出力の状態を変更するブロック
 */
public class WioDigitalOutputBlock extends Block {
    private static Logger logger = LogManager.getLogger("WioDigitalOutputBlock");
    private final String token;

    WioNodeService.GenericDigitalOutputService service = WioNode.getGenericDigitalOutput();

    /* RedStoneの入力状態 */
    private boolean isOn = false;

    public WioDigitalOutputBlock(String token) {
        super(Material.IRON);
        setCreativeTab(CreativeTabs.REDSTONE);
        setUnlocalizedName("WioNode DigitalOutput");
        this.token = token;
    }

    /*
     * ブロックの周辺環境が変化した際に呼び出される
     * @param state*
     * @param worldIn
     * @param pos
     * @param blockIn
     */
    public void neighborChanged(IBlockState state, World worldIn, BlockPos pos, Block blockIn) {
        BlockPos blockpos1 = pos.up();
        boolean flag = worldIn.isBlockPowered(pos) || worldIn.isBlockPowered(blockpos1);

        // クライアントの場合のみ実行
        if (!worldIn.isRemote) {
            // 状態に変化があったらアップデートを予約する
            if (flag != isOn) {
                worldIn.scheduleUpdate(pos, this, 4);
            }
        }
        // 周辺の状態に合わせて変更する
        isOn = flag;
    }

    /**
     * Tickの更新時に呼び出される
     * @param worldIn
     * @param pos
     * @param state
     * @param rand
     */
    public void updateTick(World worldIn, BlockPos pos, IBlockState state, Random rand) {
        Call call;
        if (isOn) {
            call = service.post(HIGH, token);
            logger.info("post On");
        } else {
            call = service.post(LOW, token);
            logger.info("post Off");
        }
        call.enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                logger.info(response.message());
            }

            @Override
            public void onFailure(Call call, Throwable t) {
                logger.info("post failure");
            }
        });
    }

    @Nullable
    public Item getItemDropped(IBlockState state, Random rand, int fortune) {
        return Item.getItemFromBlock((Block)Block.REGISTRY.getObject(new ResourceLocation("WioNode DigitalOutput")));
    }

    public ItemStack getItem(World worldIn, BlockPos pos, IBlockState state) {
        return new ItemStack((Block)Block.REGISTRY.getObject(new ResourceLocation("WioNode DigitalOutput")));
    }

    protected ItemStack createStackedBlock(IBlockState state) {
        return new ItemStack((Block)Block.REGISTRY.getObject(new ResourceLocation("WioNode DigitalOutput")));
    }
}

デジタル入力のブロックの実装は以下です。

デジタル入力ではブロックが配置されたときにスレッドを生成してWio NodeのWeb APIをポーリングするようにしています。Web APIの結果に応じて周辺状況を次回Tick更新時に変更を行うようにします。

WioDigitalInputBlock.java

package com.example.examplemod;

import jp.dip.iwatan.WioNode;
import jp.dip.iwatan.WioNodeService;
import jp.dip.iwatan.models.GenericDigitalInput;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Blocks;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.Explosion;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

import javax.annotation.Nullable;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * WioNodeの汎用デジタル入力の状態に応じてレッドストーン出力を行うブロック
 */
public class WioDigitalInputBlock extends Block {

    /**
     * WioNodeの監視タスク
     */
    private class WatchTask implements Runnable {
        private final String token;

        WioNodeService.GenericDigitalInputService service = WioNode.getGenericDigitalInput();

        private WatchTask(String token) {
            this.token = token;
        }

        @Override
        public void run() {
            while(isSet) {
                try {
                    Call call = service.get(token);
                    call.enqueue(new Callback<GenericDigitalInput>() {
                        @Override
                        public void onResponse(Call<GenericDigitalInput> call, Response<GenericDigitalInput> response) {
                            int input = response.body().input;
                            logger.info("input:" + String.valueOf(input));
                            if (input == 0) {
                                isOn = false;
                            } else {
                                isOn = true;
                            }
                            // WioNodeの状態に合わせて次のTick更新時に周辺状況を変更するようにスケジュールする
                            worldIn.scheduleUpdate(pos, context, context.tickRate(worldIn));
                            worldIn.notifyNeighborsOfStateChange(pos, context);
                            for (EnumFacing enumfacing : EnumFacing.values()) {
                                worldIn.notifyNeighborsOfStateChange(pos.offset(enumfacing), context);
                            }
                        }

                        @Override
                        public void onFailure(Call<GenericDigitalInput> call, Throwable t) {
                            logger.info("onFailure:" + t.getMessage());
                        }
                    });
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private World worldIn;
    private BlockPos pos;
    private Block context;

    /** WioNodeへの入力状態 */
    private boolean isOn = false;
    /** WioNodeのトークン */
    private final String token;
    /** WioNodeのデジタル入力監視タスク */
    private ExecutorService executorService;

    private boolean isSet = false;

    private static Logger logger = LogManager.getLogger("WioDigitalInputBlock");

    public WioDigitalInputBlock(String token) {
        super(Material.IRON);
        this.setTickRandomly(true);
        setCreativeTab(CreativeTabs.REDSTONE);
        setUnlocalizedName("WioNode DigitalInput");
        this.context = this;
        this.token = token;
        this.executorService = Executors.newSingleThreadExecutor();
    }

    public int tickRate(World worldIn) {
        return 2;
    }

    /**
     * ブロックを配置時に呼び出されるコールバック
     * @param worldIn
     * @param pos
     * @param state
     */
    public void onBlockAdded(World worldIn, BlockPos pos, IBlockState state) {
        logger.info("onBlockAdded");
        this.worldIn = worldIn;
        this.pos = pos;
        this.isSet = true;
        this.executorService.execute(new WatchTask(token));
    }

    /**
     * プレイヤーによってブロックが破壊された時に呼び出されるコールバック
     * @param worldIn
     * @param pos
     * @param state
     */
    public void onBlockDestroyedByPlayer(World worldIn, BlockPos pos, IBlockState state) {
        logger.info("onBlockDestroyedByPlayer");
        // 通信の終了
        this.isSet = false;
    }

    /**
     * 爆破によってブロックが破壊された時に呼び出されるコールバック
     * @param worldIn
     * @param pos
     * @param explosionIn
     */
    public void onBlockDestroyedByExplosion(World worldIn, BlockPos pos, Explosion explosionIn) {
        logger.info("onBlockDestroyedByExplosion");
        // 通信の終了
        this.isSet = false;
    }

    public void neighborChanged(IBlockState state, World worldIn, BlockPos pos, Block blockIn) {
        BlockPos blockpos1 = pos.up();
        boolean flag = worldIn.isBlockPowered(pos) || worldIn.isBlockPowered(blockpos1);
        worldIn.scheduleUpdate(pos, this, this.tickRate(worldIn));
    }

    /**
     * レッドストーンの出力があるかを設定する
     * @param state
     * @return
     */
    public boolean canProvidePower(IBlockState state) {
        return true;
    }

    /**
     * レッドストーン出力の取得
     * @param blockState
     * @param blockAccess
     * @param pos
     * @param side
     * @return
     */
    public int getWeakPower(IBlockState blockState, IBlockAccess blockAccess, BlockPos pos, EnumFacing side) {
        int value = isOn ? 15 : 0;
        return value;
    }

    /**
     * レッドストーン出力の取得
     * @param blockState
     * @param blockAccess
     * @param pos
     * @param side
     * @return
     */
    public int getStrongPower(IBlockState blockState, IBlockAccess blockAccess, BlockPos pos, EnumFacing side) {
        return blockState.getWeakPower(blockAccess, pos, side);
    }

    @Nullable
    public Item getItemDropped(IBlockState state, Random rand, int fortune) {
        return Item.getItemFromBlock(Blocks.REDSTONE_LAMP);
    }

    public ItemStack getItem(World worldIn, BlockPos pos, IBlockState state) {
        return new ItemStack(Blocks.REDSTONE_LAMP);
    }

    protected ItemStack createStackedBlock(IBlockState state) {
        return new ItemStack(Blocks.REDSTONE_LAMP);
    }
}

ブロックの登録

MOD上に実装したブロックを登録します。 Creativeモードで表示されるタブの指定などを行います。 また、Wio Nodeの設定時に取得したトークンを設定しています。ここで決め打ちでトークンを記載しているので1台のWio Nodeとだけ通信を行うようになっています。 もし複数台のWio Nodeと通信を行うようにする場合は、さらなる工夫が必要です。例えばカンバン用にゲーム内でテキスト入力をさせてトークン設定をできるようにするなどといった具合です。

ExampleMod.java

package com.example.examplemod;

import net.minecraft.block.Block;
import net.minecraft.init.Blocks;
import net.minecraft.item.ItemBlock;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.registry.GameRegistry;

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION)
public class ExampleMod
{
    public static final String MODID = "MoT";
    public static final String VERSION = "1.0";

    public static Block wioDigitalOutputBlock;
    public static Block wioDigitalInputBlock;

    @EventHandler
    public void preInit(FMLInitializationEvent event) {
        String token = "ここにWioNodeのトークンを記入する";
        wioDigitalOutputBlock = new WioDigitalOutputBlock(token);
        wioDigitalInputBlock = new WioDigitalInputBlock(token);

        ResourceLocation wioBlockDoRegistryName = new ResourceLocation(MODID, "WioNode DigitalOutput");
        ItemBlock wioDoItemBlock = new ItemBlock(wioDigitalOutputBlock);
        GameRegistry.register(wioDigitalOutputBlock, wioBlockDoRegistryName);
        GameRegistry.register(wioDoItemBlock, wioBlockDoRegistryName);

        ResourceLocation wioBlockDiRegistryName = new ResourceLocation(MODID, "WioNode DigitalInput");
        ItemBlock wioDiItemBlock = new ItemBlock(wioDigitalInputBlock);
        GameRegistry.register(wioDigitalInputBlock, wioBlockDiRegistryName);
        GameRegistry.register(wioDiItemBlock, wioBlockDiRegistryName);
    }

    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        // some example code
        System.out.println("DIRT BLOCK >> "+Blocks.DIRT.getUnlocalizedName());
    }
}

動作確認

コードの実装が終われば、実際に動かして動作を確認します。 実行する設定をMinecraft Clientに変更して実行ボタンを押します。

f:id:g-editor:20161130162614p:plain

しばらく待つといつもどおりにゲームが起動します。今回はCreativeモード+スーパーフラットでワールドを作成します。

ワールドに入ったらインベントリを確認してみましょう。

f:id:g-editor:20161130162615p:plain

テクスチャの設定を行っていないので紫チェックの謎のブロックがあると思います。 それが今回作成したWio Nodeと通信を行えるブロックです!

これを配置することで冒頭で紹介したような機能を実現することができるようになりました。

注意点

1.Wio Nodeはコンパクトなサイズのためどこかに固定できるような穴が開いていません。そのため、通電中にどこか金属部品などに接触してショートをしてしまう可能性があります。十分に作業環境に注意して作業を進めましょう。

2.クロック回路内に高速でON/OFFを繰り返すようなリクエストを行うブロックを追加してしまうと、処理がなかなか終わりません。Wio Nodeとの通信に遅延が発生するためです。

3.デジタル入力用のブロックは200ms周期でWebAPIをポーリングしているため、スイッチを高速で切り替えると処理の取りこぼしが発生することがあります。

おわりに

IoTの技術を活用して仮想現実と現実世界とをつなぎ、今話題の映像を利用したVR(仮想現実)とは違う形でVRを実現してみました。 今回つないだのはWio Nodeとゲーム。Wio Node自体はかなり汎用的に使えるので今後は利用しているCIツールでエラーを検知したら回転灯が点灯するといったことも実現してみたいと思っています。

ぐるなびではエンジニアを募集しています。ビールとIoTをこよなく愛するエンジニアの方は一度お話しましょう!


岩田直樹

ボルダリングとロードバイクが好きなエンジニアっぽいヘンジニア。
三度の飯とビールが好き。IoT界隈やフロントエンド、アプリの勉強会と幅広く出没してます。
好きなエディタはVimです。