Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
9d63670
fix for https://github.com/kkonteh97/SwiftOBD2/issues/41
valexa May 17, 2026
8055c82
added new pids from https://github.com/kkonteh97/SwiftOBD2/issues/42
valexa May 17, 2026
ba93b7c
fix optional
valexa May 17, 2026
2660fe7
fix mock
valexa May 17, 2026
351ebd9
obd2service.swift — Added @Published public private(set) var peripher…
valexa May 17, 2026
95da1b3
bleManager.swift
valexa May 17, 2026
f8e33ec
fix data decode
valexa May 17, 2026
fe054b2
fix connection issues
valexa May 21, 2026
6d660ad
remove scanning automatically
valexa May 23, 2026
1ba9774
added CVNDecoder and trim
valexa May 23, 2026
9497c8d
add safe array lookup subscript
valexa May 24, 2026
195d9b2
fix FuelTypes lookup
valexa May 24, 2026
e389d2e
fix BLEDataProcessor.swift:67: Assertion failed: Concurrent command d…
valexa May 24, 2026
2738533
Silently adds a peripheral restored from CoreBluetooth state — does n…
valexa May 25, 2026
b3db17b
add monitoring
valexa May 29, 2026
c213775
add sendable conformance
valexa May 30, 2026
13ba789
3. UAS multi-byte decoders now reject 1-byte garbage responses (decod…
valexa May 30, 2026
861d69e
add BECM and scanForUDSDTCs
valexa May 30, 2026
a8d053d
added serial support
valexa May 30, 2026
e76629d
wifi support changes
valexa May 31, 2026
7f5b460
remove demo
valexa May 31, 2026
56cc7fc
remove ecuReadCharacteristic ecuWriteCharacteristic restrictions
valexa May 31, 2026
5e1b7e6
add serialPath
valexa May 31, 2026
58af939
lookup safety
valexa May 31, 2026
eb833c2
add serial manager
valexa May 31, 2026
726de31
WiFi ATZ fix — wifiManager.swift: ATZ is now fire-and-forget. The old…
valexa May 31, 2026
7a26813
Logging (elm327 → OBDService.onLog → ViewModel.log → connectionLogs):
valexa May 31, 2026
d00d7f3
add baud logging
valexa May 31, 2026
2804d35
add baud probe
valexa May 31, 2026
04f8a4d
add settings for logging
valexa May 31, 2026
aa9bded
add switchCANBus
valexa May 31, 2026
5c02a1c
update readme
valexa May 31, 2026
ede226c
support bare '>' acknowledgment
valexa Jun 2, 2026
0c75924
add code init
valexa Jun 4, 2026
116341b
remove buggy peripheral restore
valexa Jun 9, 2026
4e0bb17
Audit fixes: WiFi monitor crash, BLE races, serial continuation safety
valexa Jun 10, 2026
9f8f2ba
Fix BLE crash on overlapping commands
valexa Jun 13, 2026
ff897f1
improve connection
valexa Jun 19, 2026
f696e27
Skip individually-malformed frames rather than aborting the whole res…
valexa Jun 30, 2026
3e1cf54
codes.swift — new DTCStatus enum (confirmed/pending/permanent) carryi…
valexa Jun 30, 2026
5e63ed5
connectAsync now honors the timeout, routes every outcome through an …
valexa Jul 3, 2026
a5badae
BLE connect/disconnect hardening + pending-connect API for auto-recon…
valexa Jul 3, 2026
e5a5c8b
Fix 1 — getStatus() off-by-one (elm327.swift:345): added the compensa…
valexa Jul 6, 2026
4da3bf5
Merge pull request #1 from valexa/develop
valexa Jul 6, 2026
955b70b
Fix multi-frame CAN gap detection + expose Status fields publicly
Jul 7, 2026
f1728f4
Fix multi-ECU PID under-reporting, WiFi line-drop, error visibility
Jul 7, 2026
adcc3e5
Fix pedal-position label, legacy frame gap-check, enable ATAT1/ATCAF1
Jul 7, 2026
d098938
Decode all 8 PID 0101 emissions monitors, CAN-first protocol sweep
Jul 7, 2026
f9d8171
Fix supported-PID block offset, twosComp no-op, and 4 more from audit
Jul 7, 2026
ecf6e46
Fix ECU grouping collision on 29-bit CAN (protocol 7/9)
Jul 7, 2026
abaa0c2
Routed through OBDLogger so the consuming app's log-level preference…
valexa Jul 7, 2026
2fd2cbb
Apply the same ECU-grouping fix + frame resilience to legacy protocols
Jul 7, 2026
b443e16
Fix 29-bit CAN frames being parsed with 11-bit padding
Jul 7, 2026
e469363
BLE: actually honor the retries parameter in sendCommand
Jul 7, 2026
f14a2f3
Add Mode 02 freeze-frame API (requestFreezeFrame)
Jul 7, 2026
31a00d8
Truncate single-frame payload to its declared length (drop CAN padding)
Jul 7, 2026
28e3e55
Fix BLE buffer surviving a timeout, add missing PID sanity bounds
Jul 7, 2026
a2fdbfd
Deterministic ECU selection for single-response reads
Jul 7, 2026
8483f8c
NO DATA is a routine reply (module asleep, unsupported PID), not a t…
valexa Jul 7, 2026
420d8dd
Merge valexa/develop (upstream PR #43) into our branch
Jul 8, 2026
ac0d338
Retries fix (SwiftOBD2/Sources/SwiftOBD2/Communication/BLE/bleManager…
valexa Jul 9, 2026
6ac70e9
Transient serial dropout (attempts 1 & 2 — connection died right afte…
valexa Jul 10, 2026
f48f868
SwiftOBD2 package (wifiManager.swift): the socket is now pinned to th…
valexa Jul 19, 2026
0e965f7
Changes in MacSerialManager.swift:
valexa Jul 19, 2026
f94d880
bleManager.swift:495 — BLEManagerError now conforms to LocalizedError…
valexa Jul 20, 2026
bb877ad
elm327.swift — guards an empty reply, strips the auto-mode A prefix o…
valexa Jul 26, 2026
8535db8
make ECUID Sendable
valexa Aug 8, 2026
48fd01f
Route all logging through OBDLogger
valexa Aug 21, 2026
f2e2fec
Merge PR #4: 29-bit CAN, multi-ECU reads, DTC decoding
valexa Sep 17, 2026
57e75b4
Fix File
valexa Sep 17, 2026
ff69b91
Restore Bluetooth state so the app wakes for a known adapter
valexa Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
533 changes: 326 additions & 207 deletions Readme.md

Large diffs are not rendered by default.

180 changes: 122 additions & 58 deletions Sources/SwiftOBD2/Communication/BLE/BLECharacteristicHandler.swift
Original file line number Diff line number Diff line change
@@ -1,66 +1,97 @@
import Foundation
import OSLog
import CoreBluetooth

class BLECharacteristicHandler {
private var ecuReadCharacteristic: CBCharacteristic?
private var ecuWriteCharacteristic: CBCharacteristic?
private let messageProcessor: BLEMessageProcessor
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "BLECharacteristicHandler")
private var ecuWriteCharacteristic: CBCharacteristic?
private let messageProcessor: BLEMessageProcessor

// Device Information Service (0x180A) — Bluetooth SIG standard, all readable UTF-8 strings
// except 2A23 (System ID, 8-byte binary) and 2A2A (IEEE cert, binary).
private static let deviceInfoLabels: [String: String] = [
"2A29": "Manufacturer",
"2A24": "Model",
"2A25": "Serial Number",
"2A27": "Hardware Revision",
"2A26": "Firmware Revision",
"2A28": "Software Revision",
"2A23": "System ID",
"2A2A": "IEEE Certification",
]

// ISSC/Microchip Transparent UART (service 49535343-FE7D-4AE5-8FA9-9FAFD205E455)
// Alternative ELM327 channel found on RN4870/ISP1807 modules — FFF0 is preferred.
private static let isscUUIDs: Set<String> = [
"49535343-6DAA-4D02-ABF6-19569ACA69FE", // TX / Notify
"49535343-ACA3-481C-91EC-D85E28A60318", // RX / Write Without Response
]

private(set) var deviceInfo: [String: String] = [:]
var onDeviceInfoUpdated: (([String: String]) -> Void)?

var isReady: Bool {
ecuReadCharacteristic != nil && ecuWriteCharacteristic != nil
}

init(messageProcessor: BLEMessageProcessor) {
self.messageProcessor = messageProcessor
}

var isReady: Bool {
ecuReadCharacteristic != nil && ecuWriteCharacteristic != nil
}
func setupCharacteristics(_ characteristics: [CBCharacteristic], on peripheral: CBPeripheral) {
for characteristic in characteristics {
let uuid = characteristic.uuid.uuidString.uppercased()

// Device Information Service — read and store, don't treat as OBD channel
if Self.deviceInfoLabels[uuid] != nil {
if characteristic.properties.contains(.read) {
peripheral.readValue(for: characteristic)
}
continue
}

init(messageProcessor: BLEMessageProcessor) {
self.messageProcessor = messageProcessor
}
// ISSC UART — recognised, not used (FFF0 preferred)
if Self.isscUUIDs.contains(uuid) {
obdDebug("ISSC UART characteristic recognised (unused): \(uuid)", category: .bluetooth)
continue
}

// OBD characteristics — subscribe to notify where supported
if characteristic.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: characteristic)
}

func setupCharacteristics(_ characteristics: [CBCharacteristic], on peripheral: CBPeripheral) {
for characteristic in characteristics {
// Set up notifications for characteristics that support it
if characteristic.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: characteristic)
}

// Assign characteristics based on UUID and properties
switch characteristic.uuid.uuidString.uppercased() {
case "FFE1": // for service FFE0 (read and write)
if characteristic.properties.contains(.write) {
ecuWriteCharacteristic = characteristic
}
if characteristic.properties.contains(.read) || characteristic.properties.contains(.notify) {
ecuReadCharacteristic = characteristic
}

case "FFF1": // for service FFF0 (read only)
if characteristic.properties.contains(.read) || characteristic.properties.contains(.notify) {
ecuReadCharacteristic = characteristic
}

case "FFF2": // for service FFF0 (write only)
if characteristic.properties.contains(.write) {
ecuWriteCharacteristic = characteristic
}

case "2AF0": // for service 18F0 (read)
if characteristic.properties.contains(.read) || characteristic.properties.contains(.notify) {
ecuReadCharacteristic = characteristic
}

case "2AF1": // for service 18F0 (write)
if characteristic.properties.contains(.write) {
ecuWriteCharacteristic = characteristic
}

default:
logger.debug("Unknown characteristic: \(characteristic.uuid.uuidString)")
}
}

logger.info("Characteristics setup - Read: \(self.ecuReadCharacteristic != nil), Write: \(self.ecuWriteCharacteristic != nil)")
}
switch uuid {
case "FFE1": // FFE0 service — single characteristic handles both read and write
if characteristic.properties.contains(.write) {
ecuWriteCharacteristic = characteristic
}
if characteristic.properties.contains(.read) || characteristic.properties.contains(.notify) {
ecuReadCharacteristic = characteristic
}

case "FFF1": // FFF0 service — notify (read)
if characteristic.properties.contains(.read) || characteristic.properties.contains(.notify) {
ecuReadCharacteristic = characteristic
}

case "FFF2": // FFF0 service — write
if characteristic.properties.contains(.write) {
ecuWriteCharacteristic = characteristic
}

case "2AF0": // 18F0 service — read
ecuReadCharacteristic = characteristic

case "2AF1": // 18F0 service — write
ecuWriteCharacteristic = characteristic

default:
obdInfo("Unknown characteristic: \(uuid) — properties: \(characteristic.properties.rawValue)", category: .bluetooth)
}
}

obdInfo("Characteristics setup — Read: \(self.ecuReadCharacteristic != nil), Write: \(self.ecuWriteCharacteristic != nil)", category: .bluetooth)
}

func discoverCharacteristics(for service: CBService, on peripheral: CBPeripheral) {
switch service.uuid {
Expand All @@ -71,6 +102,7 @@ class BLECharacteristicHandler {
case CBUUID(string: "18F0"):
peripheral.discoverCharacteristics([CBUUID(string: "2AF0"), CBUUID(string: "2AF1")], for: service)
default:
// Discover all characteristics for unknown services (Device Info, ISSC, etc.)
peripheral.discoverCharacteristics(nil, for: service)
}
}
Expand All @@ -80,15 +112,31 @@ class BLECharacteristicHandler {
let data = "\(command)\r".data(using: .ascii) else {
throw BLEManagerError.missingPeripheralOrCharacteristic
}

peripheral.writeValue(data, for: characteristic, type: .withResponse)
logger.info("Sent command: \(command)")
// Routed through OBDLogger so the consuming app's log-level preference
// can silence this per-command line (a raw os.Logger call can't be gated).
obdDebug("Sent command: \(command)", category: .communication)
}

func handleUpdatedValue(_ data: Data, from characteristic: CBCharacteristic) {
let uuid = characteristic.uuid.uuidString.uppercased()

// Device info read response
if let label = Self.deviceInfoLabels[uuid] {
let decoded = Self.decodeDeviceInfoValue(data: data, uuid: uuid)
if !decoded.isEmpty {
deviceInfo[label] = decoded
onDeviceInfoUpdated?(deviceInfo)
}
return
}

guard characteristic == ecuReadCharacteristic else {
if let responseString = String(data: data, encoding: .utf8) {
logger.info("Unknown characteristic: \(characteristic)\nResponse: \(responseString)")
// A characteristic we don't handle produced a notification — log and ignore
if let text = String(data: data, encoding: .utf8) {
obdDebug("Unhandled notification from \(uuid): \(text)", category: .bluetooth)
} else {
obdDebug("Unhandled notification from \(uuid): \(data.map { String(format: "%02X", $0) }.joined(separator: " "))", category: .bluetooth)
}
return
}
Expand All @@ -99,5 +147,21 @@ class BLECharacteristicHandler {
func reset() {
ecuReadCharacteristic = nil
ecuWriteCharacteristic = nil
deviceInfo = [:]
}

// MARK: - Decoding

private static func decodeDeviceInfoValue(data: Data, uuid: String) -> String {
guard !data.isEmpty else { return "" }
switch uuid {
case "2A23": // System ID — 8-byte manufacturer-assigned binary identifier
return data.map { String(format: "%02X", $0) }.joined(separator: ":")
case "2A2A": // IEEE 11073 Regulatory Certification — binary, show as hex
return data.map { String(format: "%02X", $0) }.joined(separator: " ")
default: // All others are UTF-8 strings
return String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
}
}
}
Loading