Skip to content

Commit

Permalink
Merge pull request #29 from combustion-inc/feature/uart_crc
Browse files Browse the repository at this point in the history
CRC for UART messages and isConnectable flag
  • Loading branch information
jjohnstz authored Aug 15, 2022
2 parents 9abc423 + db1ae4b commit 40c5fc9
Show file tree
Hide file tree
Showing 12 changed files with 140 additions and 59 deletions.
8 changes: 6 additions & 2 deletions Sources/CombustionBLE/BleManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ protocol BleManagerDelegate: AnyObject {
func didDisconnectFrom(identifier: UUID)
func handleSetIDResponse(identifier: UUID, success: Bool)
func handleSetColorResponse(identifier: UUID, success: Bool)
func updateDeviceWithAdvertising(advertising: AdvertisingData, rssi: NSNumber, identifier: UUID)
func updateDeviceWithAdvertising(advertising: AdvertisingData, isConnectable: Bool, rssi: NSNumber, identifier: UUID)
func updateDeviceWithLogResponse(identifier: UUID, logResponse: LogResponse)
func updateDeviceWithSessionInformation(identifier: UUID, sessionInformation: SessionInformation)
func updateDeviceWithStatus(identifier: UUID, status: DeviceStatus)
Expand Down Expand Up @@ -137,6 +137,7 @@ extension BleManager: CBCentralManagerDelegate{
advertisementData: [String : Any], rssi RSSI: NSNumber) {

let manufatureData: Data = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data ?? Data()
let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? Bool ?? false

if let advData = AdvertisingData(fromData: manufatureData) {
// For now, only add probes.
Expand All @@ -145,7 +146,10 @@ extension BleManager: CBCentralManagerDelegate{
// Store peripheral reference for later use
peripherals.insert(peripheral)

delegate?.updateDeviceWithAdvertising(advertising: advData, rssi: RSSI, identifier: peripheral.identifier)
delegate?.updateDeviceWithAdvertising(advertising: advData,
isConnectable: isConnectable,
rssi: RSSI,
identifier: peripheral.identifier)
} else {
// print("Ignoring device with type \(advData.type)")
}
Expand Down
10 changes: 9 additions & 1 deletion Sources/CombustionBLE/Device.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public class Device : ObservableObject {
/// Current connection state of device
@Published public internal(set) var connectionState: ConnectionState = .disconnected

/// Connectable flag set in advertising packet
@Published public internal(set) var isConnectable = false

/// Signal strength to device
@Published public internal(set) var rssi: Int

Expand Down Expand Up @@ -103,6 +106,12 @@ public class Device : ObservableObject {

func updateDeviceStale() {
stale = Date().timeIntervalSince(lastUpdateTime) > Constants.STALE_TIMEOUT


// If device data is stale, assume its not longer connectable
if(stale) {
isConnectable = false
}
}

public func isDFURunning() -> Bool {
Expand All @@ -126,7 +135,6 @@ extension Device {
/// Attempt to connect to the device.
public func connect() {
// Mark that we should maintain a connection to this device.
// TODO - this doesn't seem to be propagating back to the UI??
maintainingConnection = true

if(connectionState != .connected) {
Expand Down
6 changes: 3 additions & 3 deletions Sources/CombustionBLE/DeviceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,14 @@ extension DeviceManager : BleManagerDelegate {
}
}

func updateDeviceWithAdvertising(advertising: AdvertisingData, rssi: NSNumber, identifier: UUID) {
func updateDeviceWithAdvertising(advertising: AdvertisingData, isConnectable: Bool, rssi: NSNumber, identifier: UUID) {
if devices[identifier.uuidString] != nil {
if let probe = devices[identifier.uuidString] as? Probe {
probe.updateWithAdvertising(advertising, RSSI: rssi)
probe.updateWithAdvertising(advertising, isConnectable: isConnectable, RSSI: rssi)
}
}
else {
let device = Probe(advertising, RSSI: rssi, identifier: identifier)
let device = Probe(advertising, isConnectable: isConnectable, RSSI: rssi, identifier: identifier)
addDevice(device: device)
}
}
Expand Down
40 changes: 24 additions & 16 deletions Sources/CombustionBLE/Probe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ public class Probe : Device {
/// Time at which probe instant read was last updated
internal var lastInstantRead: Date?

public init(_ advertising: AdvertisingData, RSSI: NSNumber, identifier: UUID) {
public init(_ advertising: AdvertisingData, isConnectable: Bool, RSSI: NSNumber, identifier: UUID) {
serialNumber = advertising.serialNumber
id = advertising.id
color = advertising.color

super.init(identifier: identifier, RSSI: RSSI)

updateWithAdvertising(advertising, RSSI: RSSI)
updateWithAdvertising(advertising, isConnectable: isConnectable, RSSI: RSSI)
}

override func updateConnectionState(_ state: ConnectionState) {
Expand Down Expand Up @@ -108,21 +108,29 @@ extension Probe {
}


func updateWithAdvertising(_ advertising: AdvertisingData, RSSI: NSNumber) {
if(advertising.mode == .Normal) {
currentTemperatures = advertising.temperatures
}
else if(advertising.mode == .InstantRead ){
updateInstantRead(advertising.temperatures.values[0])
}
func updateWithAdvertising(_ advertising: AdvertisingData, isConnectable: Bool, RSSI: NSNumber) {
// Always update probe RSSI and isConnectable flag
self.rssi = RSSI.intValue
self.isConnectable = isConnectable

rssi = RSSI.intValue

id = advertising.id
color = advertising.color
batteryStatus = advertising.batteryStatus

lastUpdateTime = Date()
// Only update rest of data if not connected to probe. Otherwise, rely on status
// notifications to update data
if(connectionState != .connected)
{
if(advertising.mode == .Normal) {
currentTemperatures = advertising.temperatures
}
else if(advertising.mode == .InstantRead ){
updateInstantRead(advertising.temperatures.values[0])
}


id = advertising.id
color = advertising.color
batteryStatus = advertising.batteryStatus

lastUpdateTime = Date()
}
}

/// Updates the Device based on newly-received DeviceStatus message. Requests missing records.
Expand Down
4 changes: 2 additions & 2 deletions Sources/CombustionBLE/SimulatedProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class SimulatedProbe: Probe {
init() {
let advertising = AdvertisingData(fakeSerial: UInt32.random(in: 0 ..< UINT32_MAX),
fakeTemperatures: ProbeTemperatures.withRandomData())
super.init(advertising, RSSI: SimulatedProbe.randomeRSSI(), identifier: UUID())
super.init(advertising, isConnectable: true, RSSI: SimulatedProbe.randomeRSSI(), identifier: UUID())

firmareVersion = "v1.2.3"
hardwareRevision = "v0.31-A1"
Expand Down Expand Up @@ -64,7 +64,7 @@ class SimulatedProbe: Probe {
private func updateFakeAdvertising() {
let advertising = AdvertisingData(fakeSerial: UInt32.random(in: 0 ..< UINT32_MAX),
fakeTemperatures: ProbeTemperatures.withRandomData())
updateWithAdvertising(advertising, RSSI: SimulatedProbe.randomeRSSI())
updateWithAdvertising(advertising, isConnectable: true, RSSI: SimulatedProbe.randomeRSSI())
}

private func updateFakeStatus() {
Expand Down
18 changes: 5 additions & 13 deletions Sources/CombustionBLE/UART/LogRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,15 @@ SOFTWARE.
import Foundation

class LogRequest: Request {
static let PAYLOAD_LENGTH: UInt8 = 8

init(minSequence: UInt32, maxSequence: UInt32) {
super.init(payloadLength: LogRequest.PAYLOAD_LENGTH, type: .Log)
var payload = Data()

var min = minSequence
let minData = Data(bytes: &min, count: MemoryLayout.size(ofValue: min))
self.data[Request.HEADER_SIZE + 0] = minData[0]
self.data[Request.HEADER_SIZE + 1] = minData[1]
self.data[Request.HEADER_SIZE + 2] = minData[2]
self.data[Request.HEADER_SIZE + 3] = minData[3]
payload.append(Data(bytes: &min, count: MemoryLayout.size(ofValue: min)))

var max = maxSequence
let maxData = Data(bytes: &max, count: MemoryLayout.size(ofValue: max))
self.data[Request.HEADER_SIZE + 4] = maxData[0]
self.data[Request.HEADER_SIZE + 5] = maxData[1]
self.data[Request.HEADER_SIZE + 6] = maxData[2]
self.data[Request.HEADER_SIZE + 7] = maxData[3]
payload.append(Data(bytes: &max, count: MemoryLayout.size(ofValue: max)))

super.init(payload: payload, type: .Log)
}
}
28 changes: 19 additions & 9 deletions Sources/CombustionBLE/UART/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,35 @@ class Request {
static let HEADER_SIZE = 6

/// Contains message data.
var data: Data
var data = Data()

/// Constructor for Request object.
/// - parameter payloadLength: Length of payload of message
/// - parameter type: Type of message
init(payloadLength: UInt8, type: MessageType) {
let messageSize = Request.HEADER_SIZE + Int(payloadLength)
data = Data(repeating: 0, count: messageSize)
init(payload: Data, type: MessageType) {

// Sync Bytes { 0xCA, 0xFE }
data[0] = 0xCA
data[1] = 0xFE
data.append(0xCA)
data.append(0xFE)

// CRC : TODO
// Calculate CRC over Message Type, payload length, payload
var crcData = Data()

// Message type
data[4] = type.rawValue
crcData.append(type.rawValue)

// Payload length
data[5] = payloadLength
crcData.append(UInt8(payload.count))

// Payload
crcData.append(payload)

var crcValue = crcData.crc16ccitt()

// CRC
data.append(Data(bytes: &crcValue, count: MemoryLayout.size(ofValue: crcValue)))

// Message Type, payload length, payload
data.append(crcData)
}
}
19 changes: 17 additions & 2 deletions Sources/CombustionBLE/UART/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ extension Response {
return nil
}

// CRC : TODO (bytes 2,3)

// Message type
let typeByte = data.subdata(in: 4..<5)
let typeRaw = typeByte.withUnsafeBytes {
Expand All @@ -93,6 +91,23 @@ extension Response {
$0.load(as: UInt8.self)
}

// CRC
let crcBytes = data.subdata(in: 2..<4)
let crc = crcBytes.withUnsafeBytes {
$0.load(as: UInt16.self)
}

let crcDataLength = 3 + Int(payloadLength)
var crcData = data.dropFirst(4)
crcData = crcData.dropLast(crcData.count - crcDataLength)

let calculatedCRC = crcData.crc16ccitt()

guard crc == calculatedCRC else {
print("Response::fromData(): Invalid CRC")
return nil
}

let responseLength = Int(payloadLength) + HEADER_LENGTH

// Invalid number of bytes
Expand Down
4 changes: 1 addition & 3 deletions Sources/CombustionBLE/UART/SessionInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@ public struct SessionInformation {
}

class SessionInfoRequest: Request {
static let PAYLOAD_LENGTH: UInt8 = 0

init() {
super.init(payloadLength: SessionInfoRequest.PAYLOAD_LENGTH, type: .SessionInfo)
super.init(payload: Data(), type: .SessionInfo)
}
}

Expand Down
8 changes: 4 additions & 4 deletions Sources/CombustionBLE/UART/SetColor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ SOFTWARE.
import Foundation

class SetColorRequest: Request {
static let PAYLOAD_LENGTH: UInt8 = 1

init(color: ProbeColor) {
super.init(payloadLength: SetColorRequest.PAYLOAD_LENGTH, type: .SetColor)
self.data[Request.HEADER_SIZE] = color.rawValue
var payload = Data()
payload.append(color.rawValue)

super.init(payload: payload, type: .SetColor)
}
}

Expand Down
8 changes: 4 additions & 4 deletions Sources/CombustionBLE/UART/SetID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ SOFTWARE.
import Foundation

class SetIDRequest: Request {
static let PAYLOAD_LENGTH: UInt8 = 1

init(id: ProbeID) {
super.init(payloadLength: SetIDRequest.PAYLOAD_LENGTH, type: .SetID)
self.data[Request.HEADER_SIZE] = id.rawValue
var payload = Data()
payload.append(id.rawValue)

super.init(payload: payload, type: .SetID)
}
}

Expand Down
46 changes: 46 additions & 0 deletions Sources/CombustionBLE/Utilities/Data+CRC.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Data+CRC.swift

/*--
MIT License

Copyright (c) 2022 Combustion Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--*/

import Foundation

extension Data {
func crc16ccitt() -> UInt16 {
var crc: UInt16 = 0xFFFF // initial value
let polynomial: UInt16 = 0x1021 // 0001 0000 0010 0001 (0, 5, 12)

self.forEach { (byte) in
for i in 0...7 {
let bit = (byte >> (7 - i) & 1) == 1
let c15 = (crc >> (15) & 1) == 1
crc = crc << 1
if (c15 != bit) {
crc = crc ^ polynomial
}
}
}
return crc & 0xffff
}
}

0 comments on commit 40c5fc9

Please sign in to comment.