ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Swift3 で CoreBluetooth 実装するときにあったら良さそうな Data Extension

こんにちは、iOSエンジニアのしだです。
iOSアプリからBLE経由で Characteristics の値を読み込む場合、20バイト程度のデータをデコードする必要があります。 主に Bluetooth SIG で定められているGATTのデータフォーマットで、あったら便利な Data の Extension を共有します。

準備

  • Xcode 8.3.3 (Swift 3.1)

数値と Data の相互変換

extension Data {
    
    init<T>(from value: T) {
        var v = value
        self.init(buffer: UnsafeBufferPointer(start: &v, count: 1))
    }
    
    func to<T>(type: T.Type) -> T {
        return withUnsafeBytes { $0.pointee }
    }
}


/// 使い方
let value = Float(3.14)
let data = Data(from: value)    // [0xC3, 0xF5, 0x48, 0x40]
Data(data).to(type: Float.self) // 3.14

数値だけではありませんがオブジェクトとDataの変換があるとなにかと便利です。 リトルエンディアンになっているので必要に応じて .reversed() します。

参考

Data から 文字列の変換

Manufacturer Name String (0x2A29)Model Number String (0x2A24) などの Characteristic値 は UTF8 文字列なのですぐにアクセスできるようにします。

extension Data {

    var utf8s: String? {
        return String(bytes: self, encoding: .utf8)
    }
}


/// 使い方
let text = Data([0x55, 0x6E, 0x69, 0x66, 0x61]).utf8s! // "Unifa"

Data から System ID の変換

Device Information Service (0x0160) の System ID (0x2A23) の Characteristic 値 は Manufacturer Identifier (40bit)Organization Unique Identifier (24bit) の8バイトあります。 16進数文字列表示で「:」区切りで見たい場合に利用します。

extension Data {
    
    var systemId: String {
        return self.reversed()
            .map { String(format: "%02X", $0) }
            .joined(separator: ":")
    }
}


/// 使い方
Data([0xEF, 0xCD, 0xAB, 0xFE, 0xFF, 0x56, 0x34, 0x12]).systemId // 12:34:56:FF:FE:AB:CD:EF

Data から FLOAT の変換

GATTに定義されてる Temperature Measurement (0x2AC1) という Characteristic 値 に FLOAT というデータフォーマットがあります。
FLOAT は 32-bit あり、仮数が 24-bit Signed Integer で、指数が 8-bit Signed Integer になっています。 SFLOAT は 16bit あり、仮数が 12bit Signed Integer、指数が 4bit Signed Integer です。

FLOAT (IEEE-11073 32-bit FLOAT)

extension Data {

    var ieeeFloat32: Double {
        var mantissa = Data(self[0...2]).to(type: Int32.self)
        if mantissa & 0x00800000 != 0 {
            mantissa = -1 * ((~mantissa & 0x00FFFFFF) + 1)
        }
        
        let exponential = Data([self[3]]).to(type: Int8.self)
        return Double(mantissa) * pow(10, Double(exponential))
    }
}


/// 使い方
Data([0x6C, 0x01, 0x00, 0xFF]).ieeeFloat32 // 36.4

SFLOAT (IEEE-11073 16-bit SFLOAT)

extension Data {
    
    var ieeeFloat16: Double {
        let value = Data(self[0..<2]).to(type: UInt16.self)
        
        var mantissa = Int16(value & 0x0FFF)
        if mantissa & 0x0800 != 0 {
            mantissa = -1 * ((~mantissa & 0x0FFF) + 1)
        }
        
        var exponential = Int16(value >> 12)
        if exponential & 0x0008 != 0 {
            exponential = -1 * ((~exponential & 0x000F) + 1)
        }
        return Double(mantissa) * pow(10, Double(exponential))
    }
}


/// 使い方
Data([0x72, 0x00]).ieeeFloat16 // 114

FLOAT、SFLOATともに 特殊な値(Nan, +Infinity, -Infinity)を考慮してないので使用する際は気を付けてください。

Data から 日付 の変換

Bluetooth SIG にある Date Time (0x2A08) の表現です。年を 2バイト、月・日・時・分・秒をそれぞれ 1バイトの計7バイトで表現されている場合に利用できます。

extension Data {

    var dateTime: Date? {
        let year = Data(self[0...1]).to(type: Int16.self)
        let month = Int(self[2])
        let day = Int(self[3])
        let hour = Int(self[4])
        let minute = Int(self[5])
        let second = Int(self[6])
        let dateComponent = DateComponents(
            calendar: Calendar(identifier: .gregorian),
            year: Int(year),
            month: month,
            day: day,
            hour: hour,
            minute: minute,
            second: second)
        return dateComponent.date
    }
}


/// 使い方
let bytes: [UInt8] = [0xE1, 0x07, 0x08, 0x18, 0x0B, 0x3A, 0x3B]
Data(bytes).dateTime // 2017-08-24 11:58:59

まとめ

Bluetooth SIG で定められるGATTのデータフォーマットのデコード実装をいくつか上げました。 独自のGATTを定義している場合も多いのであまり需要はない気がしますが何かの参考になればと思います。