mirror of
https://github.com/morgan9e/SensorReader
synced 2026-04-14 00:14:33 +09:00
init
This commit is contained in:
152
EnvSensorReader/BLEManager.swift
Normal file
152
EnvSensorReader/BLEManager.swift
Normal file
@@ -0,0 +1,152 @@
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
import Combine
|
||||
|
||||
class BLEManager: NSObject, ObservableObject {
|
||||
@Published var readings: [SensorReading] = []
|
||||
@Published var isScanning = false
|
||||
@Published var bluetoothState: CBManagerState = .unknown
|
||||
|
||||
private var centralManager: CBCentralManager!
|
||||
private var seenNonces: Set<String> = []
|
||||
|
||||
private let deviceName = "EnvSensor"
|
||||
private let companyID: UInt16 = 0xFFFF
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
centralManager = CBCentralManager(delegate: self, queue: nil)
|
||||
}
|
||||
|
||||
func startScanning() {
|
||||
guard centralManager.state == .poweredOn else {
|
||||
print("Bluetooth not ready")
|
||||
return
|
||||
}
|
||||
|
||||
seenNonces.removeAll()
|
||||
centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
|
||||
isScanning = true
|
||||
print("Started scanning for EnvSensor devices...")
|
||||
}
|
||||
|
||||
func stopScanning() {
|
||||
centralManager.stopScan()
|
||||
isScanning = false
|
||||
print("Stopped scanning")
|
||||
}
|
||||
|
||||
private func parseManufacturerData(_ data: Data) -> (nonce: UInt16, temp: Double, hum: Double, pres: Double, voltage: Double, current: Double)? {
|
||||
guard data.count == 16 else { return nil }
|
||||
|
||||
let bytes = [UInt8](data)
|
||||
|
||||
// Parse according to struct format: '<HhHIHi'
|
||||
// H = unsigned short (2 bytes)
|
||||
// h = signed short (2 bytes)
|
||||
// I = unsigned int (4 bytes)
|
||||
// i = signed int (4 bytes)
|
||||
|
||||
let nonce = UInt16(bytes[0]) | (UInt16(bytes[1]) << 8)
|
||||
|
||||
let tempRaw = Int16(bitPattern: UInt16(bytes[2]) | (UInt16(bytes[3]) << 8))
|
||||
let temp = Double(tempRaw) / 100.0
|
||||
|
||||
let humRaw = UInt16(bytes[4]) | (UInt16(bytes[5]) << 8)
|
||||
let hum = Double(humRaw) / 100.0
|
||||
|
||||
let presRaw = UInt32(bytes[6]) | (UInt32(bytes[7]) << 8) | (UInt32(bytes[8]) << 16) | (UInt32(bytes[9]) << 24)
|
||||
let pres = Double(presRaw) / 10.0
|
||||
|
||||
let voltageRaw = UInt16(bytes[10]) | (UInt16(bytes[11]) << 8)
|
||||
let voltage = Double(voltageRaw) / 100.0
|
||||
|
||||
let currentRaw = Int32(bitPattern: UInt32(bytes[12]) | (UInt32(bytes[13]) << 8) | (UInt32(bytes[14]) << 16) | (UInt32(bytes[15]) << 24))
|
||||
let current = Double(currentRaw) / 100.0
|
||||
|
||||
return (nonce, temp, hum, pres, voltage, current)
|
||||
}
|
||||
}
|
||||
|
||||
extension BLEManager: CBCentralManagerDelegate {
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
bluetoothState = central.state
|
||||
|
||||
switch central.state {
|
||||
case .poweredOn:
|
||||
print("Bluetooth is powered on")
|
||||
case .poweredOff:
|
||||
print("Bluetooth is powered off")
|
||||
isScanning = false
|
||||
case .unauthorized:
|
||||
print("Bluetooth is unauthorized")
|
||||
case .unsupported:
|
||||
print("Bluetooth is not supported")
|
||||
default:
|
||||
print("Bluetooth state: \(central.state.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||
// Check device name
|
||||
guard let name = peripheral.name, name == deviceName else {
|
||||
return
|
||||
}
|
||||
|
||||
// Check for manufacturer data
|
||||
guard let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
|
||||
manufacturerData.count >= 2 else {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract company ID (first 2 bytes, little-endian)
|
||||
let companyIDBytes = manufacturerData.prefix(2)
|
||||
let extractedCompanyID = UInt16(companyIDBytes[0]) | (UInt16(companyIDBytes[1]) << 8)
|
||||
|
||||
guard extractedCompanyID == companyID else {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the payload (skip the first 2 bytes which are the company ID)
|
||||
let payload = manufacturerData.dropFirst(2)
|
||||
|
||||
guard let parsed = parseManufacturerData(payload) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Create unique key for deduplication
|
||||
let key = "\(peripheral.identifier.uuidString)-\(parsed.nonce)"
|
||||
guard !seenNonces.contains(key) else {
|
||||
return
|
||||
}
|
||||
seenNonces.insert(key)
|
||||
|
||||
// Create reading
|
||||
let reading = SensorReading(
|
||||
timestamp: Date(),
|
||||
deviceAddress: peripheral.identifier.uuidString,
|
||||
nonce: parsed.nonce,
|
||||
temperature: parsed.temp,
|
||||
humidity: parsed.hum,
|
||||
pressure: parsed.pres,
|
||||
voltage: parsed.voltage,
|
||||
current: parsed.current,
|
||||
rssi: RSSI.intValue
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.readings.insert(reading, at: 0)
|
||||
|
||||
// Keep only last 100 readings
|
||||
if self.readings.count > 100 {
|
||||
self.readings = Array(self.readings.prefix(100))
|
||||
}
|
||||
}
|
||||
|
||||
print("[\(reading.timestampString)] (\(String(format: "%04X", parsed.nonce))) \(peripheral.identifier.uuidString)")
|
||||
print(" T=\(String(format: "%5.1f", parsed.temp))°C H=\(String(format: "%5.1f", parsed.hum))% P=\(String(format: "%7.1f", parsed.pres))hPa")
|
||||
print(" V=\(String(format: "%5.2f", parsed.voltage))V I=\(String(format: "%7.2f", parsed.current))mA P=\(String(format: "%7.2f", reading.power))mW")
|
||||
print(" RSSI=\(String(format: "%3d", RSSI.intValue))dBm")
|
||||
print()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user