34
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

blockchain上で自動支払を行うスマートコンセントを作ってみる

Posted at

自動支払をするスマートコンセント

Ethereumのスマートコントラクトを書いてみたくて、スマートコンセントを練習として作ってみました。

物理的にはひとまずこのような形状をしています。筐体を3Dプリントしようとしましたが、一旦力尽きました。。

Screen Shot 2017-12-28 at 18.17.55.png

概要は下記のような感じです。

Screen Shot 2017-12-28 at 18.17.55.png

このコンセプトは「C2Cで一般ユーザが別のユーザに電気を販売することができる」モデルですが、自分でサーバは立てておらず、2つのデバイスがBlockchainを経由してtrasnsactionを行います。uberやairbnbとは違い中央集権的なサーバが不要となっているため、運営が不在の状況でもサービスを提供することができるかもしれません。1

使っているフレームワークなどは以下のとおりです。

Screen Shot 2017-12-28 at 18.18.02.png

それぞれのモジュールの間の通信はこのようになっています。

Screen Shot 2017-12-28 at 18.18.07.png

似たようなプロジェクトが幾つか発表されています。

  • Nayuta
    • コンセント側は同じようなコンセプトで、支払いをスマホで行っているところは少し違うようです。今回はスマホレスにしてみたかったため、プラグにコンピュータを組み込みました
    • プロダクトデザインカッコいいな。
  • BlockCharge
    • 電気スタンドと電気自動車の間で自動で支払いを行うかたちです。
    • ドローンともつながってワイヤレス給電する未来いいな
  • Toyota Open Road Project
    • blockchainを使っているかはわからないですが、似たコンセプトです。
    • プロジェクト更新止まっちゃってるのかな、、

githubこちらですが、まだバグバグなので頑張ります、、

スマートコントラクトを作成する

Ethereum上で動作するスマートコントラクトをsolidityで書いています。
シーケンス図の3で、コンセントからhashが送られてくると、コントラクトはhashをmapに保存します。

mapping(bytes32 => address) reserved;

function sendOutletHash(bytes32 _hash) public {
    reserved[_hash] = msg.sender;
}

その後、シーケンス図8で、keyをコントラクトに送信します。ここで、keyとhashの照合が行われ、支払い(coinの移動)が実施されます。正常に取引が完了すると、eventがコンセントに送信されます(シーケンス図の10)。

mapping(address => uint256) public coinBalance; // remaining coin of each devices

event log(string);
event startSupply(address);

function payFromPlug(string _key, uint256 _coin) public {
    bytes32 encrypted = keccak256(_key);
    address targetOutlet = reserved[encrypted];
    if (targetOutlet == 0) {
        log("ERROR: Cannot find ready outlet");
        return;
    }

    if ((int256)(coinBalance[msg.sender] - _coin) < 0) {
        log("ERROR: You dont have enough coin to supply!");
        return;
    }
    coinBalance[msg.sender]   -= _coin;
    coinBalance[targetOutlet] += _coin;
    remainingTime[encrypted] = now + 60 * 1 minutes;

    startSupply(targetOutlet);
}

ハードウェアを作成する

ハードウェアはざっくり以下のような感じです。赤線が電源、青線がGPIOです。

プラグ側

Screen Shot 2017-12-28 at 19.42.47.png

コンセント側

Screen Shot 2017-12-28 at 19.42.50.png

なお、コンセント側を家の電源(ブレーカーから壁の中を伸びてるやつ)に直接(コンセント等を経由せず)繋げる場合、電気工事士の資格が必要なので注意しましょう。

プラグ側のコードを作成する

プラグの情報処理は、コンセントに刺されて起動するところから始まります2

全体の流れ、こんな感じです。

plug/wifi.js
    setup() {
        this.disconnect().then(()=> {
            return this.connectOutlet();
        }).then(()=> {
            return this.getKeyFromOutlet();
        }).then(()=> {
            return this.disconnect();
        }).then(()=> {
            return this.connectMain();
        }).then(()=> {
            return this.sendEvent();
        }).catch((err) => {
            log.warn(err);
        });
    }

シーケンス図6で、wifiの接続先を通常のアクセスポイントからコンセントのローカルAPに変更するのですが、wireless-toolsというモジュールを使って以下のように実現しました。wpa_supplicant.enableでWi-Fiの接続先を変更できます。

plug/wifi.js
    connectOutlet() {
        const options = {
            interface: 'wlan0',
            ssid: 'SmartOutlet',
            driver: 'nl80211'
        };
        return new Promise((resolve, reject) => {
            wpa_supplicant.enable(options, (err) => {
                if (err) {
                    reject("connectOutlet:" + err);
                }
                resolve();
            });
        });
    }

コンセントのローカルAPに接続できたら、コンセントに立っているhttpサーバからkeyを取得します。

plug/wifi.js
    getKeyFromOutlet() {
        let _this = this;
        return new Promise((resolve, reject) => {
            http.get(outlet_server_url, (res) => {
                let body = '';
                res.setEncoding('utf8');
                res.on('data', (chunk) => {
                    body += chunk;
                });
                res.on('end', (res) => {
                    _this.key = body;
                    resolve();
                });
            }).on('error', (e) => {
                reject("http error:" + e);
            });
        });
    }

最後に、接続先を通常のアクセスポイントに戻し、keyをコントラクトに送信します。

plug/coin.js
// すでにblockchain上にスマートコントラクトをデプロイしており、アドレスを知っているとします
const address_contract = "0x71A2897b2B222225855240c10114FdE25865b3fB";

const Web3 = require('web3');
const net = require('net')
// 事前に、gethはローカル(raspberry pi上)で動作し、IPCで接続できるようにしておきます。
let web3 = new Web3('/home/pi/.ethereum/rinkeby/geth.ipc', net);
let plugContract = new web3.eth.Contract(abi, address_contract);

...

    execPayFromPlug(key, coin) {
        plugContract.methods.payFromPlug(key, coin).send(
            {from: this.address, gasPrice: 2000000000, gas: 3000000},
            (error, result)=> {
                log.debug("payFromPlug result:" + result);
            }).on('transactionHash', (hash) => {
            log.debug("transactionHash: " + hash);
        })
    }

あとは、blockchainのtransactionが完了し、コンセント側で承認完了されるのを待つだけです。

コンセント側のコードを作成する

コンセント側は2つのアプリが動いています。1つはコンセントの電気の流れを制御したり、スマートコントラクトとやりとりするメインのアプリ。もう一つは小さなHTTPサーバで、シーケンス図7でプラグにkeyを渡すのに使用されます。

今回、コンセント側のRaspberry Piは2つのwifiモジュールを持つ構成としました3。一つはRaspberry Pi 3に組みおまれているもので、上記メインのアプリ用にstation modeで動作します。もう一つはUSB wifiモジュールで上記HTTPサーバにアクセスするためのAPとして動作します。

まずはWebサーバ側。非常にシンプルです。keyはメインアプリで生成されたあと、一時的に/home/pi/key.txtに記録されるとしています。

outletserver/app.js
const http = require('http');
const fs = require('fs');
const ip = '192.168.1.1';
const key = '/home/pi/key.txt';

http.createServer(function (req, res) {
    const key = fs.readFileSync(key, 'utf8');
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end(key);
}).listen(8000, ip);

次にメインのアプリ側です。
メインのアプリはプラグが接続され、スイッチが押されたことをGPIOで検知するところから始まります(シーケンス図1)。

outlet/gpio.js
    checkSwitch() {
        let value = this.readGpio(SW_IN, 'utf8');
        if (parseInt(value) == 1) {
            this.writeGpio(SSR, 1); // コンセントから電気が流れるようにする
            this.emit('switch', true);

            this.initialPowerTimer = setTimeout(() => {
                this.writeGpio(SSR, 0); // 一定時間後(処理が行われない場合)コンセントからの電気をOFFにする
                this.emit('switch', false);
            }, 60 * 1000);
        }
    }

続いて、ワンタイムkeyとhashを生成します。hashはランダムな16文字として生成したkeyをkeccak256でハッシュ化したものです。

outlet/outlet.js
    this.gpio.on('switch', (enabled) => {
        if (enabled) {
            const hashpair = _this.generateKeyAndHash(); // keyとhashを生成する
            _this.coin.sendOutletHash('0x' + hashpair.datahash); // hashをblockchainに送付
            fs.writeFile(key, hashpair.datakey); // keyをサーバに渡すため、ファイルとして保存
        }
    });
    
....

    generateKeyAndHash() {
        const l = 16;
        const c = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        const cl = c.length;
        let key = "";
        for(let i=0; i<l; i++){
            key += c[Math.floor(Math.random()*cl)];
        }
        let hash = createKeccakHash('keccak256').update(key).digest('hex');

        return {datakey: key, datahash: hash};
    }
outlet/coin.js
    // hashを送付
    sendOutletHash(hash) {
        web3.eth.personal.unlockAccount(this.address, this.password, 60)
            .then((response) => {
                this.execSendOutletHash(hash)
            }).catch((error) => {
                console.log(error);
            });
    }

    execSendOutletHash(hash) {
        plugContract.methods.sendOutletHash(hash).send(
            {from: this.address,
                gasPrice: 20000000000,
                gas: 3000000},
            (error, result)=> {
            }).on('transactionHash', (hash) => {
            log.debug("transactionHash: " + hash);
        });
    }

その後、コントラクト上でkeyhashの照合が行われ、startSupply(address)というeventがbroadcastされるので、それをsubscribeし、コンセントの電源を切らないようにすれば完成です。

outlet/outlet.js
    startPaymentWatching() {
        let subscription = web3.eth.subscribe('logs', {
            address: address_contract,
            topics: [web3.utils.sha3('startSupply(address)')]
        }, (error, result) => {
            if (error)
                log.error(error);
            this.emit('contract', true);
        });
    }

今後

まだまだバグや動きが不審な点があるので修正しつつ、以下のようなあたりを改善していきたいと思います。

  • コントラクトのテストを書く
  • 本体のテストも書く
  • 取引を見える化する
    • ここで結局Webサーバが必要になるのか、、
  • ハードウェアをもう少しちゃんとつくる
    • Raspberry Pi(Linux)じゃなくてRTOSとかで動けば、プラグデバイスの起動時間も短く、価格も安くなりそうなのですが、geth的なものをRTOSまたはOSなしで動かすことってできるのかな、、
  • 取引用コインの入出金の方法を検討する
  • テストネットではなく本線ネットにつなげてみる??

参考URLなど

  1. じゃあだれが責任をもって運用をおこなうのかとか、もうからないのに誰がつくるのかなどは別のお話です

  2. 今回は、プラグが刺されてスイッチが押された際、短時間のみ電源供給をするようにしました

  3. 今後wifiモジュール一つで動作するように修正したいと考えています

34
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?