WolkSense-Hexiwear/iOS/Hexiwear/Hexiwear.swift
2016-07-21 12:22:05 +02:00

1022 lines
40 KiB
Swift

//
// Hexiwear application is used to pair with Hexiwear BLE devices
// and send sensor readings to WolkSense sensor data cloud
//
// Copyright (C) 2016 WolkAbout Technology s.r.o.
//
// Hexiwear is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Hexiwear is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
//
// Hexiwear.swift
//
import Foundation
import CoreBluetooth
let deviceName = "HEXIWEAR"
let deviceNameLong = "HEXIWEAR"
let deviceNameOtap = "HEXIOTAP"
let deviceNameLongOtap = "HEXIOTAP"
// Service UUIDs
let GAServiceUUID = CBUUID(string: "1800") // Generic Access
let DIServiceUUID = CBUUID(string: "180A") // Device Information
let BatteryServiceUUID = CBUUID(string: "180F") // Battery service
// HEXIWEAR Custom Service UUIDs
let MotionServiceUUID = CBUUID(string: "2000")
let WeatherServiceUUID = CBUUID(string: "2010")
let HealthServiceUUID = CBUUID(string: "2020")
let AlertServiceUUID = CBUUID(string: "2030")
let HexiwearModeServiceUUID = CBUUID(string: "2040")
let OTAPServiceUUID = CBUUID(string: "01FF5550-BA5E-F4EE-5CA1-EB1E5E4B1CE0")
// Characteristic UUIDs
// General Access
let GADeviceNameUUID = CBUUID(string: "2A00") // UTF8 string
let GAAppearanceUUID = CBUUID(string: "2A01") // 16bit?
let GAPPConnParamsUUID = CBUUID(string: "2A04") // uint16[4] - some suitable values
let SerialNumberUUID = CBUUID(string: "2A25")
// Device information
let DIManufacturerUUID = CBUUID(string: "2A29") // UTF8 string
let DIHWRevisionUUID = CBUUID(string: "2A27") // UTF8 string
let DIFWRevisionUUID = CBUUID(string: "2A26") // UTF8 string
// Battery level
let BatteryLevelUUID = CBUUID(string: "2A19") // uint8 - battery level in %
// Motion
let MotionAccelerometerUUID = CBUUID(string: "2001") // int16[3] x, y, z in +/- 4g. Multiplied by 100
let MotionMagnetometerUUID = CBUUID(string: "2003") // int16[3] x, y, z magnet in uT. Multiplied by 100
let MotionGyroUUID = CBUUID(string: "2002") // int16[3] x, y, z +/- 256 deg/sec. Multiplied by 100
// Weather
let WeatherLightUUID = CBUUID(string: "2011") // uint8 light level in %
let WeatherTemperatureUUID = CBUUID(string: "2012") // int16 temperature in Celsius. Multiplied by 100
let WeatherHumidityUUID = CBUUID(string: "2013") // uint16 relative humidity in %. Multiplied by 100
let WeatherPressureUUID = CBUUID(string: "2014") // uint16 in Pascals. Multiplied by 100
// Health
let HealthHeartRateUUID = CBUUID(string: "2021") // uint8 beats per minute
let HealthStepsUUID = CBUUID(string: "2022") // uint16 number of steps
let HealthCaloriesUUID = CBUUID(string: "2023") // uint16 value of kcal
// Alert
let AlertINUUID = CBUUID(string: "2031") // uint8[20] Alerts and commands from Phone TO Watch
let AlertOUTUUID = CBUUID(string: "2032") // uint8[20] Alerts and commands from Watch TO Phone
// Hexiwear mode
let HexiwearModeUUID = CBUUID(string: "2041") // UInt8
// OTAP
let OTAPControlPointUUID = CBUUID(string: "01FF5551-BA5E-F4EE-5CA1-EB1E5E4B1CE0")
let OTAPDataUUID = CBUUID(string: "01FF5552-BA5E-F4EE-5CA1-EB1E5E4B1CE0")
let OTAPStateUUID = CBUUID(string: "01FF5553-BA5E-F4EE-5CA1-EB1E5E4B1CE0") // 0 - no otap, 1 - KW40, 2 - MK64
//MARK:- Watch readings
struct DeviceInfo {
var manufacturer: String
var firmwareRevision: String
init() {
manufacturer = ""
firmwareRevision = ""
}
}
public enum HexiwearMode: Int {
case IDLE = 0 // All sensors off
case WATCH = 1 // temp and battery
case SENSOR_TAG = 2 // All sensors on
case WEATHER_STATION = 3 // Temperature, humidity, pressure data available
case MOTION_CONTROL = 4 // Accel
case HEARTRATE = 5 // heart rate data available
case PEDOMETER = 6 // Pedometer data available
case COMPASS = 7 // ?
static func getReadingsForMode(mode: HexiwearMode) -> [HexiwearReading] {
switch mode {
case .SENSOR_TAG: return [.BATTERY, .ACCELEROMETER, .MAGNETOMETER, .GYRO, .TEMPERATURE, .HUMIDITY, .PRESSURE, .LIGHT]
case .PEDOMETER: return [.PEDOMETER, .CALORIES]
case .HEARTRATE: return [.HEARTRATE]
default: return []
}
}
}
public enum HexiwearReading: Int {
case BATTERY = 0
case ACCELEROMETER = 1
case MAGNETOMETER = 2
case GYRO = 3
case TEMPERATURE = 4
case HUMIDITY = 5
case PRESSURE = 6
case LIGHT = 7
case PEDOMETER = 8
case HEARTRATE = 9
case CALORIES = 10
}
public enum OtapState: UInt8 {
case NO_OTAP = 0
case KW40 = 1
case MK64 = 2
}
struct HexiwearReadings {
var batteryLevel: Double?
var motionAccelX: Double?
var motionAccelY: Double?
var motionAccelZ: Double?
var motionMagnetX: Double?
var motionMagnetY: Double?
var motionMagnetZ: Double?
var motionGyroX: Double?
var motionGyroY: Double?
var motionGyroZ: Double?
var ambientLight: Double?
var ambientTemperature: Double?
var relativeHumidity: Double?
var airPressure: Double?
var heartRate: Int?
var steps: Int?
var calories: Int?
var lastWeatherDate: NSDate?
var lastGyroDate: NSDate?
var lastAccelDate: NSDate?
var lastMagnetDate: NSDate?
var lastStepsDate: NSDate?
var lastCaloriesDate: NSDate?
var lastHeartRateDate: NSDate?
var hexiwearMode: HexiwearMode
private let nf = NSNumberFormatter()
private let mqttNF = NSNumberFormatter()
private let mqttValueNF = NSNumberFormatter()
init() {
nf.numberStyle = .NoStyle
nf.minimumFractionDigits = 1
nf.maximumFractionDigits = 1
nf.minimumIntegerDigits = 1
mqttNF.numberStyle = .NoStyle
mqttNF.minimumFractionDigits = 0
mqttNF.maximumFractionDigits = 0
mqttNF.minimumIntegerDigits = 1
mqttValueNF.numberStyle = .NoStyle
mqttValueNF.minimumFractionDigits = 1
mqttValueNF.maximumFractionDigits = 1
mqttValueNF.minimumIntegerDigits = 1
mqttValueNF.minusSign = ""
mqttValueNF.decimalSeparator = ""
hexiwearMode = .IDLE
}
init(batteryLevel: Double?, temperature: Double?, pressure: Double?, humidity: Double?, accelX: Double?, accelY: Double?, accelZ: Double?, gyroX: Double?, gyroY: Double?, gyroZ: Double?, magnetX: Double?, magnetY: Double?, magnetZ: Double?, steps: Int?, calories: Int?, heartRate: Int?, ambientLight: Double?, hexiwearMode: HexiwearMode) {
self.init()
self.batteryLevel = batteryLevel
self.ambientTemperature = temperature
self.airPressure = pressure
self.relativeHumidity = humidity
self.motionAccelX = accelX
self.motionAccelY = accelY
self.motionAccelZ = accelZ
self.motionGyroX = gyroX
self.motionGyroY = gyroY
self.motionGyroZ = gyroZ
self.motionMagnetX = magnetX
self.motionMagnetY = magnetY
self.motionMagnetZ = magnetZ
self.steps = steps
self.calories = calories
self.heartRate = heartRate
self.ambientLight = ambientLight
self.hexiwearMode = hexiwearMode
}
func batteryLevelAsString() -> String {
return stringFromReading(batteryLevel)
}
func motionAccelXAsString() -> String {
return stringFromReading(motionAccelX)
}
func motionAccelYAsString() -> String {
return stringFromReading(motionAccelY)
}
func motionAccelZAsString() -> String {
return stringFromReading(motionAccelZ)
}
func motionMagnetXAsString() -> String {
return stringFromReading(motionMagnetX)
}
func motionMagnetYAsString() -> String {
return stringFromReading(motionMagnetY)
}
func motionMagnetZAsString() -> String {
return stringFromReading(motionMagnetZ)
}
func motionGyroXAsString() -> String {
return stringFromReading(motionGyroX)
}
func motionGyroYAsString() -> String {
return stringFromReading(motionGyroY)
}
func motionGyroZAsString() -> String {
return stringFromReading(motionGyroZ)
}
func ambientLightAsString() -> String {
return stringFromReading(ambientLight)
}
func ambientTemperatureAsString() -> String {
return stringFromReading(ambientTemperature)
}
func relativeHumidityAsString() -> String {
return stringFromReading(relativeHumidity)
}
func airPressureAsString() -> String {
return stringFromReading(airPressure)
}
func heartRateAsString() -> String {
return stringFromReading(heartRate)
}
func stepsAsString() -> String {
return stringFromReading(steps)
}
func caloriesAsString() -> String {
return stringFromReading(calories)
}
//MARK:- Readings private helpers
private func stringFromReading(readingValue: Double?) -> String {
guard let readingValue = readingValue else { return "--" }
return nf.stringFromNumber(readingValue) ?? "--"
}
private func stringFromReading(readingValue: Int?) -> String {
guard let readingValue = readingValue else { return "--" }
return mqttNF.stringFromNumber(readingValue) ?? "--"
}
private func ambientTemperatureAsMQTTString() -> String? {
guard let temperature = ambientTemperature else { return nil }
if let temp = mqttNF.stringFromNumber(temperature * 10.0) {
return "T:\(temp)"
}
return nil
}
private func relativeHumidityAsMQTTString() -> String? {
guard let humidity = relativeHumidity else { return nil }
if let temp = mqttNF.stringFromNumber(humidity * 10.0) {
return "H:\(temp)"
}
return nil
}
private func airPressureAsMQTTString() -> String? {
guard let pressure = airPressure else { return nil }
if let temp = mqttNF.stringFromNumber(pressure * 10.0) {
return "P:\(temp)"
}
return nil
}
private func MQTTValue(doubleValue: Double) -> String? {
let sign = doubleValue >= 0.0 ? "+" : "-"
guard let stringValue = mqttValueNF.stringFromNumber(doubleValue) else {
return nil
}
return sign + stringValue
}
private func ambientLightAsMQTTString() -> String? {
guard let light = ambientLight else { return nil }
guard let lightValue = MQTTValue(light) else { return nil }
return "LT:\(lightValue)"
}
private func accelAsMQTTString() -> String? {
guard let x = motionAccelX,
y = motionAccelY,
z = motionAccelZ else { return nil }
guard let ax = MQTTValue(x) else { return nil }
guard let ay = MQTTValue(y) else { return nil }
guard let az = MQTTValue(z) else { return nil }
return "ACL:\(ax)\(ay)\(az)"
}
private func gyroAsMQTTString() -> String? {
guard let x = motionGyroX,
y = motionGyroY,
z = motionGyroZ else { return nil }
guard let gx = MQTTValue(x) else { return nil }
guard let gy = MQTTValue(y) else { return nil }
guard let gz = MQTTValue(z) else { return nil }
return "GYR:\(gx)\(gy)\(gz)"
}
private func magnetAsMQTTString() -> String? {
guard let x = motionMagnetX,
y = motionMagnetY,
z = motionMagnetZ else { return nil }
guard let mx = MQTTValue(x) else { return nil }
guard let my = MQTTValue(y) else { return nil }
guard let mz = MQTTValue(z) else { return nil }
return "MAG:\(mx)\(my)\(mz)"
}
private func stepsAsMQTTString() -> String? {
guard let s = steps else { return nil }
return "STP:\(s)"
}
private func caloriesAsMQTTString() -> String? {
guard let c = calories else { return nil }
return "KCAL:\(c)"
}
private func heartRateAsMQTTString() -> String? {
guard let h = heartRate else { return nil }
return "BPM:\(h)"
}
private func arrayOfReadingsToMQTT(readings: [String]) -> String? {
guard readings.count > 0 else { return nil }
let rtcTimestamp = Int64(NSDate().timeIntervalSince1970)
let readingsMQTT = "RTC \(rtcTimestamp);READINGS "
var readingsMQTTTail = ""
for item in readings {
readingsMQTTTail += (item + ",")
}
if readingsMQTTTail.isEmpty {
return nil
}
return readingsMQTT + "R:\(rtcTimestamp)," + String(readingsMQTTTail.characters.dropLast()) + ";"
}
func asMQTTMessage() -> String? {
let mqttPressure = airPressureAsMQTTString()
let mqttTemperature = ambientTemperatureAsMQTTString()
let mqttHumidity = relativeHumidityAsMQTTString()
let mqttLight = ambientLightAsMQTTString()
let mqttAccel = accelAsMQTTString()
let mqttGyro = gyroAsMQTTString()
let mqttMagnet = magnetAsMQTTString()
let mqttSteps = stepsAsMQTTString()
let mqttCalories = caloriesAsMQTTString()
let mqttHeartRate = heartRateAsMQTTString()
let readings = [mqttPressure, mqttTemperature, mqttHumidity, mqttLight, mqttAccel, mqttGyro, mqttMagnet, mqttSteps, mqttCalories, mqttHeartRate]
let filteredReadings = readings.filter { return $0 != nil }.map { return $0!}
return arrayOfReadingsToMQTT(filteredReadings)
}
}
class Hexiwear {
//MARK:- BLE checks and validations
// Check name of device from advertisement data
class func hexiwearFound (advertisementData: [NSObject : AnyObject]!) -> Bool {
let nameOfDeviceFound = (advertisementData as NSDictionary).objectForKey(CBAdvertisementDataLocalNameKey) as? NSString
return (nameOfDeviceFound == deviceName || nameOfDeviceFound == deviceNameLong)
}
// Check name of device from advertisement data
class func hexiotapFound (advertisementData: [NSObject : AnyObject]!) -> Bool {
let otapServices = (advertisementData as NSDictionary).objectForKey(CBAdvertisementDataServiceUUIDsKey) as? [CBUUID]
if let otapServiceAdvertised = otapServices {
return otapServiceAdvertised.contains(OTAPServiceUUID)
}
return false
}
// Check if the service has a valid UUID
class func validService (service : CBService, isOTAPEnabled: Bool) -> Bool {
if isOTAPEnabled {
return service.UUID == OTAPServiceUUID
}
return service.UUID == BatteryServiceUUID
|| service.UUID == MotionServiceUUID
|| service.UUID == WeatherServiceUUID
|| service.UUID == HealthServiceUUID
|| service.UUID == OTAPServiceUUID
|| service.UUID == DIServiceUUID
|| service.UUID == HexiwearModeServiceUUID
|| service.UUID == AlertServiceUUID
}
// Check if the characteristic has a valid data UUID
class func validDataCharacteristic (characteristic : CBCharacteristic, isOTAPEnabled: Bool) -> Bool {
if isOTAPEnabled {
return characteristic.UUID == OTAPControlPointUUID
|| characteristic.UUID == OTAPDataUUID
|| characteristic.UUID == OTAPStateUUID
}
return characteristic.UUID == BatteryLevelUUID
|| characteristic.UUID == MotionAccelerometerUUID
|| characteristic.UUID == MotionMagnetometerUUID
|| characteristic.UUID == MotionGyroUUID
|| characteristic.UUID == WeatherLightUUID
|| characteristic.UUID == WeatherTemperatureUUID
|| characteristic.UUID == WeatherHumidityUUID
|| characteristic.UUID == WeatherPressureUUID
|| characteristic.UUID == HealthHeartRateUUID
|| characteristic.UUID == HealthStepsUUID
|| characteristic.UUID == HealthCaloriesUUID
|| characteristic.UUID == OTAPControlPointUUID
|| characteristic.UUID == OTAPDataUUID
|| characteristic.UUID == SerialNumberUUID
|| characteristic.UUID == DIManufacturerUUID
|| characteristic.UUID == DIHWRevisionUUID
|| characteristic.UUID == DIFWRevisionUUID
|| characteristic.UUID == HexiwearModeUUID
|| characteristic.UUID == AlertINUUID
}
//MARK:- Characteristics value parsing
// Get battery level value
class func getBatteryLevel(value : NSData?) -> Double? {
guard let value = value else { return nil }
let dataFromSensor: [UInt8] = dataToIntegerArray(value)
let batteryLevel = Double(dataFromSensor[0])
return batteryLevel
}
// Get motion accelerometer values
class func getMotionAccelerometerValues(value : NSData?) -> (x: Double, y: Double, z: Double)? {
return getXYZValues(value, divideBy: 100.0)
}
// Get motion magnetometer values
class func getMotionMagnetometerValues(value : NSData?) -> (x: Double, y: Double, z: Double)? {
return getXYZValues(value, divideBy: 100.0)
}
// Get motion gyro values
class func getMotionGyroValues(value : NSData?) -> (x: Double, y: Double, z: Double)? {
return getXYZValues(value, divideBy: 1.0)
}
// Get ambient light value
class func getAmbientLight(value : NSData?) -> Double? {
guard let value = value else { return nil }
let dataFromSensor: [UInt8] = dataToIntegerArray(value)
let ambientLight = Double(dataFromSensor[0])
return ambientLight
}
// Get ambient temperature value
class func getAmbientTemperature(value : NSData?) -> Double? {
guard let value = value else { return nil }
let dataFromSensor: [Int16] = dataToIntegerArray(value)
let ambientTemperature = Double(dataFromSensor[0])/100
return ambientTemperature
}
// Get relative Humidity
class func getRelativeHumidity(value: NSData?) -> Double? {
guard let value = value else { return nil }
let dataFromSensor: [UInt16] = dataToIntegerArray(value)
let humidity = Double(dataFromSensor[0])/100
return humidity
}
// Get pressure value
class func getPressureData(value: NSData?) -> Double? {
guard let value = value else { return nil }
let dataFromSensor: [UInt16] = dataToIntegerArray(value)
let pressure = Double(dataFromSensor[0])/10
return pressure
}
// Get heart rate
class func getHeartRate(value: NSData?) -> Int? {
guard let value = value else { return nil }
let dataFromSensor: [UInt8] = dataToIntegerArray(value)
let heartRate = Int(dataFromSensor[0])
return heartRate
}
// Get steps
class func getSteps(value: NSData?) -> Int? {
guard let value = value else { return nil }
let dataFromSensor: [UInt16] = dataToIntegerArray(value)
let steps = Int(dataFromSensor[0])
return steps
}
// Get calories
class func getCalories(value: NSData?) -> Int? {
guard let value = value else { return nil }
let dataFromSensor: [UInt16] = dataToIntegerArray(value)
let calories = Int(dataFromSensor[0])
return calories
}
// Get hexiwear mode
class func getHexiwearMode(value: NSData?) -> Int? {
guard let value = value else { return nil }
let dataFromSensor: [UInt8] = dataToIntegerArray(value)
let mode = Int(dataFromSensor[0])
return mode
}
// Get otap status
class func getOtapState(value: NSData?) -> OtapState {
guard let value = value else { return OtapState.NO_OTAP }
let dataFromSensor: [UInt8] = dataToIntegerArray(value)
guard dataFromSensor.count > 0 else { return .NO_OTAP }
let state = OtapState(rawValue: dataFromSensor[0]) ?? OtapState.NO_OTAP
return state
}
// Get Manufacturer
class func getManufacturer(value: NSData?) -> String? {
return getStringFromNSData(value)
}
// Get Firmware revision
class func getFirmwareRevision(value: NSData?) -> String? {
return getStringFromNSData(value)
}
// Get Current time
class func getCurrentTimestampForHexiwear(isTimeZoneOffsetIncluded: Bool) -> [UInt8] {
let timezone = NSTimeZone.localTimeZone()
let currentTimestamp = NSDate()
let currentTimestampAsUInt32 = UInt32(currentTimestamp.timeIntervalSince1970)
let timezoneOffsetInSecs = timezone.secondsFromGMT
let timezoneOffset = isTimeZoneOffsetIncluded ? UInt32(abs(timezoneOffsetInSecs)) : UInt32(0)
let correctedTimestampForHexiwear = timezoneOffsetInSecs > 0 ? currentTimestampAsUInt32 + timezoneOffset : currentTimestampAsUInt32 - timezoneOffset
var returnArray: [UInt8] = [3, 4] // 3 - AlertIn command id for setting time, 4 - lenght of value in bytes
returnArray += uint32ToLittleEndianBytesArray(correctedTimestampForHexiwear)
while returnArray.count < 20 {
returnArray.append(0x00) // append remaining bytes (to 20) with zeroes
}
return returnArray
}
private class func getStringFromNSData(value: NSData?) -> String? {
guard let value = value else { return nil }
let str = String(data: value, encoding: NSUTF8StringEncoding)
return str
}
//MARK:- Hexiwear private helpers
private class func getXYZValues(value : NSData?, divideBy: Double) -> (x: Double, y: Double, z: Double)? {
guard let value = value else { return nil }
let dataFromSensor: [Int16] = dataToIntegerArray(value)
let valueX = Double(dataFromSensor[0]) / divideBy
let valueY = Double(dataFromSensor[1]) / divideBy
let valueZ = Double(dataFromSensor[2]) / divideBy
return (valueX, valueY, valueZ)
}
}
//MARK:- OTAP
// BLE OTAP Protocol definitions
let OTAP_CmdIdFieldSize: UInt16 = 1
let OTAP_ImageIdFieldSize: UInt16 = 2
let OTAP_ImageVersionFieldSize: UInt16 = 8
let OTAP_ChunkSeqNumberSize: UInt16 = 1
let OTAP_MaxChunksPerBlock: UInt16 = 256
let OTAP_StartPositionSize: UInt16 = 4
let OTAP_BlockSize: UInt16 = 4
let OTAP_ChunkSize: UInt16 = 2
let OTAP_TransferMethodSize: UInt16 = 1
let OTAP_TransferChannelSize: UInt16 = 2
let OTAP_ImageFileHeaderLength: UInt16 = 58
let OTAP_CmdErrorStatusSize: UInt16 = 1
// ATT_MTU
let OTAP_ImageChunkDataSizeAtt: UInt16 = 20 - OTAP_CmdIdFieldSize - OTAP_ChunkSeqNumberSize
let OTAP_TransferMethodAtt: UInt8 = 0x00
let OTAP_TransferChannelAtt: UInt16 = 0x0004
let GAP_PPCP_connectionInterval: Double = 0.050 // 50.0ms
let GAP_PPCP_packetLeeway: Double = 0.0025 // 2.5ms per packet
// Protocol
protocol ConvertableToBinary {
func convertToBinary() -> [UInt8]
}
// OTA Image Header
struct OTAImageFileHeader: ConvertableToBinary {
let fileIdentifier: UInt32
let headerVersion: UInt16
let headerLength: UInt16
let headerFieldControl: UInt16
let companyId: UInt16
let imageId: UInt16 // here will be 1 - KW40, 2 - MK64 as is in 5553 characteristics
let imageVersion: [UInt8] // length = OTAP_ImageVersionFieldSize - this info will be visible in DIS Firmware version string upon OTAP upload and restart
let headerString: [UInt8] // length = BLE_OTAP_HeaderStrLength
let totalImageFileSize: UInt32
init?(data: NSData?) {
// There should be some data
guard let data = data else { return nil }
let length = Int(OTAP_ImageFileHeaderLength) // header size is 58 bytes
var dataBytes: [UInt8] = [UInt8](count: length, repeatedValue: 0x00)
data.getBytes(&dataBytes, length: length)
fileIdentifier = uint32FromFourBytes(dataBytes[0], lohiByte: dataBytes[1], hiloByte: dataBytes[2], hihiByte: dataBytes[3])
headerVersion = uint16FromTwoBytes(dataBytes[4], hiByte: dataBytes[5])
headerLength = uint16FromTwoBytes(dataBytes[6], hiByte: dataBytes[7])
headerFieldControl = uint16FromTwoBytes(dataBytes[8], hiByte: dataBytes[9])
companyId = uint16FromTwoBytes(dataBytes[10], hiByte: dataBytes[11])
imageId = uint16FromTwoBytes(dataBytes[12], hiByte: dataBytes[13])
imageVersion = Array(dataBytes[14...21])
headerString = Array(dataBytes[22...53])
totalImageFileSize = uint32FromFourBytes(dataBytes[54], lohiByte: dataBytes[55], hiloByte: dataBytes[56], hihiByte: dataBytes[57])
print(self)
}
func convertToBinary() -> [UInt8] {
var bytesArray:[UInt8] = []
bytesArray.appendContentsOf(uint32ToLittleEndianBytesArray(fileIdentifier))
bytesArray.appendContentsOf(uint16ToLittleEndianBytesArray(headerVersion))
bytesArray.appendContentsOf(uint16ToLittleEndianBytesArray(headerLength))
bytesArray.appendContentsOf(uint16ToLittleEndianBytesArray(headerFieldControl))
bytesArray.appendContentsOf(uint16ToLittleEndianBytesArray(companyId))
bytesArray.appendContentsOf(imageVersion)
bytesArray.appendContentsOf(headerString)
bytesArray.appendContentsOf(uint32ToLittleEndianBytesArray(totalImageFileSize))
return bytesArray
}
func imageVersionAsString() -> String {
guard imageVersion.count >= 3 else { return "" }
let major = imageVersion[0]
let minor = imageVersion[1]
let build = imageVersion[2]
return "\(major).\(minor).\(build)"
}
}
// BLE OTAP Protocol statuses
enum OTAP_Status: UInt8 {
case StatusSuccess = 0x00 /*!< The operation was successful. */
case StatusImageDataNotExpected = 0x01 /*!< The OTAP Server tried to send an image data chunk to the OTAP Client but the Client was not expecting it. */
case StatusUnexpectedTransferMethod = 0x02 /*!< The OTAP Server tried to send an image data chunk using a transfer method the OTAP Client does not support/expect. */
case StatusUnexpectedCmdOnDataChannel = 0x03 /*!< The OTAP Server tried to send an unexpected command (different from a data chunk) on a data Channel (ATT or CoC) */
case StatusUnexpectedL2capChannelOrPsm = 0x04 /*!< The selected channel or PSM is not valid for the selected transfer method (ATT or CoC). */
case StatusUnexpectedOtapPeer = 0x05 /*!< A command was received from an unexpected OTAP Server or Client device. */
case StatusUnexpectedCommand = 0x06 /*!< The command sent from the OTAP peer device is not expected in the current state. */
case StatusUnknownCommand = 0x07 /*!< The command sent from the OTAP peer device is not known. */
case StatusInvalidCommandLength = 0x08 /*!< Invalid command length. */
case StatusInvalidCommandParameter = 0x09 /*!< A parameter of the command was not valid. */
case StatusFailedImageIntegrityCheck = 0x0A /*!< The image integrity check has failed. */
case StatusUnexpectedSequenceNumber = 0x0B /*!< A chunk with an unexpected sequence number has been received. */
case StatusImageSizeTooLarge = 0x0C /*!< The upgrade image size is too large for the OTAP Client. */
case StatusUnexpectedDataLength = 0x0D /*!< The length of a Data Chunk was not expected. */
case StatusUnknownFileIdentifier = 0x0E /*!< The image file identifier is not recognized. */
case StatusUnknownHeaderVersion = 0x0F /*!< The image file header version is not recognized. */
case StatusUnexpectedHeaderLength = 0x10 /*!< The image file header length is not expected for the current header version. */
case StatusUnexpectedHeaderFieldControl = 0x11 /*!< The image file header field control is not expected for the current header version. */
case StatusUnknownCompanyId = 0x12 /*!< The image file header company identifier is not recognized. */
case StatusUnexpectedImageId = 0x13 /*!< The image file header image identifier is not as expected. */
case StatusUnexpectedImageVersion = 0x14 /*!< The image file header image version is not as expected. */
case StatusUnexpectedImageFileSize = 0x15 /*!< The image file header image file size is not as expected. */
case StatusInvalidSubElementLength = 0x16 /*!< One of the sub-elements has an invalid length. */
case StatusImageStorageError = 0x17 /*!< An image storage error has occurred. */
case StatusInvalidImageCrc = 0x18 /*!< The computed CRC does not match the received CRC. */
case StatusInvalidImageFileSize = 0x19 /*!< The image file size is not valid. */
case StatusInvalidL2capPsm = 0x1A /*!< A block transfer request has been made via the L2CAP CoC method but the specified Psm is not known. */
case StatusNoL2capPsmConnection = 0x1B /*!< A block transfer request has been made via the L2CAP CoC method but there is no valid PSM connection. */
case NumberOfStatuses = 0x1C
}
// OTAP Protocol Commands
enum OTAP_Command: UInt8 {
case NoCommand = 0x00 /*!< No command. */
case NewImageNotification = 0x01 /*!< OTAP Server -> OTAP Client - A new image is available on the OTAP Server */
case NewImageInfoRequest = 0x02 /*!< OTAP Client -> OTAP Server - The OTAP Client requests image information from the OTAP Server */
case NewImageInfoResponse = 0x03 /*!< OTAP Server -> OTAP Client - The OTAP Server sends requested image information to the OTAP Client */
case ImageBlockRequest = 0x04 /*!< OTAP Client -> OTAP Server - The OTAP Client requests an image block from the OTAP Server */
case ImageChunk = 0x05 /*!< OTAP Server -> OTAP Client - The OTAP Server sends an image chunk to the OTAP Client */
case ImageTransferComplete = 0x06 /*!< OTAP Client -> OTAP Server - The OTAP Client notifies the OTAP Server that an image transfer was completed*/
case ErrorNotification = 0x07 /*!< Bidirectional - An error has occurred */
case StopImageTransfer = 0x08 /*!< OTAP Client -> OTAP Server - The OTAP Client request the OTAP Server to stop an image transfer. */
}
// OTAP New Image Notification Command
struct OTAP_CMD_NewImgNotification: ConvertableToBinary {
let command: OTAP_Command = OTAP_Command.NewImageNotification
let imageId: [UInt8] // length = OTAP_ImageIdFieldSize
let imageVersion: [UInt8] // length = OTAP_ImageVersionFieldSize
let imageFileSize: UInt32
func convertToBinary() -> [UInt8] {
var bytesArray:[UInt8] = []
bytesArray.append(command.rawValue)
bytesArray.appendContentsOf(imageId)
bytesArray.appendContentsOf(imageVersion)
bytesArray.appendContentsOf(uint32ToLittleEndianBytesArray(imageFileSize))
return bytesArray
}
}
// OTAP New Image Info Request Command
struct OTAP_CMD_NewImgInfoReq {
let command: OTAP_Command = OTAP_Command.NewImageInfoRequest
let currentImageId: [UInt8] // length = OTAP_ImageIdFieldSize
let currentImageVersion: [UInt8] // length = OTAP_ImageVersionFieldSize
init?(data: NSData?) {
// There should be some data
guard let data = data else { return nil }
var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00)
let length = data.length
data.getBytes(&dataBytes, length: length)
// Data length should be CmdId + ImageId + ImageVersion = 1 + 2 + 8 = 11bytes
guard length == Int(OTAP_CmdIdFieldSize + OTAP_ImageIdFieldSize + OTAP_ImageVersionFieldSize) else { return nil }
currentImageId = Array(dataBytes[1...2])
currentImageVersion = Array(dataBytes[3...dataBytes.count-1])
print(self)
}
}
// OTAP New Image Info Response Command
struct OTAP_CMD_NewImgInfoRes: ConvertableToBinary {
let command: OTAP_Command = OTAP_Command.NewImageInfoResponse
let imageId: [UInt8] // length = OTAP_ImageIdFieldSize
let imageVersion: [UInt8] // length = OTAP_ImageVersionFieldSize
let imageFileSize: UInt32
init(imageId: [UInt8], imageVersion: [UInt8], imageFileSize: UInt32) {
self.imageId = imageId
self.imageVersion = imageVersion
self.imageFileSize = imageFileSize
print(self)
}
func convertToBinary() -> [UInt8] {
var bytesArray:[UInt8] = []
bytesArray.append(command.rawValue)
bytesArray.appendContentsOf(imageId)
bytesArray.appendContentsOf(imageVersion)
bytesArray.appendContentsOf(uint32ToLittleEndianBytesArray(imageFileSize))
return bytesArray
}
}
// OTAP Image Block Request Command
struct OTAP_CMD_ImgBlockReq {
let command: OTAP_Command = OTAP_Command.ImageBlockRequest
let imageId: [UInt8] // length = OTAP_ImageIdFieldSize
let startPosition: UInt32
let blockSize: UInt32
let chunkSize: UInt16
let transferMethod: UInt8 // should be OTAP_TransferMethodAtt = 0x00
let l2capChannelOrPsm: UInt16 // should be OTAP_TransferChannelAtt = 0x0004
init?(data: NSData?) {
// There should be some data
guard let data = data else { return nil }
var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00)
let length = data.length
data.getBytes(&dataBytes, length: length)
// Data length should be CmdId + ImageId + StartPosition + BlockSize + ChunkSize + TransferMethodSize + TransferChannelSize = 1 + 2 + 4 + 4 + 2 + 1 + 2 = 16bytes
guard length == Int(OTAP_CmdIdFieldSize + OTAP_ImageIdFieldSize + OTAP_StartPositionSize + OTAP_BlockSize + OTAP_ChunkSize + OTAP_TransferMethodSize + OTAP_TransferChannelSize) else { return nil }
imageId = Array(dataBytes[1...2])
startPosition = uint32FromFourBytes(dataBytes[3], lohiByte: dataBytes[4], hiloByte: dataBytes[5], hihiByte: dataBytes[6])
blockSize = uint32FromFourBytes(dataBytes[7], lohiByte: dataBytes[8], hiloByte: dataBytes[9], hihiByte: dataBytes[10])
chunkSize = uint16FromTwoBytes(dataBytes[11], hiByte: dataBytes[12])
transferMethod = dataBytes[13]
l2capChannelOrPsm = uint16FromTwoBytes(dataBytes[14], hiByte: dataBytes[15])
print(self)
}
}
// OTAP Image Chunk Command - for ATT transfer method only
struct OTAP_CMD_ImgChunkAtt: ConvertableToBinary {
let command: OTAP_Command = OTAP_Command.ImageChunk
let seqNumber: UInt8 // Max 256 chunks per block. */
let data: [UInt8] // length = OTAP_ImageChunkDataSizeAtt
init(seqNumber: UInt8, data: [UInt8]) {
self.seqNumber = seqNumber
self.data = data
}
func convertToBinary() -> [UInt8] {
var bytesArray:[UInt8] = []
bytesArray.append(command.rawValue)
bytesArray.append(seqNumber)
bytesArray.appendContentsOf(data)
return bytesArray
}
}
// OTAP Image Transfer Complete Command
struct OTAP_CMD_ImgTransferComplete {
let command: OTAP_Command = OTAP_Command.ImageTransferComplete
let imageId: [UInt8] // length = OTAP_ImageIdFieldSize
let status: OTAP_Status // length = OTAP_CmdErrorStatusSize
init?(data: NSData?) {
// There should be some data
guard let data = data else { return nil }
var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00)
let length = data.length
data.getBytes(&dataBytes, length: length)
// Data length should be CmdId + ImageId + ErrorStatus = 1 + 2 + 1 = 4bytes
guard length == Int(OTAP_CmdIdFieldSize + OTAP_ImageIdFieldSize + OTAP_CmdErrorStatusSize) else { return nil }
imageId = Array(dataBytes[1...2])
if let s = OTAP_Status(rawValue: dataBytes[3]) {
status = s
}
else {
status = OTAP_Status.NumberOfStatuses
}
print(self)
}
}
// OTAP Error Notification Command
struct OTAP_CMD_ErrNotification: ConvertableToBinary {
let command: OTAP_Command = OTAP_Command.ErrorNotification
let cmdId: UInt8 // The command which caused the error
let errStatus: UInt8 // Actually OTAP_Status code
init?(data: NSData?) {
// There should be some data
guard let data = data else { return nil }
var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00)
let length = data.length
data.getBytes(&dataBytes, length: length)
// Data length should be CmdId + CmdId + ErrorStatus = 1 + 1 + 1 = 3bytes
guard length == Int(OTAP_CmdIdFieldSize + OTAP_CmdIdFieldSize + OTAP_CmdErrorStatusSize) else { return nil }
cmdId = dataBytes[1]
errStatus = dataBytes[2]
print(self)
}
func convertToBinary() -> [UInt8] {
var bytesArray:[UInt8] = []
bytesArray.append(command.rawValue)
bytesArray.append(cmdId)
bytesArray.append(errStatus)
return bytesArray
}
}
// OTAP Stop Image Transfer Command
struct OTAP_CMD_StopImgTransfer {
let command: OTAP_Command
let imageId: [UInt8] // length = OTAP_ImageIdFieldSize
}
// Extract command id from NSData
func getOTAPCommand(data: NSData?) -> OTAP_Command {
// There should be some data
guard let data = data else { return OTAP_Command.NoCommand }
var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00)
let length = data.length
data.getBytes(&dataBytes, length: length)
let cmdId: UInt8 = dataBytes[0]
return OTAP_Command(rawValue: cmdId) ?? OTAP_Command.NoCommand
}
// Conversion helpers
func uint32ToLittleEndianBytesArray(uint32: UInt32) -> [UInt8] {
var littleEndian = uint32.littleEndian
let bytePtr = withUnsafePointer(&littleEndian) {
UnsafeBufferPointer<UInt8>(start: UnsafePointer($0), count: sizeofValue(littleEndian))
}
let byteArray = Array(bytePtr)
return byteArray
}
func uint16ToLittleEndianBytesArray(uint16: UInt16) -> [UInt8] {
var littleEndian = uint16.littleEndian
let bytePtr = withUnsafePointer(&littleEndian) {
UnsafeBufferPointer<UInt8>(start: UnsafePointer($0), count: sizeofValue(littleEndian))
}
let byteArray = Array(bytePtr)
return byteArray
}
func uint16FromTwoBytes(loByte: UInt8, hiByte: UInt8) -> UInt16 {
let lowByte = UInt16(loByte & 0xFF)
let highByte = UInt16(hiByte & 0xFF) << 8
return highByte | lowByte
}
func int16FromTwoBytes(loByte: UInt8, hiByte: UInt8) -> Int16 {
let lowByte = Int16(loByte & 0xFF)
let highByte = Int16(hiByte & 0xFF) << 8
return highByte | lowByte
}
func uint32FromFourBytes(loloByte: UInt8, lohiByte: UInt8, hiloByte: UInt8, hihiByte: UInt8) -> UInt32 {
let lowLowByte = UInt32(loloByte & 0xFF)
let lowHighByte = UInt32(lohiByte & 0xFF) << 8
let highLowByte = UInt32(hiloByte & 0xFF) << 16
let highHighByte = UInt32(hihiByte & 0xFF) << 24
return highHighByte | highLowByte | lowHighByte | lowLowByte
}
func dataToIntegerArray<T: IntegerType>(value: NSData) -> [T] {
let dataLength = value.length
let count = dataLength / sizeof(T)
var array = [T](count: count, repeatedValue: 0x00)
value.getBytes(&array, length:dataLength)
return array
}