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.

asked Jan 22, 2016 at 19:44
\$\endgroup\$
5
  • \$\begingroup\$ Welcome to Code Review! I hope you get some helpful reviews. \$\endgroup\$ Commented Jan 22, 2016 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\$ Commented Jan 22, 2016 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\$ Commented Jan 22, 2016 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\$ Commented Jan 23, 2016 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\$ Commented Jan 23, 2016 at 0:25

1 Answer 1

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

answered Jan 23, 2016 at 1:30
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.