ユニファ開発者ブログ

ユニファ株式会社システム開発部メンバーによるブログです。

BLE Nano + ホールセンサー + Raspberry Pi でドアセンサーを作る

 こんにちは、ユニファの赤沼です。みなさんのオフィスではセキュリティはどうされてますでしょうか?ユニファではエントランスから会議室エリアもしくは執務室へ入る場合にはICカードによるドアロックを設置しています。ただ、会議室エリアと執務室を隔てる扉は、いずれのエリアもセキュリティ通過後であるため特に何もしていないのですが、開きっぱなしになっているとお客様が会議室エリアを通られる際に執務室の中も見えてしまうことになり、あまりよろしくありません。そこで今回はドアセンサーを自作して、ひとまずは開きっぱなしになっていることを検知できるようにしてみたいと思います。

構成

 全体の構成としては今回はひとまずシンプルに、インターネットへのアクセスは行わず、Raspberry Pi を Central、 ドアセンサーを Peripheral として BLE で接続し、一定時間以上ドアが開きっぱなしの状態だったら Raspberry Pi にログを出力してみます。

 ドアセンサーの構成としては、今回はホールセンサーを使用し、ドア側に貼り付けた磁石と壁側に設置したホールセンサーの距離が離れた場合をドアが開いているとみなします。ホールセンサーとは磁石や電流が発する磁界を電気信号に変えるセンサーで、下記サイトに詳しい解説が掲載されていました。

www.fujiele.co.jp

 今回使用したホールセンサーは下記製品で、ユニポーラのスイッチタイプでS極を近づけると出力がLowになります。

www.aitendo.com

 今回はBLEモジュール搭載の開発ボードとして BLE Nano を使用します。ホールセンサーをブレッドボードで BLE Nano と下記の図のように接続します。左上の黒い三本足のパーツがホールセンサーです。LED は動作確認用で、ドアが開いているときは赤く点灯させます。また、今回のホールセンサーは動作電圧が4.5V〜24Vで、 BLE Nano の出力の 3.3V では電圧が不足するため、単三電池4本を電源としてホールセンサーと BLE Nano に電力を供給します。

f:id:akanuma-hiroaki:20171219064313p:plain:w300

BLE Nano のファームウェア実装

 BLE Nano は mbed OS に対応しているので、今回は mbed でファームウェアを実装します。ただ mbed OS 5 には対応していないので、 mbed OS 2 での実装になります。開発環境の構築やプロジェクトの作成については今回は割愛させていただきます。

 まずホールセンサーの値を扱うための BLE のサービスを下記のように hall_service.h として実装します。今回はとりあえずの社内利用だけなので、 Characteristic としてはホールセンサーの値だけで、 UUID も適当にしてしまっています。

#ifndef __HALL_SERVICE_H__
#define __HALL_SERVICE_H__

class HallService {
public:
  const static uint16_t HALL_SERVICE_UUID = 0xA000;
  const static uint16_t HALL_VALUE_CHARACTERISTIC_UUID = 0xA001;

  HallService(BLE &_ble, uint16_t initialValueForHallCharacteristic) :
    ble(_ble), hallValue(HALL_VALUE_CHARACTERISTIC_UUID, &initialValueForHallCharacteristic, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY)
  {
    GattCharacteristic *charTable[] = {&hallValue};
    GattService hallService(HALL_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.gattServer().addService(hallService);
  }

  void updateHallValue(unsigned short newValue) {
    ble.gattServer().write(hallValue.getValueHandle(), (uint8_t *)&newValue, sizeof(bool));
  }

private:
  BLE &ble;
  ReadOnlyGattCharacteristic<uint16_t> hallValue;
};

#endif

 そして main.cpp を下記のように実装します。

#include "mbed.h"
#include "ble/BLE.h"
#include "hall_service.h"

Serial pc(USBTX, USBRX);
DigitalOut led(P0_4, 0);
DigitalIn hallSensor(P0_5, PullUp);
BLE ble;
Ticker ticker;

const static char DEVICE_NAME[] = "DoorSensor";
static const uint16_t uuid16_list[] = {HallService::HALL_SERVICE_UUID};

static volatile bool triggerSensorPolling = false;

unsigned short hallValue;

HallService *hallServicePtr;

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
  BLE::Instance().gap().startAdvertising();
}

void periodicCallback(void)
{
  if(hallValue != hallSensor.read()) {
    hallValue = hallSensor.read();
    led = hallValue;

    triggerSensorPolling = true;
  }
}

void onBleInitError(BLE &ble, ble_error_t error)
{
}

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
  BLE& ble = params->ble;
  ble_error_t error = params->error;

  if (error != BLE_ERROR_NONE) {
    onBleInitError(ble, error);
    return;
  }

  if (ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
    return;
  }

  ble.gap().setDeviceName((const uint8_t *) DEVICE_NAME);
  ble.gap().onDisconnection(disconnectionCallback);

  uint16_t initialValueForHallCharacteristic = 0;
  hallServicePtr = new HallService(ble, initialValueForHallCharacteristic);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
  ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
  ble.gap().setAdvertisingInterval(1000);
  ble.gap().startAdvertising();
}


int main(void)
{
  pc.printf("Starting door_sensor...\r\n");

  ticker.attach(periodicCallback, 1);

  ble.init(bleInitComplete);

  while (ble.hasInitialized() == false) { /* spin loop */ }

  while(true) {
    if (triggerSensorPolling && ble.getGapState().connected) {
      triggerSensorPolling = false;
      pc.printf("%u\r\n", hallValue);
      hallServicePtr->updateHallValue(hallValue);
    } else {
      ble.waitForEvent();
    }
  }
}

 詳細な説明は割愛しますが、毎秒ホールセンサーの値を読み取り、値が変わっていたら BLE の Characteristic が持つ値を更新し、LEDの点灯状態も更新しています。ここまでの内容でビルドして BLE Nano に書き込むとひとまず Peripheral 側としては動作するようになり、下記動画のようにドアの開閉でLEDが点灯/消灯します。

Raspberry Pi 側のアプリ実装

 では次に Central 側になる Raspberry Pi のアプリケーション実装です。ユニファのサービスではサーバ側は Ruby で書かれているということもあり、Ruby で実装してみます。 まずは BLE デバイスをある程度汎用的に扱えるように以前に実装していた BLE クラスがあるのでこれを使用します。 BlueZ による BLE サービスへのアクセスをラップしたもので、BLE デバイスへアクセスするときに共通的に使用する処理を実装してあります。

require 'bundler/setup'
require 'dbus'

class BLE
  attr_reader :bus

  SERVICE_NAME = 'org.bluez'
  SERVICE_PATH = '/org/bluez'
  ADAPTER      = 'hci0'

  DEVICE_IF          = 'org.bluez.Device1'
  SERVICE_IF         = 'org.bluez.GattService1'
  CHARACTERISTIC_IF  = 'org.bluez.GattCharacteristic1'
  DBUS_PROPERTIES_IF = 'org.freedesktop.DBus.Properties'

  SERVICE_RESOLVED_PROPERTY = 'ServicesResolved'
  UUID_PROPERTY             = 'UUID'

  PROPERTIES_CHANGED_SIGNAL = 'PropertiesChanged'

  SERVICE_RESOLVE_CHECK_INTERVAL = 0.1
  DISCOVERY_WAITING_SECOND       = 20

  module UUID
    GENERIC_ATTRIBUTE_SERVICE  = '00001801-0000-1000-8000-00805f9b34fb'
    DEVICE_INFORMATION_SERVICE = '0000180a-0000-1000-8000-00805f9b34fb'
    BATTERY_SERVICE            = '0000180f-0000-1000-8000-00805f9b34fb'

    BATTERY_DATA = '00002a19-0000-1000-8000-00805f9b34fb'
  end

  class Device
    attr_reader :bluez, :name, :address

    def initialize(bluez, bluez_device, name, address, rssi = nil)
      @bluez        = bluez
      @bluez_device = bluez_device
      @name         = name
      @address      = address
      @rssi         = rssi
    end

    def connect
      @bluez_device.introspect
      @bluez_device.Connect
      @bluez_device.introspect

      while !properties[SERVICE_RESOLVED_PROPERTY] do
        sleep(SERVICE_RESOLVE_CHECK_INTERVAL)
      end

      @name = properties['Name']
    end

    def disconnect
      @bluez_device.Disconnect
    end

    def properties
      @bluez_device.introspect
      @bluez_device.GetAll(DEVICE_IF).first
    end

    def services
      services = []
      @bluez_device.subnodes.each do |node|
        service = @bluez.object("#{@bluez_device.path}/#{node}")
        service.introspect
        properties = service.GetAll(SERVICE_IF).first
        services << Service.new(@bluez, service, properties[UUID_PROPERTY])
      end

      services
    end

    def service_by_uuid(uuid)
      services.each do |service|
        return service if service.uuid == uuid
      end

      raise 'Service not found.'
    end

    def read_battery_level_once
      service = service_by_uuid(BLE::UUID::BATTERY_SERVICE)
      characteristic = service.characteristic_by_uuid(BLE::UUID::BATTERY_DATA)
      characteristic.read.first
    end

    def read_battery_level
      service = service_by_uuid(BLE::UUID::BATTERY_SERVICE)
      characteristic = service.characteristic_by_uuid(BLE::UUID::BATTERY_DATA)
      yield(characteristic.read.first)
      characteristic.start_notify do |v|
        yield(v.first)
      end
    end
  end

  class Service
    attr_reader :uuid

    def initialize(bluez, bluez_service, uuid)
      @bluez         = bluez
      @bluez_service = bluez_service
      @uuid          = uuid
    end

    def properties
      @bluez_service.introspect
      @bluez_service.GetAll(SERVICE_IF).first
    end

    def characteristics
      characteristics = []
      @bluez_service.subnodes.each do |node|
        characteristic = @bluez.object("#{@bluez_service.path}/#{node}")
        characteristic.introspect
        properties = characteristic.GetAll(CHARACTERISTIC_IF).first
        characteristics << Characteristic.new(characteristic, properties[UUID_PROPERTY])
      end

      characteristics
    end

    def characteristic_by_uuid(uuid)
      characteristics.each do |characteristic|
        return characteristic if characteristic.uuid == uuid
      end

      raise 'Characteristic not found.'
    end
  end

  class Characteristic
    attr_reader :uuid

    def initialize(bluez_characteristic, uuid)
      @bluez_characteristic = bluez_characteristic
      @uuid = uuid
    end

    def properties
      @bluez_characteristic.introspect
      @bluez_characteristic.GetAll(CHARACTERISTIC_IF).first
    end

    def start_notify
      @bluez_characteristic.StartNotify
      @bluez_characteristic.default_iface = DBUS_PROPERTIES_IF
      @bluez_characteristic.on_signal(PROPERTIES_CHANGED_SIGNAL) do |_, v|
        yield(v['Value'])
      end
    end

    def write(value)
      @bluez_characteristic.WriteValue(value, {})
    end

    def read
      @bluez_characteristic.ReadValue({}).first
    end

    def inspect
      @bluez_characteristic.inspect
    end
  end

  def initialize
    @bus = DBus::system_bus
    @bluez = @bus.service(SERVICE_NAME)

    @adapter = @bluez.object("#{SERVICE_PATH}/#{ADAPTER}")
    @adapter.introspect
  end

  def devices
    @adapter.StartDiscovery
    sleep(DISCOVERY_WAITING_SECOND)

    devices = []
    @adapter.introspect
    @adapter.subnodes.each do |node|
      device = @bluez.object("#{SERVICE_PATH}/#{ADAPTER}/#{node}")
      device.introspect

      next unless device.respond_to?(:GetAll)

      properties = device.GetAll(DEVICE_IF).first
      name    = properties['Name']
      address = properties['Address']
      rssi    = properties['RSSI']

      next if name.nil? || rssi.nil?

      devices << Device.new(@bluez, device, name, address, rssi)
    end

    @adapter.StopDiscovery
    devices
  end

  def device_by_name(name)
    devices.each do |device|
      return device if device.name.downcase.include?(name.downcase)
    end

    raise 'No devices found.'
  end

  def devices_by_name(name)
    devices.select do |device|
      device.name.downcase.include?(name.downcase)
    end
  end
end

 上記の BLE クラスを利用する形で、今回のドアセンサーを実現するための DoorSensor クラスを下記のように実装します。クラス設計や定数の持ち方などは正直かなり適当ですが、とりあえず動くので良しとします。

require 'bundler/setup'
require './ble.rb'

class DoorSensor
  attr_accessor :opend_at

  DEVICE_NAME = 'DoorSensor'

  module UUID
    DOOR_SENSOR_SERVICE = '0000a000-0000-1000-8000-00805f9b34fb'
    DOOR_SENSOR_VALUE   = '0000a001-0000-1000-8000-00805f9b34fb'
  end

  def self.find_all
    ble = BLE.new
    devices = ble.devices_by_name(DEVICE_NAME)
    devices.map do |device|
      device.services
      DoorSensor.new(ble, device)
    end
  end

  def initialize(ble, device)
    @ble    = ble
    @device = device
    @status = 0
    @opend_at = nil
  end

  def name
    @device.name
  end

  def address
    @device.address
  end

  def connect
    @device.connect
  end

  def read_door_sensor_value
    service = @device.service_by_uuid(DoorSensor::UUID::DOOR_SENSOR_SERVICE)
    characteristic = service.characteristic_by_uuid(DoorSensor::UUID::DOOR_SENSOR_VALUE)
    characteristic.start_notify do |door_sensor_value|
      yield(door_sensor_value.first)
    end
  end

  def run
    main = DBus::Main.new
    main << @ble.bus

    main.run
  end

  def monitor(log)
    Thread.new do
      begin
        loop do
          if !@opend_at.nil? && Time.now - @opend_at > 30
            log.warn('The door is still opening!')
          end
          sleep(5)
        end

      rescue => e
        log.error(e.backtrace.join("\n"))
      end
    end
  end

  def disconnect
    @device.disconnect
  end
end
if $0 == __FILE__
  log = Logger.new('logs/door_sensor.log')

  log.debug('Finding DoorSensor...')
  door_sensors = DoorSensor.find_all
  log.debug("#{door_sensors.size} DoorSensors found.")

  begin
    door_sensor = door_sensors.first
    log.debug("Connecting to #{door_sensor.name}: #{door_sensor.address}")
    door_sensor.connect
    log.debug("Connected. #{door_sensor.name}")

    door_sensor.read_door_sensor_value do |door_sensor_value|
      if door_sensor_value == 1
        door_sensor.opend_at = Time.now
      else
        door_sensor.opend_at = nil
      end
      @status = door_sensor_value
      log.info("#{door_sensor.address}: #{door_sensor_value}")
    end

    door_sensor.monitor(log)
    door_sensor.run
  rescue => e
    log.error(e.backtrace.join("\n"))
    puts e
  ensure
    door_sensor.disconnect
  end
end

 Peripheral(BLE Nano)側でセンサーの値が変わるとシグナルが送られてくるので、 read_door_sensor_value メソッド内でそれを待ち受け、ドアが開いたときにはその時間を記録しておきます。また別スレッドでは記録された時間を5秒ごとに監視し、30秒以上経っている場合にはログにWARNを出力します。これを $ bundle exec ruby door_sensor.rb & という感じで実行すると、下記のようにログが出力されていきます。

D, [2017-12-18T23:16:31.459778 #11409] DEBUG -- : Finding DoorSensor...
D, [2017-12-18T23:16:57.241822 #11409] DEBUG -- : 1 DoorSensors found.
D, [2017-12-18T23:16:57.242144 #11409] DEBUG -- : Connecting to DoorSensor: F4:CB:D1:35:5E:BA
D, [2017-12-18T23:16:59.618002 #11409] DEBUG -- : Connected. DoorSensor
I, [2017-12-18T23:20:05.662297 #11409]  INFO -- : F4:CB:D1:35:5E:BA: 1
I, [2017-12-18T23:20:11.669245 #11409]  INFO -- : F4:CB:D1:35:5E:BA: 0
I, [2017-12-18T23:20:17.676412 #11409]  INFO -- : F4:CB:D1:35:5E:BA: 1
I, [2017-12-18T23:20:27.666568 #11409]  INFO -- : F4:CB:D1:35:5E:BA: 0
I, [2017-12-18T23:20:31.649165 #11409]  INFO -- : F4:CB:D1:35:5E:BA: 1
W, [2017-12-18T23:21:04.905865 #11409]  WARN -- : The door is still opening!
W, [2017-12-18T23:21:09.906403 #11409]  WARN -- : The door is still opening!
W, [2017-12-18T23:21:14.906943 #11409]  WARN -- : The door is still opening!
W, [2017-12-18T23:21:19.907616 #11409]  WARN -- : The door is still opening!
I, [2017-12-18T23:21:22.679355 #11409]  INFO -- : F4:CB:D1:35:5E:BA: 0

まとめ

 今回はとりあえずログに出力しているだけですが、 Raspberry Pi が Wi-Fi に繋がっていれば色々なことができるので、 Slack へのメッセージ通知などをできるようにしてみたいと思います。あと、今は回路もブレッドボードにさしてるだけですが、抜けやすかったりで心配な面もあるので、ある程度確認できたら基盤に半田付けして実装してみたいと思います。

スマホで使えるPhotoShopで写真を加工

こんにちは。10月後半に入社しました山岸です。システム開発部でQA業務を担当しております。 今回はそんなQAのことでも開発のこともまったく関係のない写真の加工についてご紹介したいと思います。

今回紹介させていただくのは言わずとしれたPhotoShopです。

スマホのアプリは無料で使用できるって知っていますか??

この記事ではPhotoShopFixというPhotoShopCCの一部機能を使用できるアプリを紹介します。

www.adobe.com

iOS

Adobe Photoshop Fix

Adobe Photoshop Fix

  • Adobe
  • 写真/ビデオ
  • 無料
Android play.google.com

肝心の利用方法ですがAdobeのアカウントを作るだけで使用出来ます。 なにが出来るかというと・・・

能面がこうなります。
f:id:unifa_tech:20171207155838j:plainf:id:unifa_tech:20171207155844p:plain

◆このアプリでは顔のパーツ認識機能を使って部分ごとに顔を修正することが可能です。
まずこの下部の「ゆがみ」を選択します
f:id:unifa_tech:20171207160052p:plain

顔タブで部分が認証されていますので、それぞれ部位ごとに修正していきます。
f:id:unifa_tech:20171207160125p:plain

額の高さの調整、顔全体の大きさの調整、顔の幅の調整
f:id:unifa_tech:20171207160248p:plain

目の距離、サイズ、傾き、高さ、幅の調整
f:id:unifa_tech:20171207160322p:plain

鼻の幅、高さの調整
f:id:unifa_tech:20171207160519p:plain

顎の輪郭、顎の長さ、頬のくぼみの調整
f:id:unifa_tech:20171207160541p:plain

上唇の幅、下唇の幅、口角、唇の幅、高さの調整
f:id:unifa_tech:20171207160558p:plain


◆この調整で出来ない部分はワープツールを使用して調整していきます。
(小鼻の調整だったり、顎の形だったり、目尻/目頭の調整などなど)
f:id:unifa_tech:20171207160628p:plain

◆各部位の調整が終わったら最初の画面に戻って修復タブからスポット修正を行います。
今回のモデルの能面はキレイなので必要ありませんが、実際に人を修正する場合、
シミだったり、ほうれい線、ほくろなどはこれで消しちゃいます。
f:id:unifa_tech:20171207160651p:plain

◆修復タブの作業が終わったらスムーズタブからお肌を滑らかにしていきます。
女性の場合、ファンデーションが浮いていたりしたらだいたいこれでキレイに出来ます。
f:id:unifa_tech:20171207160711p:plain

◆明るさタブで明るさを調整していきます。
目のクマや前髪の影等々はこれで消します。(能面にはないのですが…)
f:id:unifa_tech:20171207160734p:plain

◆カラータブで彩度を部分的に調整していきます。
これはカラコンをしていたら目立つようにしたり、チークを強調させたりに使ってます。
f:id:unifa_tech:20171207160811p:plain

あとはペイントツールで少し手直しして完成です。
f:id:unifa_tech:20171207160830p:plain

以上になります。慣れるとだいたい10分かからずに出来るようになると思います。 今回は能面さんをモデルにしましたが、人の写真でやる場合がほとんどです。

ここまでご覧いただきありがとうございました。

【引用先】

能面「小姫」 #白竹創業祭 / norio_nomura

もしデータベースのトランザクションが使えなかったら

こんにちは、システム開発部のちょうです。

毎日バックエンドの開発に一番馴染みのあるものがデータベース、そしていろんな機能はトランザクションベースで開発されたのです。トランザクションはデータベース基本の機能で、トランザクションなしにはデータベースだと言えないぐらい重要です。でももしトランザクションが使えなくなったら、みんな開発できなくなるでしょうか。答えはNoです。実際、

  • トランザクションだけで対応できない
  • データベースにトランザクション機能がない
  • 分散システムでトランザクションが使えない

ようなケースは珍しくありません。

続きを読む

Perceptual Hashを使って画像の類似度を計算してみる

最近、引越しをしたWebエンジニアの本間です。 引越しの作業は大変面倒でしたが、新しい街に来た時のワクワク感がやっぱりいいなーと感じております。

さて、弊社のサービスである「写真サービス るくみー」では、毎日たくさんの写真をアップロードしていただいているのですが、中には内容がほとんど同じ写真が入ってしまうことがあります。 これらの写真がそのまま販売されてしまうと、写真を選ぶ際に邪魔になったり、間違って複数枚購入してしまうことがあるため、可能な限り避けたい事象です。 「同じ内容」の写真を自動で判別する方法がないか調査していたところ「Perceptual Hash」という手法を見つけました。 Pythonでの画像処理の勉強も兼ねて、今回この手法を紹介してみようと思います。

続きを読む

SupersetをECS(Docker)で導入してみる

おはこんばんちはユニファのインフラのすずきです。

今肩こりが悪化しすぎて右腕が痛くなって接骨院通っています。 右腕の痛みは治ったのですが肩のほうが痛くないのが異常だと言われ緩めるために引き続き通い続けています。 みなさんも普段の姿勢とか肩を動かす運動とかして慢性的な肩こりにならないようにしていただけたらと思います。

あとiPhoneX買いましたanimojiキモいですね!

さて本題、弊社DBの情報をグラフなどで可視化したり、一覧で取得したりなどするためにRedashを利用しています。 特定のサービスでしか利用していなかったので、先日他のサービスでも利用できるように共通の環境に移設しました(ついでにDocker化)。
ただ、Redash自体は他の候補を検討せず導入したものであり、実現したいことも増えてきたのでこれを期にRedash以外を検討してみようということになりました。
そこで、最近良く目にするようになってきたSupersetがどんなものかお試しで利用してみようとなりました。 丁度RedashがDockerで動いてるのでSupersetもDockerで動かしてみようとおもったので、その内容をブログにしてみました。

続きを読む

SORACOM Inventory によるデバイス管理(Limited Preview お試し)

 こんにちは、ユニファの赤沼です。

 今年の7月に SORACOM Inventory が発表され、 Limited Preview の受付が開始されました。ユニファでもすぐに申し込んでいたのですが、先日利用案内をいただくことができたので早速試してみました。案内をいただくまでに4か月ほどかかっていますが、それだけ申し込みが多く注目度が高いということなのかと思います。

soracom.jp

SORACOM Inventory とは

f:id:akanuma-hiroaki:20171110091212p:plain:right:w150

 詳細については公式サイトをご覧いただければと思いますが、一言で言うとデバイス管理サービスです。 SORACOM Air により回線に接続したデバイス上で LwM2M に対応したエージェントを動作させることで、デバイスの自動登録やデバイスの管理が可能になります。

 Limited Preview 中は無料で利用することができますが、本サービス利用時には下記サイトにあるような料金が発生します。

soracom.jp

SIMグループの設定

 SORACOM Inventory を Limited Preview で使うには、利用する SIM が所属するSIMグループを、あらかじめ用意された Limited Preview 専用の VPG(Virtual Private Gateway) に API を使って紐づける必要があります。SORACOM の API は Reference ページから実際に API にリクエストを投げられるようになっていますので、下記のようにSIMグループの設定変更の API にリクエストを投げます。下記画像ではグループIDと VPGID はマスクしていますが、実際には対象のグループIDと SORACOM から案内のあった VPGID を指定します。

f:id:akanuma-hiroaki:20171109211634p:plain

エージェントのインストール

 次に各デバイスで動作させるエージェントをインストールします。 Limited Preview ではエージェント実装のサンプルとして、 Eclipse Wkaama ベースの C エージェント、 Java エージェント、 Android エージェントが配布されます。今回はまず Raspberry Pi 上で C エージェントを動かしてみます。

 まずはエージェントのビルドに必要な cmake をインストールします。

$ sudo apt-get install cmake

 次に Eclipse Wakaama を clone し、 submodule をセットアップします。

pi@raspberrypi:~/tmp $ git clone https://github.com/eclipse/wakaama
Cloning into 'wakaama'...
remote: Counting objects: 5924, done.
remote: Total 5924 (delta 0), reused 0 (delta 0), pack-reused 5924
Receiving objects: 100% (5924/5924), 2.13 MiB | 444.00 KiB/s, done.
Resolving deltas: 100% (4194/4194), done.
Checking connectivity... done.
pi@raspberrypi:~/tmp $ 
pi@raspberrypi:~/tmp $ cd wakaama/
pi@raspberrypi:~/tmp/wakaama $ 
pi@raspberrypi:~/tmp/wakaama $ git submodule init
Submodule 'platforms/Linux/tinydtls' (https://git.eclipse.org/r/tinydtls/org.eclipse.tinydtls) registered for path 'examples/shared/tinydtls'
pi@raspberrypi:~/tmp/wakaama $ 
pi@raspberrypi:~/tmp/wakaama $ git submodule update
Cloning into 'examples/shared/tinydtls'...
remote: Total 343 (delta 0), reused 343 (delta 0)
Receiving objects: 100% (343/343), 443.92 KiB | 211.00 KiB/s, done.
Resolving deltas: 100% (110/110), done.
Checking connectivity... done.
Submodule path 'examples/shared/tinydtls': checked out '0016138fe3998552eee3987a1c09da43a23c9fb5'

 続いて、 wakaama に SORACOM Inventory 用のパッチを適用します。 Limited Preview の案内と同時に配布されたパッチの圧縮ファイルを適当なディレクトリに展開してビルドします。(出力は省略)

pi@raspberrypi:~/tmp/wakaama $ tar zxf wakaama_patch.tgz
pi@raspberrypi:~/tmp/wakaama $ cd ../
pi@raspberrypi:~/tmp $ mkdir build
pi@raspberrypi:~/tmp $ cd build/
pi@raspberrypi:~/tmp/build $ cmake -DDTLS=1 ../wakaama/examples/client
pi@raspberrypi:~/tmp/build $ make

デバイスの登録

 それではエージェントを起動してデバイス情報を登録してみたいと思います。 SORACOM Air でネットワークに接続した上で、下記のようにエージェントを起動します。 State が STATE_READY になれば成功です。

pi@raspberrypi:~/tmp/build $ ./lwm2mclient -b -n raspberry_pi -h bootstrap.soracom.io
Using MAC address b827ebb3dcad as endpoint name
Trying to bind LWM2M Client to port 56830
LWM2M Client "raspberry_pi" started on port 56830
>  -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_REGISTERING
 -> State: STATE_REGISTERING
 -> State: STATE_REGISTERING
decrypt_verify(): found 24 bytes cleartext
 -> State: STATE_REGISTERING
decrypt_verify(): found 22 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY

データ確認

 エージェントから登録されたデバイス情報は SORACOM コンソールから確認できます。

f:id:akanuma-hiroaki:20171110064638p:plain

f:id:akanuma-hiroaki:20171110063814p:plain

 上記は一部ですが、他にも LwM2M の仕様に沿って多くの項目が表示されていて、バッテリーレベル、通信強度、緯度、経度などの項目が確認できます。ただし、それぞれの情報が取得できるか、どんな内容が取得できているかはエージェントの実装に依存します。 Limited Preview の案内で配布されているエージェントは検証用のサンプルなので、実際に本番サービスに使用する場合は自前で使用するデバイスに対応したエージェントを実装する必要があります。

Java エージェントによる登録・確認

 Java版のエージェントでも動作を確認してみます。手元の Raspberry Pi には Java がインストールされていなかったので、まずは Java をインストールします。

pi@raspberrypi:~ $ sudo apt-get install oracle-java8-jdk

 Limited Preview ではビルド済みの Java エージェントが配布されていますので、解凍すれば実行できます。

pi@raspberrypi:~/tmp $ unzip soracom-inventory-agent-example-0.0.1.zip 

 SORACOM Air でネットワークに接続した上で、下記のようにエージェントを実行します。

pi@raspberrypi:~/tmp $ cd soracom-inventory-agent-example-0.0.1/bin/
pi@raspberrypi:~/tmp/soracom-inventory-agent-example-0.0.1/bin $ ./soracom-inventory-agent-example-start -e raspberry_pi
2017-11-06 23:21:36,348 INFO SORACOMInventoryAgentExample - using endpoint [raspberry_pi]
2017-11-06 23:21:37,757 INFO InventoryAgentInitializer - set bootstrap security object.
2017-11-06 23:21:37,771 INFO InventoryAgentInitializer - set instance to initializer.objectId:3 instance num:1
2017-11-06 23:21:37,772 INFO InventoryAgentInitializer - set instance to initializer.objectId:14 instance num:1
2017-11-06 23:21:39,148 INFO LeshanClient - Starting Leshan client ...
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.CoapServer start
INFO: Starting server
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Starting endpoint at coaps://0.0.0.0:0
Nov 06, 2017 11:21:39 PM org.eclipse.californium.scandium.DTLSConnector start
INFO: DTLS connector listening on [0.0.0.0/0.0.0.0:48720] with MTU [1,280] using (inbound) datagram buffer size [16,474 bytes]
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Started endpoint at coaps://0.0.0.0:48720
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Starting endpoint at coap://0.0.0.0:0
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Started endpoint at coap://0.0.0.0:50733
2017-11-06 23:21:39,319 INFO LeshanClient - Leshan client started [endpoint:raspberry_pi].
2017-11-06 23:21:39,353 INFO RegistrationEngine - Trying to start bootstrap session to coap://100.127.127.100:5683 ...
2017-11-06 23:21:39,834 INFO RegistrationEngine - Bootstrap started
2017-11-06 23:21:40,242 INFO RegistrationEngine - Bootstrap finished ServersInfo [bootstrap=Bootstrap Server [uri=coaps://beam.soracom.io:5684], deviceMangements={123=DM Server [uri=coaps://jp.inventory.soracom.io:5684, lifetime=60, binding=U]}].
2017-11-06 23:21:40,244 INFO BootstrapObserver - Bootstrap success: coap://100.127.127.100:5683
2017-11-06 23:21:40,247 INFO BootstrapObserver - Bootstrap server: Bootstrap Server [uri=coaps://beam.soracom.io:5684], Device management servers: {123=DM Server [uri=coaps://jp.inventory.soracom.io:5684, lifetime=60, binding=U]}
2017-11-06 23:21:40,287 INFO FileCredentialStore - save credential to .soracom-inventory-credentials.dat
2017-11-06 23:21:40,291 INFO RegistrationEngine - Trying to register to coaps://jp.inventory.soracom.io:5684 ...
2017-11-06 23:21:42,966 INFO RegistrationEngine - Next registration update in 54.0s...
2017-11-06 23:21:42,969 INFO RegistrationEngine - Registered with location '/rd/lWFi3sMHIp'.
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.network.config.NetworkConfig store
INFO: writing properties to file /home/pi/tmp/soracom-inventory-agent-example-0.0.1/bin/Californium.properties
2017-11-06 23:21:43,378 INFO ObservableInventoryObjectEnabler - observe start. observeStartDelayMillis:10000 observeInternalMillis:60000
2017-11-06 23:21:43,378 INFO ObservableInventoryObjectEnabler - observe start. observeStartDelayMillis:10000 observeInternalMillis:60000
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#7b184646f5c054fb and resource /3
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#48f72e0076f29416 and resource /3
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#086126447a85b86a and resource /1
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#97c1ac3d635941e2 and resource /3
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.server.ServerMessageDeliverer deliverRequest
INFO: Did not find resource [6, 0, 5] requested by jp.inventory.soracom.io/52.198.95.62:5,684
2017-11-06 23:21:53,376 INFO ObservableInventoryObjectEnabler - fire resource change for observation.
2017-11-06 23:21:53,377 INFO ObservableInventoryObjectEnabler - fire resource change for observation.
2017-11-06 23:22:36,970 INFO RegistrationEngine - Trying to update registration to coaps://jp.inventory.soracom.io:5684 ...
2017-11-06 23:22:37,896 INFO RegistrationEngine - Next registration update in 54.0s...
2017-11-06 23:22:37,897 INFO RegistrationEngine - Registration update succeed.
2017-11-06 23:22:53,376 INFO ObservableInventoryObjectEnabler - fire resource change for observation.
2017-11-06 23:22:53,377 INFO ObservableInventoryObjectEnabler - fire resource change for observation.

 登録された情報をコンソールから確認してみます。

f:id:akanuma-hiroaki:20171110065601p:plain

 Endpoint はエージェントの起動コマンドで同一の値を渡しているので同じになっていますが、そのほかの項目では C のエージェントで登録した場合と比べて、取得できている項目や内容に違いがあります。これはそれぞれのエージェントの実装で取得している内容が異なっているためと思われます。

Wi-Fi 接続でのデータ取得

 SORACOM Inventory では大きく分けて、下記の2つのステップがあります。

  • デバイスの登録

  • デバイスの管理(データ取得、コマンド実行)

 デバイスの登録については SORACOM Air で回線に接続して行う必要がありますが、登録時にそれ以降の通信用の鍵の交換などが行われますので、一度登録が完了すればデバイスの管理は Wi-Fi 等、 SORACOM Air 以外の接続方法でも行うことが可能です。例えば Limited Preview で配布されている Java エージェントでは、デバイス登録時に下記のような認証用ファイルが作成されます。

pi@raspberrypi:~/tmp/soracom-inventory-agent-example-0.0.1/bin $ ls -l .soracom-inventory-credentials.dat 
-rw-r--r-- 1 pi pi 460 Nov  6 23:21 .soracom-inventory-credentials.dat

 上記ファイルがある状態でエージェントを実行すると、デバイスの登録のステップは実行されず、データの更新のステップから実行され、 Wi-Fi 接続でも下記のように実行可能です。

pi@raspberrypi:~/tmp/soracom-inventory-agent-example-0.0.1/bin $ ./soracom-inventory-agent-example-start -e raspberry_pi
2017-11-06 23:27:25,390 INFO SORACOMInventoryAgentExample - using endpoint [raspberry_pi]
2017-11-06 23:27:26,161 INFO FileCredentialStore - load credential from .soracom-inventory-credentials.dat
2017-11-06 23:27:26,162 INFO InventoryAgentInitializer - set security object from credential.
2017-11-06 23:27:26,171 INFO FileCredentialStore - load credential from .soracom-inventory-credentials.dat
2017-11-06 23:27:26,174 INFO InventoryAgentInitializer - set instance to initializer.objectId:3 instance num:1
2017-11-06 23:27:26,174 INFO InventoryAgentInitializer - set instance to initializer.objectId:14 instance num:1
2017-11-06 23:27:26,912 INFO LeshanClient - Starting Leshan client ...
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.CoapServer start
INFO: Starting server
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Starting endpoint at coaps://0.0.0.0:0
Nov 06, 2017 11:27:26 PM org.eclipse.californium.scandium.DTLSConnector start
INFO: DTLS connector listening on [0.0.0.0/0.0.0.0:48720] with MTU [1,280] using (inbound) datagram buffer size [16,474 bytes]
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Started endpoint at coaps://0.0.0.0:48720
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Starting endpoint at coap://0.0.0.0:0
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Started endpoint at coap://0.0.0.0:52646
2017-11-06 23:27:27,006 INFO LeshanClient - Leshan client started [endpoint:raspberry_pi].
2017-11-06 23:27:27,043 INFO RegistrationEngine - Trying to register to coaps://jp.inventory.soracom.io:5684 ...
2017-11-06 23:27:29,016 INFO RegistrationEngine - Next registration update in 54.0s...
2017-11-06 23:27:29,021 INFO RegistrationEngine - Registered with location '/rd/H12GIitHqU'.
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.network.config.NetworkConfig load
INFO: loading properties from file /home/pi/tmp/soracom-inventory-agent-example-0.0.1/bin/Californium.properties
2017-11-06 23:27:29,225 INFO ObservableInventoryObjectEnabler - observe start. observeStartDelayMillis:10000 observeInternalMillis:60000
2017-11-06 23:27:29,238 INFO ObservableInventoryObjectEnabler - observe start. observeStartDelayMillis:10000 observeInternalMillis:60000
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#11e177e05af7f852 and resource /3
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#654af6b769a78c4b and resource /3
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#28587f4d02931bb6 and resource /3
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.server.ServerMessageDeliverer deliverRequest
INFO: Did not find resource [6, 0, 5] requested by jp.inventory.soracom.io/52.198.95.62:5,684
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#8801847f7f072340 and resource /1
2017-11-06 23:27:39,220 INFO ObservableInventoryObjectEnabler - fire resource change for observation.
2017-11-06 23:27:39,239 INFO ObservableInventoryObjectEnabler - fire resource change for observation.

 ちなみに Limited Preview で配布されている C のエージェントでは、 SORACOM Air 以外での接続での管理はサポートされていません。

コンソールからのコマンド実行

 SORACOM Inventory ではデバイスのデータを確認するだけでなく、デバイスに対してコマンドを実行することができます。これも実際にどんなコマンドが実行できるかはエージェントの実装に依存しますが、 Limited Preview の C エージェントではデバイスの再起動ができます。SORACOM Air で回線に接続して C エージェントを起動した上で、コンソールのデバイス詳細画面の reboot の項目の右側にあるボタンをクリックします。

f:id:akanuma-hiroaki:20171110081518p:plain

 下記のような確認ダイアログが表示されますので、再起動して問題なければ コマンド実行 をクリックします。

f:id:akanuma-hiroaki:20171110082020p:plain

 すると C エージェントの出力が下記のように続き、 Raspberry Pi が再起動されます。

decrypt_verify(): found 9 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 18 bytes cleartext

         REBOOT

 -> State: STATE_READY
 -> State: STATE_READY
〜〜〜中略〜〜〜
-> State: STATE_READY
 -> State: STATE_READY
reboot time expired, rebooting ...Connection to raspberrypi.local closed by remote host.
Connection to raspberrypi.local closed.

まとめ

 IoT デバイスを用いたサービスを展開しようとした場合、デバイスの管理は大きな課題であり、デバイスの状態を詳しく把握できるかどうかが問い合わせ対応時等のユーザ満足度にも大きく影響します。自前でデバイス管理の仕組みを作ろうと思うと大変ですが、 SORACOM Inventory を使うことでデバイスの管理はかなりやりやすくなりそうに思えます。デバイスが IoT Gateway などを介してネットに接続している場合でも、 Gateway が SORACOM Air でネットに接続していればデバイスの登録は可能な点や、一度登録してしまえば Wi-Fi 等の接続でも運用できるのも嬉しいところです。エージェントを自前で実装する必要がある点や、デバイス上でエージェントを動作させる必要がある点はサービス設計時に考慮が必要ですが、それ以外の対応が大幅に減らせることを考えれば、 SORACOM Inventory を利用するメリットは大きいと思います。

Vision.framework を使って QR コードを読む

こんにちは、iOSエンジニアのしだです。
急に寒くなってきて秋をすっ飛ばしていきなり冬になってしまった感じがします。

iOS11 で追加された Vision.framework を使ってQRコードを読み込みしたいと思います。(n番煎じ感あります。)

QR コード

iOSで QRコード を読む場合2種類あります。

  • AVFoundation.framework (AVCaptureMetadataOutputObjectsDelegate)
  • Vision.framework

AVCaptureMetadataOutputObjectsDelegate は AVFoundation 内でキャプチャ中に検出するのに対して、Vision.framework ではキャプチャした画像に対して検出する違いがあります。
あと、AVCamBarcode サンプルコードを動かした感じ一度に4つのQRコードが検出されましたが、Vision.framework の方は時間はかかりますが 4つ以上は検出できました。

できたもの

用意したQRコードは、1から16の文字列をQRコードにしたものです。そして検出されたQRコードに緑枠を表示するシンプルなものです。
iPad mini 2 で試しましたが、16個検出するのに1分以上かかりました。

f:id:unifa_tech:20171031140022j:plain

Vision.framework 使い方

Requests を作って、RequestHandlerで実行して、Observations で結果を受け取るようになっています。

Requests

  • VNDetectFaceRectanglesRequest: 顔検出
  • VNDetectBarcodesRequest: QRコードなどのバーコード検出
  • VNDetectTextRectanglesRequest: テキスト検出
  • VNTrackObjectRequest: オブジェクト検出
  • など...

これらの Requests を作成して、 RequestHandler で実行します。そうすると Requests と対になる Observation が返ってきます。
例えば、 VNDetectFaceRectanglesRequest の場合 VNFaceObservation が結果として受け取れます。

バーコード検出

AVCaptureVideoDataOutput から受け取った CVPixelBuffer からQRコードを検出してます。検出したQRコードに緑枠を描画するために結果を AVCaptureVideoPreviewLayer のビューに渡しています。

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }

        let handler = VNSequenceRequestHandler()
        let barcodesDetectionRequest = VNDetectBarcodesRequest { [weak self] (request, error) in
            if let error = error {
                NSLog("%@, %@", #function, error.localizedDescription)
            }
            guard let results = request.results else { return }
            
            ...

            DispatchQueue.main.async {
                let barcodes: [VNBarcodeObservation] = results.flatMap { $0 as? VNBarcodeObservation }
                self?.previewView.barcodes = barcodes
            }
        }
        try? handler.perform([barcodesDetectionRequest], on: pixelBuffer)
    }
}

検出したQRコードに緑枠を付ける

VNBarcodeObservation には、検出した文字列(payloadStringValue)や CIBarcodeDescriptor を持っています。 また矩形の座標をもっているので描画します。

class PreviewView: UIView {
    let targetLayerName = "box"

    /// バーコードを設定したら画面更新
    var barcodes: [VNBarcodeObservation] = [] {
        didSet {
            self.setNeedsDisplay()
        }
    }

    ...

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        /// 緑枠クリア
        self.layer.sublayers?
            .filter { $0.name == targetLayerName }
            .forEach { $0.removeFromSuperlayer() }

        guard let previewLayer = self.layer as? AVCaptureVideoPreviewLayer else {
            return
        }

        for barcode in barcodes {
            /// VNRectangleObservation が持っている座標がY軸反転しているのでアフィン変換する
            let t = CGAffineTransform(translationX: 0, y: 1)
                .scaledBy(x: 1, y: -1)
            let tl = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.topLeft.applying(t))
            let tr = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.topRight.applying(t))
            let bl = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.bottomLeft.applying(t))
            let br = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.bottomRight.applying(t))
            
            /// 緑枠を描画
            let l = rectangle(UIColor.green, topLeft: tl, topRight: tr, bottomLeft: bl, bottomRight: br)
            self.layer.addSublayer(l)
        }
    }

    func rectangle(_ color: UIColor, topLeft: CGPoint, topRight: CGPoint, bottomLeft: CGPoint, bottomRight: CGPoint) -> CALayer {
        let lineWidth: CGFloat = 2
        let path: CGMutablePath = CGMutablePath()
        path.move(to: topLeft)
        path.addLine(to: topRight)
        path.addLine(to: bottomRight)
        path.addLine(to: bottomLeft)
        path.addLine(to: topLeft)
        let layer = CAShapeLayer()
        layer.name = targetLayerName
        layer.fillColor = UIColor.clear.cgColor
        layer.strokeColor = color.cgColor
        layer.lineWidth = lineWidth
        layer.lineJoin = kCALineJoinMiter
        layer.path = path
        return layer
    }
}

おわりに

Vision.framework であれば複数のQRコードを検出できるのですが、検出数によって処理の時間が変わってきます。 4つ検出するだけであれば1秒くらいで検出できましたが、9つQRコード検出には10秒以上かかっていました。
同時にQRコードを検出することはないと思いますがなにかあれば参考にしたいと思います。

コードはこちらにあります。

bitbucket.org

参考