bluejay
bluejay copied to clipboard
Update Sendable/Receivable docs to leverage new Swift Codable pack/unpack library
Once we move the library to Swift 4, it's worth investigating if we can leverage Codable to simplify the implementation of Sendable/Receivable conformance in any way.
I suspect that just using them exclusively won't work, because going to/from Bluetooth data often involves some tweaks to byte widths (having a Int that is actually only stored in 24 bits) or enforcing specific orderings of data. However it seems possible that we could either have a way to specify that a given type is simple enough that we are willing to trust the default Codeable implementation to do the right thing with regards to types and ordering, and/or have a way give some hinting information on ordering and bitwidths so that we can correctly pack/unpack the underlying data given the normal name based default encode/decode implementation.
Transplanting comment by @larryonoff from #91 that is more appropriate here:
I didn't mean using Codable 1-by-1. I meant implementing something similar like Encoder / Decoder. > Codable doesn't work for the Data case.
I think it should be something like
struct SuperPacket: BluetoothCodable {
let doubleValue: Double
let int16Value: Int16
let doublesArray: [Double]
init(_ decoder: Decoder) throws {
doubleValue = try decoder.read()
int16Value = try decoder.read()
doublesArray = try decoder.read()
}
}
Looks this case looks very trivial. What about keyPaths from Swift 4?
I'm not sure that with the Encode/Decoder system is a significant win over what that implementation would be with Bluejay right now using the existing Data category for extraction (though it doesn't automatically handle an array, however a dynamically sized array is also a pretty problematic from an automatic encoding/decoding standpoint, because there is no standard way to encode the length that you can be sure the actual device will understand, and lots of potential values would not be encodable due to packet sized limit), Assuming you didn't have an array in the packet, the equivalent code right now would be
struct SuperPacket: Receivable {
let doubleValue: Double
let int16Value: Int16
init(bluetoothData: Data) {
doubleValue = data.extract(start:0, length: 8)
int16Value = data.extract(start:8, length: 2)
}
}
Which is not enough different from the Decoder based approach to be worth switching IMO. Having to explicitly specify the layout is a bit of a pain when things are tightly packed and natively sized, but also allows more flexibility such being able to handle padding and bit shaving more cleanly (for example storing a value as 24 bits, even though there isn't any native 24 bit type in Swift), i.e:
struct SuperPacket: Receivable {
let int16ValueA: Int16
let int16ValueB: Int16
let intValueStoredIn24Bits: Int
init(bluetoothData: Data) {
int16ValueA = data.extract(start:0, length: 2)
int16ValueB = data.extract(start:3, length: 2) //skipped a byte due to padding
intValueStoredIn24Bits = data.extract(start:5, length: 3)
}
}
Hijacking the Codable infrastructure like that also has the significant disadvantage that it means you can't easily have a single model object that is both used to communicate with the device and with a network service (where you might want to use Codable to encode the struct in JSON). We have at least one class in a project using Bluejay that conforms to BOTH Receivable and another class for network serialization (Mappable from ObjectMapper right now, but would be Codable longer term). If we had to use a custom implementation of init(from:Decoder)
to get bluetooth serialization, it would then be impossible to also use Codable to serialize from JSON.
What I think WOULD potentially be a win is if we could add an interface that let you specify a mapping of CodingKeys to locations in the bluetooth packet, and then that data could be used to either encode or decode data based on the autogenerated key-oriented conformances to Encodable and Decodable, something like:
struct SuperPacket: Codable, BluetoothCodable {
let doubleValue: Double
let int16Value: Int16
static var bluetoothLayout : [(key: CodingKey, start: Int, length: Int)] = {
return [
(key: .doubleValue, start:0, length: 8),
(key: .int16Value, start:8, length: 2)
]}()
}
Where BluetoothCodable is a class that has default implementations of the current Sendable and Receivable using the standard complier implementations of Encodable and Decodable and the bluetoothLayout from conforming classes.
That way you can specify the layout in a single format and get BOTH encoding and decoding (or at least, if you only need one, always specify the layout the same way, whereas right now the implementations for Sendable and Receivable look quite different), and it's still compatible with using codable the more normal way to serialize to JSON.
You could also imagine another protocol (you'd want to specifically conform to it separately) that could supply a default implementation of bluetoothLayout based on reflection, so for really simple tightly packed classes you could just conform to that protocol and get bluetooth serialization for "free".
I'm just not sure if all the above is actually possible 😄
I think that we should provide a way not specifying start, because parsing usually happens one after another.
So what about the following
struct SuperPacket: Codable, BluetoothCodable {
let doubleValue: Double
let int16Value: Int16
static var bluetoothLayout : [(key: CodingKey, bCodable: BluetoothCodable)] = {
return [
(key: .doubleValue, bCodable: Double.self),
(key: .int16Value, bCodable: Int16.self)
]}()
}
It's usually in order, but it's occasionally not tightly packed, so there has to be some way to represent gaps in the data, which specifying start does allow (though there are other ways to do it, like having things be tightly packed, but having to specify where there is padding via dummy variables or a skip operation or something). I agree that it would be nice to not have to specify that unless you need it though.
Having a more complicated type (i.e., an enum) for the layout items rather than having them be uniform would let you add advanced layout options without complicating the basic case, so my example from above with a padding byte and a non-native width could maybe be something like
struct SuperPacket: Receivable {
let int16ValueA: Int16
let int16ValueB: Int16
let intValueStoredIn24Bits: Int
static var bluetoothLayout : [LayoutElement] = {
return [
.direct(.int16ValueA),
.padding(1),
.direct(.int15ValueB),
.truncated(. intValueStoredIn24Bits, bytes: 3)
]}()
}
I believe @nbrooke has done some work on this already. We will sync up and plan out releasing this some time soon, but this is not a priority yet until some of the recently reported crashes are fixed. Will move this back to Backlog, but put it high in the list so we can get this back into the Next column soon.