6
\$\begingroup\$

I have a device I connect to on TCP socket. I send it a message with what I want and it sends back all sorts of useful information:

Header:

+---------------+----------+----------+
| MagicWord     | 2 bytes  | 0xCAFE   |
| Version       | 1 byte   | 0x02     |
| Type          | 1 byte   | Variable |
| UTC offset    | 1 byte   | Variable |
| Reserved      | 1 byte   | Variable |
| PayloadSize   | 2 bytes  | Variable | 
+---------------+----------+----------+

Payload:

+---------------+----------+----------+
| StartTime     | 4 bytes  | Variable |
| Duration      | 4 bytes  | Variable |
| Temp          | 2 bytes  | Variable |
| EstimatedDist | 2 bytes  | Variable |
| NumKnocks     | 1 byte   | Variable |
| Reserved      | 1 byte   |          |
+---------------+----------+----------+

Here is my attempt at how I got the variables from the message:

var location:[(String, [UInt8], Int)] = []

//Name, message, location in data

let dict  = [
    ("startTime", [UInt8](), 8),
    ("duration", [UInt8](), 12),
    ("Temp", [UInt8](), 16),
    ("EstimatedDist", [UInt8](), 18),
    ("NumKnocks", [UInt8](), 20),
    ("Reserverd", [UInt8](), 21)]

for item in dict {
    location.append(item)
}

var i = 0
var itemLen = Int()
for item in location {
    if 0...4 ~= i {

        //find length of byte by length subtraction of item itself from item above.
        itemLen = location[i + 1].2 - location[i].2
    }
    let typeRange = NSRange(location: location[i].2, length: itemLen)
    let typeData = data.subdataWithRange(typeRange)
    var arr = Array(UnsafeBufferPointer(start: UnsafePointer<UInt8>(typeData.bytes), count: typeData.length))
    location[i].1 = arr
    i = i + 1
}
0000   fe ca 02 30 00 00 0e 00 4e 5b a2 56 12 19 00 00
0010   22 00 12 19 07 00

In my situation data is NSData from a GCDAsyncSocket response with the header removed.

For testing i've been doing and connecting to port.

echo -e "\xfe\xca\x02\x30\x00\x00\x0e\x00\x4e\x5b\xa2\x56\x12\x19\x00\x00\x22\x00\x12\x19\x07\x00" | nc -kl -c 1975

I can connect, send the packet, get the message back thats all fine. I can get the variables from the packet. However, I don't feel it's that elegant. I want to know what I should be looking at to try to improve. Examples (with explanations) would be great.

\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! I hope you get some helpful reviews. \$\endgroup\$ – SirPython Jan 22 '16 at 20:31
  • \$\begingroup\$ It would be extraordinarily helpful to actually be able to instantiate the data variable so this code could be run and I could get a better idea of what is actually being accomplished. Can you give an example of the raw data packet? \$\endgroup\$ – nhgrif Jan 22 '16 at 22:52
  • \$\begingroup\$ data is just the packet as NSData with the header removed, in my situations straight from GCDAsyncSocket received response. I'll edit my post for more detail. \$\endgroup\$ – Magrafear Jan 22 '16 at 23:15
  • \$\begingroup\$ So to be clear, the data your code is actually trying to work with (given the above example) is this: <4e5ba256 12190000 22001219 0700>, correct? \$\endgroup\$ – nhgrif Jan 23 '16 at 0:03
  • 1
    \$\begingroup\$ For this data: <feca0230 00000e00 4e5ba256 12190000 22001219 0700>, I'm getting these values: startTime: [78, 91, 162, 86] duration: [18, 25, 0, 0] Temp: [34, 0] EstimatedDist: [18, 25] NumKnocks: [7] Reserverd: [0] --- is this correct for your script? Importantly... for endianness... should temperature be 34 degrees, or 8704? \$\endgroup\$ – nhgrif Jan 23 '16 at 0:25
10
\$\begingroup\$

One things for certain... an array of tuples which is basically being used as a glorified dictionary is never going to suffice as an acceptable data model. Moreover, an array of bytes isn't really that much useful than what we start with.

So what's clear, we need an actual data model. So names and data types may need some twiddling, but here's what I started with:

class Information {
    let startTime: UInt32
    let duration: UInt32
    let temperature: UInt16
    let estimatedDistance: UInt16
    let knockCount: UInt8
    let reserved: UInt8

    // TODO: implement initializers
}

This is the model for our data. This is the easiest way for the entire rest of our app to access the data. This will be way easier to use.

All that's left is getting from our data packet (<4e5ba256 12190000 22001219 0700>) to an actual instantiated Information object.

I know that we can get from an NSData object to an array of UInt8s, but I notice, we're going to need some UInt16s and some UInt32s. So, I extended UInt16 and UInt32 to include some constructors for taking an array of bytes:

extension UInt16 {
    init?(bytes: [UInt8]) {
        if bytes.count != 2 {
            return nil
        }

        var value: UInt16 = 0
        for byte in bytes.reverse() {
            value = value << 8
            value = value | UInt16(byte)
        }
        self = value
    }
}

extension UInt32 {
    init?(bytes: [UInt8]) {
        if bytes.count != 4 {
            return nil
        }

        var value: UInt32 = 0
        for byte in bytes.reverse() {
            value = value << 8
            value = value | UInt32(byte)
        }
        self = value
    }
}

If it's not immediately obvious how this is useful, keep reading. We'll see in a bit.

Now, what do we need to input and what do we need to get in return? At the end of the day, we need an Information object, which is this class of six variously sized unsigned integers. What we start with, however, is some raw data. So ideally, we'd like to simply do something like this:

let myInformation = Information(data: data)

Right? That'd be nice, eh?

So the end goal is to have an Information initializer which takes an NSData object.

Well, let's start with the simplest initializer for the most ideal case:

init(startTime: UInt32, duration: UInt32, temperature: UInt16, estimatedDistance: UInt16, knockCount: UInt8, reserved: UInt8) {
    self.startTime = startTime
    self.duration = duration
    self.temperature = temperature
    self.estimatedDistance = estimatedDistance
    self.knockCount = knockCount
    self.reserved = reserved
}

If we get the needed values exactly as we need them, we can just directly assign them as such. This is just a member-wise initializer.

Let's rephrase that initializer in the form of UInt8 (or [UInt8]) though. This next one will be marked as a failable convenience initializer.

convenience init?(startTimeBytes: [UInt8], durationBytes: [UInt8], temperatureBytes: [UInt8], estimatedDistanceBytes: [UInt8], knockCount: UInt8, reserved: UInt8) {
    if let
        startTime = UInt32(bytes: startTimeBytes),
        duration = UInt32(bytes: durationBytes),
        temperature = UInt16(bytes: temperatureBytes),
        estimatedDistance = UInt16(bytes: estimatedDistanceBytes) {
            self.init(startTime: startTime, duration: duration, temperature: temperature, estimatedDistance: estimatedDistance, knockCount: knockCount, reserved: reserved)
    } else {
        self.init(fail: true)
    }
}

(Don't worry about self.init(faile: true) right now. I'll paste that implementation in a second. For now, just note that it's saving me some boilerplate copy-pasta.)

We've got an if let trying to bind four variables using the UInt32 and UInt16 initializers we extended (which of course return optionals). So if the passed in arrays are of the wrong size, this initializer will return nil, otherwise, it will return us the Information object we need.

So, what's left? The next step up is the exact initializer that we want... the one that takes the NSData argument:

convenience init?(data: NSData) {
    if data.length == 14 {
        let allBytes = Array(UnsafeBufferPointer(start: UnsafePointer<UInt8>(data.bytes), count: data.length))
        self.init(startTimeBytes: Array(allBytes[0..<4]), durationBytes: Array(allBytes[4..<8]), temperatureBytes: Array(allBytes[8..<10]), estimatedDistanceBytes: Array(allBytes[10..<12]), knockCount: allBytes[12], reserved: allBytes[13])
    } else {
        self.init(fail: true)
    }
}

We optimize a little bit here by checking the data length (which is also preventing any array index out of bounds exceptions). And then assuming the data is the right size, we call the previously mentioned convenience initializer.

For the sake of completeness, here's what self.init(fail: true) does:

private init?(fail: Bool = true) {
    self.startTime = 0
    self.duration = 0
    self.temperature = 0
    self.estimatedDistance = 0
    self.knockCount = 0
    self.reserved = 0
    if fail {
        return nil
    }
}

Because of the way Swift initializers work, having this initializer saves us from having to assign every property in every single initializer. If we know any of our other failable initializers have entered a failure state, we just call this and we're done.


The code, start to finish:

import Foundation

extension UInt16 {
    init?(bytes: [UInt8]) {
        if bytes.count != 2 {
            return nil
        }

        var value: UInt16 = 0
        for byte in bytes.reverse() {
            value = value << 8
            value = value | UInt16(byte)
        }
        self = value
    }
}

extension UInt32 {
    init?(bytes: [UInt8]) {
        if bytes.count != 4 {
            return nil
        }

        var value: UInt32 = 0
        for byte in bytes.reverse() {
            value = value << 8
            value = value | UInt32(byte)
        }
        self = value
    }
}

let bytes: [UInt8] = [0x4e, 0x5b, 0xa2, 0x56, 0x12, 0x19, 0x00, 0x00, 0x22, 0x00, 0x12,0x19, 0x07, 0x00]
let data = NSData(bytes: bytes, length: bytes.count)

class Information {
    let startTime: UInt32
    let duration: UInt32
    let temperature: UInt16
    let estimatedDistance: UInt16
    let knockCount: UInt8
    let reserved: UInt8

    private init?(fail: Bool = true) {
        self.startTime = 0
        self.duration = 0
        self.temperature = 0
        self.estimatedDistance = 0
        self.knockCount = 0
        self.reserved = 0
        if fail {
            return nil
        }
    }

    init(startTime: UInt32, duration: UInt32, temperature: UInt16, estimatedDistance: UInt16, knockCount: UInt8, reserved: UInt8) {
        self.startTime = startTime
        self.duration = duration
        self.temperature = temperature
        self.estimatedDistance = estimatedDistance
        self.knockCount = knockCount
        self.reserved = reserved
    }

    convenience init?(startTimeBytes: [UInt8], durationBytes: [UInt8], temperatureBytes: [UInt8], estimatedDistanceBytes: [UInt8], knockCount: UInt8, reserved: UInt8) {
        if let
            startTime = UInt32(bytes: startTimeBytes),
            duration = UInt32(bytes: durationBytes),
            temperature = UInt16(bytes: temperatureBytes),
            estimatedDistance = UInt16(bytes: estimatedDistanceBytes) {
                self.init(startTime: startTime, duration: duration, temperature: temperature, estimatedDistance: estimatedDistance, knockCount: knockCount, reserved: reserved)
        } else {
            self.init(fail: true)
        }
    }

    convenience init?(data: NSData) {
        if data.length == 14 {
            let allBytes = Array(UnsafeBufferPointer(start: UnsafePointer<UInt8>(data.bytes), count: data.length))
            self.init(startTimeBytes: Array(allBytes[0..<4]), durationBytes: Array(allBytes[4..<8]), temperatureBytes: Array(allBytes[8..<10]), estimatedDistanceBytes: Array(allBytes[10..<12]), knockCount: allBytes[12], reserved: allBytes[13])
        } else {
            self.init(fail: true)
        }
    }
}

func stringFromTimeInterval(interval:NSTimeInterval) -> String {
    let ti = NSInteger(interval)

    let ms = Int((interval % 1) * 1000)

    let seconds = ti % 60
    let minutes = (ti / 60) % 60
    let hours = (ti / 3600)

    return NSString(format: "%0.2d:%0.2d:%0.2d.%0.3d",hours,minutes,seconds,ms) as String
}

let i = Information(data: data)!
let timestamp = NSDate(timeIntervalSince1970: NSTimeInterval(i.startTime))
let duration = stringFromTimeInterval(NSTimeInterval(i.duration))
let temperature = i.temperature
let estimatedDistance = i.estimatedDistance
let knockCount = i.knockCount
let reserved = i.reserved

And this is the result I get for the given data:

enter image description here

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.