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.
1 Answer 1
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 UInt8
s, but I notice, we're going to need some UInt16
s and some UInt32
s. 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:
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\$<4e5ba256 12190000 22001219 0700>
, correct? \$\endgroup\$<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\$