Raspberry Pi B+ + OpenECHO でECHONET Lite対応機器自作

相も変わらず新しいことにチャレンジすることができていませんが、

  • ちょっとECHONET Lite対応機器が必要になった
  • Raspberry Pi B+を買ったまま放置していた

ということで、以前一度やっていますが、改めてRaspberry Pi B+をECHONET Lite機器として動作させてみます。前回はLチカで終わってしまったので、今回はほんのちょっとだけ進化させて、電気錠として振る舞ってくれるものを作ってみます。。。。といっても、開状態では緑色LED、閉状態では赤色LEDを光らせるだけなのでほぼ同じですが。

「その程度ならわざわざ実デバイスにせんでもシミュレータとかエミュレータとかでよいではないか!」と言われたらそれはもっともなんですが、やっぱり実物が動いているのが見えるのと見えないのとでは、「ホントにできるんだ!」という信頼感が全然違うと思うのです。

ということで、始めます。

1. Raspberry Pi B+の立ち上げ

ここからNOOBSをダウンロード。この記事の作成時点では、バージョンは1.3.10。”Offline and network install”のZIPファイルを選択。

ダウンロードできたら、解凍して”INSTRUCTIONS-README.txt”をチェック。SDカードのフォーマットが必要なので、Macを使っている人はここから”SD Formatter 4.0 for Mac”をダウンロード(もちろんWindows版もあります)。

SDカード(B+モデルはマイクロSDカードを使います)をPCに挿したら、先ほどの”SD Formatter”を起動。適当な名前をつけて、上書きフォーマットを実行。ちなみに、今回はSanDiskの”Ultra Plus microSDHC UHS-I カード(8GB, CLASS10)”を使っています。

フォーマットが正常に終了したら、NOOBS_v1_3_10の中身を丸ごと全部、フォーマットしたSDカードにコピー。コピーが完了したら、Raspberry PiにSDカード、マウス、キーボード、ついでにWi-Fiモジュールを挿して、電力供給開始。

2. Raspberry Pi B+の環境設定

無事にNOOBSが立ち上がったら、Raspbianを選択してインストール。インストール中に言語とキーボード設定が選べるようになっているので、それぞれ日本語とjpを選択。

インストールが完了するとRaspbianが立ち上がり、設定画面が表示されるが、要所要所文字化けしているので、一旦何も選択せずに、キーボードで”Finish”を選択してReturn(Enter)。コマンドラインに戻ったら”startx”でデスクトップ画面に移行。ターミナルを開いて、改めて

$ sudo raspi-config

で設定画面を開けば、文字化けなしで設定可能。とりあえずはタイムゾーンだけ”アジア->東京”に設定して再起動すればOK。

ついでに、デスクトップにあるWi-Fi Configから無線接続の設定も行っておく。そうするとRaspberry PiにIPアドレスが割り振られるので、この先は普段使いのPCからSSHログインで作業できるようになる。

3. Java, Processingの設定

自分が以前書いたとおり。2014/9/13時点でもそのまま同じようにいける。

4. OpenECHOの設定

自分が以前書いたとおり。今回は電気錠を作るので、”OpenECHO-master/Processing/libraries/OpenECHO/examples/Tutorial6b_ElectricLock/TTutorial6b_ElectricLock.pdeに、pi4jのLチカサンプルコードを組み合わせます。

import java.io.IOException;
import processing.net.*;

import com.sonycsl.echo.Echo;
import com.sonycsl.echo.EchoProperty;
import com.sonycsl.echo.eoj.EchoObject;
import com.sonycsl.echo.eoj.device.DeviceObject;
import com.sonycsl.echo.eoj.profile.NodeProfile;
import com.sonycsl.echo.eoj.device.housingfacilities.ElectricLock;

import com.sonycsl.echo.processing.defaults.DefaultNodeProfile;

import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalOutput;
import com.pi4j.io.gpio.PinState;
import com.pi4j.io.gpio.RaspiPin;

GpioController gpio;
GpioPinDigitalOutput closePin, openPin;


// 電気錠の在室・不在状態のプロパティは必須ではありません。
// このようなプロパティを実装する際は対応するメソッドをオーバーライドし
// プロパティマップに登録します。
public class MyElectricLock extends ElectricLock {

  byte[] mStatus = {0x30};
  byte[] mLocation = {0x00};
  byte[] mVersion = {0x01, 0x01, 0x61, 0x00};
  byte[] mFaultStatus = {0x42};
  byte[] mManufacturerCode = {0,0,0};

  byte[] mLockStatus = {0x41}; // 0x41:Open 0x42:Close
  byte[] mOccupantStatus = {0x42};

  // 必須でないプロパティマップは明示的に追加登録します。
  protected void setupPropertyMaps() {
    // 必須なものは、super.setupProperyMapsで登録されています。
    super.setupPropertyMaps();

    // 状態変化時アナウンスプロパティマップに登録する際はaddStatusChangeAnnouncementProperty,
    // Setプロパティマップに登録する際はaddSetProperty,
    // Getプロパティマップに登録する際はaddGetPropertyを使います.
    addGetProperty(EPC_OCCUPANT_NON_OCCUPANT_STATUS);
  }

  protected byte[] getOperationStatus() {  return mStatus;  }
  protected boolean setInstallationLocation(byte[] edt) {
    changeInstallationLocation(edt[0]);
    return true;
  }
  protected byte[] getInstallationLocation() {return mLocation;}
  public void changeInstallationLocation(byte location) {
    if(mLocation[0] == location) return ;
    mLocation[0] = location;
    try {
      inform().reqInformInstallationLocation().send();
    } catch (IOException e) { e.printStackTrace(); }
  }
  protected byte[] getStandardVersionInformation() {return mVersion;}
  protected byte[] getFaultStatus() {  return mFaultStatus;}
  protected byte[] getManufacturerCode() {return mManufacturerCode;}
  protected boolean setLockSetting1(byte[] edt) {
    //mLockStatus = edt;
    mLockStatus[0] = edt[0];
    if(mLockStatus[0] == 0x41){ // Command: Open
      openPin.setState(PinState.HIGH);
      closePin.setState(PinState.LOW);
    }else{ // Command: Close
      openPin.setState(PinState.LOW);
      closePin.setState(PinState.HIGH);
    }

    try{
      inform().reqInformLockSetting1().send();
    }catch(IOException e){
      e.printStackTrace();
    }

    return true;
  }
  protected byte[] getLockSetting1() {
    return mLockStatus;
  }

  // 在室・不在状態を得るメソッドをオーバーライドします.
  protected byte[] getOccupantNonOccupantStatus() {return mOccupantStatus;}
}

void setup() {
  // System.outにログを表示するようにします。
  Echo.addEventListener( new Echo.Logger(System.out) ) ;


  try {
    Echo.start( new DefaultNodeProfile(), new DeviceObject[] {
      new MyElectricLock()
    }
    );
    NodeProfile.getG().reqGetSelfNodeInstanceListS().send();
  }
  catch( IOException e) {
    e.printStackTrace();
  }

  // Set pi4j
  gpio = GpioFactory.getInstance();
  closePin = gpio.provisionDigitalOutputPin(RaspiPin.GPIO_00, "CloseLED", PinState.HIGH);
  openPin  = gpio.provisionDigitalOutputPin(RaspiPin.GPIO_02, "OpenLED", PinState.LOW);

  println("Started") ;
}

ちなみにこの.pdeファイルとその格納フォルダの名前を”ElectrickLock”とか”MyElectricLock”(<-ソース内での継承クラス名)にしてしまうと起動に失敗するので注意。

ここまでくれば、Raspberry Pi上でProcessingを実行すれば、とりあえずはECHONET Liteデバイス(電子錠)として見えるようになってくれているはずです。ちなみに電子工作はこんな感じで超やっつけです。

th_electricLock

一応、開状態にすると緑色LED、閉状態にすると赤色LEDが点灯します。この動作確認用に書いた、他のPCからこのECHONET Liteデバイス(電子錠)を見つける用のProcessingソースも書いておきます。

import controlP5.*;

import com.sonycsl.echo.Echo;
import com.sonycsl.echo.node.EchoNode;
import com.sonycsl.echo.eoj.EchoObject;
import com.sonycsl.echo.EchoProperty;
import com.sonycsl.echo.eoj.profile.NodeProfile;
import com.sonycsl.echo.eoj.device.DeviceObject;
import com.sonycsl.echo.eoj.device.housingfacilities.ElectricLock;

import com.sonycsl.echo.processing.defaults.DefaultNodeProfile;
import com.sonycsl.echo.processing.defaults.DefaultController;

// Use to create GUI
ControlP5 cp5;

// Set Appliace IP Address
final String targetIp = "192.168.24.62"; // Raspberry Piのアドレス

ElectricLock myLock;

static final int ELECTRIC_LOCK = 0;

String[] applianceName = {
  "ElectricLock"
};

int applianceNum = applianceName.length;
boolean applianceFound[] = {false};

String[] btFnName = {
  "searchAppliances",
  "lockOpen",
  "lockClose"
};

String[] btLabel = {
  "Search Appliances",
  "ElectricLock: OPEN",
  "ElectricLock: CLOSE"
};

void setup(){
  // Set display window size
  size(400,400);
  frameRate(30);
  myDraw();

  // Set GUI
  textSize(12);
  cp5 = new  ControlP5(this);

  int btFnNum = btFnName.length;
  for(int i=0;i<btFnNum;i++){
    /* // Processing 2.1.1
    Button bt = cp5.addButton(btFnName[i])
                   .setCaptionLabel(btLabel[i])
                   .setSize(200,20)
                   .setPosition(0,120+30*i);
    */
    // Processing 2.0.3
    ControlP5 bt = new ControlP5(this);
    bt.addBang(btFnName[i], 0, 120+40*i, 100, 20);
    bt.controller(btFnName[i]).setLabel(btLabel[i]);
  }
  
  // Set OpenEcho
  try{
    Echo.start(new DefaultNodeProfile(), new DeviceObject[]{new DefaultController()});
  }catch(IOException e){
    e.printStackTrace();
  }
  
  Echo.addEventListener(new Echo.Logger(System.out));
  Echo.addEventListener(new Echo.EventListener(){
    
    public void onNewElectricLock(ElectricLock device){
        super.onNewElectricLock(device);
        if(!device.getNode().getAddress().getHostAddress().equals(targetIp)){
          println("A ElectricLock has found, but this is NOT yours.");
          return;
        }
        
        println("Your ElectricLock has Found!");
        applianceFound[ELECTRIC_LOCK] = true;
        myDraw();
        myLock = (ElectricLock)device;
        
        device.setReceiver(new ElectricLock.Receiver(){
          // Set Callback to get return values
          
          protected void onSetOperationStatus(EchoObject eoj, short tid, byte esv, EchoProperty property, boolean success){
            super.onSetOperationStatus(eoj, tid, esv, property, success);  
            if(!success){
              println("error in call reqSetOperationStatus");
              println("setOperationStatus Param=" + toHexStr(property.edt));
              return;
            }
            showMessage("Success: onSetOperationStatus");
          }
         
          protected void onGetOperationStatus(EchoObject eoj, short tid, byte esv, EchoProperty property, boolean success){
            super.onGetOperationStatus(eoj, tid, esv, property, success);
            if(!success){
              showMessage("error in call reqGetOperationStatus");
              return;
            }
            showMessage("OperationStatus=" + toHexStr(property.edt));
          }   
          
          protected void onSetLockSetting1(EchoObject eoj, short tid, byte esv, EchoProperty property, boolean success){
            super.onSetLockSetting1(eoj, tid, esv, property, success);  
            if(!success){
              println("error in call reqSetLockSetting1");
              println("setLockSetting1 Param=" + toHexStr(property.edt));
              return;
            }
            showMessage("Success: onSetLockSetting1");
          }
         
          protected void onGetLockSetting1(EchoObject eoj, short tid, byte esv, EchoProperty property, boolean success){
            super.onGetLockSetting1(eoj, tid, esv, property, success);
            if(!success){
              showMessage("error in call reqGetLockSetting1");
              return;
            }
            showMessage("LockSetting1=" + toHexStr(property.edt));
          }
          
          
        }); // End of setReceiver
    } // End of onNewElectricLock   
  }); // End of addEventListener
  
  // Search controllable appliances
  searchAppliances();
    
} // End of setup()

void draw(){

};

void myDraw(){
  background(0,0,0);
  for(int i=0;i<applianceNum;i++){
    if(applianceFound[i]){
      fill(0,255,0); // Green
      text(applianceName[i] + "  [FOUND!]", 0, 20+20*i);
    }else{
      fill(255,255,255); // white
      text(applianceName[i] + "  [NOT FOUND]", 0, 20+20*i);
    }
  } 
}

public void searchAppliances(){
  println("----");
  try{
    // find other devices
    NodeProfile.getG().reqGetSelfNodeInstanceListS().send();
  
    println("Wait 3 seconds ...");
    try{
      Thread.sleep(3000);
    }catch(InterruptedException e){
      e.printStackTrace();
    }
        
    println("----");
    EchoNode[] nodes = Echo.getNodes();
    for(int i=0;i<nodes.length;i++){
      EchoNode en = nodes[i];
      println("node id=" + en.getAddress().getHostAddress());
      println("node profile=" + en.getNodeProfile());
      DeviceObject[] dos = en.getDevices();
      println("There are " + dos.length + " devices in this node");
      for(int j=0;j<dos.length;j++){
        DeviceObject d = dos[j];
        String typeName = d.getClass().getSuperclass().getSimpleName();
        println("device type = " + typeName);
      }
      println("----");
    }
  }catch(IOException e){
    e.printStackTrace();
  }
}


public void lockOpen(){
  println("--lockOpen--");
  if(applianceFound[ELECTRIC_LOCK]){
    try{
      myLock.set()
       .reqSetLockSetting1(new byte[]{0x41}) // Open
       .send();
    }catch(IOException e){
       e.printStackTrace();
    }
  }else{
    showMessage("There are NO your controllable ElectrickLocks.");
  }
}

public void lockClose(){
  println("--lockClose--");
  if(applianceFound[ELECTRIC_LOCK]){
    try{
      myLock.set()
       .reqSetLockSetting1(new byte[]{0x21}) // Close
       .send();
    }catch(IOException e){
       e.printStackTrace();
    }
  }else{
    showMessage("There are NO your controllable ElectrickLocks.");
  }
}


String toHexStr(byte[] arg){
  String ret = "" ;
  for(int i=0;i<arg.length;++i){
    ret += Integer.toHexString(arg[i] & 0xff) + " ";
  }
  return ret;
}

String toIntStr(byte[] arg){
  String ret = "" ;
  for(int i=0;i<arg.length;++i){
    ret += Integer.toString(arg[i] & 0xff) + " ";
  }
  return ret;
}

void showMessage(String message){
   myDraw();
   fill(255,255,255); // white
   text(message, 0, 350);
   println(message);
}

今のところECHONET Liteデバイスを立ち上げるためには、

  1. Raspberry PI起動、ログイン
  2. GUI環境立ち上げ(startx)
  3. Processing IDEでELElectricLockテストプログラム実行

という手続きが必要で、気軽にECHONET LIteのテストをできる環境じゃありません。理想は、Rasbperry Piの電源を入れるだけで自動的にECHONET Liteデバイスとして見えるようになることです。

というわけで、そのための追加作業を次回やりたいと思います。。。時間があれば。