0

I'm trying to send date information to a BLE device as part of pairing process. The BLE device documentation says that my application should set time and date to the BLE device at pairing mode, which I'm guessing is how the bond is set? (please correct me if I am wrong. Nowhere in the documentation does it say how to send encryption keys or anything of the sort).

The BLE documentation says it requires to receive 7 bytes

Name Byte Format Value
Year 2 16bit 07DD~08CF
Month 1 8bit 01~0C
Day 1 8bit 01~1F
Hours 1 8bit 00~18
Minutes 1 8bit 00~3B
Second 1 8bit 00~3B

When I use nRF connect to attempt connection with the BLE device, I see the Date UUID (0x2A08), and when I retrieve the value, it gives me 0xDD-07-01-01-00-12-09 in Byte Array (Hex) format, or in Date: Tue, Jan 1, 2013, 00:18:09

I followed this post to be able to convert date object into byte array.

But when I do:

let date = Date()
let array = Array(date.data)

I get back:

2022-06-17 04:05:51 +0000
[0, 0, 128, 143, 26, 46, 196, 65]

I think the timezone +0000 is adding onto the byte array, but I am not sure how to remove that timezone part of the date. Even using DateFormatter to specify yyyy-MM-dd HH:mm:ss gives me the timezone.

Also, I know very very little about bytes and all that stuff, but the array of bytes printed above doesnt look like the 8bit format that my BLE documentation requests.

Is there any way that I can better retrieve current Date (or custom set time) into 8bit byte array that I can write back to the BLE device?

Thank you

chriisong
  • 3
  • 2
  • Convert the date to `DateComponents` with `Calendar`. Except the year all components can be directly converted to `UInt8` and for the year you need one division. Then append the bytes to an `UInt8` array in the proper oder and convert it to `Data`. – vadian Jun 17 '22 at 04:53
  • What timezone are you expecting it UTC or current timezone? – Leo Dabus Jun 17 '22 at 05:09
  • just current timezone @LeoDabus – chriisong Jun 17 '22 at 05:09

2 Answers2

0

You can create a method to convert FixedWidthInteger to bytes (big or little endian) for the year component, convert the other date componentes to bytes and append them to the result:

enum Endianness {
    case big, little
}

extension FixedWidthInteger {
    func data<D: DataProtocol & RangeReplaceableCollection>(
        using endianness: Endianness = .big
    ) -> D {
        withUnsafeBytes(of: endianness == .big ? bigEndian : littleEndian, D.init)
    }
}

extension Calendar {
    static let iso8601 = Self(identifier: .iso8601)
}

extension Date {
    func component(_ component: Calendar.Component, using calendar: Calendar = .iso8601) -> Int {
        calendar.component(component, from: self)
    }
    func data<D: DataProtocol & RangeReplaceableCollection>() -> D {
        var dataProtocol: D = .init()
        dataProtocol += UInt16(component(.year)).data() as D
        dataProtocol.append(UInt8(component(.month)))
        dataProtocol.append(UInt8(component(.day)))
        dataProtocol.append(UInt8(component(.hour)))
        dataProtocol.append(UInt8(component(.minute)))
        dataProtocol.append(UInt8(component(.second)))
        return dataProtocol
    }
}

Usage:

let data: Data = Date().data()
print(Array(data))  

This will print

[7, 230, 6, 17, 2, 10, 46]

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • I appreciate the conciseness of your answer, but wondering what the "230" is in part of your byte array. the format that the BLE device is looking for is like : 07DD~08CF, 01~0C, 01~1F, 00~18, 00~3B. Is it just different bit? If so, how can I make the conversion? Sorry Im just really not familiar in this topic :( – chriisong Jun 17 '22 at 16:03
  • Do you need bytes or hexa string? One byte 0-255 represents two hexa characters 0-9A-F (16*16). [7,230] two bytes are the same as “0x07E6” in hexa string which means 2022 decimal value. `(07E6)₁₆ = (0 × 16³) + (7 × 16²) + (14 × 16¹) + (6 × 16⁰) = (2022)₁₀` – Leo Dabus Jun 17 '22 at 18:25
  • In your post you said **The BLE documentation says it requires to receive 7 bytes** – Leo Dabus Jun 17 '22 at 18:41
  • If you need `Data` instead of `[UInt8]` array of bytes you can explicitly set the resulting type `let data: Data = Date().data()`. That’s why I’ve made it a generic method. – Leo Dabus Jun 17 '22 at 18:42
  • The BLE documentation says it requires to receive 7 bytes of the date components (two for year, then month, day, hour, minute, second), in `Data` format, because `CBPeripheral`'s `writeValue(_:for:type:)` takes `Data` not UInt8 – chriisong Jun 20 '22 at 20:54
  • @chriisong As I have mentioned above you just need to explicitly set the resulting type to `Data` check my last edit. – Leo Dabus Jun 21 '22 at 05:37
  • this worked magnificently. Thank you – chriisong Jun 21 '22 at 05:38
0

First you need to get clear on the byte ordering so you can convert the 2-byte year into an array of bytes. I will assume that BLE expects byte ordering in big endian format. If not, you will have to change this up:

extension UInt16 {
    var fromLocalToBLE: UInt16 {
        CFSwapInt16HostToBig(self)
    }
    var fromBLEToLocal: UInt16 {
        CFSwapInt16BigToHost(self)
    }
}

To convert a Date to a byte array you need to start with the year. We extract the year, cast it to a UInt16, convert it to a byte array, then swap the byte ordering if necessary. For the remaining components just cast them to a UInt8 and store them at the tail of the byte array.

extension Date {
    func bleData(calendar: Calendar) -> [UInt8] {

        func byte(_ c: Calendar.Component) -> UInt8 {
            return UInt8(calendar.component(c, from: self))
        }

        let yearBytes: [UInt8] = {
            let year = UInt16(calendar.component(.year, from: self)).fromLocalToBLE
            return withUnsafeBytes(of: year) { pointer in
                pointer.map { $0 }
            }
        }()

        return yearBytes + [
            byte(.month),
            byte(.day),
            byte(.hour),
            byte(.minute),
            byte(.second),
        ]
    }
}

If you want to convert a byte array to a Date extract the first 16-bits for the year and swap the bytes if necessary. Take the year and all the remaining bytes to construct your DateComponents. Once you have that you can convert them to a Date.

extension Date {
    init(bleData: [UInt8], calendar: Calendar) {

        let year = bleData.withUnsafeBytes { src -> Int in
            var year: UInt16 = 0
            withUnsafeMutableBytes(of: &year) { dest in
                _ = src.copyBytes(to: dest, count: 2)
            }
            return Int(year.fromBLEToLocal)
        }

        let comp = DateComponents(
            year: year,
            month: Int(bleData[2]),
            day: Int(bleData[3]),
            hour: Int(bleData[4]),
            minute: Int(bleData[5]),
            second: Int(bleData[6])
        )

        self = calendar.date(from: comp)!
    }
}

You can do a round-trip test as follows:

    let origDate = Date()
    let origDateBytes = origDate.bleData(calendar: .current)

    print(origDateBytes.map { String(format: "0x%02X", $0) })

    let reverseDate = Date(bleData: origDateBytes, calendar: .current)

    func showDate(name: String, date: Date) {
        let df = DateFormatter()
        df.dateStyle = .medium
        df.timeStyle = .medium
        print(name, df.string(from: date))
    }

    showDate(name: "Orig:", date: origDate)
    showDate(name: "Reversed:", date: reverseDate)

Prints:

["0x07", "0xE6", "0x06", "0x11", "0x01", "0x21", "0x18"]
Orig: Jun 17, 2022 at 1:33:24 AM
Reversed: Jun 17, 2022 at 1:33:24 AM
Rob C
  • 4,877
  • 1
  • 11
  • 24
  • You should never use the device’s calendar to interpret the calendar year. It is very error prone and could result in more than 500 years offset – Leo Dabus Jun 17 '22 at 13:27
  • @LeoDabus Thanks, I wasn't sure which Calendar to use. I probably should have made a note of that. – Rob C Jun 17 '22 at 16:38