-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
42 changed files
with
2,228 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// swift-tools-version: 5.9 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "LWPKit", | ||
products: [ | ||
// Products define the executables and libraries a package produces, making them visible to other packages. | ||
.library( | ||
name: "LWPKit", | ||
targets: ["LWPKit"]), | ||
], | ||
targets: [ | ||
// Targets are the basic building blocks of a package, defining a module or a test suite. | ||
// Targets can depend on other targets in this package and products from dependencies. | ||
.target( | ||
name: "LWPKit"), | ||
.testTarget( | ||
name: "LWPKitTests", | ||
dependencies: ["LWPKit"]), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/** | ||
Hub Type (System Type and Device Number) | ||
|
||
[2.1. System Type and Device Number](https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#system-type-and-device-number) | ||
*/ | ||
public enum HubType: UInt8, CaseIterable, Sendable { | ||
|
||
case duploTrain = 0x20 | ||
case boost = 0x40 | ||
case poweredUp = 0x41 | ||
case remoteControl = 0x42 | ||
case mario = 0x43 | ||
case luigi = 0x44 | ||
case peach = 0x45 | ||
case controlPlus = 0x80 | ||
case spikeEssential = 0x83 | ||
} | ||
|
||
extension HubType: CustomStringConvertible { | ||
|
||
public var description: String { | ||
switch self { | ||
case .duploTrain: | ||
return "Duplo Train Base" | ||
case .boost: | ||
return "BOOST Move Hub" | ||
case .poweredUp: | ||
return "Powered Up Smart Hub" | ||
case .remoteControl: | ||
return "Powered Up Remote Control" | ||
case .mario: | ||
return "Mario" | ||
case .luigi: | ||
return "Luigi" | ||
case .peach: | ||
return "Peach" | ||
case .controlPlus: | ||
return "CONTROL+ Smart Hub" | ||
case .spikeEssential: | ||
return "SPIKE Essential Hub" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/** | ||
Manufacturer Data | ||
|
||
[2. Advertising](https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#document-2-Advertising) | ||
*/ | ||
import Foundation | ||
|
||
public struct ManufacturerData: Sendable { | ||
public let buttonState: Bool | ||
public let hubType: HubType | ||
|
||
public init?(data: Data) { | ||
guard data.count == 8 else { return nil } | ||
guard data[0] == 0x97, data[1] == 0x03 else { return nil } | ||
|
||
self.buttonState = data[2] != 0 | ||
|
||
guard let hubType = HubType(rawValue: data[3]) else { return nil } | ||
self.hubType = hubType | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
extension FixedWidthInteger { | ||
|
||
public var binaryCodedDecimal: Self? { | ||
let numOfDigit = bitWidth / 4 | ||
var value: Self = 0 | ||
for i in stride(from: numOfDigit - 1, through: 0, by: -1) { | ||
let v = (self >> (4 * i)) & 0x0f | ||
guard 0...9 ~= v else { return nil } | ||
value = value * 10 + v | ||
} | ||
return value | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
LEGO Specific GATT Service | ||
|
||
[3. LEGO Specific GATT Service](https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#lego-specific-gatt-service) | ||
*/ | ||
public struct GATT { | ||
public static let serviceUUID = "00001623-1212-efde-1623-785feabcd123" | ||
public static let characteristicUUID = "00001624-1212-efde-1623-785feabcd123" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import Foundation | ||
|
||
public typealias ByteCollection = RandomAccessCollection<UInt8> | ||
|
||
public protocol ByteCollectionDecodable { | ||
|
||
init(_ bytes: some ByteCollection) throws | ||
} | ||
|
||
public protocol ByteCollectionEncodable { | ||
|
||
func bytes() throws -> [UInt8] | ||
func data() throws -> Data | ||
} | ||
|
||
extension ByteCollectionEncodable { | ||
|
||
public func data() throws -> Data { | ||
return try Data(bytes()) | ||
} | ||
} | ||
|
||
public typealias ByteCollectionCodable = ByteCollectionDecodable & ByteCollectionEncodable |
125 changes: 125 additions & 0 deletions
125
Sources/LWPKit/MessageStructures/ByteCollectionView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
public struct ByteCollectionView<C: ByteCollection> { | ||
|
||
public let bytes: C | ||
|
||
public init(_ bytes: C) { | ||
self.bytes = bytes | ||
} | ||
|
||
public var count: Int { | ||
bytes.count | ||
} | ||
|
||
public func uint8(_ offset: Int) throws -> UInt8 { | ||
let index = bytes.index(bytes.startIndex, offsetBy: offset) | ||
guard index < bytes.endIndex else { | ||
throw Error.outOfRange(index: index, endIndex: bytes.endIndex) | ||
} | ||
return bytes[index] | ||
} | ||
|
||
public func int8(_ offset: Int) throws -> Int8 { | ||
return try Int8(bitPattern: uint8(offset)) | ||
} | ||
|
||
public func uint16(_ offset: Int) throws -> UInt16 { | ||
return try UInt16(uint8(offset)) + (UInt16(uint8(offset + 1)) << 8) | ||
} | ||
|
||
public func int16(_ offset: Int) throws -> Int16 { | ||
return try Int16(bitPattern: uint16(offset)) | ||
} | ||
|
||
public func uint32(_ offset: Int) throws -> UInt32 { | ||
return try UInt32(uint16(offset)) + (UInt32(uint16(offset + 2)) << 16) | ||
} | ||
|
||
public func int32(_ offset: Int) throws -> Int32 { | ||
return try Int32(bitPattern: uint32(offset)) | ||
} | ||
|
||
public func float(_ offset: Int) throws -> Float { | ||
return try Float(bitPattern: uint32(offset)) | ||
} | ||
|
||
public func bool(_ offset: Int) throws -> Bool { | ||
return try uint8(offset) != 0x00 | ||
} | ||
|
||
public func rawRepresentable<V: RawRepresentable>(_ offset: Int) throws -> V where V.RawValue == UInt8 { | ||
let rawValue = try uint8(offset) | ||
guard let value = V(rawValue: rawValue) else { | ||
throw Error.undefined(type: V.self, rawValue: .uint8(rawValue)) | ||
} | ||
return value | ||
} | ||
|
||
public func rawRepresentable<V: RawRepresentable>(_ offset: Int) throws -> V where V.RawValue == Int8 { | ||
let rawValue = try int8(offset) | ||
guard let value = V(rawValue: rawValue) else { | ||
throw Error.undefined(type: V.self, rawValue: .int8(rawValue)) | ||
} | ||
return value | ||
} | ||
|
||
public func rawRepresentable<V: RawRepresentable>(_ offset: Int) throws -> V where V.RawValue == UInt16 { | ||
let rawValue = try uint16(offset) | ||
guard let value = V(rawValue: rawValue) else { | ||
throw Error.undefined(type: V.self, rawValue: .uint16(rawValue)) | ||
} | ||
return value | ||
} | ||
|
||
public func rawRepresentable<V: RawRepresentable>(_ offset: Int) throws -> V where V.RawValue == Int16 { | ||
let rawValue = try int16(offset) | ||
guard let value = V(rawValue: rawValue) else { | ||
throw Error.undefined(type: V.self, rawValue: .int16(rawValue)) | ||
} | ||
return value | ||
} | ||
} | ||
|
||
extension ByteCollectionView { | ||
|
||
public func suffix(_ offset: Int) throws -> C.SubSequence { | ||
let index = bytes.index(bytes.startIndex, offsetBy: offset) | ||
guard index <= bytes.endIndex else { | ||
throw Error.outOfRange(index: index, endIndex: bytes.endIndex) | ||
} | ||
return bytes[index ..< bytes.endIndex] | ||
} | ||
|
||
public func string(_ offset: Int) throws -> String { | ||
guard let string = String(bytes: try suffix(offset), encoding: .utf8) else { | ||
throw Error.invalidBytes | ||
} | ||
return string | ||
} | ||
|
||
public func bytes(_ offset: Int) throws -> [UInt8] { | ||
return try Array<UInt8>(suffix(offset)) | ||
} | ||
} | ||
|
||
extension ByteCollectionView { | ||
|
||
public enum Error: Swift.Error { | ||
case outOfRange(index: C.Index, endIndex: C.Index) | ||
case undefined(type: any RawRepresentable.Type, rawValue: RawValue) | ||
case invalidBytes | ||
} | ||
|
||
public enum RawValue { | ||
case uint8(UInt8) | ||
case int8(Int8) | ||
case uint16(UInt16) | ||
case int16(Int16) | ||
} | ||
} | ||
|
||
extension ByteCollection { | ||
|
||
public var view: ByteCollectionView<Self> { | ||
return ByteCollectionView(self) | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
Sources/LWPKit/MessageStructures/CommonMessageHeader.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
/** | ||
Common Message Header | ||
|
||
[3.1. Common Message Header](https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#common-message-header) | ||
*/ | ||
public struct CommonMessageHeader: Sendable { | ||
|
||
public let messageLength: Int | ||
public let hubID: UInt8 | ||
public let messageType: MessageType | ||
|
||
public var length: Int { | ||
return messageLength > 0x7f ? 4 : 3 | ||
} | ||
|
||
public init(length: Int, hubID: UInt8, messageType: MessageType) { | ||
self.messageLength = length | ||
self.hubID = hubID | ||
self.messageType = messageType | ||
} | ||
} | ||
|
||
extension CommonMessageHeader: ByteCollectionDecodable { | ||
|
||
public init(_ bytes: some ByteCollection) throws { | ||
let view = bytes.view | ||
|
||
let firstByte = try view.uint8(0) | ||
let isLongMessage = firstByte > 0x7f | ||
|
||
var length = Int(firstByte & 0x7f) | ||
if isLongMessage { | ||
length += Int(try view.uint8(1)) << 7 | ||
} | ||
let offset = isLongMessage ? 2 : 1 | ||
|
||
self.messageLength = length | ||
self.hubID = try view.uint8(offset) | ||
self.messageType = try view.rawRepresentable(offset + 1) | ||
} | ||
} | ||
|
||
extension CommonMessageHeader: ByteCollectionEncodable { | ||
|
||
public func bytes() throws -> [UInt8] { | ||
var bytes = [UInt8]() | ||
|
||
bytes.append(UInt8(messageLength & 0x7f)) | ||
if messageLength > 0x7f { | ||
bytes.append(UInt8(truncatingIfNeeded: messageLength >> 7)) | ||
} | ||
|
||
bytes.append(hubID) | ||
bytes.append(messageType.rawValue) | ||
|
||
return bytes | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/** | ||
LEGO Wireless Protocol Message | ||
|
||
[3. LEGO Specific GATT Service](https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#lego-specific-gatt-service) | ||
*/ | ||
public protocol Message: Sendable { | ||
|
||
static var messageType: MessageType { get } | ||
} | ||
|
||
public protocol EncodableMessage: Message, ByteCollectionEncodable { | ||
|
||
func bytes() throws -> [UInt8] | ||
func payload() throws -> [UInt8] | ||
} | ||
|
||
extension EncodableMessage { | ||
|
||
public func bytes() throws -> [UInt8] { | ||
let payload = try payload() | ||
let payloadLength = payload.count | ||
let headerLength = payloadLength > 0x7f - 3 ? 4 : 3 | ||
|
||
let length = headerLength + payloadLength | ||
let messageType = Self.messageType | ||
let header = CommonMessageHeader(length: length, hubID: 0, messageType: messageType) | ||
|
||
return try header.bytes() + payload | ||
} | ||
} | ||
|
||
public protocol DecodableMessage: Message, ByteCollectionDecodable { | ||
|
||
init(_ bytes: some ByteCollection) throws | ||
init(header: CommonMessageHeader, payload: some ByteCollection) throws | ||
init(payload: some ByteCollection) throws | ||
} | ||
|
||
extension DecodableMessage { | ||
|
||
public init(_ bytes: some ByteCollection) throws { | ||
let header = try CommonMessageHeader(bytes) | ||
let payload = try bytes.view.suffix(header.length) | ||
try self.init(header: header, payload: payload) | ||
} | ||
|
||
public init(header: CommonMessageHeader, payload: some ByteCollection) throws { | ||
guard header.messageType == Self.messageType else { | ||
throw DecodableMessageError.unmatch(messageType: header.messageType, type: Self.self) | ||
} | ||
guard header.messageLength - header.length <= payload.count else { | ||
throw DecodableMessageError.invalidPayload(length: payload.count) | ||
} | ||
|
||
try self.init(payload: payload) | ||
} | ||
} | ||
|
||
public enum DecodableMessageError: Error { | ||
case unmatch(messageType: MessageType, type: DecodableMessage.Type) | ||
case invalidPayload(length: Int) | ||
} |
Oops, something went wrong.