Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
bricklife committed Mar 22, 2024
1 parent d1f2bc0 commit 4c7613b
Show file tree
Hide file tree
Showing 42 changed files with 2,228 additions and 0 deletions.
23 changes: 23 additions & 0 deletions Package.swift
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"]),
]
)
43 changes: 43 additions & 0 deletions Sources/LWPKit/Advertising/HubType.swift
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"
}
}
}
21 changes: 21 additions & 0 deletions Sources/LWPKit/Advertising/ManufacturerData.swift
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
}
}
13 changes: 13 additions & 0 deletions Sources/LWPKit/Extensions/FixedWidthInteger+BCD.swift
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
}
}
9 changes: 9 additions & 0 deletions Sources/LWPKit/GATT.swift
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"
}
23 changes: 23 additions & 0 deletions Sources/LWPKit/MessageStructures/ByteCollection.swift
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 Sources/LWPKit/MessageStructures/ByteCollectionView.swift
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 Sources/LWPKit/MessageStructures/CommonMessageHeader.swift
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
}
}
62 changes: 62 additions & 0 deletions Sources/LWPKit/MessageStructures/Message.swift
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)
}
Loading

0 comments on commit 4c7613b

Please sign in to comment.