Swift3でのBLEアプリ開発-その1-
Swift3でBLEアプリを作ってみる
これは Bluetooth Low Energy Advent Calendar 2016 の12月1日の記事です。
Swift3が急速に広がっていますが
Objective-Cに続く新しい言語としてApple社からSwiftが公開されて、はやバージョン3になりました。
Swift3ではApplication Binary Interface (ABI)の大きな変更が入り、そのためSwift2でビルドされたライブラリをSwift3から呼び出せないなど、Swift2とSwift3との混在ができません。この破壊的な変更が強制力になり、ライブラリやアプリケーションのSwift3対応が加速されています。
自分がアプリケーションやライブラリをSwift3で書き直さねばならないときは、どのような言語で書いても同じ振る舞いをするものを言語の都合で書き直す手間が無駄に思えて、恨み言を言いながら作業をします。これって、ほんっと作業なんですよね…
ですが、Swift2からSwift3への移行はかなりスムースにいきます。それはXcodeの変換ツールのおかげだけではありません。アプリ開発は、いろいろなライブラリを取り込んで作りますが、それらのプロジェクトが生きているライブラリが軒並み、Swift3での破壊的な変更のお陰で、一気にSwift3対応を勧めていたおかげです。
自分が作業をするときは恨み言を言いますが、他人の仕事だと、べんりだなーって思うので、いい加減なものです。ですが、ライブラリがSwift3に移行して逆にデフォルトがSwift3となり、アプリをSwift3へ移行する圧力も高まっているんだろうなと思ったりします。
Swift2版はgithubで切られたブランチで参照して取ってこないとないよとか、おもしろいことになるのと、バグ修正があっても、Swift3に移行完了していたら、わざわざSwift2もメンテするかなとか思いますしね。
iOSでBLEを使うアプリは、CoreBluetoothフレームワークを使います。ですからSwift3であっても、これまでのアプリづくりと何ら変わるところはありません。
しかしSwiftならではの話もあるので、ともあれ、Swift3でBLEのライブラリあるいはアプリを作る話でもしてみます。
SenStickのソースコードをもとにして
BLE系の開発コードは、アプリケーションを作ってもらうためのSDKをバイナリ、場合によってはソースコードで公開することはよくあります。ですが、BLEデバイスの製造販売が利益源ですから、そのBLEデバイス側のソースを公開することはありません。
BLEのセンサロガー https://github.com/ubi-naist/SenStick は全てがオープンソースで公開されているので、これをサンプルにします。これのコードは私が書いています。
お仕事のコードはたいてい納品したら相手のものですから、誰かの目に触れることは滅多とないのですが、これは大学のプロジェクトでもあり、またハードウェアはゴミでしかない、という話が通じる人たちなので、公開されたっぽいです。
アプリでBLEデバイスを使うSDK tree/master/SenStickSDK これのコードを見ていきます。
バイト配列と値型の変換
BLEのキャラクタリスティクスを通じてやり取りするのはバイト配列です。キャラクタリスから読み出したバイト配列は、値や構造体に変換しなければなりません。また、キャラクタリスに書き込む場合は、その逆に値や構造体をバイト配列に変換します。
このバイト列と値との変換は、extensionで作っています。 PackableType.swift にまとめています。
なんとなくByte型
public typealias Byte = UInt8
まず、UInt8にByteという別名を与えます。単なる好みで、どうでもいいことだし、UInt8でいいじゃないと思うのですが、このコードを書いていたときの気の迷いです。あとで、UInt8で置換しときます…
public protocol PackableType {
func pack(byteOrder: ByteOrder) -> [Byte]
static func unpack(_ data: Array<Byte>, byteOrder: ByteOrder) -> Self?
}
そしてバイト配列と値型それぞれの変換メソッド名を与えます。これはシリアライズ/デシリアライズなのですが、アルファベットにするとserialize/deserializeと文字数が長いです。そのわりに、‘de’の部分しか違わないので、まずありえないのですがなんとなーくタイプミスしそうだな(賢い補完があるからそんなことあるわけないのですが)と、なんとなーくで、pack/unpackという名前にしてみました。
気の迷いです。素直にserialize/deserializeでよかったなって、今書いてて思います。
public enum ByteOrder : CustomStringConvertible {
case littleEndian
case bigEndian
public var description: String {
switch self {
case .littleEndian: return "LittleEndian"
case .bigEndian: return "BigEndian"
}
}
}
エンディアンを指定するためにenum型で定義しておきます。デバッグ時に文字列で表示できると便利(かな)と思うので、enum側は必ず文字列に変換できるようにコーディングしています。
enum SomEnumType : String {
case littleEndian = "littleEndian"
列挙型を文字列に自動変換したいならば、文字列で列挙型を定義してもいいのですが、littleEndianはlittleEndianである、みたいな区別するための識別子を列挙しているものを、わざわざ人間が読み出しやすくするためだけに、文字列を割り当てて列挙するのは何か違う気がするので、そういう書き方はしていません。
これもコンパイラが.toString()みたいなことをしたら自動的にソースコードのenumの名前を表示してくれればいいので、人間がコードを書くようなものじゃない気もするのですが。
CustomStringConvertible および CustomDebugStringConvertible https://developer.apple.com/reference/swift/customdebugstringconvertible カスタムな文字列変換、デバッグ用の文字列変換のプロトコルが解説されています。
これは、Objective-CのNSObjectにある、descriptionのオーバーロードに相当します。Swift2では、 Printable および DebugPrintable がありましたが、それらの名前が変更されてたものです。
これを使ってわざわざ文字列変換を書いているわけです。
解説に、通常は String(reflecting:) で debugPrint(_:) を呼び出すと書いてあります。正直、なんのこっちゃなのですがSwiftはリフレクション、クラスや列挙型などの型情報を読み出す機能、があるので、Swift3だとprint("\(列挙型な変数)")とすれば、ソースコードにある列挙型の名前そのものが出てきます。
わざわざ列挙型と同じ名前を文字列に明示的に変換するだけの CustomStringConvertible なんて、いらんないですね。削っときます…
16ビット幅の整数の変換
extension UInt16 : PackableType {
public func pack(byteOrder: ByteOrder = .littleEndian) -> [Byte]
{
var buf = [UInt8](repeating: 0, count: 2)
switch byteOrder {
case .littleEndian:
buf[0] = UInt8(UInt16(0x00ff) & self)
buf[1] = UInt8(UInt16(0x00ff) & (self >> 8))
case .bigEndian:
buf[1] = UInt8(UInt16(0x00ff) & self)
buf[0] = UInt8(UInt16(0x00ff) & (self >> 8))
}
return buf
}
そして実装していきます。宣言したPackableTypeプロトコルでUInt16を拡張することにして、pack()を実装していきます。バイトオーダは、Bluetoothのエンディアンは通常はリトルエンディアンなので、それをデフォルトの引数にしています。
UInt8の長さ2の配列を作り、UInt16の下位1バイトを取り出してUInt8にして代入していく、とてもシンプルなコードです。
public static func unpack<C: Collection>(_ data: C, byteOrder: ByteOrder = .littleEndian) -> UInt16? where C.Iterator.Element == Byte ,C.Index == Int
{
guard data.count >= 2 else {
assert(false, #function)
return nil
}
let bytes = Array(data)
switch byteOrder {
case .littleEndian:
return UInt16(bytes[0]) + UInt16(bytes[1]) << 8
case .bigEndian:
return UInt16(bytes[0]) << 8 + UInt16(bytes[1])
}
}
}
ではバイト配列からUInt16への変換は、こんな感じ。Arrayを作って、1バイトづつUInt16に変換して、エンディアンに合わせて桁をあわせて足し算しています。
変換のソースは、<C: Collection>のジェネリックで指定しています。要素はByte型、配列のインデックスはInt型のコレクションであれば、なんでも使えるはず。
なんでArray(data)で変換しているんだ、コレクション型でdataを直接使えばいいじゃないか、というのがありますが、これが結構泣ける話があります。
Array型の変数の部分配列を取り出すと、ArraySlice型が出てきます。このArraySlice型はもとのArrayの指定された範囲を参照するものですが、インデックスの開始が0ではなく、参照した配列の中での位置が、配列の先頭要素になります。
let arr = [1, 2, 3, 4, 5]
let sliced = arr[2..<4] // slicedは、[3, 4]
sliced[0..<1] // fatal error: Negative ArraySlice index is out of range
sliced[(0+sliced.startIndex)..<(1+sliced.startIndex)] // [3]
何を言ってるかわからないので、コードにすると、こんな感じです。
5つ要素がある配列があり、その部分配列2..<4(インデックス2,3の要素の部分配列)を取り出します。このときArray型はArraySlice型を返します。このArraySliceで要素を参照するときは、部分要素のインデックスは、元の配列のインデックスを使います。0から始まらないのです。
もしも、インデックス0の要素を取り出そうと、0..<1(いや、0でいいですけど)、と指定すると、元の配列の0番目の要素にアクセスしにいって、しかしそれはこのArraySliceの範囲外なので、例外が飛んできます。
つまり配列でwhereで要素とインデックスに制約をかけても、ArrayとArraySliceどちらも受け入れてしまう、ArraySliceもちゃんと扱えるように、要素参照のインデックスで必ず、スタートのインデックスを加算すればいいけど、めんどくさい。たぶん、ArraySliceをインデックス参照でstartIndex基準にするような何かをジェネリック的に拡張でかければいいんだろうけど、自分で何言ってるのかわかんないくらい、どうやって書くのそれって思うし。
だから、とりあえず何も考えずにArray()にして、見なかったことにしているわkです。
extension Int16 : PackableType {
public func pack(byteOrder: ByteOrder = .littleEndian) -> [Byte]
{
let v = UInt16(bitPattern: self)
return v.pack(byteOrder: byteOrder)
}
符号なしはわかった、では符号ありはどうしているのかといえば、UInt16(bitPattern:)で、Int16をビットパターンとしてUInt16に変換して、UInt16のpack()を使っています。符号ありで0x00ffとの論理積やシフトを扱うのがこわかった(確認するのもめんどくさい)のです。
public static func unpack<C: Collection>(_ data: C, byteOrder: ByteOrder = .littleEndian) -> Int16? where C.Iterator.Element == Byte ,C.Index == Int
{
guard let value = UInt16.unpack(data, byteOrder: byteOrder) else {
assert(false, #function)
return nil
}
return Int16(bitPattern: value)
}
}
バイト列からの変換も、一度UInt16にして、それをbitPatternとしてInt16にしています。符号が絡むシフト演算とか、怖さしかないですから。(ユニットテスト入れろ、という話です。)
func hoge_unpack(_ data: [Byte]) -> UInt16
{
let value = UInt16.unpack(data[0..<2])
return value
}
使うのはこんな感じで。考えたら、UInt16なら配列の長さが2以上あればいいのだから、別にわざわざ部分配列を作って渡さなくても、配列をそのまま渡せばいいだけに思えます。Swift1の頃は、メソッドに渡したインスタンスを逐一copyして渡していたみたいですが、let と var をちゃんと見る今時なら、たぶん関数呼び出しのたびに不要なコピーはしないと思いますし(たぶん)。
バイト配列から文字列への変換
バイト配列をよく使うので、デバッグ時にそれらを見やすい文字列、(0x00, 0x01、みたいな)、表示になるように、[UInt8]を拡張しています。
protocol _UInt8Type { }
extension UInt8: _UInt8Type {}
extension Array where Element : _UInt8Type {
func toHexString() -> String
{
var s = String()
for (_, value) in self.enumerated() {
s += String(format:"0x%02x,", value as! CVarArg)
}
return s
}
}
Arrayの要素がUInt8のものを拡張しているのですが、これElement: UInt8とすると、 type ‘Element’ constrained to non-protocol type ‘UInt8’ とコンパイルエラーになります。ジェネリックタイプがうんたらとか解説がありますが、理解できないのでスルー。
しかたないので、UInt8に空のプロトコルをくっつけて、そのプロトコルで制約をかけて拡張しています。
これがSwift3.1だと https://github.com/apple/swift/blob/master/CHANGELOG.md
SR-1009:
Constrained extensions allow same-type constraints between generic parameters and concrete types. This enables you to create extensions, for example, on Array with Int elements:
extension Array where Element == Int { }
と書けるそうです。でも、Xcode8.1 は Swift3.0.1 なので、これが使えるのはまだ先です。
まとめ
Swift3でバイト配列と値型の相互変換のコード部分を解説しました。よくわからない? 私もです。