1 contributor
//
// File.swift
// USB Meter
//
// Created by Bogdan Timofte on 03/03/2020.
// Copyright © 2020 Bogdan Timofte. All rights reserved.
//
//MARK: Store and documentation: https://www.aliexpress.com/item/32968303350.html
//MARK: Protocol: https://sigrok.org/wiki/RDTech_UM_series
//MARK: Pithon Code: https://github.com/rfinnie/rdserialtool
//MARK: HM-10 Code: https://github.com/hoiberg/HM10-BluetoothSerial-iOS
//MARK: Package dependency https://github.com/krzyzanowskim/CryptoSwift
import CoreBluetooth
import SwiftUI
/**
Supprted USB Meters
# UM25C
# TC66
* Reverse Engineering
[UM Series](https://sigrok.org/wiki/RDTech_UM_series)
[TC66C](https://sigrok.org/wiki/RDTech_TC66C)
*/
enum Model: CaseIterable {
case UM25C
case UM34C
case TC66C
}
class Meter : NSObject, ObservableObject, Identifiable {
enum OperationalState: Int, Comparable {
case notPresent
case peripheralNotConnected
case peripheralConnectionPending
case peripheralConnected
case peripheralReady
case comunicating
case dataIsAvailable
static func < (lhs: OperationalState, rhs: OperationalState) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
@Published var operationalState = OperationalState.peripheralNotConnected {
didSet {
switch operationalState {
case .notPresent:
break
case .peripheralNotConnected:
if enableAutoConnect {
track("\(name) - Reconnecting...")
btSerial.connect()
}
case .peripheralConnectionPending:
break
case .peripheralConnected:
break
case .peripheralReady:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Change `2.0` to the desired number of seconds.
self.dataDumpRequest()
}
case .comunicating:
break
case .dataIsAvailable:
break
}
}
}
static func operationalColor(for state: OperationalState) -> Color {
switch state {
case .notPresent:
return .red
case .peripheralNotConnected:
return .blue
case .peripheralConnectionPending:
return .yellow
case .peripheralConnected:
return .yellow
case .peripheralReady:
return .orange
case .comunicating:
return .orange
case .dataIsAvailable:
return .green
}
}
private var wdTimer: Timer?
@Published var lastSeen = Date() {
didSet {
wdTimer?.invalidate()
if operationalState == .peripheralNotConnected {
wdTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false, block: {_ in
track("\(self.name) - Lost advertisments...")
self.operationalState = .notPresent
})
} else if operationalState == .notPresent {
operationalState = .peripheralNotConnected
}
}
}
var uuid: UUID
var model: Model
var modelString: String
var name: String {
didSet {
appData.meterNames[btSerial.macAddress.description] = name
}
}
var color : Color {
get {
return model.color
}
}
var capabilities: MeterCapabilities {
model.capabilities
}
var availableDataGroupIDs: [UInt8] {
capabilities.availableDataGroupIDs
}
var supportsDataGroupCommands: Bool {
capabilities.supportsDataGroupCommands
}
var supportsUMSettings: Bool {
capabilities.supportsScreenSettings
}
var supportsRecordingThreshold: Bool {
capabilities.supportsRecordingThreshold
}
var supportsFahrenheit: Bool {
capabilities.supportsFahrenheit
}
var supportsChargerDetection: Bool {
capabilities.supportsChargerDetection
}
var chargerTypeDescription: String {
capabilities.chargerTypeDescription(for: chargerTypeIndex)
}
@Published var btSerial: BluetoothSerial
@Published var measurements = Measurements()
private var commandQueue: [Data] = []
private var dataDumpRequestTimestamp = Date()
class DataGroupRecord {
@Published var ah: Double
@Published var wh: Double
init(ah: Double, wh: Double) {
self.ah = ah
self.wh = wh
}
}
@Published var selectedDataGroup: UInt8 = 0
@Published var dataGroupRecords: [Int : DataGroupRecord] = [:]
@Published var screenBrightness: Int = -1 {
didSet {
if oldValue != screenBrightness {
screenBrightnessTimestamp = Date()
if oldValue != -1 {
setSceeenBrightness(to: UInt8(screenBrightness))
}
}
}
}
private var screenBrightnessTimestamp = Date()
@Published var screenTimeout: Int = -1 {
didSet {
if oldValue != screenTimeout {
screenTimeoutTimestamp = Date()
if oldValue != -1 {
setScreenSaverTimeout(to: UInt8(screenTimeout))
}
}
}
}
private var screenTimeoutTimestamp = Date()
@Published var voltage: Double = 0
@Published var current: Double = 0
@Published var power: Double = 0
@Published var temperatureCelsius: Double = 0
@Published var temperatureFahrenheit: Double = 0
@Published var usbPlusVoltage: Double = 0
@Published var usbMinusVoltage: Double = 0
@Published var recordedAH: Double = 0
@Published var recordedWH: Double = 0
@Published var recording: Bool = false
@Published var recordingTreshold: Double = 0 /* MARK: Seteaza inutil la pornire {
didSet {
if recordingTreshold != oldValue {
setrecordingTreshold(to: (recordingTreshold*100).uInt8Value)
}
}
} */
@Published var currentScreen: UInt16 = 0
@Published var recordingDuration: UInt32 = 0
@Published var loadResistance: Double = 0
@Published var modelNumber: UInt16 = 0
@Published var chargerTypeIndex: UInt16 = 0
private var enableAutoConnect: Bool = false
init ( model: Model, with serialPort: BluetoothSerial ) {
uuid = serialPort.peripheral.identifier
//dataStore.meterUUIDS.append(serialPort.peripheral.identifier)
modelString = serialPort.peripheral.name!
self.model = model
btSerial = serialPort
name = appData.meterNames[serialPort.macAddress.description] ?? serialPort.macAddress.description
super.init()
btSerial.delegate = self
//name = dataStore.meterNames[macAddress.description] ?? peripheral.name!
for index in stride(from: 0, through: 9, by: 1) {
dataGroupRecords[index] = DataGroupRecord(ah:0, wh: 0)
}
}
func dataDumpRequest() {
if commandQueue.isEmpty {
switch model {
case .UM25C:
btSerial.write(UMProtocol.snapshotRequest, expectedResponseLength: 130)
case .UM34C:
btSerial.write(UMProtocol.snapshotRequest, expectedResponseLength: 130)
case .TC66C:
btSerial.write(TC66Protocol.snapshotRequest, expectedResponseLength: 192)
}
dataDumpRequestTimestamp = Date()
// track("\(name) - Request sent!")
} else {
track("Request delayed for: \(commandQueue.first!.hexEncodedStringValue)")
btSerial.write( commandQueue.first! )
commandQueue.removeFirst()
DispatchQueue.main.asyncAfter( deadline: .now() + 1 ) {
self.dataDumpRequest()
}
}
}
/**
received data parser
- parameter buffer cotains response for data dump request
- Decription metod for TC66C AES ECB response found [here](https:github.com/krzyzanowskim/CryptoSwift/issues/693)
*/
func parseData ( from buffer: Data) {
//track("\(name)")
switch model {
case .UM25C:
do {
apply(umSnapshot: try UMProtocol.parseSnapshot(from: buffer, model: model))
} catch {
track("\(name) - Error: \(error)")
}
case .UM34C:
do {
apply(umSnapshot: try UMProtocol.parseSnapshot(from: buffer, model: model))
} catch {
track("\(name) - Error: \(error)")
}
case .TC66C:
do {
apply(tc66Snapshot: try TC66Protocol.parseSnapshot(from: buffer))
} catch {
track("\(name) - Error: \(error)")
}
}
measurements.addValues(timestamp: dataDumpRequestTimestamp, power: power, voltage: voltage, current: current)
// DispatchQueue.global(qos: .userInitiated).asyncAfter( deadline: .now() + 0.33 ) {
// //track("\(name) - Scheduled new request.")
// }
operationalState = .dataIsAvailable
dataDumpRequest()
}
private func apply(umSnapshot snapshot: UMSnapshot) {
modelNumber = snapshot.modelNumber
voltage = snapshot.voltage
current = snapshot.current
power = snapshot.power
temperatureCelsius = snapshot.temperatureCelsius
temperatureFahrenheit = snapshot.temperatureFahrenheit
selectedDataGroup = snapshot.selectedDataGroup
for (index, record) in snapshot.dataGroupRecords {
dataGroupRecords[index] = DataGroupRecord(ah: record.ah, wh: record.wh)
}
usbPlusVoltage = snapshot.usbPlusVoltage
usbMinusVoltage = snapshot.usbMinusVoltage
chargerTypeIndex = snapshot.chargerTypeIndex
recordedAH = snapshot.recordedAH
recordedWH = snapshot.recordedWH
recordingTreshold = snapshot.recordingThreshold
recordingDuration = snapshot.recordingDuration
recording = snapshot.recording
if screenTimeoutTimestamp < dataDumpRequestTimestamp {
if screenTimeout != snapshot.screenTimeout {
screenTimeout = snapshot.screenTimeout
}
} else {
track("\(name) - Skip updating screenTimeout (changed after request).")
}
if screenBrightnessTimestamp < dataDumpRequestTimestamp {
if screenBrightness != snapshot.screenBrightness {
screenBrightness = snapshot.screenBrightness
}
} else {
track("\(name) - Skip updating screenBrightness (changed after request).")
}
currentScreen = snapshot.currentScreen
loadResistance = snapshot.loadResistance
}
private func apply(tc66Snapshot snapshot: TC66Snapshot) {
voltage = snapshot.voltage
current = snapshot.current
power = snapshot.power
loadResistance = snapshot.loadResistance
for (index, record) in snapshot.dataGroupRecords {
dataGroupRecords[index] = DataGroupRecord(ah: record.ah, wh: record.wh)
}
temperatureCelsius = snapshot.temperatureCelsius
usbPlusVoltage = snapshot.usbPlusVoltage
usbMinusVoltage = snapshot.usbMinusVoltage
}
func nextScreen() {
switch model {
case .UM25C:
commandQueue.append(UMProtocol.nextScreen)
case .UM34C:
commandQueue.append(UMProtocol.nextScreen)
case .TC66C:
commandQueue.append(TC66Protocol.nextPage)
}
}
func rotateScreen() {
switch model {
case .UM25C:
commandQueue.append(UMProtocol.rotateScreen)
case .UM34C:
commandQueue.append(UMProtocol.rotateScreen)
case .TC66C:
commandQueue.append(TC66Protocol.rotateScreen)
}
}
func previousScreen() {
switch model {
case .UM25C:
commandQueue.append(UMProtocol.previousScreen)
case .UM34C:
commandQueue.append(UMProtocol.previousScreen)
case .TC66C:
commandQueue.append(TC66Protocol.previousPage)
}
}
func clear() {
guard supportsDataGroupCommands else { return }
commandQueue.append(UMProtocol.clearCurrentGroup)
}
func clear(group id: UInt8) {
guard supportsDataGroupCommands else { return }
commandQueue.append(UMProtocol.selectDataGroup(id))
clear()
commandQueue.append(UMProtocol.selectDataGroup(selectedDataGroup))
}
func selectDataGroup ( id: UInt8) {
guard supportsDataGroupCommands else { return }
track("\(name) - \(id)")
selectedDataGroup = id
commandQueue.append(UMProtocol.selectDataGroup(selectedDataGroup))
}
private func setSceeenBrightness ( to value: UInt8) {
track("\(name) - \(value)")
guard supportsUMSettings else { return }
commandQueue.append(UMProtocol.setScreenBrightness(value))
}
private func setScreenSaverTimeout ( to value: UInt8) {
track("\(name) - \(value)")
guard supportsUMSettings else { return }
commandQueue.append(UMProtocol.setScreenSaverTimeout(value))
}
func setrecordingTreshold ( to value: UInt8) {
guard supportsRecordingThreshold else { return }
commandQueue.append(UMProtocol.setRecordingThreshold(value))
}
/**
Connect to meter.
1. It calls BluetoothSerial.connect
*/
func connect() {
enableAutoConnect = true
btSerial.connect()
}
/**
Disconnect from meter.
It calls BluetoothSerial.disconnect
*/
func disconnect() {
enableAutoConnect = false
btSerial.disconnect()
}
}
extension Meter : SerialPortDelegate {
func opertionalStateChanged(to serialPortOperationalState: BluetoothSerial.OperationalState) {
lastSeen = Date()
//track("\(name) - \(serialPortOperationalState)")
switch serialPortOperationalState {
case .peripheralNotConnected:
operationalState = .peripheralNotConnected
case .peripheralConnectionPending:
operationalState = .peripheralConnectionPending
case .peripheralConnected:
operationalState = .peripheralConnected
case .peripheralReady:
operationalState = .peripheralReady
}
}
func didReceiveData(_ data: Data) {
lastSeen = Date()
operationalState = .comunicating
parseData(from: data)
}
}