Java
DLNA
IoT
UPnP
SORACOM

DLNA対応TVをSORACOM Inventoryに登録してWebコンソールから操作する

はじめに

ソラコムの安川です。4月に行われたSORACOM Technology CampにてSORACOM InventoryをPublic Betaとしてリリースさせていただきました。皆様お試し頂けましたでしょうか?

リンク先のブログにもある通り、Public Beta化に際しては、Private BetaやLimited Previewでお試しいただいたお客様からのフィードバックを基に新機能を追加した形でリリースしたのですが、その新機能の1つにカスタムモデル対応というのがあります。今回はその機能を活用してDeep Diveしてみたいと思います。

SORACOM InventoryではOMA DM LwM2Mという標準化されたデバイス管理のためのプロトコルに従って、デバイス上で走るエージェントとSORACOM側のサーバが通信をします。リリース当初は標準で定められたオブジェクト定義のみの対応だったため、温度センサや加速度センサなど、汎用的なデバイスは既存の定義に従って実装が可能だったものの、標準に当てはまらないデバイスを繋げようとするとうまくInventoryで表現できないという課題がありました。

今回リリースしたカスタムモデル対応は、その名の示すとおり、任意のオブジェクトモデル定義を登録して、SORACOM Inventory上でサーバとデバイスがやり取りする際のメッセージ解釈に利用したり、Webコンソールでの表示に利用したりできる機能です。すなわちこの機能を使えば標準で定義されていないデバイスタイプや仕様でも、独自の定義を登録してInventoryで管理できてしまうというわけです。

前置きが長くなりましたが、この機能を開発してる最中からずっとやりたかったことがありまして、それを実際やってみた記事をずっと書きたいと思ってました。それが何というと、タイトル通り、目の前にあるTVをSORACOM Inventoryで操作するって話です!

(ちなみにこのブログはDeep Dive系のやってみた記事のつもりなので、締め切りギリギリですがTry SORACOM チャレンジキャンペーンに応募して見たいと思います。対象: 企業・個人問わずどなたでもご参加いただけます。 って書いてあったので。クールなノベルティ当たるといいなぁ。)

DLNA対応TVを操作する

昨今は一般のご家庭のTVが大抵DLNAに対応していて、EthernetもしくはWiFiで家庭内のホームネットワークに接続してる時代となりました。皆様のご自宅のTVもきっと普段からDLNAで他の機器とやり取りしていることと思います。眼の前にそういう環境がある皆様におかれましては、このブログに書いてあることを実際にやってみることができます。

具体的には、DLNA対応TVをUPnPでDiscoverして、OMA DM LwM2MプロトコルSORACOM Inventoryに接続、InventoryのWebコンソールで操作するって話です。(アルファベットの略語アレルギーの方、すみません、拒絶反応が出てると思いますが、とりあえずなんかいろいろごちゃごちゃやってTVを操作すんのねと思っていてくれれば大丈夫です。)

2015年のアドベントカレンダーのときにThingをDiscoverしてBeamっていうブログを書きましたが、そのときはDLNA対応TVをUPnPでDiscoverしてBeam MQTTでAWS IoTに登録して操作するという話でした。そのMQTT, AWS IoTがLwM2M, SORACOM Inventoryで置き換わったと思えばわかりやすいですね(わかりやすい、ですよね?)。

ちなみに当時はnode.jsでUPnP<->MQTTエージェント的なものを作りましたが、SORACOM Inventoryの場合はJava Clientライブラリを使うとカスタムモデル定義からのエージェント構築が非常にスムーズなので、今回はJavaで行ってみたいと思います。

今回も前回のブログ執筆時と同様に、動画の再生、Volumeの上げ下げ、TVの電源On/Offの認識あたりのユースケースを中心に実装してみたいと思います。

実際に動いてる様子を動画に撮ったので、イメージを掴むためにまずはこちらをご覧いただきつつ、これを実装する手順が以降に書いてあるとご理解いただけると幸いです。

DLNA TVをSORACOM Inventoryでコントロール

実際に作ってみる

SORACOM InventoryのSDKとしては、 @c9katayama 謹製のJava Clientライブラリが公開されていて、そのReadmeに詳しいAgentの実装手順 が書いてあるのでこれを見ながら進めていくとスムーズです。

具体的にはDLNA TV用にカスタムモデルを定義して、その定義ファイルからJavaのコードを生成して埋めていくという手順に従っていきます。

1.カスタムモデル定義を作成する

モデル定義はXMLかJSON、どちらにも対応しています。
今回はJava ClientライブラリのReadmeにあった例を雑にコピって、編集したのでXMLとなっています。
今回のユースケースを実装するにあたってはUPnPの RenderingControlサービス と、 AVTransportサービス が必要なので、それぞれの仕様書を参考にしつつActionをパラメータやアクションを定義していきます。

悩んだのは各リソースにつけるIDの値なのですが、とりあえず各パラメータやアクションの仕様書上のセクション番号を使うという非常に雑な選択肢で今回はいくことにしました。

こんな感じでパラメータやActionの定義を書いていきます。

resource-example.xml
  <Item ID="2216">
    <Name>Volume</Name>
    <Operations>R</Operations>
    <MultipleInstances>Single</MultipleInstances>
    <Mandatory>Mandatory</Mandatory>
    <Type>Integer</Type>
    <RangeEnumeration />
    <Units></Units>
    <Description>Current audio volume of the renderer</Description>
  </Item>

(全部やろうとすると心が折れるので、実装したいユースケースに絞ってやりました。。)

出来上がったオブジェクト定義ファイルがこちら:
- upnp-rendering-control.xml
- upnp-av-transport.xml

これをカスタムモデルとして登録しておけばSORACOM Inventory側の準備は完了で、このあとデバイスがオンラインになった際にコンソールにも対応する項目が自動的に現れるようになります。

image.png

カスタムオブジェクトモデル定義に関する詳細はこちらをご参照ください。

2. オブジェクト定義ファイルからJavaコードを生成してメソッドを実装する

Inventory client libraryはオブジェクト定義からのクラスファイル生成もサポートしてる優れものなので、その機能を使ってみましょう。

Githubにあるサンプルコードを参考にしながら前のセクションで作ったファイルからクラスファイルを生成するように少し変更したSource Generatorのコードがこちらです。

JavaSourceGenerator.java
package models;

import io.soracom.inventory.agent.core.util.TypedAnnotatedObjectTemplateClassGenerator;

import java.io.File;

public class JavaSourceGenerator {

    public static void main(String[] args) {
        String javaPackage = "models";
        File sourceFileDir = new File("src/main/java");
        TypedAnnotatedObjectTemplateClassGenerator generator = new TypedAnnotatedObjectTemplateClassGenerator(
                javaPackage, sourceFileDir);
        File[] modelFiles = new File("src/main/resources").listFiles();
        for (File modelFile : modelFiles) {
            generator.generateTemplateClassFromObjectModel(modelFile);
        }
        System.out.println("Finished generating Java source.");
        System.exit(0);
    }
}

これを実行すると、 models パッケージに対応するクラスが生成されます。
- UPnPAVTransportServiceObject.java
- UPnPRenderingControlServiceObject.java

中身を見てみるとかこんな感じ。

UPnPRenderingControlServiceObject.java
package models;
import io.soracom.inventory.agent.core.lwm2m.*;
import java.util.Date;
import org.eclipse.leshan.core.node.ObjectLink;

/**
 * LwM2M device object model for UPnP Rendering Control service.
 **/
@LWM2MObject(objectId = 30001, name = "UPnP Rendering Control Service", multiple = true)
public class UPnPRenderingControlServiceObject extends AnnotatedLwM2mInstanceEnabler {

    /**
     * Mute status of the renderer
     **/
    @Resource(resourceId = 2215, operation = Operation.Read)
    public Boolean readMute()   {
        throw LwM2mInstanceResponseException.notFound();
    }

    /**
     * Current audio volume of the renderer
     **/
    @Resource(resourceId = 2216, operation = Operation.Read)
    public Integer readVolume() {
        throw LwM2mInstanceResponseException.notFound();
    }

    /**
     * Sets the renderer audio volume.
     **/
    @Resource(resourceId = 2430, operation = Operation.Execute)
    public void executeSetVolume(String executeParameter)   {
        throw LwM2mInstanceResponseException.notFound();
    }
}

あとは各メソッドに対応するUPnP アクションを呼んだりするように実装していけばいいというわけです。流れに沿っていく感じなのでやるべきことがわかりやすくていいですね!

3. LwM2Mリソースに対応する各メソッドを実装する

今回はUPnPのデバイスを対象にしているので、次はUPnPのライブラリを使って実装していくことになります。軽くググった感じだと clingというライブラリ が使いやすそうだったのでこれを使うことにしました。

build.gradle に下記のように追加してあげれば準備は完了です。

build.gradle
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'application'

repositories {
  mavenCentral()
  maven { url 'https://soracom.github.io/maven-repository/' }
  maven { url 'http://4thline.org/m2/' } // <-追加
}

def INVENTORY_AGENT_VERSION="0.0.5"

dependencies {
    compile "io.soracom:soracom-inventory-agent-for-java-core:$INVENTORY_AGENT_VERSION"
    compile "org.fourthline.cling:cling-core:2.1.1" // <-追加
    testCompile 'junit:junit:4.12'
}

jar {
    manifest {
        attributes "Main-Class": "App"
    }
}


mainClassName = 'App'

実装した UpnpRenderingControlServiceObject のメソッドをいくつか見てみるとこんな感じになりました。

UpnpRenderingControlServiceObject.java
    /**
     * Current audio volume of the renderer
     **/
    @Resource(resourceId = 2216, operation = Operation.Read)
    public Long readVolume()    {
        ActionInvocation actionInvocation =
                new ActionInvocation(renderingControlService.getAction(ACTION_GET_VOLUME));
        actionInvocation.setInput(ARG_INSTANCE_ID, new UnsignedIntegerFourBytes(0));
        actionInvocation.setInput(ARG_CHANNEL, VALUE_CHANNEL_MASTER);
        Map<String, ActionArgumentValue> output = UpnpController.getInstance().executeUpnpAction(actionInvocation);
        return ((UnsignedIntegerTwoBytes)output.get(ARG_CURRENT_VOLUME).getValue()).getValue();
    }

    /**
     * Sets the renderer audio volume.
     **/
    @Resource(resourceId = 2430, operation = Operation.Execute)
    public void executeSetVolume(String executeParameter)   {

        ActionInvocation setVolumeInvocation =
                new ActionInvocation(renderingControlService.getAction(ACTION_SET_VOLUME));
        setVolumeInvocation.setInput(ARG_INSTANCE_ID, new UnsignedIntegerFourBytes(0));
        setVolumeInvocation.setInput(ARG_CHANNEL, VALUE_CHANNEL_MASTER);
        setVolumeInvocation.setInput(ARG_DESIRED_VOLUME, new UnsignedIntegerTwoBytes(executeParameter));
        UpnpController.getInstance().executeUpnpAction(setVolumeInvocation);
    }

要はLwM2Mで受け取ったリクエストをUPnPの対応するActionに変換していく感じです。どちらのプロトコルも根本となっているデバイスの表現の仕方のコンセプトが近いので割とすんなりと変換可能でした。

実装が完了した2つのファイルはこちらです:
- UpnpAvTransportServiceObject.java
- UpnpRenderingControlServiceObject

この2つのオブジェクト(と基本となるDeviceオブジェクト)を実装したAgentを作ればInventoryに登録してAPI及びUser Consoleで操作可能になります。

今回は MediaRendererAgent という名前で作ってみました。
ここには載せてませんが、UPnPデバイスを見つけたらLwM2MのDeviceオブジェクトに対応させる UpnpDeviceAgent というクラスをベースクラスとして別途作っておいて、MediaRendererAgentはそれを継承して今回定義した2つのオブジェクトを追加するような作りにしてあります。

MediaRendererAgent.java
package org.yasukawa.inventory.upnp.media_renderer;

import org.fourthline.cling.model.meta.RemoteDevice;
import org.fourthline.cling.model.meta.Service;
import org.fourthline.cling.model.types.ServiceType;
import org.yasukawa.inventory.upnp.UpnpDeviceAgent;

public class MediaRendererAgent extends UpnpDeviceAgent {
    public static final String MEDIA_RENDERER_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1";
    public static final String RENDERING_CONTROL_SERVICE_TYPE = "urn:schemas-upnp-org:service:RenderingControl:1";
    public static final String AV_TRANSPORT_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1";
    public static final String ATTR_LAST_CHANGE = "LastChange";
    public static final String ATTR_INSTANCE_ID = "InstanceID";

    public MediaRendererAgent(RemoteDevice device){
        super(device);
        for ( Service service : device.getServices()){
            System.out.println(service.getServiceType());
            System.out.println(service.getServiceId());
        }
        super.initializer.addInstancesForObject(new UpnpRenderingControlServiceObject(
                device.findService(ServiceType.valueOf(RENDERING_CONTROL_SERVICE_TYPE)))
        );
        super.initializer.addInstancesForObject(new UpnpAvTransportServiceObject(
                device.findService(ServiceType.valueOf(AV_TRANSPORT_SERVICE_TYPE)))
        );
    }
}

4. UPnP デバイスをDiscoverしたらInventory Agentを作ってRegisterする

ここまでできたらあとはUPnPのデバイスをDiscoveryして、TVなどMedia Rendererが見つかったらMedia Renderer Agentのインスタンスを作ってRegisterするようにしてあげればInventoryのAPIでTVが操作可能になります!熱い!

今回使ってる clingだと、UpnpRegistryListener というインターフェイスを実装したクラスのインスタンスをUPnP control pointに登録した上でDiscoveryをスタートしてあげれば、デバイスが見つかるたびにコールバックしてもらえるような作りになってました。なのでそのコールバックをもらって、見つかったデバイスがMedia RendererだったらMedia Renderer Agentのインスタンスを作ってスタートするようなロジックを書く感じになってます。具体的には以下のような感じです。

InventoryUpnpRegistryListener.java
    @Override
    public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
        logger.info("Device {} with type {} is added", device.getIdentity().toString(), device.getType().toString());
        if (MediaRendererAgent.MEDIA_RENDERER_DEVICE_TYPE.equals(device.getType().toString())){
            logger.info("Media renderer found: " + device.getDetails().getFriendlyName());
            MediaRendererAgent agent = new MediaRendererAgent(device);
            logger.info("Starting agent for {} and registering to SORACOM Inventory",
                    device.getIdentity().toString());
            agent.start();
            agentMap.put(device.getIdentity().toString(), agent);
        }
    }

    @Override
    public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
        logger.info("Device {} with type {} is removed", device.getIdentity().toString(), device.getType().toString());
        if (agentMap.containsKey(device.getIdentity().toString())){
            logger.info("Stopping agent for {} and deregistering from SORACOM Inventory",
                    device.getIdentity().toString());
            agentMap.remove(device.getIdentity().toString()).stop();
        }
    }

ついでにデバイスが削除されたイベントが来たらAgentをStopしてあげるようにしておきます。こうすることでTVの電源をOffにしたらInventoryの方でもOfflineになるという動きがちゃんと実現できます。

あとはいよいよメインのApp.javaでUPnP Control Pointにこの UpnpRegistryListenerを登録してDiscoveryをスタートすれば完成!

App.java
import org.fourthline.cling.UpnpService;
import org.fourthline.cling.UpnpServiceImpl;
import org.fourthline.cling.model.message.header.STAllHeader;
import org.yasukawa.inventory.upnp.InventoryUpnpRegistryListener;
import org.yasukawa.inventory.upnp.UpnpController;

public class App {

    public static void main(String[] args) throws Exception {

        UpnpService upnpService = new UpnpServiceImpl();
        UpnpController.initialize(upnpService);
        upnpService.getRegistry().addListener(new InventoryUpnpRegistryListener(upnpService));
        upnpService.getControlPoint().search(new STAllHeader());
    }
}

実際に動かしてみる

今回作ったコードは こちらのレポジトリ においてあり、下記の手順で配布用のzip archiveを作ることができます。

$ git clone https://github.com/kntyskw/inventory-upnp
$ cd inventory-upnp
$ gradle distZip

これで作られた ./build/distributions/inventory-upnp.zipを家のLANとSORACOM Air両方に繋がったラズパイ等にコピーして展開すれば実行準備は完了です(JREはある前提で)。

image.png

あとはこのアーカイブをラズパイ等のUPnPゲートウェイとして動かすデバイスにデプロイして実行します。

余談ですが、最近僕はラズパイにソフトウェアをデプロイするお手軽な方法として DockerコンテナをIoTデバイスにデプロイできる resin.io を使ってます。詳細は本家のWebsiteを見ていただければと思いますが、resin.ioの環境をセットアップ済みの場合、下記の手順で更新したコンテナをラズパイにデプロイできちゃいます。

$ git add inventory-upnp.zip
$ git commit -m "add/update inventory-upnp.zip"
$ git push resin master

さらに、どこにいてもresin.ioのWebコンソールからラズパイにSSHできちゃったりしますし、Rebootやコンテナの更新も思いのままです。よかったら皆様もお試しください。

手順はどうあれ、アーカイブをデプロイして展開したら、展開先のディレクトリで下記のコマンドを実行します。

$ ./bin/inventory-upnp

するとラズパイがUPnPゲートウェイとして動作して、DLNA対応TVが見つかればそれをSORACOM Inventoryに登録して操作可能にしてくれます!そこから先は冒頭の動画の通りに操作可能というわけです。

image.png

おわりに

いかがでしたでしょうか?UPnPとLwM2M(もっというとOMA DM)のデバイスの取扱いの概念が根本的に似ていることもあって、スムーズにUPnPのデバイスをSORACOM Inventoryに登録するまでを実装できた感じでしたね。

今回はUPnPを取り上げましたが、Discoveryとコマンド実行、EventへのSubscribeなどの仕組みがあるプロトコルであれば同様にInventory Agentを実装してSORACOM Inventoryから利用可能にすることができます。例えばAppleTVなんかもBonjour使えば同様にいけると思います。

これはすなわち、プロトコルゲートウェイとなるようなデバイス上にSORACOM Air SIMとInventory Agentを動作させれば周囲のデバイスをまとめてInventoryから管理可能にできるというわけです。(しかもSORACOM Air + Inventoryの組み合わせならプロトコルゲートウェイへの認証情報共有も自動!)ぜひ皆様もご家庭内のデバイスをSORACOM Inventoryに登録してどこからでも監視・制御可能にしてみてはいかがでしょうか?

ちなみにDiscovery、EventへのSubscribeで思い出しましたが、皆様7月4日のSORACOM DiscoveryというEventへのSubscribeはお済みでしょうか?お済みでない方は残席は少なくなりつつあるもののまだ間に合うと思いますので、こちらのリンク からぜひお申込みください!ちなみにこちらのリンク からだと私の紹介というフラグがつくのでどうぞ こちらのリンク からお願いします。(しつこいw)

ちょうど今からSilicon Valleyから東京に向かう便の搭乗待ちをしてます。今年も皆様とSORACOM Discovery の会場でUPnPとSORACOM Inventoryみたいなもの含め、マニアックなトピックでディスカッションできるのを楽しみにしております!