├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── Smart BMS Utility └── Smart BMS Utility │ ├── AppDelegate.swift │ ├── BMSData.swift │ ├── BMSStructs.swift │ ├── BluetoothInterface.swift │ ├── Classes │ ├── BubbleTabBar.swift │ ├── BubbleTabBarController.swift │ └── CBTabBarButton.swift │ ├── ConfigurationController.swift │ ├── DevicesController.swift │ ├── GPSController.swift │ ├── NotificationController.swift │ ├── OverviewController.swift │ ├── ProtectionStatusController.swift │ ├── SceneDelegate.swift │ ├── SettingController.swift │ ├── Settings.swift │ ├── ViewController.swift │ ├── VoltageInfoCell.swift │ ├── VoltageInfoProgressView.swift │ ├── WiFiInterface.swift │ ├── demoDevice.swift │ ├── detailCell.swift │ ├── device.swift │ ├── deviceCell.swift │ ├── editDeviceController.swift │ ├── editDeviceController2.swift │ ├── fileController.swift │ ├── gpsCell.swift │ ├── listLogFilesController.swift │ ├── loggingCheckboxCell.swift │ ├── loggingController.swift │ ├── loggingGraphController.swift │ ├── loggingViewController.swift │ ├── moreController.swift │ ├── rightDetailCell.swift │ ├── sensorRenameCell.swift │ ├── sensorRenameController.swift │ ├── shareLogCell.swift │ └── tabBarController.swift ├── changelog.md ├── compatibility.md └── img ├── Banner.png ├── app-store.png ├── google-play.png └── windows-banner.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: NeariX67 4 | custom: ["https://www.paypal.me/nearix846"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.lock 3 | Smart BMS Utility/Pods/ 4 | Smart BMS Utility/Smart BMS Utility.xcodeproj/ 5 | Smart BMS Utility/Smart BMS Utility.xcworkspace/ 6 | Smart BMS Utility/Smart BMS Utility/Assets.xcassets/ 7 | Smart BMS Utility/Smart BMS Utility/Base.lproj/ 8 | *.plist 9 | *.entitlements 10 | .DS_Store 11 | 12 | !V3_screenshot.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartBMS Utility 2 | 3 | 4 | SmartBMS Utility Banner 5 | 6 | 7 | 8 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/NeariX67/SmartBMSUtility) 9 | ![GitHub last commit](https://img.shields.io/github/last-commit/NeariX67/SmartBMSUtility) 10 | ![GitHub issues](https://img.shields.io/github/issues-raw/NeariX67/SmartBMSUtility) 11 | 12 | 13 | ## Summary 14 | Welcome to SmartBMS Utility, an advanced app for optimizing energy management and extending battery life. Monitor and control your Battery Management System (BMS) from your smartphone with real-time data on charge, consumption, and more. Make informed decisions to minimize wastage and increase efficiency. Customize settings for personalized charging and discharging plans. Your data is securely stored locally. Currently compatible with JBD BMS. Easy setup for eco-conscious homeowners, solar enthusiasts, and campers. Unleash your BMS's potential for sustainable energy. Download now for smarter energy use! Contact us for inquiries. Thank you for your interest and support! 15 | 16 | Support for Daly BMS since version 3.1 17 | 18 | ## Download App 19 | 20 | | Android | iOS & MacOS | Windows | 21 | |:-:|:-:|:-:| 22 | | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.nearix.smart_bms_utility) | [Get it on the iOS App Store](https://apps.apple.com/de/app/apple-store/id1540178292) | [Get it on the Windows Store](https://www.microsoft.com/store/apps/9PGTNGPSJQ55) 23 | 24 | [Changelog](/changelog.md) 25 | 26 | ## BMS compatability 27 | 28 | Check out our [compatibility list](https://github.com/KG-Development/SmartBMSUtility/blob/main/compatibility.md). 29 | 30 | If you know the type of BMS your battery has, this list is for you: 31 | 32 | | BMS | State | 33 | |-----------|---------------| 34 | | JBD | Working | 35 | | Daly BT | Working* | 36 | | Daly WiFi | Not supported | 37 | | JK | Not supported | 38 | | Ant | Not supported | 39 | 40 | \* Note: Some devices may require the unfiltered search to be discovered. 41 | 42 | ## Platform-Features 43 | 44 | | Platform | Bluetooth Connectivity | Native Serial Communication | 45 | |----------|------------------------|-----------------------------| 46 | | iOS | ✔ Working | ❌ Unsupported | 47 | | macOS | ✔ Working | ✔ Working | 48 | | Android | ✔ Working | ❌ Unsupported | 49 | | Windows | ✔ Working | ✔ Working | 50 | 51 | ### Become a Beta Tester? 52 | 53 | If you want to have access to features before anyone else, you can contact us and register as beta tester. You will be able to help us release a stable and flawless release version. 54 | 55 | [iOS Tester](https://testflight.apple.com/join/YWdbkZ8s) 56 | 57 | [Android Tester](https://SmartBMsUtility.com/contact) 58 | 59 | ### You can buy a JBD BMS here 60 | 61 | [lithiumbatterypcb.com](https://www.lithiumbatterypcb.com/) 62 | 63 | [overkillsolar.com](https://overkillsolar.com/) 64 | 65 | or buy prebuild batteries here: 66 | 67 | [Liontron](https://liontron.com) 68 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 11.10.20. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate, UISceneDelegate { 12 | 13 | 14 | 15 | public static var timer: Timer? 16 | 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 19 | AppDelegate.timer = Timer.scheduledTimer(timeInterval: TimeInterval(Double(SettingController.settings.refreshTime) / 1000.0), target: self, selector: #selector(sendPacketNotification), userInfo: nil, repeats: true) 20 | fileController.createDirectories() 21 | UserDefaults.standard.set(false, forKey: "_UIConstraintBasedLayoutLogUnsatisfiable") 22 | return true 23 | } 24 | 25 | func applicationDidEnterBackground(_ application: UIApplication) { 26 | print("Invalidating timer...") 27 | AppDelegate.timer?.invalidate() 28 | } 29 | 30 | // MARK: UISceneSession Lifecycle 31 | 32 | 33 | func sceneDidEnterBackground(_ scene: UIScene) { 34 | print("sceneDidEnterBackground") 35 | } 36 | 37 | func sceneWillEnterForeground(_ scene: UIScene) { 38 | print("sceneWillEnterForeground") 39 | } 40 | 41 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 42 | print("connectingSceneSession") 43 | // Called when a new scene session is being created. 44 | // Use this method to select a configuration to create the new scene with. 45 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 46 | } 47 | 48 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 49 | print("didDiscardSceneSessions") 50 | // Called when the user discards a scene session. 51 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 52 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 53 | } 54 | 55 | func sceneDidDisconnect(_ scene: UIScene) { 56 | print("sceneDidDisconnect") 57 | } 58 | 59 | func sceneWillResignActive(_ scene: UIScene) { 60 | print("sceneWillResignActive") 61 | } 62 | 63 | 64 | 65 | @objc func sendPacketNotification() { 66 | // print("AppDelegate: sendPacketNotification()") 67 | if DevicesController.connectionMode == .bluetooth && !(OverviewController.BLEInterface?.pauseTransmission ?? false) { 68 | if UIApplication.shared.applicationState == .background { 69 | if !SettingController.settings.backgroundUpdating { 70 | return 71 | } 72 | } 73 | NotificationCenter.default.post(name: Notification.Name("BluetoothSendNeeded"), object: nil) 74 | } 75 | if SettingController.settings.useDemo && DevicesController.connectionMode == .demo { 76 | NotificationCenter.default.post(name: Notification.Name("DemoDeviceNeeded"), object: nil) 77 | } 78 | } 79 | 80 | } 81 | 82 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/BMSData.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | 4 | class BMSData { 5 | 6 | static var parsingError = false 7 | static var ReadWriteMode = false 8 | 9 | static var lastCurrentArray = [Int16](repeating: 0, count: 8) 10 | static var lastCurrentIndex = -1 11 | 12 | static var lastProtectionState: [UInt8] = [0x00, 0x00] 13 | 14 | static var lastControlStatus: UInt8? 15 | 16 | static func validateChecksum(response: [UInt8] ) -> Bool { 17 | if response.count <= 5 || response[0] != 0xDD || response[response.count-1] != 0x77 { 18 | return false 19 | } 20 | var checksum: UInt32 = 0x10000 21 | for i in stride(from: 2, through: response.count-4, by: 1) { 22 | checksum = checksum - UInt32(response[i]) 23 | } 24 | // print("Checksum: \(checksum + 1 == convertByteToUInt16(data1: response[response.count-3], data2: response[response.count-2]))") 25 | return checksum == convertByteToUInt16(data1: response[response.count-3], data2: response[response.count-2]) 26 | } 27 | 28 | static func isWriteAnswer(response: [UInt8]) -> Bool { 29 | return response[0] == 0xDD && response[3] == 0 && response[4] == 0 && response[5] == 0 && response[6] == 0x77 30 | } 31 | 32 | 33 | static func dataToBMSReading(bytes: [UInt8]) { 34 | let data = bytes 35 | 36 | if data[2] == 0x81 || data[2] == 0x80 { 37 | BMSData.ReadWriteMode = false 38 | } 39 | if (data[1] == 0xE1 || data[1] == 0x00) && data[2] == 0x80 { 40 | let device = DevicesController.getConnectedDevice() 41 | if device != nil && !(device!.settings.liontronMode ?? false) { 42 | device!.settings.liontronMode = true 43 | device!.saveDeviceSettings() 44 | print("Activated LionTron-Mode") 45 | NotificationCenter.default.post(name: Notification.Name("LionTronMode"), object: nil) 46 | } 47 | } 48 | 49 | 50 | if(data.count == 0 || data[0] != 0xDD || data[2] != 0x00 || data[data.count-1] != 0x77 || data[3] != (data.count - 7)) { 51 | print("Invalid packet received!") 52 | for i in 0...data.count-1 { 53 | print(String(format: "%02X ", data[i]), separator: "", terminator: "") 54 | } 55 | print("") 56 | return 57 | } 58 | switch data[1] { 59 | case 3: 60 | if(data.count <= 29) { 61 | print("Data size not correct (3)! \(data[1])") 62 | return 63 | } 64 | cmd_basicInformation.totalVoltage = convertByteToUInt16(data1: data[4], data2: data[5]) 65 | cmd_basicInformation.current = convertByteToInt16(data1: data[6], data2: data[7]) 66 | if self.lastCurrentIndex == -1 { 67 | if cmd_basicInformation.current != nil { 68 | for i in 0...self.lastCurrentArray.count-1 { 69 | self.lastCurrentArray[i] = cmd_basicInformation.current ?? 0 70 | } 71 | self.lastCurrentIndex = 0 72 | } 73 | } 74 | self.lastCurrentArray[self.lastCurrentIndex] = cmd_basicInformation.current ?? 0 75 | if self.lastCurrentIndex == self.lastCurrentArray.count-1 { 76 | self.lastCurrentIndex = 0 77 | } 78 | else { 79 | self.lastCurrentIndex += 1 80 | } 81 | cmd_basicInformation.residualCapacity = convertByteToUInt16(data1: data[8], data2: data[9]) 82 | cmd_basicInformation.nominalCapacity = convertByteToUInt16(data1: data[10], data2: data[11]) 83 | cmd_basicInformation.cycleLife = convertByteToUInt16(data1: data[12], data2: data[13]) 84 | cmd_basicInformation.productDate = convertByteToUInt16(data1: data[14], data2: data[15]) 85 | var cellIndex = 0 86 | for i in (0...15).reversed() { 87 | cmd_basicInformation.balanceCells[cellIndex] = checkBit(byte: data[16 + Int(i/8)], pos: i % 8) 88 | // print((cmd_basicInformation.balanceCells[cellIndex]) ? "1" : "0", separator: "", terminator: "") 89 | cellIndex += 1 90 | } 91 | // print(" ", separator: "", terminator: "") 92 | for i in (16...31).reversed() { 93 | cmd_basicInformation.balanceCells[cellIndex] = checkBit(byte: data[16 + Int(i/8)], pos: i % 8) 94 | // print((cmd_basicInformation.balanceCells[cellIndex]) ? "1" : "0", separator: "", terminator: "") 95 | cellIndex += 1 96 | } 97 | // print("") 98 | cmd_basicInformation.protection.CellBlockOverVoltage = checkBit(byte: data[21], pos: 7) 99 | cmd_basicInformation.protection.CellBlockUnderVoltage = checkBit(byte: data[21], pos: 6) 100 | cmd_basicInformation.protection.BatteryOverVoltage = checkBit(byte: data[21], pos: 5) 101 | cmd_basicInformation.protection.BatteryUnderVoltage = checkBit(byte: data[21], pos: 4) 102 | cmd_basicInformation.protection.ChargingOverTemp = checkBit(byte: data[21], pos: 3) 103 | cmd_basicInformation.protection.ChargingUnderTemp = checkBit(byte: data[21], pos: 2) 104 | cmd_basicInformation.protection.DischargingOverTemp = checkBit(byte: data[21], pos: 1) 105 | cmd_basicInformation.protection.DischargingUnderTemp = checkBit(byte: data[21], pos: 0) 106 | cmd_basicInformation.protection.ChargingOverCurr = checkBit(byte: data[20], pos: 7) 107 | cmd_basicInformation.protection.DischargingOverCurr = checkBit(byte: data[20], pos: 6) 108 | cmd_basicInformation.protection.ShortCircuit = checkBit(byte: data[20], pos: 5) 109 | cmd_basicInformation.protection.ICError = checkBit(byte: data[20], pos: 4) 110 | cmd_basicInformation.protection.MOSLockIn = checkBit(byte: data[20], pos: 3) 111 | cmd_basicInformation.version = data[22] 112 | cmd_basicInformation.rsoc = data[23] 113 | cmd_basicInformation.controlStatus = data[24] 114 | if BMSData.lastControlStatus == nil { 115 | BMSData.lastControlStatus = cmd_basicInformation.controlStatus 116 | } 117 | else if BMSData.lastControlStatus != cmd_basicInformation.controlStatus { 118 | OverviewController.waitingForMosStatus = false 119 | BMSData.lastControlStatus = cmd_basicInformation.controlStatus 120 | } 121 | 122 | cmd_basicInformation.chargingPort = checkBit(byte: data[24], pos: 7) 123 | cmd_basicInformation.dischargingPort = checkBit(byte: data[24], pos: 6) 124 | // print("Recv Charging: \(cmd_basicInformation.chargingPort.description)") 125 | // print("Recv Discharging: \(cmd_basicInformation.dischargingPort.description)") 126 | // print("Recv code: \(data[24])") 127 | cmd_basicInformation.numberOfCells = data[25] 128 | if (DevicesController.getConnectedDevice()?.settings.cellCount ?? 0) != cmd_basicInformation.numberOfCells ?? 0 { 129 | DevicesController.getConnectedDevice()?.settings.cellCount = Int(cmd_basicInformation.numberOfCells ?? 0) 130 | DevicesController.getConnectedDevice()?.saveDeviceSettings() 131 | } 132 | cmd_basicInformation.numberOfTempSensors = data[26] 133 | // printHex(data: Data(data)) 134 | if (cmd_basicInformation.numberOfTempSensors ?? 0) > 0 { 135 | // print(cmd_basicInformation.numberOfTempSensors ?? 0) 136 | for i in 0...Int(cmd_basicInformation.numberOfTempSensors ?? 0) - 1 { 137 | if data[27+(i*2)] != nil || data[28+(i*2)] != nil { 138 | cmd_basicInformation.temperatureReadings[i] = UInt16ToTemp(reading: convertByteToUInt16(data1: data[27+(i*2)], data2: data[28+(i*2)])) 139 | } 140 | else { 141 | if !parsingError { 142 | NotificationCenter.default.post(name: Notification.Name("parsingFailed"), object: nil) 143 | } 144 | parsingError = true 145 | } 146 | } 147 | } 148 | NotificationCenter.default.post(name: Notification.Name("OverviewDataAvailable"), object: nil) 149 | if (data[20] ^ lastProtectionState[0]) != 0x00 || (data[21] ^ lastProtectionState[1]) != 0x00 { 150 | // print("BMSData: AlertAvailable()") 151 | NotificationCenter.default.post(name: Notification.Name("AlertAvailable"), object: nil) 152 | } 153 | lastProtectionState = [data[20], data[21]] 154 | 155 | break 156 | case 4: 157 | for i in 0...Int(data[3]/2)-1 { 158 | cmd_voltages.voltageOfCell[i] = convertByteToUInt16(data1: data[i*2+4], data2: data[i*2+5]) 159 | } 160 | if data[3] < 32 { 161 | for i in Int(data[3]/2)...31 { 162 | cmd_voltages.voltageOfCell[i] = 0 163 | } 164 | } 165 | if loggingController.shouldLogCurrentEntry() { 166 | loggingController.WriteDataLine() 167 | } 168 | break 169 | case 0x10: 170 | cmd_configuration.FullCapacity = convertByteToUInt16(data1: data[4], data2: data[5]) 171 | case 0x11: 172 | cmd_configuration.CycleCapacity = convertByteToUInt16(data1: data[4], data2: data[5]) 173 | case 0x12: 174 | cmd_configuration.CellFullVoltage = convertByteToUInt16(data1: data[4], data2: data[5]) 175 | case 0x13: 176 | cmd_configuration.CellEmptyVoltage = convertByteToUInt16(data1: data[4], data2: data[5]) 177 | case 0x14: 178 | cmd_configuration.RateDsg = convertByteToUInt16(data1: data[4], data2: data[5]) 179 | case 0x15: 180 | // cmd_configuration.ProdDate = convertByteToUInt16(data1: data[4], data2: data[5]) 181 | //TODO: Convert to date 182 | break 183 | case 0x17: 184 | cmd_configuration.CycleCount = convertByteToUInt16(data1: data[4], data2: data[5]) 185 | case 0x18: 186 | cmd_configuration.ChgOTPtrig = convertByteToUInt16(data1: data[4], data2: data[5]) 187 | case 0x19: 188 | cmd_configuration.ChgOTPrel = convertByteToUInt16(data1: data[4], data2: data[5]) 189 | case 0x1A: 190 | cmd_configuration.ChgUTPtrig = convertByteToUInt16(data1: data[4], data2: data[5]) 191 | case 0x1B: 192 | cmd_configuration.ChgUTPrel = convertByteToUInt16(data1: data[4], data2: data[5]) 193 | case 0x1C: 194 | cmd_configuration.DsgOTPtrig = convertByteToUInt16(data1: data[4], data2: data[5]) 195 | case 0x1D: 196 | cmd_configuration.DsgOTPrel = convertByteToUInt16(data1: data[4], data2: data[5]) 197 | case 0x1E: 198 | cmd_configuration.DsgUTPtrig = convertByteToUInt16(data1: data[4], data2: data[5]) 199 | case 0x1F: 200 | cmd_configuration.DsgUTPrel = convertByteToUInt16(data1: data[4], data2: data[5]) 201 | case 0x20: 202 | cmd_configuration.PackOVPtrig = convertByteToUInt16(data1: data[4], data2: data[5]) 203 | case 0x21: 204 | cmd_configuration.PackOVPrel = convertByteToUInt16(data1: data[4], data2: data[5]) 205 | case 0x22: 206 | cmd_configuration.PackUVPtrig = convertByteToUInt16(data1: data[4], data2: data[5]) 207 | case 0x23: 208 | cmd_configuration.PackUVPrel = convertByteToUInt16(data1: data[4], data2: data[5]) 209 | case 0x24: 210 | cmd_configuration.CellOVPtrig = convertByteToUInt16(data1: data[4], data2: data[5]) 211 | case 0x25: 212 | cmd_configuration.CellOVPrel = convertByteToUInt16(data1: data[4], data2: data[5]) 213 | case 0x26: 214 | cmd_configuration.CellUVPtrig = convertByteToUInt16(data1: data[4], data2: data[5]) 215 | case 0x27: 216 | cmd_configuration.CellUVPrel = convertByteToUInt16(data1: data[4], data2: data[5]) 217 | case 0x28: 218 | cmd_configuration.ChgOCP = convertByteToUInt16(data1: data[4], data2: data[5]) 219 | case 0x29: 220 | var value: UInt32 = 0x10000 - UInt32(convertByteToUInt16(data1: data[4], data2: data[5])) 221 | if value == 0x10000 { 222 | value = 0 223 | } 224 | cmd_configuration.DsgOCP = UInt16(value) 225 | case 0x2A: 226 | cmd_configuration.BalanceStartVoltage = convertByteToUInt16(data1: data[4], data2: data[5]) 227 | case 0x2B: 228 | cmd_configuration.BalanceVoltageDelta = convertByteToUInt16(data1: data[4], data2: data[5]) 229 | case 0x2D: 230 | cmd_configuration.LEDCapacityIndicator = checkBit(byte: data[5], pos: 2) 231 | cmd_configuration.LEDEnable = checkBit(byte: data[5], pos: 3) 232 | cmd_configuration.BalanceOnlyWhileCharging = checkBit(byte: data[5], pos: 4) 233 | cmd_configuration.BalanceEnable = checkBit(byte: data[5], pos: 5) 234 | cmd_configuration.LoadDetect = checkBit(byte: data[5], pos: 6) 235 | cmd_configuration.HardwareSwitch = checkBit(byte: data[5], pos: 7) 236 | case 0x2E: 237 | cmd_configuration.NTCSensorEnable[0] = checkBit(byte: data[5], pos: 7) 238 | cmd_configuration.NTCSensorEnable[1] = checkBit(byte: data[5], pos: 6) 239 | cmd_configuration.NTCSensorEnable[2] = checkBit(byte: data[5], pos: 5) 240 | cmd_configuration.NTCSensorEnable[3] = checkBit(byte: data[5], pos: 4) 241 | cmd_configuration.NTCSensorEnable[4] = checkBit(byte: data[5], pos: 3) 242 | cmd_configuration.NTCSensorEnable[5] = checkBit(byte: data[5], pos: 2) 243 | cmd_configuration.NTCSensorEnable[6] = checkBit(byte: data[5], pos: 1) 244 | cmd_configuration.NTCSensorEnable[7] = checkBit(byte: data[5], pos: 0) 245 | case 0x2F: 246 | cmd_configuration.CellCount = data[5] 247 | case 0x32: 248 | cmd_configuration.Capacity80 = convertByteToUInt16(data1: data[4], data2: data[5]) 249 | case 0x33: 250 | cmd_configuration.Capacity60 = convertByteToUInt16(data1: data[4], data2: data[5]) 251 | case 0x34: 252 | cmd_configuration.Capacity40 = convertByteToUInt16(data1: data[4], data2: data[5]) 253 | case 0x35: 254 | cmd_configuration.Capacity20 = convertByteToUInt16(data1: data[4], data2: data[5]) 255 | case 0x36: 256 | cmd_configuration.HardCellOVP = convertByteToUInt16(data1: data[4], data2: data[5]) 257 | case 0x37: 258 | cmd_configuration.HardCellUVP = convertByteToUInt16(data1: data[4], data2: data[5]) 259 | case 0x3A: 260 | cmd_configuration.ChgUTPdel = data[4] 261 | cmd_configuration.ChgOTPdel = data[5] 262 | case 0x3B: 263 | cmd_configuration.DsgUTPdel = data[4] 264 | cmd_configuration.DsgOTPdel = data[5] 265 | case 0x3C: 266 | cmd_configuration.PackUVPdel = data[4] 267 | cmd_configuration.PackOVPdel = data[5] 268 | case 0x3D: 269 | cmd_configuration.CellOVPdel = data[5] 270 | cmd_configuration.CellUVPdel = data[4] 271 | case 0x3E: 272 | cmd_configuration.ChgOCPdel = data[4] 273 | cmd_configuration.ChgOCPrel = data[5] 274 | case 0x3F: 275 | cmd_configuration.DsgOCPdel = data[4] 276 | cmd_configuration.DsgOCPrel = data[5] 277 | case 0xA0: 278 | cmd_configuration.SerialNumber = ASCIItoString(bytes: data) 279 | case 0xA1: 280 | cmd_configuration.Model = ASCIItoString(bytes: data) 281 | case 0xA2: 282 | cmd_configuration.Barcode = ASCIItoString(bytes: data) 283 | default: 284 | print("invalid command code!") 285 | return 286 | } 287 | if ConfigurationController.requestSendStarted && (data[1] < 3 || data[1] > 5) { 288 | // print("Received value for address \(data[1])") 289 | ConfigurationController.sendNextReadRequest(address: data[1]) 290 | let userInfo = [ "commandCode" : data[1] ] 291 | NotificationCenter.default.post(name: Notification.Name("ConfigurationDataAvailable"), object: nil, userInfo: userInfo) 292 | } 293 | 294 | return 295 | 296 | } 297 | 298 | static func convertByteToUInt16(data1: UInt8, data2: UInt8) -> UInt16 { 299 | return UInt16(data1) << 8 + UInt16(data2) 300 | } 301 | static func convertByteToInt16(data1: UInt8, data2: UInt8) -> Int16 { 302 | return Int16(data1) << 8 + Int16(data2) 303 | } 304 | 305 | static func checkBit(byte: UInt8, pos: Int) -> Bool { 306 | switch pos { 307 | case 0: 308 | return (byte & 0b10000000) == 0b10000000; 309 | case 1: 310 | return (byte & 0b01000000) == 0b01000000; 311 | case 2: 312 | return (byte & 0b00100000) == 0b00100000; 313 | case 3: 314 | return (byte & 0b00010000) == 0b00010000; 315 | case 4: 316 | return (byte & 0b00001000) == 0b00001000; 317 | case 5: 318 | return (byte & 0b00000100) == 0b00000100; 319 | case 6: 320 | return (byte & 0b00000010) == 0b00000010; 321 | case 7: 322 | return (byte & 0b00000001) == 0b00000001; 323 | default: 324 | print("invalid pos") 325 | return false 326 | } 327 | } 328 | 329 | static func UInt16ToTemp(reading: UInt16) -> Double { 330 | let temp: Int32 = Int32(reading) 331 | if SettingController.settings.thermalUnit == .celsius { 332 | return Double(temp-2731) / Double(10.0) //SWIFT WTF 333 | } 334 | else { 335 | return (Double(temp-2731) / 10.0 * 1.8) + 32 336 | } 337 | } 338 | 339 | static func convertToString(value: UInt16) -> String { 340 | return String(format: "%.2f", Double(Double(value) / Double(100))) 341 | } 342 | static func convertToVoltageString(value: UInt16, decimalplaces: Int) -> String { 343 | return String(format: "%.\(decimalplaces)f", Double(Double(value) / Double(1000))) 344 | } 345 | 346 | static func ASCIItoString(bytes: [UInt8]) -> String { 347 | var opStr = "" 348 | if bytes[4] >= 1 { 349 | for i in 5...bytes.count-4 { 350 | if bytes[i] > 0 { 351 | opStr += String(Character(UnicodeScalar(bytes[i]))) 352 | } 353 | } 354 | } 355 | return opStr 356 | } 357 | 358 | static func getLowestCell() -> (Int, UInt16) { 359 | var lowestIndex = 0 360 | var lowestReading: UInt16 = cmd_voltages.voltageOfCell[0] 361 | for i in 0...cmd_voltages.voltageOfCell.count-1 { 362 | if cmd_voltages.voltageOfCell[i] == 0 { 363 | return (lowestIndex, lowestReading) 364 | } 365 | if cmd_voltages.voltageOfCell[i] < lowestReading { 366 | lowestReading = cmd_voltages.voltageOfCell[i] 367 | lowestIndex = i 368 | } 369 | } 370 | return (lowestIndex, lowestReading) 371 | } 372 | static func getHighestCell() -> (Int, UInt16) { 373 | var highestIndex = 0 374 | var highestReading: UInt16 = 0 375 | for i in 0...cmd_voltages.voltageOfCell.count-1 { 376 | if cmd_voltages.voltageOfCell[i] == 0 { 377 | return (highestIndex, highestReading) 378 | } 379 | if cmd_voltages.voltageOfCell[i] > highestReading { 380 | highestReading = cmd_voltages.voltageOfCell[i] 381 | highestIndex = i 382 | } 383 | } 384 | return (highestIndex, highestReading) 385 | } 386 | static func getAvgCell() -> Int { 387 | var average = 0 388 | var count = 0 389 | for i in 0...cmd_voltages.voltageOfCell.count-1 { 390 | if cmd_voltages.voltageOfCell[i] == 0 { 391 | break 392 | } 393 | count += 1 394 | average += Int(cmd_voltages.voltageOfCell[i]) 395 | } 396 | if count == 0 { 397 | return 0 398 | } 399 | return average / count 400 | } 401 | 402 | static func returnAverage() -> Int16 { 403 | var sum: Int32 = 0 404 | for i in 0...self.lastCurrentArray.count-1 { 405 | sum += Int32(self.lastCurrentArray[i]) 406 | } 407 | return Int16(sum / Int32(self.lastCurrentArray.count)) 408 | } 409 | 410 | static func generateRequest(command: UInt8) -> [UInt8] { 411 | let data: [UInt8] = [0xDD, 0xA5, command, 0x00, 0xFF, 0xFF-(command-1), 0x77] 412 | return data 413 | } 414 | 415 | static func getDischargingCurrent() -> Double { 416 | if cmd_basicInformation.current == nil { 417 | return 0 418 | } 419 | let value = min(0, cmd_basicInformation.current!) 420 | return Double(-value) / 100.0 421 | } 422 | 423 | static func getChargingCurrent() -> Double { 424 | if cmd_basicInformation.current == nil { 425 | return 0 426 | } 427 | let value = max(0, cmd_basicInformation.current!) 428 | return Double(value) / 100.0 429 | } 430 | 431 | static func printHex(data: Data) { 432 | var hexString = "" 433 | if data.count > 0 { 434 | for i in 0...data.count-1 { 435 | hexString += String(format: "%02X ", data[i]) 436 | } 437 | } 438 | print(hexString) 439 | var indexString = "" 440 | for i in 0...data.count-1 { 441 | indexString += String(format: "%02d ", i) 442 | } 443 | print(indexString) 444 | } 445 | 446 | static func protectionArray() -> [Bool] { 447 | let pr = cmd_basicInformation.protection 448 | return [pr.CellBlockOverVoltage ?? false, pr.CellBlockUnderVoltage ?? false, pr.BatteryOverVoltage ?? false, pr.BatteryUnderVoltage ?? false, pr.ChargingOverTemp ?? false, pr.ChargingUnderTemp ?? false, pr.DischargingOverTemp ?? false, pr.DischargingUnderTemp ?? false, pr.ChargingOverCurr ?? false, pr.DischargingOverCurr ?? false, pr.ShortCircuit ?? false, pr.ICError ?? false, pr.MOSLockIn ?? false] 449 | } 450 | 451 | static func protectionDescription(index: Int) -> String { 452 | switch index { 453 | case 0: 454 | return "Cell overvoltage!" 455 | case 1: 456 | return "Cell undervoltage!" 457 | case 2: 458 | return "Battery overvoltage!" 459 | case 3: 460 | return "Battery undervoltage!" 461 | case 4: 462 | return "Temperature above charging temperature limit!" 463 | case 5: 464 | return "Temperature below charging temperature limit!" 465 | case 6: 466 | return "Temperature above discharging temperature limit!" 467 | case 7: 468 | return "Temperature below discharging temperature limit!" 469 | case 8: 470 | return "Charging overcurrent!" 471 | case 9: 472 | return "Discharging overcurrent" 473 | case 10: 474 | return "Short circuit detected!" 475 | case 11: 476 | return "BMS error detected!" 477 | case 12: 478 | return "MOS locked!" 479 | default: 480 | return "Unknown" 481 | } 482 | } 483 | 484 | } 485 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/BMSStructs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BMSStructs.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 12.10.20. 6 | // 7 | 8 | import Foundation 9 | 10 | class bmsRequest { 11 | 12 | var startByte: UInt8 = 0xDD 13 | var statusByte: UInt8 = 0xA5 //Read 14 | var commandByte: UInt8 = 0x03 //Basic information 15 | var lengthByte: UInt8 = 0x00 16 | var dataBytes: [UInt8] = [] 17 | var checksumByte: UInt16 = 0xFFFF 18 | var stopByte: UInt8 = 0x77 19 | 20 | public func getBytes() -> [UInt8] { 21 | var result: [UInt8] = [] 22 | 23 | //TODO: Append instead of set 24 | result.append(self.startByte) 25 | result.append(self.statusByte) 26 | result.append(self.commandByte) 27 | result.append(UInt8(self.dataBytes.count)) 28 | result[3] = UInt8(self.dataBytes.count) 29 | if self.dataBytes.count > 1 { 30 | for i in 0...self.dataBytes.count-1 { 31 | result.append(self.dataBytes[i]) 32 | } 33 | } 34 | result.append(bmsRequest.toUInt8Arr(value: self.getChecksum()).0) 35 | result.append(bmsRequest.toUInt8Arr(value: self.getChecksum()).1) 36 | result.append(self.stopByte) 37 | return result 38 | } 39 | 40 | func generateBasicInfoRequest() -> [UInt8] { 41 | self.statusByte = 0xA5 42 | self.commandByte = 0x03 43 | self.dataBytes = [] 44 | self.lengthByte = UInt8(self.dataBytes.count) 45 | self.checksumByte = self.getChecksum() 46 | return getBytes() 47 | } 48 | 49 | func generateVoltageRequest() -> [UInt8] { 50 | self.statusByte = 0xA5 51 | self.commandByte = 0x04 52 | self.dataBytes = [] 53 | self.lengthByte = UInt8(self.dataBytes.count) 54 | self.checksumByte = self.getChecksum() 55 | return getBytes() 56 | } 57 | 58 | private func getChecksum() -> UInt16 { 59 | var checksum: UInt16 = 0 60 | checksum += UInt16(self.commandByte) 61 | checksum += UInt16(self.lengthByte) 62 | for (_, data) in self.dataBytes.enumerated() { 63 | checksum += UInt16(data) 64 | } 65 | return UInt16(65536 - Int(checksum)) 66 | } 67 | 68 | public static func toUInt8Arr(value: UInt16) -> (UInt8, UInt8) { 69 | let one = UInt8((value & 0xFF00) >> 8) 70 | let two = UInt8(value & 0x00FF) 71 | return (one, two) 72 | } 73 | 74 | } 75 | 76 | class bmsResponse { 77 | 78 | var startByte: UInt8 = 0xDD 79 | var commandByte: UInt8 = 0x03 //Basic information 80 | var statusByte: UInt8 = 0x00 //Read 81 | var lengthByte: UInt8 = 0x00 82 | var dataBytes: [UInt8] = [0x00] 83 | var checksumByte: UInt16 = 0xFFFF 84 | var stopByte: UInt8 = 0x77 85 | 86 | } 87 | 88 | 89 | class cmd_basicInformation { 90 | static var totalVoltage: UInt16? 91 | static var current: Int16? 92 | static var residualCapacity: UInt16? 93 | static var nominalCapacity: UInt16? 94 | static var cycleLife: UInt16? 95 | static var productDate: UInt16? 96 | static var balanceCells = [Bool](repeating: true, count: 32) 97 | static var protection: protectionStatus = protectionStatus() 98 | static var version: UInt8? 99 | static var rsoc: UInt8? 100 | static var controlStatus: UInt8? 101 | static var numberOfCells: UInt8? 102 | static var numberOfTempSensors: UInt8? 103 | static var temperatureReadings = [Double](repeating: 0, count: 8) 104 | static var chargingPort: Bool = true 105 | static var dischargingPort: Bool = true 106 | } 107 | 108 | class protectionStatus { 109 | var CellBlockOverVoltage: Bool? 110 | var CellBlockUnderVoltage: Bool? 111 | var BatteryOverVoltage: Bool? 112 | var BatteryUnderVoltage: Bool? 113 | var ChargingOverTemp: Bool? 114 | var ChargingUnderTemp: Bool? 115 | var DischargingOverTemp: Bool? 116 | var DischargingUnderTemp: Bool? 117 | var ChargingOverCurr: Bool? 118 | var DischargingOverCurr: Bool? 119 | var ShortCircuit: Bool? 120 | var ICError: Bool? 121 | var MOSLockIn: Bool? 122 | //... reserved 123 | } 124 | 125 | class cmd_voltages { 126 | static var voltageOfCell = [UInt16](repeating: 0, count: 32) 127 | } 128 | 129 | class cmd_bmsVersion { 130 | static var version: UInt32? 131 | } 132 | 133 | class cmd_configuration { 134 | 135 | public enum connectionType:UInt8 { 136 | case FullCapacity = 0x10 137 | case CycleCapacity = 0x11 138 | case CellFullVoltage = 0x12 139 | case CellEmptyVoltage = 0x13 140 | case RateDsg = 0x14 141 | case ProdDate = 0x15 142 | case CycleCount = 0x17 143 | case ChgOTPtrig = 0x18 144 | case ChgOTPrel = 0x19 145 | case ChgUTPtrig = 0x1A 146 | case ChgUTPrel = 0x1B 147 | case DsgOTPtrig = 0x1C 148 | case DsgOTPrel = 0x1D 149 | case DsgUTPtrig = 0x1E 150 | case DsgUTPrel = 0x1F 151 | case PackOVPtrig = 0x20 152 | case PackOVPrel = 0x21 153 | case PackUVPtrig = 0x22 154 | case PackUVPrel = 0x23 155 | case CellOVPtrig = 0x24 156 | case CellOVPrel = 0x25 157 | case CellUVPtrig = 0x26 158 | case CellUVPrel = 0x27 159 | case ChgOCP = 0x28 160 | case DsgOCP = 0x29 161 | case BalanceStartVoltage = 0x2A 162 | case BalanceVoltageDelta = 0x2B 163 | case BalanceSwitches = 0x2D 164 | case NTCSensorEnable = 0x2E 165 | case CellCount = 0x2F 166 | case Capacity80 = 0x32 167 | case Capacity60 = 0x33 168 | case Capacity40 = 0x34 169 | case Capacity20 = 0x35 170 | case HardCellOVP = 0x36 171 | case HardCellUVP = 0x37 172 | case ChargeTempDelay = 0x3A 173 | case DischargeTempDelay = 0x3B 174 | case PackVoltageProtectionDelay = 0x3C 175 | case CellVoltageProtectionDelay = 0x3D 176 | case ChargeOvercurrent = 0x3E 177 | case DischargeOvercurrent = 0x3F 178 | case SerialNumber = 0xA0 179 | case Model = 0xA1 180 | case Barcode = 0xA2 181 | } 182 | 183 | 184 | static let Addresses: [UInt8] = [0x10, 0x11, 0x12, 0x13, 0x14, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2D, 0x2E, 0x2F, 0x32, 0x33, 0x34, 0x35, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0xA1, 0xA2] //TODO: Update this when adding new elements 185 | 186 | 187 | static var FullCapacity: UInt16? 188 | static var CycleCapacity: UInt16? 189 | static var CellFullVoltage: UInt16? 190 | static var CellEmptyVoltage: UInt16? 191 | static var RateDsg: UInt16? 192 | static var ProdDate: String? 193 | static var CycleCount: UInt16? 194 | static var ChgOTPtrig: UInt16? 195 | static var ChgOTPrel: UInt16? 196 | static var ChgUTPtrig: UInt16? 197 | static var ChgUTPrel: UInt16? 198 | static var DsgOTPtrig: UInt16? 199 | static var DsgOTPrel: UInt16? 200 | static var DsgUTPtrig: UInt16? 201 | static var DsgUTPrel: UInt16? 202 | static var PackOVPtrig: UInt16? 203 | static var PackOVPrel: UInt16? 204 | static var PackUVPtrig: UInt16? 205 | static var PackUVPrel: UInt16? 206 | static var CellOVPtrig: UInt16? 207 | static var CellOVPrel: UInt16? 208 | static var CellUVPtrig: UInt16? 209 | static var CellUVPrel: UInt16? 210 | static var ChgOCP: UInt16? 211 | static var DsgOCP: UInt16? 212 | static var BalanceStartVoltage: UInt16? 213 | static var BalanceVoltageDelta: UInt16? 214 | static var LEDCapacityIndicator = false 215 | static var LEDEnable = false 216 | static var BalanceOnlyWhileCharging = false 217 | static var BalanceEnable = false 218 | static var LoadDetect = false 219 | static var HardwareSwitch = false 220 | static var NTCSensorEnable = [Bool](repeating: false, count: 8) 221 | static var CellCount: UInt8? 222 | static var Capacity80: UInt16? 223 | static var Capacity60: UInt16? 224 | static var Capacity40: UInt16? 225 | static var Capacity20: UInt16? 226 | static var HardCellOVP: UInt16? 227 | static var HardCellUVP: UInt16? 228 | static var ChgUTPdel: UInt8? 229 | static var ChgOTPdel: UInt8? 230 | static var DsgUTPdel: UInt8? 231 | static var DsgOTPdel: UInt8? 232 | static var PackUVPdel: UInt8? 233 | static var PackOVPdel: UInt8? 234 | static var CellOVPdel: UInt8? 235 | static var CellUVPdel: UInt8? 236 | static var ChgOCPdel: UInt8? 237 | static var ChgOCPrel: UInt8? 238 | static var DsgOCPdel: UInt8? 239 | static var DsgOCPrel: UInt8? 240 | static var SerialNumber: String? 241 | static var Model: String? 242 | static var Barcode: String? 243 | 244 | static func printConfiguration() { 245 | print("FullCapacity: \(cmd_configuration.FullCapacity ?? 0)") 246 | print("CycleCapacity: \(cmd_configuration.CycleCapacity ?? 0)") 247 | print("CellFullVoltage: \(cmd_configuration.CellFullVoltage ?? 0)") 248 | print("CellEmptyVoltage: \(cmd_configuration.CellEmptyVoltage ?? 0)") 249 | print("RateDsg: \(cmd_configuration.RateDsg ?? 0)") 250 | print("ProdDate: \(cmd_configuration.ProdDate ?? "")") 251 | print("CycleCount: \(cmd_configuration.CycleCount ?? 0)") 252 | print("ChgOTPtrig: \(cmd_configuration.ChgOTPtrig ?? 0)") 253 | print("ChgOTPrel: \(cmd_configuration.ChgOTPrel ?? 0)") 254 | print("ChgUTPtrig: \(cmd_configuration.ChgUTPtrig ?? 0)") 255 | print("ChgUTPrel: \(cmd_configuration.ChgUTPrel ?? 0)") 256 | print("DsgOTPtrig: \(cmd_configuration.DsgOTPtrig ?? 0)") 257 | print("DsgOTPrel: \(cmd_configuration.DsgOTPrel ?? 0)") 258 | print("DsgUTPtrig: \(cmd_configuration.DsgUTPtrig ?? 0)") 259 | print("DsgUTPrel: \(cmd_configuration.DsgUTPrel ?? 0)") 260 | print("PackOVPtrig: \(cmd_configuration.PackOVPtrig ?? 0)") 261 | print("PackOVPrel: \(cmd_configuration.PackOVPrel ?? 0)") 262 | print("PackUVPtrig: \(cmd_configuration.PackUVPtrig ?? 0)") 263 | print("PackUVPrel: \(cmd_configuration.PackUVPrel ?? 0)") 264 | print("CellOVPtrig: \(cmd_configuration.CellOVPtrig ?? 0)") 265 | print("CellOVPrel: \(cmd_configuration.CellOVPrel ?? 0)") 266 | print("CellUVPtrig: \(cmd_configuration.CellUVPtrig ?? 0)") 267 | print("CellUVPrel: \(cmd_configuration.CellUVPrel ?? 0)") 268 | print("ChgOCP: \(cmd_configuration.ChgOCP ?? 0)") 269 | print("DsgOCP: \(cmd_configuration.DsgOCP ?? 0)") 270 | print("BalanceStartVoltage: \(cmd_configuration.BalanceStartVoltage ?? 0)") 271 | print("BalanceVoltageDelta: \(cmd_configuration.BalanceVoltageDelta ?? 0)") 272 | print("LEDCapacityIndicator: \(cmd_configuration.LEDCapacityIndicator)") 273 | print("LEDEnable: \(cmd_configuration.LEDEnable)") 274 | print("BalanceOnlyWhileCharging: \(cmd_configuration.BalanceOnlyWhileCharging)") 275 | print("BalanceEnable: \(cmd_configuration.BalanceEnable)") 276 | print("LoadDetect: \(cmd_configuration.LoadDetect)") 277 | print("HardwareSwitch: \(cmd_configuration.HardwareSwitch)") 278 | print("NTCSensorEnable: \(cmd_configuration.NTCSensorEnable)") 279 | print("CellCount: \(cmd_configuration.CellCount ?? 0)") 280 | print("Capacity80: \(cmd_configuration.Capacity80 ?? 0)") 281 | print("Capacity60: \(cmd_configuration.Capacity60 ?? 0)") 282 | print("Capacity40: \(cmd_configuration.Capacity40 ?? 0)") 283 | print("Capacity20: \(cmd_configuration.Capacity20 ?? 0)") 284 | print("HardCellOVP: \(cmd_configuration.HardCellOVP ?? 0)") 285 | print("HardCellUVP: \(cmd_configuration.HardCellUVP ?? 0)") 286 | print("ChgUTPdel: \(cmd_configuration.ChgUTPdel ?? 0)") 287 | print("ChgOTPdel: \(cmd_configuration.ChgOTPdel ?? 0)") 288 | print("DsgUTPdel: \(cmd_configuration.DsgUTPdel ?? 0)") 289 | print("DsgOTPdel: \(cmd_configuration.DsgOTPdel ?? 0)") 290 | print("PackUVPdel: \(cmd_configuration.PackUVPdel ?? 0)") 291 | print("PackOVPdel: \(cmd_configuration.PackOVPdel ?? 0)") 292 | print("CellOVPdel: \(cmd_configuration.CellOVPdel ?? 0)") 293 | print("CellUVPdel: \(cmd_configuration.CellUVPdel ?? 0)") 294 | print("ChgOCPdel: \(cmd_configuration.ChgOCPdel ?? 0)") 295 | print("ChgOCPrel: \(cmd_configuration.ChgOCPrel ?? 0)") 296 | print("DsgOCPdel: \(cmd_configuration.DsgOCPdel ?? 0)") 297 | print("DsgOCPrel: \(cmd_configuration.DsgOCPrel ?? 0)") 298 | print("SerialNumber: \(cmd_configuration.SerialNumber ?? "")") 299 | print("Model: \(cmd_configuration.Model ?? "")") 300 | print("Barcode: \(cmd_configuration.Barcode ?? "")") 301 | print("NTC1: \(cmd_configuration.NTCSensorEnable[0])") 302 | print("NTC2: \(cmd_configuration.NTCSensorEnable[1])") 303 | print("NTC3: \(cmd_configuration.NTCSensorEnable[2])") 304 | print("NTC4: \(cmd_configuration.NTCSensorEnable[3])") 305 | print("NTC5: \(cmd_configuration.NTCSensorEnable[4])") 306 | print("NTC6: \(cmd_configuration.NTCSensorEnable[5])") 307 | print("NTC7: \(cmd_configuration.NTCSensorEnable[6])") 308 | print("NTC8: \(cmd_configuration.NTCSensorEnable[7])") 309 | } 310 | 311 | } 312 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/BluetoothInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothInterface.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin on 14.10.20. 6 | // 7 | 8 | import CoreBluetooth 9 | 10 | class BluetoothInterface: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { 11 | var centralManager: CBCentralManager! 12 | 13 | var tempData = Data() 14 | 15 | let serviceUUID = CBUUID(string: "FF00") 16 | let rxUUID = CBUUID(string: "FF01") 17 | let txUUID = CBUUID(string: "FF02") 18 | 19 | var pauseTransmission = false 20 | 21 | func initBluetooth() { 22 | print("BluetoothInterface: initBluetooth()") 23 | NotificationCenter.default.removeObserver(self) 24 | NotificationCenter.default.addObserver(self, selector: #selector(sendRequest), name: NSNotification.Name("BluetoothSendNeeded"), object: nil) 25 | NotificationCenter.default.addObserver(self, selector: #selector(refreshBluetooth), name: NSNotification.Name("refreshBluetooth"), object: nil) 26 | self.centralManager = CBCentralManager(delegate: self, queue: nil) 27 | } 28 | 29 | 30 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 31 | switch central.state { 32 | case .unknown: 33 | break 34 | case .resetting: 35 | break 36 | case .unsupported: 37 | break 38 | case .unauthorized: 39 | break 40 | case .poweredOff: 41 | break 42 | case .poweredOn: 43 | centralManager.scanForPeripherals(withServices: [serviceUUID]) 44 | // centralManager.scanForPeripherals(withServices: nil) 45 | break 46 | @unknown default: 47 | break 48 | } 49 | } 50 | 51 | 52 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 53 | let bluetoothDevice = device() 54 | 55 | bluetoothDevice.peripheral = peripheral 56 | bluetoothDevice.peripheral?.delegate = self 57 | bluetoothDevice.loadDeviceSettings() 58 | bluetoothDevice.settings.dongleName = peripheral.name 59 | bluetoothDevice.type = .bluetooth 60 | 61 | let index = DevicesController.indexFromID(id: peripheral.identifier) 62 | if index == -1 { 63 | DevicesController.deviceArray.append(bluetoothDevice) 64 | NotificationCenter.default.post(name: NSNotification.Name("reloadDevices"), object: nil) 65 | fileController.createDeviceDirectory(identifier: peripheral.identifier.uuidString) 66 | } else { 67 | DevicesController.deviceArray[index] = bluetoothDevice 68 | NotificationCenter.default.post(name: NSNotification.Name("reloadDevices"), object: nil) 69 | fileController.createDeviceDirectory(identifier: peripheral.identifier.uuidString) 70 | } 71 | 72 | } 73 | 74 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 75 | let index = DevicesController.indexFromID(id: peripheral.identifier) 76 | if index == -1 { 77 | print("BluetoothInterface: Did not find correct index for \(peripheral.identifier)") 78 | return 79 | } 80 | print("BluetoothInterface: \(peripheral.name!) connected... ") 81 | centralManager.stopScan() 82 | DevicesController.deviceArray[index].connected = true 83 | DevicesController.deviceArray[index].peripheral?.discoverServices(nil) 84 | } 85 | 86 | func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { 87 | let index = DevicesController.indexFromID(id: peripheral.identifier) 88 | if index == -1 { 89 | print("BluetoothInterface: Did not find correct index for \(peripheral.identifier)") 90 | return 91 | } 92 | DevicesController.deviceArray[index].connected = false 93 | print("BluetoothInterface: Failed to connect to \(peripheral.name!)") 94 | NotificationCenter.default.post(name: Notification.Name("didFailToConnect"), object: nil) 95 | 96 | } 97 | 98 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 99 | if (peripheral.services?.count ?? 0) > 0 { 100 | for i in 0...(peripheral.services?.count ?? 0)-1 { 101 | if peripheral.services?[i].uuid == CBUUID(string: "FF00") { 102 | let index = DevicesController.indexFromID(id: peripheral.identifier) 103 | if index == -1 { 104 | print("BluetoothInterface: Did not find correct index for \(peripheral.identifier)") 105 | return 106 | } 107 | DevicesController.deviceArray[index].service = peripheral.services?[i] 108 | peripheral.discoverCharacteristics(nil, for: (DevicesController.deviceArray[index].service)!) 109 | return 110 | } 111 | // print(peripheral.services?[i] ?? "") 112 | } 113 | } 114 | } 115 | 116 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 117 | if (service.characteristics?.count ?? 0) > 0 { 118 | for i in 0...(service.characteristics?.count ?? 0)-1 { 119 | if service.characteristics?[i].uuid == CBUUID(string: "FF01") { 120 | let index = DevicesController.indexFromID(id: peripheral.identifier) 121 | if index == -1 { 122 | print("BluetoothInterface: Did not find correct index for \(peripheral.identifier)") 123 | return 124 | } 125 | DevicesController.deviceArray[index].connected = true 126 | DevicesController.deviceArray[index].RXcharacteristic = service.characteristics?[i] 127 | 128 | 129 | peripheral.setNotifyValue(true, for: (DevicesController.deviceArray[index].RXcharacteristic)!) 130 | } 131 | else if service.characteristics?[i].uuid == CBUUID(string: "FF02") { 132 | let index = DevicesController.indexFromID(id: peripheral.identifier) 133 | if index == -1 { 134 | print("BluetoothInterface: Did not find correct index for \(peripheral.identifier)") 135 | return 136 | } 137 | DevicesController.deviceArray[index].connected = true 138 | DevicesController.deviceArray[index].TXcharacteristic = service.characteristics?[i] 139 | } 140 | } 141 | } 142 | } 143 | 144 | func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { 145 | if characteristic.isNotifying { 146 | //peripheral.writeValue(Data(data), for: bmsTXCharacteristic!, type: .withoutResponse) 147 | } 148 | } 149 | 150 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 151 | let data = characteristic.value 152 | 153 | let index = DevicesController.indexFromID(id: peripheral.identifier) 154 | if index == -1 { 155 | print("BluetoothInterface: Did not find correct index for \(peripheral.identifier)") 156 | return 157 | } 158 | 159 | if data?.first == 0xDD && data?.last != 0x77 && data?.count == 20 { 160 | tempData = data! 161 | DevicesController.deviceArray[index].waitingForMultiPart = true 162 | } 163 | else if DevicesController.deviceArray[index].waitingForMultiPart && data?.first != 0xDD && data?.last == 0x77 { 164 | tempData.append(data!) 165 | DevicesController.deviceArray[index].waitingForMultiPart = false 166 | } 167 | else if DevicesController.deviceArray[index].waitingForMultiPart && data?.first != 0xDD /*&& data?.last == 0x77*/ { 168 | tempData.append(data!) 169 | DevicesController.deviceArray[index].waitingForMultiPart = true 170 | } 171 | else if data?.first == 0xDD && data?.last == 0x77 { 172 | tempData = data! 173 | DevicesController.deviceArray[index].waitingForMultiPart = false 174 | } 175 | 176 | // if !DevicesController.deviceArray[index].waitingForMultiPart { 177 | // print("<<< \(printHex(data: tempData))") 178 | // } 179 | 180 | if tempData.count == 7 && BMSData.isWriteAnswer(response: [UInt8](tempData)) { 181 | if tempData[1] == 0x00 { 182 | // print("Enabled ReadWriteMode") 183 | BMSData.ReadWriteMode = true 184 | tempData.removeAll() 185 | return 186 | } 187 | else if tempData[1] == 0x01 { 188 | // print("Disabled ReadWriteMode") 189 | BMSData.ReadWriteMode = false 190 | tempData.removeAll() 191 | return 192 | } 193 | else if tempData[1] == 0xE1 { 194 | OverviewController.waitingForMosStatus = false 195 | } 196 | // print(printHex(data: tempData)) 197 | if tempData[2] == 0x80 || tempData[2] == 0x81 { 198 | BMSData.ReadWriteMode = false 199 | // ConfigurationController.sendNextWriteCommand(address: 0) 200 | } 201 | else { 202 | ConfigurationController.sendNextWriteCommand(address: tempData[1]) 203 | } 204 | tempData.removeAll() 205 | return 206 | } 207 | 208 | 209 | let valid = BMSData.validateChecksum(response: [UInt8](tempData)) 210 | 211 | if !(DevicesController.deviceArray[index].waitingForMultiPart) && valid && data!.count <= 20 && tempData.first == 0xDD && tempData.last == 0x77 { 212 | BMSData.dataToBMSReading(bytes: [UInt8](tempData)) 213 | // print("Received: \(printHex(data: tempData))") 214 | tempData.removeAll() 215 | } 216 | } 217 | 218 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 219 | let index = DevicesController.indexFromID(id: peripheral.identifier) 220 | if index == -1 { 221 | print("BluetoothInterface: Did not find correct index for \(peripheral.identifier)") 222 | return 223 | } 224 | print("BluetoothInterface: Disconnected from \(peripheral.identifier)") 225 | DevicesController.deviceArray[index].connected = false 226 | DevicesController.deviceArray[index].selected = false 227 | if DevicesController.deviceArray[index].peripheral != nil && DevicesController.connectionMode == .bluetooth { 228 | print("BluetoothInterface: reconnecting to \(peripheral.identifier)") 229 | centralManager.connect(DevicesController.deviceArray[index].peripheral!, options: nil) 230 | } 231 | NotificationCenter.default.post(name: Notification.Name("disconnectUpdate"), object: nil) 232 | } 233 | 234 | 235 | func printHex(data: Data) -> String { 236 | var hexString = "" 237 | if data.count > 0 { 238 | for i in 0...data.count-1 { 239 | hexString += String(format: "%02X ", data[i]) 240 | } 241 | } 242 | return hexString 243 | } 244 | 245 | func printCharacteristicProperties(characteristic: CBCharacteristic) { 246 | 247 | print(characteristic.uuid.uuidString) 248 | if characteristic.properties.rawValue & CBCharacteristicProperties.broadcast.rawValue != 0x0 { 249 | print("broadcast") 250 | } 251 | if characteristic.properties.rawValue & CBCharacteristicProperties.read.rawValue != 0x0 { 252 | print("read") 253 | } 254 | if characteristic.properties.rawValue & CBCharacteristicProperties.writeWithoutResponse.rawValue != 0x0 { 255 | print("write without response") 256 | } 257 | if characteristic.properties.rawValue & CBCharacteristicProperties.write.rawValue != 0x0 { 258 | print("write") 259 | } 260 | if characteristic.properties.rawValue & CBCharacteristicProperties.notify.rawValue != 0x0 { 261 | print("notify") 262 | } 263 | if characteristic.properties.rawValue & CBCharacteristicProperties.indicate.rawValue != 0x0 { 264 | print("indicate") 265 | } 266 | if characteristic.properties.rawValue & CBCharacteristicProperties.authenticatedSignedWrites.rawValue != 0x0 { 267 | print("authenticated signed writes ") 268 | } 269 | if characteristic.properties.rawValue & CBCharacteristicProperties.extendedProperties.rawValue != 0x0 { 270 | print("indicate") 271 | } 272 | if characteristic.properties.rawValue & CBCharacteristicProperties.notifyEncryptionRequired.rawValue != 0x0 { 273 | print("notify encryption required") 274 | } 275 | if characteristic.properties.rawValue & CBCharacteristicProperties.indicateEncryptionRequired.rawValue != 0x0 { 276 | print("indicate encryption required") 277 | } 278 | } 279 | 280 | 281 | @objc func sendRequest() { 282 | let device = DevicesController.getConnectedDevice() 283 | if device == nil { 284 | return 285 | } 286 | if device!.connected && device!.peripheral?.state == .connected && device!.TXcharacteristic != nil && device!.RXcharacteristic != nil { 287 | // print("BluetoothInterface: sendRequest() \(Date())") 288 | DispatchQueue.main.asyncAfter(deadline: .now()) { 289 | let data = BMSData.generateRequest(command: 0x03) 290 | device!.peripheral!.writeValue(Data(data), for: device!.TXcharacteristic!, type: .withoutResponse) 291 | } 292 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 293 | let data = BMSData.generateRequest(command: 0x04) 294 | device!.peripheral!.writeValue(Data(data), for: device!.TXcharacteristic!, type: .withoutResponse) 295 | } 296 | } 297 | } 298 | 299 | @objc func refreshBluetooth() { 300 | centralManager.stopScan() 301 | centralManager.scanForPeripherals(withServices: [serviceUUID]) 302 | } 303 | 304 | 305 | func sendReadRequest(address: UInt8) { 306 | let device = DevicesController.getConnectedDevice() 307 | if device == nil { 308 | return 309 | } 310 | if device!.connected && device!.peripheral?.state == .connected && device!.TXcharacteristic != nil && device!.RXcharacteristic != nil { 311 | DispatchQueue.main.asyncAfter(deadline: .now()) { 312 | if BMSData.ReadWriteMode { 313 | let data = BMSData.generateRequest(command: address) 314 | device!.peripheral!.writeValue(Data(data), for: device!.TXcharacteristic!, type: .withoutResponse) 315 | } 316 | else { 317 | print("BluetoothInterface: Could not write because we are not in ReadWriteMode. Trying to open ReadWriteMode...") 318 | self.sendOpenReadWriteModeRequest() 319 | } 320 | } 321 | } 322 | } 323 | 324 | func sendCloseReadWriteModeRequest() { 325 | print("BluetoothInterface: sendCloseReadWriteModeRequest()") 326 | self.sendCustomRequest(data: [0xDD, 0x5A, 0x01, 0x02, 0x00, 0x00, 0xFF, 0xFD, 0x77]) 327 | } 328 | func sendOpenReadWriteModeRequest() { 329 | print("BluetoothInterface: sendOpenReadWriteModeRequest()") 330 | self.sendCustomRequest(data: [0xDD, 0x5A, 0x00, 0x02, 0x56, 0x78, 0xFF, 0x30, 0x77]) 331 | } 332 | 333 | func sendCustomRequest(data: [UInt8]) { 334 | let device = DevicesController.getConnectedDevice() 335 | if device == nil { 336 | return 337 | } 338 | if device!.connected && device!.peripheral!.state == .connected && device!.TXcharacteristic != nil && device!.RXcharacteristic != nil { 339 | device!.peripheral!.writeValue(Data(data), for: device!.TXcharacteristic!, type: .withoutResponse) 340 | print("BluetoothInterface: \(printHex(data: Data(data)))") 341 | } 342 | } 343 | 344 | } 345 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/Classes/BubbleTabBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BubbleTabBar.swift 3 | // BubbleTabBar 4 | // 5 | // Created by Anton Skopin on 28/11/2018. 6 | // Copyright © 2018 cuberto. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class BubbleTabBar: UITabBar { 12 | 13 | private var buttons: [CBTabBarButton] = [] 14 | public var animationDuration: Double = 0.3 15 | 16 | open override var selectedItem: UITabBarItem? { 17 | willSet { 18 | guard let newValue = newValue else { 19 | buttons.forEach { $0.setSelected(false) } 20 | return 21 | } 22 | guard let index = items?.index(of: newValue), 23 | index != NSNotFound else { 24 | return 25 | } 26 | select(itemAt: index, animated: false) 27 | } 28 | } 29 | 30 | open override var tintColor: UIColor! { 31 | didSet { 32 | buttons.forEach { button in 33 | if (button.item as? CBTabBarItem)?.tintColor == nil { 34 | button.tintColor = tintColor 35 | } 36 | } 37 | } 38 | } 39 | 40 | override open var backgroundColor: UIColor? { 41 | didSet { 42 | barTintColor = backgroundColor 43 | } 44 | } 45 | 46 | public override init(frame: CGRect) { 47 | super.init(frame: frame) 48 | configure() 49 | } 50 | 51 | public required init?(coder aDecoder: NSCoder) { 52 | super.init(coder: aDecoder) 53 | configure() 54 | } 55 | 56 | var container: UIView = { 57 | let view = UIView() 58 | view.translatesAutoresizingMaskIntoConstraints = false 59 | view.backgroundColor = .clear 60 | return view 61 | }() 62 | 63 | private var csContainerBottom: NSLayoutConstraint! 64 | 65 | private func configure() { 66 | // backgroundColor = .yellow 67 | // isTranslucent = false 68 | // barTintColor = .systemBackground 69 | tintColor = .yellow 70 | unselectedItemTintColor = .red 71 | self.standardAppearance.stackedLayoutAppearance.normal.iconColor = .systemRed 72 | addSubview(container) 73 | container.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true 74 | container.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10).isActive = true 75 | container.topAnchor.constraint(equalTo: topAnchor, constant: 1).isActive = true 76 | let bottomOffset: CGFloat 77 | if #available(iOS 11.0, *) { 78 | bottomOffset = safeAreaInsets.bottom 79 | } else { 80 | bottomOffset = 0 81 | } 82 | csContainerBottom = container.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -bottomOffset) 83 | csContainerBottom.isActive = true 84 | } 85 | 86 | override open func safeAreaInsetsDidChange() { 87 | if #available(iOS 11.0, *) { 88 | super.safeAreaInsetsDidChange() 89 | csContainerBottom.constant = -safeAreaInsets.bottom 90 | } else { } 91 | } 92 | 93 | open override var items: [UITabBarItem]? { 94 | didSet { 95 | reloadViews() 96 | } 97 | } 98 | 99 | open override func setItems(_ items: [UITabBarItem]?, animated: Bool) { 100 | super.setItems(items, animated: animated) 101 | reloadViews() 102 | } 103 | 104 | private var spaceLayoutGuides:[UILayoutGuide] = [] 105 | 106 | private func reloadViews() { 107 | subviews.filter { String(describing: type(of: $0)) == "UITabBarButton" }.forEach { $0.removeFromSuperview() } 108 | buttons.forEach { $0.removeFromSuperview() } 109 | spaceLayoutGuides.forEach { self.container.removeLayoutGuide($0) } 110 | buttons = items?.map { self.button(forItem: $0) } ?? [] 111 | buttons.forEach { (button) in 112 | self.container.addSubview(button) 113 | button.topAnchor.constraint(equalTo: self.container.topAnchor).isActive = true 114 | button.bottomAnchor.constraint(equalTo: self.container.bottomAnchor).isActive = true 115 | } 116 | if #available(iOS 11.0, *) { 117 | buttons.first?.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10.0).isActive = true 118 | buttons.last?.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10.0).isActive = true 119 | } else { 120 | buttons.first?.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10.0).isActive = true 121 | buttons.last?.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10.0).isActive = true 122 | } 123 | let viewCount = buttons.count - 1 124 | spaceLayoutGuides = []; 125 | for i in 0.. CBTabBarButton { 141 | let button = CBTabBarButton(item: item) 142 | button.translatesAutoresizingMaskIntoConstraints = false 143 | button.setContentHuggingPriority(.required, for: .horizontal) 144 | if (button.item as? CBTabBarItem)?.tintColor == nil { 145 | button.tintColor = tintColor 146 | } 147 | button.addTarget(self, action: #selector(btnPressed), for: .touchUpInside) 148 | if selectedItem != nil && item === selectedItem { 149 | button.setSelected(true) 150 | } 151 | return button 152 | } 153 | 154 | @objc private func btnPressed(sender: CBTabBarButton) { 155 | guard let index = buttons.index(of: sender), 156 | index != NSNotFound, 157 | let item = items?[index] else { 158 | return 159 | } 160 | buttons.forEach { (button) in 161 | guard button != sender else { 162 | return 163 | } 164 | button.setSelected(false, animationDuration: animationDuration) 165 | } 166 | sender.setSelected(true, animationDuration: animationDuration) 167 | UIView.animate(withDuration: animationDuration) { 168 | self.container.layoutIfNeeded() 169 | } 170 | delegate?.tabBar?(self, didSelect: item) 171 | } 172 | 173 | func select(itemAt index: Int, animated: Bool = false) { 174 | guard index < buttons.count else { 175 | return 176 | } 177 | let selectedbutton = buttons[index] 178 | buttons.forEach { (button) in 179 | guard button != selectedbutton else { 180 | return 181 | } 182 | button.setSelected(false, animationDuration: animated ? animationDuration : 0) 183 | } 184 | selectedbutton.setSelected(true, animationDuration: animated ? animationDuration : 0) 185 | if animated { 186 | UIView.animate(withDuration: animationDuration) { 187 | self.container.layoutIfNeeded() 188 | } 189 | } else { 190 | self.container.layoutIfNeeded() 191 | } 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/Classes/BubbleTabBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BubbleTabBarController.swift 3 | // BubbleTabBar 4 | // 5 | // Created by Anton Skopin on 28/11/2018. 6 | // Copyright © 2018 cuberto. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class BubbleTabBarController: UITabBarController { 12 | 13 | fileprivate var shouldSelectOnTabBar = true 14 | 15 | open override var selectedViewController: UIViewController? { 16 | willSet { 17 | guard shouldSelectOnTabBar, 18 | let newValue = newValue else { 19 | shouldSelectOnTabBar = true 20 | return 21 | } 22 | guard let tabBar = tabBar as? BubbleTabBar, let index = viewControllers?.firstIndex(of: newValue) else { 23 | return 24 | } 25 | tabBar.select(itemAt: index, animated: false) 26 | } 27 | } 28 | 29 | open override var selectedIndex: Int { 30 | willSet { 31 | guard shouldSelectOnTabBar else { 32 | shouldSelectOnTabBar = true 33 | return 34 | } 35 | guard let tabBar = tabBar as? BubbleTabBar else { 36 | return 37 | } 38 | tabBar.select(itemAt: selectedIndex, animated: false) 39 | } 40 | } 41 | 42 | open override func viewDidLoad() { 43 | super.viewDidLoad() 44 | let tabBar = BubbleTabBar() 45 | self.setValue(tabBar, forKey: "tabBar") 46 | } 47 | 48 | open override func viewDidAppear(_ animated: Bool) { 49 | super.viewDidAppear(animated) 50 | } 51 | 52 | private var _barHeight: CGFloat = 74 53 | open var barHeight: CGFloat { 54 | get { 55 | if #available(iOS 11.0, *) { 56 | return _barHeight + view.safeAreaInsets.bottom 57 | } else { 58 | return _barHeight 59 | } 60 | } 61 | set { 62 | _barHeight = newValue 63 | updateTabBarFrame() 64 | } 65 | } 66 | 67 | private func updateTabBarFrame() { 68 | var tabFrame = self.tabBar.frame 69 | tabFrame.size.height = barHeight 70 | tabFrame.origin.y = self.view.frame.size.height - barHeight 71 | self.tabBar.frame = tabFrame 72 | tabBar.setNeedsLayout() 73 | } 74 | 75 | open override func viewWillLayoutSubviews() { 76 | super.viewWillLayoutSubviews() 77 | updateTabBarFrame() 78 | } 79 | 80 | open override func viewSafeAreaInsetsDidChange() { 81 | if #available(iOS 11.0, *) { 82 | super.viewSafeAreaInsetsDidChange() 83 | } 84 | updateTabBarFrame() 85 | } 86 | 87 | open override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { 88 | guard let idx = tabBar.items?.index(of: item) else { 89 | return 90 | } 91 | if let controller = viewControllers?[idx] { 92 | shouldSelectOnTabBar = false 93 | selectedIndex = idx 94 | delegate?.tabBarController?(self, didSelect: controller) 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/Classes/CBTabBarButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CBTabBarButton.swift 3 | // BubbleTabBar 4 | // 5 | // Created by Anton Skopin on 28/11/2018. 6 | // Copyright © 2018 cuberto. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class CBTabBarItem: UITabBarItem { 12 | @IBInspectable public var tintColor: UIColor? 13 | @IBInspectable public var rightToLeft:Bool = false 14 | } 15 | 16 | public class CBTabBarButton: UIControl { 17 | 18 | var rightToLeft:Bool = false 19 | private var _isSelected: Bool = false 20 | override public var isSelected: Bool { 21 | get { 22 | return _isSelected 23 | } 24 | set { 25 | guard newValue != _isSelected else { 26 | return 27 | } 28 | setSelected(newValue) 29 | } 30 | } 31 | 32 | override init(frame: CGRect) { 33 | super.init(frame: frame) 34 | configureSubviews() 35 | } 36 | 37 | required init?(coder aDecoder: NSCoder) { 38 | super.init(coder: aDecoder) 39 | configureSubviews() 40 | } 41 | 42 | init(item: UITabBarItem) { 43 | super.init(frame: .zero) 44 | tabImage = UIImageView(image: item.image) 45 | defer { 46 | self.item = item 47 | configureSubviews() 48 | } 49 | } 50 | 51 | private var currentImage: UIImage? { 52 | var maybeImage: UIImage? 53 | if _isSelected { 54 | maybeImage = item?.selectedImage ?? item?.image 55 | } else { 56 | maybeImage = item?.image 57 | } 58 | guard let image = maybeImage else { 59 | return nil 60 | } 61 | return image.renderingMode == .automatic ? image.withRenderingMode(.alwaysTemplate) : image 62 | } 63 | 64 | public var item: UITabBarItem? { 65 | didSet { 66 | tabImage.image = currentImage 67 | tabLabel.text = item?.title 68 | if let tabItem = item as? CBTabBarItem { 69 | if let color = tabItem.tintColor { 70 | tintColor = color 71 | } 72 | rightToLeft = tabItem.rightToLeft 73 | } 74 | } 75 | } 76 | 77 | override public var tintColor: UIColor! { 78 | didSet { 79 | if _isSelected { 80 | tabImage.tintColor = tintColor 81 | } 82 | tabLabel.textColor = tintColor 83 | tabBg.backgroundColor = tintColor.withAlphaComponent(0.2) 84 | } 85 | } 86 | 87 | private var tabImage = UIImageView() 88 | private var tabLabel = UILabel() 89 | private var tabBg = UIView() 90 | 91 | private let bgHeight: CGFloat = 42.0 92 | private var csFoldedBgTrailing: NSLayoutConstraint! 93 | private var csUnfoldedBgTrailing: NSLayoutConstraint! 94 | private var csFoldedLblLeading: NSLayoutConstraint! 95 | private var csUnfoldedLblLeading: NSLayoutConstraint! 96 | 97 | private var foldedConstraints: [NSLayoutConstraint] { 98 | return [csFoldedLblLeading, csFoldedBgTrailing] 99 | } 100 | 101 | private var unfoldedConstraints: [NSLayoutConstraint] { 102 | return [csUnfoldedLblLeading, csUnfoldedBgTrailing] 103 | } 104 | 105 | 106 | private func configureSubviews() { 107 | tabImage.contentMode = .center 108 | tabImage.translatesAutoresizingMaskIntoConstraints = false 109 | tabLabel.translatesAutoresizingMaskIntoConstraints = false 110 | tabLabel.font = UIFont.systemFont(ofSize: 14) 111 | tabLabel.adjustsFontSizeToFitWidth = true 112 | tabBg.translatesAutoresizingMaskIntoConstraints = false 113 | tabBg.isUserInteractionEnabled = false 114 | tabImage.setContentHuggingPriority(.required, for: .horizontal) 115 | tabImage.setContentHuggingPriority(.required, for: .vertical) 116 | tabImage.setContentCompressionResistancePriority(.required, for: .horizontal) 117 | tabImage.setContentCompressionResistancePriority(.required, for: .vertical) 118 | 119 | self.addSubview(tabBg) 120 | self.addSubview(tabLabel) 121 | self.addSubview(tabImage) 122 | 123 | tabBg.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true 124 | tabBg.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 125 | tabBg.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true 126 | tabBg.heightAnchor.constraint(equalToConstant: bgHeight).isActive = true 127 | 128 | if rightToLeft { 129 | tabImage.trailingAnchor.constraint(equalTo: tabBg.trailingAnchor, constant: -bgHeight/2.0).isActive = true 130 | tabImage.centerYAnchor.constraint(equalTo: tabBg.centerYAnchor).isActive = true 131 | tabLabel.centerYAnchor.constraint(equalTo: tabBg.centerYAnchor).isActive = true 132 | csFoldedLblLeading = tabLabel.leadingAnchor.constraint(equalTo: tabBg.trailingAnchor) 133 | csUnfoldedLblLeading = tabLabel.leadingAnchor.constraint(equalTo: tabBg.leadingAnchor, constant: bgHeight/4.0) 134 | csFoldedBgTrailing = tabImage.trailingAnchor.constraint(equalTo: tabBg.leadingAnchor, constant: bgHeight/2.0) 135 | csUnfoldedBgTrailing = tabLabel.trailingAnchor.constraint(equalTo: tabImage.leadingAnchor, constant: -bgHeight/2.0) 136 | } else { 137 | tabImage.leadingAnchor.constraint(equalTo: tabBg.leadingAnchor, constant: bgHeight/2.0).isActive = true 138 | tabImage.centerYAnchor.constraint(equalTo: tabBg.centerYAnchor).isActive = true 139 | tabLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 140 | csFoldedLblLeading = tabLabel.leadingAnchor.constraint(equalTo: leadingAnchor) 141 | csUnfoldedLblLeading = tabLabel.leadingAnchor.constraint(equalTo: tabImage.trailingAnchor, constant: bgHeight/4.0) 142 | csFoldedBgTrailing = tabImage.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -bgHeight/2.0) 143 | csUnfoldedBgTrailing = tabLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -bgHeight/2.0) 144 | } 145 | 146 | fold() 147 | setNeedsLayout() 148 | } 149 | 150 | private func fold(animationDuration duration: Double = 0.0) { 151 | unfoldedConstraints.forEach{ $0.isActive = false } 152 | foldedConstraints.forEach{ $0.isActive = true } 153 | UIView.animate(withDuration: duration) { 154 | self.tabBg.alpha = 0.0 155 | } 156 | UIView.animate(withDuration: duration * 0.4) { 157 | self.tabLabel.alpha = 0.0 158 | } 159 | UIView.transition(with: tabImage, duration: duration, options: [.transitionCrossDissolve], animations: { 160 | self.tabImage.tintColor = .black 161 | }, completion: nil) 162 | 163 | } 164 | 165 | private func unfold(animationDuration duration: Double = 0.0) { 166 | foldedConstraints.forEach{ $0.isActive = false } 167 | unfoldedConstraints.forEach{ $0.isActive = true } 168 | UIView.animate(withDuration: duration) { 169 | self.tabBg.alpha = 1.0 170 | } 171 | UIView.animate(withDuration: duration * 0.5, delay: duration * 0.5, options: [], animations: { 172 | self.tabLabel.alpha = 1.0 173 | }, completion: nil) 174 | UIView.transition(with: tabImage, duration: duration, options: [.transitionCrossDissolve], animations: { 175 | self.tabImage.tintColor = self.tintColor 176 | }, completion: nil) 177 | } 178 | 179 | public func setSelected(_ selected: Bool, animationDuration duration: Double = 0.0) { 180 | _isSelected = selected 181 | UIView.transition(with: tabImage, duration: 0.05, options: [.beginFromCurrentState], animations: { 182 | self.tabImage.image = self.currentImage 183 | }, completion: nil) 184 | if selected { 185 | unfold(animationDuration: duration) 186 | } else { 187 | fold(animationDuration: duration) 188 | } 189 | } 190 | 191 | override public func layoutSubviews() { 192 | super.layoutSubviews() 193 | tabBg.layer.cornerRadius = tabBg.bounds.height / 2.0 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/DevicesController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Devices.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin on 03.11.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class DevicesController: UIViewController, UITableViewDelegate, UITableViewDataSource { 11 | 12 | @IBOutlet weak var deviceTable: UITableView! 13 | 14 | static var connectionMode: device.connectionType = .disconnected 15 | 16 | static var deviceArray = [device]() 17 | 18 | static var connectedIndex = -1 19 | 20 | static var selectedDeviceID: String? 21 | 22 | var refreshCtrl = UIRefreshControl() 23 | 24 | var firstStart = true 25 | 26 | override func viewDidLoad() { 27 | print("DevicesController: viewDidLoad()") 28 | deviceTable.delegate = self 29 | deviceTable.dataSource = self 30 | 31 | deviceTable.refreshControl = refreshCtrl 32 | refreshCtrl.addTarget(self, action: #selector(refreshData), for: .valueChanged) 33 | 34 | OverviewController.BLEInterface = BluetoothInterface() 35 | NotificationCenter.default.addObserver(self, selector: #selector(reloadData), name: NSNotification.Name("reloadDevices"), object: nil) 36 | } 37 | 38 | override func viewDidAppear(_ animated: Bool) { 39 | DispatchQueue.main.asyncAfter(deadline: .now()) { 40 | SettingController.loadSettings() 41 | print("DevicesController: viewDidAppear()") 42 | self.title = "Devices" 43 | OverviewController.BLEInterface?.initBluetooth() 44 | DevicesController.connectionMode = .disconnected 45 | DevicesController.selectedDeviceID = nil 46 | DevicesController.connectedIndex = -1 47 | let device = DevicesController.getConnectedDevice() 48 | if device != nil { 49 | let peripheral = device?.peripheral 50 | if peripheral == nil { 51 | return 52 | } 53 | OverviewController.BLEInterface?.centralManager.cancelPeripheralConnection(peripheral!) 54 | } 55 | if SettingController.settings.useDemo { 56 | demoDevice.setupDemoDevice() 57 | } else { 58 | demoDevice.removeDemoDevice() 59 | } 60 | 61 | if !(SettingController.settings.LiontronHidden ?? false) { 62 | self.showLionTronWarning() 63 | } 64 | 65 | self.reloadData() 66 | self.resetApp() 67 | } 68 | } 69 | 70 | func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 71 | let edit = UIContextualAction(style: .normal, title: "Edit") { (contextualAction, view, boolValue) in 72 | boolValue(true) 73 | 74 | let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil) 75 | let mainNC = storyBoard.instantiateViewController(withIdentifier: "editDevice") as! UINavigationController 76 | // mainNC.modalPresentationStyle = .formSheet 77 | editDeviceController.deviceIndex = indexPath.section 78 | self.present(mainNC, animated: true, completion: nil) 79 | } 80 | edit.backgroundColor = .systemBlue 81 | let swipeActions = UISwipeActionsConfiguration(actions: [edit]) 82 | 83 | return swipeActions 84 | } 85 | 86 | @objc func reloadData() { 87 | deviceTable.reloadData() 88 | print(DevicesController.deviceArray.count) 89 | if DevicesController.deviceArray.count > 0 && firstStart { 90 | for i in 0...DevicesController.deviceArray.count-1 { 91 | if DevicesController.deviceArray[i].settings.autoConnect ?? false { 92 | firstStart = false 93 | print("Autoconnect device found! \(DevicesController.deviceArray[i].settings.deviceName ?? "")") 94 | tableView(deviceTable, didSelectRowAt: IndexPath(row: 0, section: i)) 95 | performSegue(withIdentifier: "deviceSegue", sender: nil) 96 | } 97 | } 98 | } 99 | } 100 | 101 | @objc func refreshData() { 102 | DevicesController.deviceArray.removeAll() 103 | NotificationCenter.default.post(name: Notification.Name("refreshBluetooth"), object: nil) 104 | 105 | firstStart = true 106 | deviceTable.reloadData() 107 | refreshCtrl.endRefreshing() 108 | } 109 | 110 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 111 | return 1 112 | } 113 | func numberOfSections(in tableView: UITableView) -> Int { 114 | return DevicesController.deviceArray.count 115 | } 116 | 117 | func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 118 | return CGFloat.leastNormalMagnitude 119 | } 120 | 121 | 122 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 123 | let cell = tableView.dequeueReusableCell(withIdentifier: "deviceCell", for: indexPath) as! deviceCell 124 | if DevicesController.deviceArray[indexPath.section].type == device.connectionType.bluetooth { 125 | cell.titleLabel.text = DevicesController.deviceArray[indexPath.section].settings.deviceName ?? "" 126 | if cell.titleLabel.text == DevicesController.deviceArray[indexPath.section].peripheral?.name { 127 | cell.subtitleLabel.text = "" 128 | } 129 | else { 130 | cell.subtitleLabel.text = DevicesController.deviceArray[indexPath.section].peripheral?.name ?? "" 131 | } 132 | return cell 133 | } 134 | else { 135 | cell.titleLabel.text = DevicesController.deviceArray[indexPath.section].settings.deviceName 136 | return cell 137 | } 138 | } 139 | 140 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 141 | if DevicesController.deviceArray[indexPath.section].type == device.connectionType.bluetooth && DevicesController.deviceArray[indexPath.section].peripheral != nil { 142 | OverviewController.BLEInterface?.centralManager.connect(DevicesController.deviceArray[indexPath.section].peripheral!, options: nil) 143 | DevicesController.connectionMode = .bluetooth 144 | DevicesController.selectedDeviceID = DevicesController.deviceArray[indexPath.section].peripheral?.identifier.uuidString 145 | } 146 | else if DevicesController.deviceArray[indexPath.section].type == device.connectionType.demo { 147 | DevicesController.connectionMode = .demo 148 | } 149 | else { 150 | print("ELSE") 151 | } 152 | } 153 | 154 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 155 | return DevicesController.deviceArray[indexPath.section].type != .demo 156 | } 157 | 158 | 159 | static func indexFromID(id: UUID) -> Int { 160 | if DevicesController.deviceArray.count == 0 { 161 | return -1 162 | } 163 | else { 164 | for i in 0...DevicesController.deviceArray.count-1 { 165 | if DevicesController.deviceArray[i].peripheral?.identifier == id { 166 | return i 167 | } 168 | } 169 | } 170 | return -1 171 | } 172 | 173 | static func getConnectedDevice() -> device? { 174 | if DevicesController.deviceArray.count > 0 { 175 | for i in 0...DevicesController.deviceArray.count-1 { 176 | if DevicesController.deviceArray[i].type == device.connectionType.demo && connectionMode == .demo { 177 | return DevicesController.deviceArray[i] 178 | } 179 | if DevicesController.deviceArray[i].getIdentifier() == selectedDeviceID { 180 | return DevicesController.deviceArray[i] 181 | } 182 | } 183 | } 184 | 185 | return nil //Should not return, hopefully 186 | } 187 | 188 | 189 | @IBAction func settingsPressed(_ sender: Any) { 190 | 191 | let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil) 192 | let mainNC = storyBoard.instantiateViewController(withIdentifier: "settingsPage") as! SettingController 193 | mainNC.modalPresentationStyle = .fullScreen 194 | self.present(mainNC, animated: true, completion: nil) 195 | } 196 | 197 | func resetApp() { 198 | print("DevicesController: resetApp()") 199 | GPSController.topSpeed = 0.0 200 | GPSController.currentSpeed = 0.0 201 | GPSController.efficiency = 0.0 202 | OverviewController.retryCount = 0 203 | OverviewController.didCheckReadWriteMode = false 204 | cmd_basicInformation.totalVoltage = nil 205 | cmd_basicInformation.current = nil 206 | cmd_basicInformation.residualCapacity = nil 207 | cmd_basicInformation.nominalCapacity = nil 208 | cmd_basicInformation.cycleLife = nil 209 | cmd_basicInformation.productDate = nil 210 | cmd_basicInformation.balanceCells = [Bool](repeating: true, count: 32) 211 | cmd_basicInformation.protection = protectionStatus() 212 | cmd_basicInformation.version = nil 213 | cmd_basicInformation.rsoc = nil 214 | cmd_basicInformation.controlStatus = nil 215 | cmd_basicInformation.numberOfCells = nil 216 | cmd_basicInformation.numberOfTempSensors = nil 217 | cmd_basicInformation.temperatureReadings = [Double](repeating: 0, count: 8) 218 | cmd_basicInformation.chargingPort = true 219 | cmd_basicInformation.dischargingPort = true 220 | cmd_voltages.voltageOfCell = [UInt16](repeating: 0, count: 32) 221 | cmd_configuration.FullCapacity = nil 222 | cmd_configuration.CycleCapacity = nil 223 | cmd_configuration.CellFullVoltage = nil 224 | cmd_configuration.CellEmptyVoltage = nil 225 | cmd_configuration.RateDsg = nil 226 | cmd_configuration.ProdDate = nil 227 | cmd_configuration.CycleCount = nil 228 | cmd_configuration.ChgOTPtrig = nil 229 | cmd_configuration.ChgOTPrel = nil 230 | cmd_configuration.ChgUTPtrig = nil 231 | cmd_configuration.ChgUTPrel = nil 232 | cmd_configuration.DsgOTPtrig = nil 233 | cmd_configuration.DsgOTPrel = nil 234 | cmd_configuration.DsgUTPtrig = nil 235 | cmd_configuration.DsgUTPrel = nil 236 | cmd_configuration.PackOVPtrig = nil 237 | cmd_configuration.PackOVPrel = nil 238 | cmd_configuration.PackUVPtrig = nil 239 | cmd_configuration.PackUVPrel = nil 240 | cmd_configuration.CellOVPtrig = nil 241 | cmd_configuration.CellOVPrel = nil 242 | cmd_configuration.CellUVPtrig = nil 243 | cmd_configuration.CellUVPrel = nil 244 | cmd_configuration.ChgOCP = nil 245 | cmd_configuration.DsgOCP = nil 246 | cmd_configuration.BalanceStartVoltage = nil 247 | cmd_configuration.BalanceVoltageDelta = nil 248 | cmd_configuration.LEDCapacityIndicator = false 249 | cmd_configuration.LEDEnable = false 250 | cmd_configuration.BalanceOnlyWhileCharging = false 251 | cmd_configuration.BalanceEnable = false 252 | cmd_configuration.LoadDetect = false 253 | cmd_configuration.HardwareSwitch = false 254 | cmd_configuration.NTCSensorEnable = [Bool](repeating: false, count: 8) 255 | cmd_configuration.CellCount = nil 256 | cmd_configuration.Capacity80 = nil 257 | cmd_configuration.Capacity60 = nil 258 | cmd_configuration.Capacity40 = nil 259 | cmd_configuration.Capacity20 = nil 260 | cmd_configuration.HardCellOVP = nil 261 | cmd_configuration.HardCellUVP = nil 262 | cmd_configuration.ChgUTPdel = nil 263 | cmd_configuration.ChgOTPdel = nil 264 | cmd_configuration.DsgUTPdel = nil 265 | cmd_configuration.DsgOTPdel = nil 266 | cmd_configuration.PackUVPdel = nil 267 | cmd_configuration.PackOVPdel = nil 268 | cmd_configuration.CellOVPdel = nil 269 | cmd_configuration.CellUVPdel = nil 270 | cmd_configuration.ChgOCPdel = nil 271 | cmd_configuration.ChgOCPrel = nil 272 | cmd_configuration.DsgOCPdel = nil 273 | cmd_configuration.DsgOCPrel = nil 274 | cmd_configuration.SerialNumber = nil 275 | cmd_configuration.Model = nil 276 | cmd_configuration.Barcode = nil 277 | } 278 | 279 | func showLionTronWarning() { 280 | let alert = UIAlertController(title: "Warning: LionTron batteries are not fully supported!", message: "This app has some issues communicating with LionTron batteries. Do not use the charging and discharging on/off buttons. The configuration view will also be useless for LionTron users.", preferredStyle: .alert) 281 | alert.addAction(UIAlertAction(title: "Understood", style: .cancel, handler: { (action) in 282 | SettingController.settings.LiontronHidden = true 283 | fileController.saveAppSettings() 284 | })) 285 | self.present(alert, animated: true, completion: nil) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/GPSController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GPSController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 01.11.20. 6 | // 7 | 8 | import UIKit 9 | import CoreLocation 10 | import MBCircularProgressBar 11 | import NotificationBannerSwift 12 | 13 | class GPSController: UIViewController, CLLocationManagerDelegate, UITableViewDelegate, UITableViewDataSource { 14 | 15 | @IBOutlet weak var speedProgressBar: MBCircularProgressBarView! 16 | @IBOutlet weak var efficiencyTableView: UITableView! 17 | 18 | @IBOutlet weak var PowerLabel: UILabel! 19 | @IBOutlet weak var efficiencyLabel: UILabel! 20 | 21 | let locationManager = CLLocationManager() 22 | 23 | var latestLocation: CLLocation! 24 | 25 | static var totalDistance: Double = 0.0 //meters 26 | 27 | var started = false 28 | var authenticated = false 29 | 30 | static var topSpeed = 0.0 //km/h or mph 31 | static var currentSpeed = 0.0 //m/s 32 | static var efficiency = 0.0 //Wh/km or Wh/mi 33 | 34 | override func viewDidLoad() { 35 | efficiencyTableView.delegate = self 36 | efficiencyTableView.dataSource = self 37 | efficiencyTableView.allowsSelection = false 38 | print("GPSController: viewDidLoad()") 39 | locationManager.delegate = self 40 | if DevicesController.getConnectedDevice()?.settings.gpsLoggingEnabled ?? false { 41 | locationManager.requestAlwaysAuthorization() 42 | } 43 | else { 44 | locationManager.requestWhenInUseAuthorization() 45 | } 46 | locationManager.desiredAccuracy = SettingController.settings.gpsAccuracy 47 | started = true 48 | locationManager.startUpdatingLocation() 49 | } 50 | 51 | override func viewWillAppear(_ animated: Bool) { 52 | self.title = "GPS" 53 | print("GPSController: viewWillAppear()") 54 | if started { 55 | locationManager.startUpdatingLocation() 56 | } 57 | speedProgressBar.unitString = (SettingController.settings.distanceUnit == .kilometers) ? "km/h" : "mph" 58 | } 59 | 60 | override func viewWillDisappear(_ animated: Bool) { 61 | print("GPSController: viewWillDisappear()") 62 | locationManager.stopUpdatingLocation() 63 | } 64 | 65 | override func viewDidAppear(_ animated: Bool) { 66 | NotificationCenter.default.addObserver(self, selector: #selector(reloadData), name: Notification.Name("OverviewDataAvailable"), object: nil) 67 | } 68 | 69 | override func viewDidDisappear(_ animated: Bool) { 70 | NotificationCenter.default.removeObserver(self) 71 | } 72 | 73 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 74 | // print(status) 75 | authenticated = status == .authorizedAlways || status == .authorizedWhenInUse 76 | print(authenticated) 77 | } 78 | 79 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 80 | if locations.last?.speed != nil { 81 | if latestLocation != nil { 82 | GPSController.totalDistance += latestLocation.distance(from: locations.last!) 83 | } 84 | 85 | 86 | let speed = locations.last?.speed ?? 0.0 87 | if speed == -1.0 { 88 | GPSController.currentSpeed = 0.0 89 | GPSController.efficiency = 0.0 90 | latestLocation = nil 91 | return 92 | } 93 | GPSController.topSpeed = max(GPSController.topSpeed, speed) 94 | if SettingController.settings.distanceUnit == .kilometers { 95 | GPSController.currentSpeed = round(speed * 3.6) 96 | } 97 | else { 98 | GPSController.currentSpeed = round(speed * 2.23694) 99 | } 100 | 101 | let power = Double((-BMSData.returnAverage())) / 100.0 * Double(cmd_basicInformation.totalVoltage ?? 0) / 100.0 102 | if GPSController.currentSpeed > 0.0 { 103 | GPSController.efficiency = power/GPSController.currentSpeed 104 | } 105 | else { 106 | GPSController.efficiency = 0.0 107 | } 108 | self.speedProgressBar.maxValue = CGFloat((SettingController.settings.distanceUnit == .kilometers) ? GPSController.topSpeed*3.6 : GPSController.topSpeed*2.23694) 109 | UIView.animate(withDuration: Double((SettingController.settings.refreshTime / 1000)) * 0.8) { 110 | self.speedProgressBar.value = CGFloat(GPSController.currentSpeed) 111 | } 112 | efficiencyTableView.reloadData() 113 | latestLocation = locations.last 114 | } 115 | } 116 | 117 | 118 | 119 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 120 | return 5 121 | } 122 | 123 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 124 | let cell = tableView.dequeueReusableCell(withIdentifier: "gpsCell", for: indexPath) as! gpsCell 125 | 126 | switch indexPath.row { 127 | case 0: 128 | cell.descriptionLabel.text = "Top speed:" 129 | cell.valueLabel.text = String(format: "%.0f", (SettingController.settings.distanceUnit == .kilometers) ? GPSController.topSpeed*3.6 : GPSController.topSpeed*2.23694) + ((SettingController.settings.distanceUnit == .kilometers) ? " km/h" : " mph") 130 | break 131 | case 1: 132 | cell.descriptionLabel.text = "Power:" 133 | let power = Double(Double(cmd_basicInformation.current ?? 0)/100) * Double(Double(cmd_basicInformation.totalVoltage ?? 0)/100) 134 | cell.valueLabel.text = String(format: "%.0f", power) + " W" 135 | break 136 | case 2: 137 | cell.descriptionLabel.text = "Efficiency:" 138 | cell.valueLabel.text = String(format: "%.1f", GPSController.efficiency) + " Wh/" + ((SettingController.settings.distanceUnit == .kilometers) ? "km" : "mi") 139 | break 140 | case 3: 141 | cell.descriptionLabel.text = "Estimated range:" 142 | //TODO: Change with battery type 143 | let remainingPower = Double(cmd_basicInformation.residualCapacity ?? 0) * Double(cmd_basicInformation.numberOfCells ?? 0) * 3.6 / 100.0 144 | // print(remainingPower) 145 | // print("\(cmd_basicInformation.residualCapacity ?? 0) \((cmd_basicInformation.numberOfCells ?? 0))") 146 | if GPSController.efficiency == 0.0 || remainingPower == 0.0 { 147 | cell.valueLabel.text = "0" + ((SettingController.settings.distanceUnit == .kilometers) ? " km" : " mi") 148 | break 149 | } 150 | cell.valueLabel.text = String(format: "%.0f", remainingPower/GPSController.efficiency) + ((SettingController.settings.distanceUnit == .kilometers) ? " km" : " mi") 151 | break 152 | case 4: 153 | cell.descriptionLabel.text = "Traveled distance:" 154 | if SettingController.settings.distanceUnit == .kilometers { 155 | if GPSController.totalDistance < 1000 { 156 | cell.valueLabel.text = String(format: "%.0f", GPSController.totalDistance) + " m" 157 | } 158 | else { 159 | cell.valueLabel.text = String(format: "%.1f", GPSController.totalDistance/1000.0) + " km" 160 | } 161 | } 162 | else { 163 | if GPSController.totalDistance < 1609.34 { 164 | cell.valueLabel.text = String(format: "%.0f", GPSController.totalDistance*3.28084) + " ft" 165 | } 166 | else { 167 | cell.valueLabel.text = String(format: "%.1f", GPSController.totalDistance/1609.34) + " mi" 168 | } 169 | } 170 | break 171 | default: 172 | break 173 | //print("GPSController: unimplemented tableviewcell: \(indexPath.row)") 174 | } 175 | return cell 176 | } 177 | 178 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 179 | return 40 180 | } 181 | 182 | @objc func reloadData() { 183 | efficiencyTableView.reloadData() 184 | } 185 | 186 | @objc func alertAvailable() { 187 | let protection = BMSData.protectionArray() 188 | for i in 0...protection.count-1 { 189 | if protection[i] { 190 | let banner = FloatingNotificationBanner(title: "Warning:", 191 | subtitle: BMSData.protectionDescription(index: i), 192 | style: .danger) 193 | banner.haptic = .medium 194 | banner.autoDismiss = true 195 | banner.show(queuePosition: .front, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 196 | } 197 | } 198 | } 199 | 200 | @objc func disconnectUpdate() { 201 | let banner = FloatingNotificationBanner(title: "Warning:", 202 | subtitle: "Lost connection to \(DevicesController.getConnectedDevice()!.getName())!", 203 | style: .warning) 204 | banner.haptic = .medium 205 | banner.autoDismiss = false 206 | banner.show(queuePosition: .front, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 207 | } 208 | 209 | @objc func didFailToConnect() { 210 | let banner = FloatingNotificationBanner(title: "Warning:", 211 | subtitle: "Connection to \(DevicesController.getConnectedDevice()!.getName()) failed!", 212 | style: .warning) 213 | banner.haptic = .medium 214 | banner.autoDismiss = true 215 | banner.show(queuePosition: .front, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/NotificationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 28.11.20. 6 | // 7 | 8 | import NotificationBannerSwift 9 | 10 | class NotificationController { 11 | 12 | func setupObserver() { 13 | NotificationCenter.default.addObserver(self, selector: #selector(alertAvailable), name: Notification.Name("AlertAvailable"), object: nil) 14 | NotificationCenter.default.addObserver(self, selector: #selector(disconnectUpdate), name: Notification.Name("disconnectUpdate"), object: nil) 15 | NotificationCenter.default.addObserver(self, selector: #selector(didFailToConnect), name: Notification.Name("didFailToConnect"), object: nil) 16 | NotificationCenter.default.addObserver(self, selector: #selector(parsingFailed), name: Notification.Name("parsingFailed"), object: nil) 17 | NotificationCenter.default.addObserver(self, selector: #selector(unavailableFunctionality), name: Notification.Name("unavailableFunctionality"), object: nil) 18 | NotificationCenter.default.addObserver(self, selector: #selector(LionTronMode), name: Notification.Name("LionTronMode"), object: nil) 19 | } 20 | 21 | @objc func parsingFailed() { 22 | let banner = FloatingNotificationBanner(title: "Warning:", 23 | subtitle: "Unable to parse temperature sensors from your BMS! Please contact me via email (justin.kuehner@gmail.com) in order to help me fix this issue!", 24 | style: .warning) 25 | banner.haptic = .medium 26 | banner.autoDismiss = true 27 | banner.dismissOnTap = true 28 | banner.dismissOnSwipeUp = true 29 | banner.show(queuePosition: .back, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 30 | } 31 | 32 | 33 | @objc func alertAvailable() { 34 | let protection = BMSData.protectionArray() 35 | for i in 0...protection.count-2 { 36 | if protection[i] { 37 | let banner = FloatingNotificationBanner(title: "Warning:", 38 | subtitle: BMSData.protectionDescription(index: i), 39 | style: .danger) 40 | banner.haptic = .medium 41 | banner.autoDismiss = true 42 | banner.dismissOnTap = true 43 | banner.dismissOnSwipeUp = true 44 | banner.show(queuePosition: .back, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 45 | } 46 | } 47 | } 48 | 49 | @objc func disconnectUpdate() { 50 | let banner = FloatingNotificationBanner(title: "Warning:", 51 | subtitle: "Lost connection to \(DevicesController.getConnectedDevice()!.getName())!", 52 | style: .warning) 53 | banner.haptic = .medium 54 | banner.autoDismiss = true 55 | banner.dismissOnTap = true 56 | banner.dismissOnSwipeUp = true 57 | banner.show(queuePosition: .back, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 58 | } 59 | 60 | @objc func didFailToConnect() { 61 | let banner = FloatingNotificationBanner(title: "Warning:", 62 | subtitle: "Connection to \(DevicesController.getConnectedDevice()!.getName()) failed!", 63 | style: .warning) 64 | banner.haptic = .medium 65 | banner.autoDismiss = true 66 | banner.dismissOnTap = true 67 | banner.dismissOnSwipeUp = true 68 | banner.show(queuePosition: .back, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 69 | } 70 | 71 | @objc func unavailableFunctionality() { 72 | let banner = FloatingNotificationBanner(title: "Warning:", 73 | subtitle: "Reading and writing is unavailable with a demo device!", 74 | style: .warning) 75 | banner.haptic = .light 76 | banner.autoDismiss = true 77 | banner.dismissOnTap = true 78 | banner.dismissOnSwipeUp = true 79 | banner.show(queuePosition: .back, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 80 | } 81 | 82 | @objc func LionTronMode() { 83 | let banner = FloatingNotificationBanner(title: "Warning:", 84 | subtitle: "Some functionality seem to be unavailable, some functions will be disabled.", 85 | style: .warning) 86 | banner.haptic = .light 87 | banner.autoDismiss = true 88 | banner.dismissOnTap = true 89 | banner.dismissOnSwipeUp = true 90 | banner.show(queuePosition: .back, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 91 | } 92 | 93 | @objc func CSVParsingFailed() { 94 | let banner = FloatingNotificationBanner(title: "Warning:", 95 | subtitle: "Failed to read and parse logfile. File might be corrupted.", 96 | style: .warning) 97 | banner.haptic = .none 98 | banner.autoDismiss = true 99 | banner.dismissOnTap = true 100 | banner.dismissOnSwipeUp = true 101 | banner.show(queuePosition: .back, bannerPosition: .top, queue: OverviewController.notificationQueue, on: nil) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/OverviewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverviewController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 11.10.20. 6 | // 7 | 8 | import UIKit 9 | import TYProgressBar 10 | import NotificationBannerSwift 11 | import LGButton 12 | 13 | class OverviewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 14 | 15 | 16 | static let onColors: [UIColor] = [UIColor(red: 0.395, green: 0.711, blue: 0.0, alpha: 0.85), UIColor(red: 0.042, green: 0.54, blue: 0.0, alpha: 0.85)] 17 | static let offColors: [UIColor] = [UIColor(red: 1, green: 0.455, blue: 0.0, alpha: 0.85), .systemRed] 18 | static let disabledColors: [UIColor] = [.gray, .systemGray] 19 | 20 | static var notificationQueue: NotificationBannerQueue! 21 | static var BLEInterface: BluetoothInterface? 22 | 23 | static var notificationManager = NotificationController() 24 | 25 | static var didCheckReadWriteMode = false 26 | 27 | @IBOutlet weak var batteryVoltageLabel: UILabel! 28 | @IBOutlet weak var batteryPowerLabel: UILabel! 29 | @IBOutlet weak var batteryCurrentLabel: UILabel! 30 | @IBOutlet weak var batteryCapacityLabel: UILabel! 31 | 32 | @IBOutlet weak var ChargingButton: LGButton! 33 | @IBOutlet weak var DischargingButton: LGButton! 34 | var chargingEnabled = true 35 | var dischargingEnabled = true 36 | var didLoadButtons = false 37 | 38 | static var retryCount = 0 39 | 40 | @IBOutlet weak var detailTableView: UITableView! 41 | 42 | @IBOutlet weak var progressBar: TYProgressBar! 43 | 44 | static var waitingForMosStatus = false 45 | var lastMosWriteReceived = Date().millisecondsSince1970 { 46 | didSet { 47 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { 48 | if Date().millisecondsSince1970 - self.lastMosWriteReceived > 1400 && OverviewController.waitingForMosStatus { 49 | print("Re-updating MOS status!...") 50 | OverviewController.retryCount += 1 51 | self.sendMosStatus() 52 | } 53 | } 54 | 55 | } 56 | } 57 | 58 | 59 | 60 | override func viewDidLoad() { 61 | super.viewDidLoad() 62 | OverviewController.notificationManager.setupObserver() 63 | //TODO: Set min/max according to LiIon/LiFePo 64 | 65 | detailTableView.delegate = self 66 | detailTableView.dataSource = self 67 | detailTableView.allowsSelection = false 68 | 69 | progressBar.debugInit() 70 | progressBar.lineDashPattern = [4, 2] 71 | progressBar.lineHeight = 14 72 | progressBar.font = UIFont(name: "HelveticaNeue-Medium", size: 23)! 73 | } 74 | 75 | override func viewDidAppear(_ animated: Bool) { 76 | 77 | NotificationCenter.default.addObserver(self, selector: #selector(dataAvailable), name: Notification.Name("OverviewDataAvailable"), object: nil) 78 | // if OverviewController.timer != nil { 79 | // return 80 | // } 81 | // if !OverviewController.timer!.isValid { 82 | // print("Timer invalid... recreating...") 83 | // OverviewController.timer = Timer.scheduledTimer(timeInterval: TimeInterval(Double(SettingController.refreshTime) / 1000.0), target: self, selector: #selector(sendPacketNotification), userInfo: nil, repeats: true) 84 | // } 85 | } 86 | 87 | override func viewDidDisappear(_ animated: Bool) { 88 | super.viewDidDisappear(animated) 89 | NotificationCenter.default.removeObserver(self) 90 | } 91 | 92 | override func viewWillAppear(_ animated: Bool) { 93 | OverviewController.notificationQueue = NotificationBannerQueue(maxBannersOnScreenSimultaneously: 2) 94 | traitCollectionDidChange(nil) 95 | detailTableView.reloadData() 96 | progressBar.isCharging = false 97 | progressBar.lastIsCharging = false 98 | updateButtons() 99 | } 100 | 101 | @objc func dataAvailable() { 102 | testWriteMode() 103 | if (cmd_basicInformation.numberOfCells ?? 0) > 0 && !didLoadButtons { 104 | dischargingEnabled = cmd_basicInformation.dischargingPort 105 | chargingEnabled = cmd_basicInformation.chargingPort 106 | didLoadButtons = true 107 | } 108 | //print("OverviewController: dataAvailable()") 109 | progressBar.isCharging = (cmd_basicInformation.current ?? 0) != 0 110 | UIView.animate(withDuration: 0.8) { 111 | self.progressBar.progress = Double(cmd_basicInformation.rsoc ?? 0)/100.0 112 | if(cmd_basicInformation.rsoc ?? 0 >= 0 && cmd_basicInformation.rsoc ?? 0 <= 20) { 113 | let color = self.mapColor(color1: .red, color2: .yellow, value: self.map(x: CGFloat(cmd_basicInformation.rsoc ?? 0), in_min: 0.0, in_max: 20, out_min: 0.0, out_max: 100)) 114 | self.progressBar.gradients = color 115 | } 116 | else if(cmd_basicInformation.rsoc ?? 0 > 20 && cmd_basicInformation.rsoc ?? 0 <= 40) { 117 | let color = self.mapColor(color1: .yellow, color2: .green, value: self.map(x: CGFloat(cmd_basicInformation.rsoc ?? 0), in_min: 21, in_max: 40, out_min: 0.0, out_max: 100)) 118 | self.progressBar.gradients = color 119 | } 120 | else { 121 | self.progressBar.gradients = [.green, UIColor(red: 0.0, green: 1.0, blue: 0.45, alpha: 1.0)] 122 | } 123 | } 124 | let device = DevicesController.getConnectedDevice() 125 | if device == nil { 126 | return 127 | } 128 | batteryVoltageLabel.text = BMSData.convertToString(value: cmd_basicInformation.totalVoltage ?? 0) + " V" 129 | let power = abs(Double(Double(cmd_basicInformation.current ?? 0)/100) * Double(Double(cmd_basicInformation.totalVoltage ?? 0)/100)) 130 | if(power > device!.peakPower) { 131 | device!.peakPower = power 132 | } 133 | if(abs(Double(cmd_basicInformation.current ?? 0)) / Double(100) > device!.peakCurrent) { 134 | device!.peakCurrent = abs(Double(cmd_basicInformation.current ?? 0)) / Double(100) 135 | } 136 | batteryPowerLabel.text = String(format: "%.0f", power) + " W" 137 | batteryCurrentLabel.text = String(format: "%.2f", Double(cmd_basicInformation.current ?? 0) / 100.0) + " A" 138 | batteryCapacityLabel.text = "\(String(format: "%.2f", Double(cmd_basicInformation.residualCapacity ?? 0) / 100.0)) of \(String(format: "%.2f", Double(cmd_basicInformation.nominalCapacity ?? 0) / 100.0)) Ah" 139 | detailTableView.reloadData() 140 | detailTableView.estimatedRowHeight = 0 141 | detailTableView.estimatedSectionHeaderHeight = 0 142 | detailTableView.estimatedSectionFooterHeight = 0 143 | updateButtons() 144 | } 145 | 146 | func mapColor(color1: UIColor, color2: UIColor, value: CGFloat) -> [UIColor] { 147 | var red1: CGFloat = 0 148 | var green1: CGFloat = 0 149 | var blue1: CGFloat = 0 150 | color1.getRed(&red1, green: &green1, blue: &blue1, alpha: nil) 151 | 152 | var red2: CGFloat = 0 153 | var green2: CGFloat = 0 154 | var blue2: CGFloat = 0 155 | color2.getRed(&red2, green: &green2, blue: &blue2, alpha: nil) 156 | 157 | let red3: CGFloat = map(x: value, in_min: 0.0, in_max: 100, out_min: red1, out_max: red2) 158 | let green3: CGFloat = map(x: value, in_min: 0.0, in_max: 100, out_min: green1, out_max: green2) 159 | let blue3: CGFloat = map(x: value, in_min: 0.0, in_max: 100, out_min: blue1, out_max: blue2) 160 | 161 | let red4: CGFloat = map(x: value, in_min: 10, in_max: 90, out_min: red1, out_max: red2) 162 | let green4: CGFloat = map(x: value, in_min: 10, in_max: 90, out_min: green1, out_max: green2) 163 | let blue4: CGFloat = map(x: value, in_min: 10, in_max: 90, out_min: blue1, out_max: blue2) 164 | return [UIColor(red: red3, green: green3, blue: blue3, alpha: 1.0), UIColor(red: red4, green: green4, blue: blue4, alpha: 0.95)] 165 | } 166 | 167 | func map(x: CGFloat, in_min: CGFloat, in_max: CGFloat, out_min: CGFloat, out_max: CGFloat) -> CGFloat { 168 | return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min 169 | } 170 | 171 | func doubleToRemainingTime(time: Double) -> String { 172 | if time >= 1.0 { 173 | var returnString = String(format: "%.0f", floor(time)) + "h" 174 | let minuteRemainder = round(time.truncatingRemainder(dividingBy: 1.0) * 60) 175 | if minuteRemainder > 0 && minuteRemainder < 60 { 176 | returnString += ", " + String(format: "%.0f", minuteRemainder) + "min" 177 | } 178 | return returnString 179 | } 180 | else if time >= 0.0 { 181 | let minuteRemainder = time.truncatingRemainder(dividingBy: 1.0) * 60 182 | return String(format: "%.0f", round(minuteRemainder)) + "min" 183 | } 184 | return "unknown" 185 | } 186 | 187 | 188 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 189 | if section == 0 { 190 | return 7 + Int(cmd_basicInformation.numberOfTempSensors ?? 0) 191 | } 192 | else { 193 | return Int(cmd_basicInformation.numberOfCells ?? 0) 194 | } 195 | } 196 | 197 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 198 | if indexPath.section == 0 { 199 | let cell = tableView.dequeueReusableCell(withIdentifier: "informationCell", for: indexPath) as! detailCell 200 | let device = DevicesController.getConnectedDevice()! 201 | 202 | switch indexPath.row { 203 | case 0: 204 | cell.descriptionLabel.text = "Lowest cellvoltage:" 205 | cell.valueLabel.text = String(format: "%.3f", Double(BMSData.getLowestCell().1) / 1000.0) + " V" 206 | break 207 | case 1: 208 | cell.descriptionLabel.text = "Highest cellvoltage:" 209 | cell.valueLabel.text = String(format: "%.3f", Double(BMSData.getHighestCell().1) / 1000.0) + " V" 210 | break 211 | case 2: 212 | cell.descriptionLabel.text = "Peak amperage:" 213 | cell.valueLabel.text = String(format: "%.2f", device.peakCurrent) + " A" 214 | break 215 | case 3: 216 | cell.descriptionLabel.text = "Peak power:" 217 | cell.valueLabel.text = String(format: "%.0f", device.peakPower) + " W" 218 | break 219 | case 4: 220 | if device.lowestVoltage == 0.0 { 221 | device.lowestVoltage = Double(Double(cmd_basicInformation.totalVoltage ?? 0)/100) 222 | } 223 | if Double(Double(cmd_basicInformation.totalVoltage ?? 0)/100) > 0.0 { 224 | device.lowestVoltage = min(device.lowestVoltage, Double(Double(cmd_basicInformation.totalVoltage ?? 0)/100)) 225 | } 226 | 227 | cell.descriptionLabel.text = "Lowest voltage:" 228 | cell.valueLabel.text = String(format: "%.2f", device.lowestVoltage) + " V" 229 | break 230 | case 5: 231 | device.highestVoltage = max(device.highestVoltage, Double(Double(cmd_basicInformation.totalVoltage ?? 0)/100)) 232 | 233 | cell.descriptionLabel.text = "Highest voltage:" 234 | cell.valueLabel.text = String(format: "%.2f", device.highestVoltage) + " V" 235 | break 236 | case 6: 237 | let current = BMSData.returnAverage() 238 | 239 | if current == 0 { 240 | cell.descriptionLabel.text = "Remaining runtime:" 241 | cell.valueLabel.text = " - " 242 | BMSData.lastCurrentIndex = -1 243 | } 244 | else if current > 0 { 245 | cell.descriptionLabel.text = "Remaining chargetime:" 246 | let chargingCapacity = Int64(cmd_basicInformation.nominalCapacity ?? 0) - Int64(cmd_basicInformation.residualCapacity ?? 0) 247 | let remainingTime = Double(chargingCapacity) / Double(current) 248 | let remainingTimeString = doubleToRemainingTime(time: remainingTime*0.9) 249 | cell.valueLabel.text = remainingTimeString 250 | } 251 | else { 252 | cell.descriptionLabel.text = "Remaining runtime:" 253 | let remainingTime = Double(cmd_basicInformation.residualCapacity ?? 0) / Double(-current) 254 | let remainingTimeString = doubleToRemainingTime(time: remainingTime) 255 | cell.valueLabel.text = remainingTimeString 256 | } 257 | default: 258 | let device = DevicesController.getConnectedDevice() 259 | if device == nil { 260 | cell.descriptionLabel.text = "Temperature \(indexPath.row-6):" 261 | } 262 | else { 263 | let description = DevicesController.getConnectedDevice()!.settings.getSensorName(index: indexPath.row-7) ?? "Temperature \(indexPath.row-6)" 264 | 265 | cell.descriptionLabel.text = description + ":" 266 | } 267 | let temperatureUnit = (SettingController.settings.thermalUnit == .celsius) ? "C" : "F" 268 | cell.valueLabel.text = String(format: "%.1f", cmd_basicInformation.temperatureReadings[indexPath.row-7]) + "°" + temperatureUnit 269 | } 270 | return cell 271 | } 272 | else { 273 | let cell = tableView.dequeueReusableCell(withIdentifier: "VoltageCell", for: indexPath) as! VoltageInfoCell 274 | 275 | cell.cellLabel.text = "Cell " + String(indexPath.row+1) + ":\t " + BMSData.convertToVoltageString(value: cmd_voltages.voltageOfCell[indexPath.row], decimalplaces: 3) + " V" 276 | let in_min = Float(cmd_configuration.CellEmptyVoltage ?? 3000) / 1000.0 277 | let in_max = Float(cmd_configuration.CellFullVoltage ?? 4200) / 1000.0 278 | cell.progressView.progress = self.map(x: Float(cmd_voltages.voltageOfCell[indexPath.row]) / 1000.0, in_min: in_min, in_max: in_max, out_min: 0.0, out_max: 1.0) 279 | 280 | UIView.animate(withDuration: Double((SettingController.settings.refreshTime / 1000)) * 0.35) { 281 | cell.balancingImage.alpha = (cmd_basicInformation.balanceCells[indexPath.row] ? CGFloat(1.0) : CGFloat(0.0)) 282 | } 283 | 284 | #if targetEnvironment(macCatalyst) 285 | if self.view.bounds.width > 900 { 286 | cell.voltageWidthConstraint.constant = 500 287 | } 288 | else { 289 | cell.voltageWidthConstraint.constant = self.view.bounds.width - 400 290 | } 291 | #else 292 | #endif 293 | 294 | 295 | return cell 296 | } 297 | } 298 | 299 | func map(x: Float, in_min: Float, in_max: Float, out_min: Float, out_max: Float) -> Float { 300 | return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; 301 | } 302 | 303 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 304 | self.progressBar.progress = 0.0 305 | self.progressBar.textColor = .label 306 | dataAvailable() 307 | } 308 | 309 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 310 | #if targetEnvironment(macCatalyst) 311 | return 36 312 | #endif 313 | return 28 314 | } 315 | 316 | func numberOfSections(in tableView: UITableView) -> Int { 317 | return 2 318 | } 319 | 320 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 321 | if section == 0 { 322 | return 0 323 | } 324 | else { 325 | return 25 326 | } 327 | } 328 | 329 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 330 | switch section { 331 | case 1: 332 | return "Cellseries voltages" 333 | default: 334 | return "" 335 | } 336 | } 337 | @IBAction func chargingPressed(_ sender: LGButton) { 338 | OverviewController.retryCount = 0 339 | chargingEnabled = !chargingEnabled 340 | sender.isLoading = true 341 | OverviewController.waitingForMosStatus = true 342 | lastMosWriteReceived = Date().millisecondsSince1970 343 | sendMosStatus() 344 | updateButtons() 345 | } 346 | 347 | @IBAction func dischargingPressed(_ sender: LGButton) { 348 | OverviewController.retryCount = 0 349 | dischargingEnabled = !dischargingEnabled 350 | sender.isLoading = true 351 | OverviewController.waitingForMosStatus = true 352 | lastMosWriteReceived = Date().millisecondsSince1970 353 | sendMosStatus() 354 | updateButtons() 355 | } 356 | 357 | func sendMosStatus() { 358 | var mosCode: UInt8 = 3 359 | if chargingEnabled { 360 | mosCode -= 1 361 | } 362 | if dischargingEnabled { 363 | mosCode -= 2 364 | } 365 | if OverviewController.retryCount <= 3 { 366 | // print("sending mosCode \(mosCode), chargingEnabled: \(chargingEnabled), dischargingEnabled: \(dischargingEnabled)") 367 | OverviewController.BLEInterface?.sendCustomRequest(data: [0xDD, 0x5A, 0xE1, 0x02, 0x00, mosCode, 0xFF, 0x1D - mosCode, 0x77]) 368 | } 369 | else { 370 | OverviewController.waitingForMosStatus = false 371 | DischargingButton.isLoading = false 372 | ChargingButton.isLoading = false 373 | } 374 | } 375 | 376 | func updateButtons() { 377 | let device = DevicesController.getConnectedDevice() 378 | if device != nil { 379 | if device!.settings.liontronMode ?? false { 380 | DischargingButton.isEnabled = false 381 | DischargingButton.isLoading = false 382 | DischargingButton.gradientRotation = 45 383 | DischargingButton.gradientHorizontal = false 384 | DischargingButton.gradientStartColor = OverviewController.disabledColors[0] 385 | DischargingButton.gradientEndColor = OverviewController.disabledColors[1] 386 | ChargingButton.isLoading = false 387 | ChargingButton.isEnabled = false 388 | ChargingButton.gradientRotation = 45 389 | ChargingButton.gradientHorizontal = false 390 | ChargingButton.gradientStartColor = OverviewController.disabledColors[0] 391 | ChargingButton.gradientEndColor = OverviewController.disabledColors[1] 392 | } 393 | else { 394 | if chargingEnabled == cmd_basicInformation.chargingPort { 395 | ChargingButton.isLoading = false 396 | } 397 | if dischargingEnabled == cmd_basicInformation.dischargingPort { 398 | DischargingButton.isLoading = false 399 | } 400 | // print("OverviewController: updateButtons()") 401 | ChargingButton.gradientStartColor = nil 402 | ChargingButton.gradientEndColor = nil 403 | ChargingButton.gradientRotation = 45 404 | ChargingButton.gradientHorizontal = false 405 | DischargingButton.gradientStartColor = nil 406 | DischargingButton.gradientEndColor = nil 407 | DischargingButton.gradientRotation = 45 408 | DischargingButton.gradientHorizontal = false 409 | 410 | 411 | if cmd_basicInformation.chargingPort { 412 | ChargingButton.gradientStartColor = OverviewController.onColors[0] 413 | ChargingButton.gradientEndColor = OverviewController.onColors[1] 414 | } 415 | else { 416 | ChargingButton.gradientStartColor = OverviewController.offColors[0] 417 | ChargingButton.gradientEndColor = OverviewController.offColors[1] 418 | } 419 | 420 | if cmd_basicInformation.dischargingPort { 421 | DischargingButton.gradientStartColor = OverviewController.onColors[0] 422 | DischargingButton.gradientEndColor = OverviewController.onColors[1] 423 | } 424 | else { 425 | DischargingButton.gradientStartColor = OverviewController.offColors[0] 426 | DischargingButton.gradientEndColor = OverviewController.offColors[1] 427 | } 428 | } 429 | } 430 | } 431 | 432 | 433 | func testWriteMode() { 434 | let device = DevicesController.getConnectedDevice() 435 | if device != nil && !(device!.settings.liontronMode ?? false) && !OverviewController.didCheckReadWriteMode { 436 | OverviewController.didCheckReadWriteMode = true 437 | DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { 438 | OverviewController.BLEInterface?.sendOpenReadWriteModeRequest() 439 | } 440 | DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) { 441 | OverviewController.BLEInterface?.sendCloseReadWriteModeRequest() 442 | } 443 | } 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/ProtectionStatusController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtectionStatusController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin on 15.10.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class ProtectionStatusController: UITableViewController { 11 | 12 | @IBOutlet weak var cellOverVoltageImage: UIImageView! 13 | @IBOutlet weak var cellUnderVoltageImage: UIImageView! 14 | @IBOutlet weak var BatteryOverVoltageImage: UIImageView! 15 | @IBOutlet weak var BatteryUnderVoltageImage: UIImageView! 16 | @IBOutlet weak var ChargingOverTempImage: UIImageView! 17 | @IBOutlet weak var ChargingUnderTempImage: UIImageView! 18 | @IBOutlet weak var DischargingOverTempImage: UIImageView! 19 | @IBOutlet weak var DischargingUnderTempImage: UIImageView! 20 | @IBOutlet weak var ChargingOverCurrentImage: UIImageView! 21 | @IBOutlet weak var DischargingOverCurrentImage: UIImageView! 22 | @IBOutlet weak var ShortcircuitImage: UIImageView! 23 | @IBOutlet weak var ICErrorImage: UIImageView! 24 | @IBOutlet weak var MOSLockInImage: UIImageView! 25 | 26 | override func viewDidLoad() { 27 | self.tableView.allowsSelection = false 28 | super.viewDidLoad() 29 | print("Protection: viewDidLoad") 30 | } 31 | 32 | override func viewDidAppear(_ animated: Bool) { 33 | super.viewDidAppear(animated) 34 | NotificationCenter.default.addObserver(self, selector: #selector(updateValues), name: Notification.Name("OverviewDataAvailable"), object: nil) 35 | print("Protection: didAppear") 36 | } 37 | 38 | override func viewDidDisappear(_ animated: Bool) { 39 | super.viewDidDisappear(animated) 40 | print("Protection: didDisappear") 41 | NotificationCenter.default.removeObserver(self) 42 | } 43 | 44 | @objc func updateValues() { 45 | print("ProtectionController: updateValues()") 46 | if cmd_basicInformation.protection.CellBlockOverVoltage ?? false { 47 | self.cellOverVoltageImage.image = UIImage(systemName: "xmark.shield.fill") 48 | self.cellOverVoltageImage.tintColor = .red 49 | } 50 | else { 51 | self.cellOverVoltageImage.image = UIImage(systemName: "checkmark.shield.fill") 52 | self.cellOverVoltageImage.tintColor = .systemGreen 53 | } 54 | if cmd_basicInformation.protection.CellBlockUnderVoltage ?? false { 55 | self.cellUnderVoltageImage.image = UIImage(systemName: "xmark.shield.fill") 56 | self.cellUnderVoltageImage.tintColor = .red 57 | } 58 | else { 59 | self.cellUnderVoltageImage.image = UIImage(systemName: "checkmark.shield.fill") 60 | self.cellUnderVoltageImage.tintColor = .systemGreen 61 | } 62 | if cmd_basicInformation.protection.BatteryOverVoltage ?? false { 63 | self.BatteryOverVoltageImage.image = UIImage(systemName: "xmark.shield.fill") 64 | self.BatteryOverVoltageImage.tintColor = .red 65 | } 66 | else { 67 | self.BatteryOverVoltageImage.image = UIImage(systemName: "checkmark.shield.fill") 68 | self.BatteryOverVoltageImage.tintColor = .systemGreen 69 | } 70 | if cmd_basicInformation.protection.BatteryUnderVoltage ?? false { 71 | self.BatteryUnderVoltageImage.image = UIImage(systemName: "xmark.shield.fill") 72 | self.BatteryUnderVoltageImage.tintColor = .red 73 | } 74 | else { 75 | self.BatteryUnderVoltageImage.image = UIImage(systemName: "checkmark.shield.fill") 76 | self.BatteryUnderVoltageImage.tintColor = .systemGreen 77 | } 78 | if cmd_basicInformation.protection.ChargingOverTemp ?? false { 79 | self.ChargingOverTempImage.image = UIImage(systemName: "xmark.shield.fill") 80 | self.ChargingOverTempImage.tintColor = .red 81 | } 82 | else { 83 | self.ChargingOverTempImage.image = UIImage(systemName: "checkmark.shield.fill") 84 | self.ChargingOverTempImage.tintColor = .systemGreen 85 | } 86 | if cmd_basicInformation.protection.ChargingUnderTemp ?? false { 87 | self.ChargingUnderTempImage.image = UIImage(systemName: "xmark.shield.fill") 88 | self.ChargingUnderTempImage.tintColor = .red 89 | } 90 | else { 91 | self.ChargingUnderTempImage.image = UIImage(systemName: "checkmark.shield.fill") 92 | self.ChargingUnderTempImage.tintColor = .systemGreen 93 | } 94 | if cmd_basicInformation.protection.DischargingOverTemp ?? false { 95 | self.DischargingOverTempImage.image = UIImage(systemName: "xmark.shield.fill") 96 | self.DischargingOverTempImage.tintColor = .red 97 | } 98 | else { 99 | self.DischargingOverTempImage.image = UIImage(systemName: "checkmark.shield.fill") 100 | self.DischargingOverTempImage.tintColor = .systemGreen 101 | } 102 | if cmd_basicInformation.protection.DischargingUnderTemp ?? false { 103 | self.DischargingUnderTempImage.image = UIImage(systemName: "xmark.shield.fill") 104 | self.DischargingUnderTempImage.tintColor = .red 105 | } 106 | else { 107 | self.DischargingUnderTempImage.image = UIImage(systemName: "checkmark.shield.fill") 108 | self.DischargingUnderTempImage.tintColor = .systemGreen 109 | } 110 | if cmd_basicInformation.protection.ChargingOverCurr ?? false { 111 | self.ChargingOverCurrentImage.image = UIImage(systemName: "xmark.shield.fill") 112 | self.ChargingOverCurrentImage.tintColor = .red 113 | } 114 | else { 115 | self.ChargingOverCurrentImage.image = UIImage(systemName: "checkmark.shield.fill") 116 | self.ChargingOverCurrentImage.tintColor = .systemGreen 117 | } 118 | if cmd_basicInformation.protection.DischargingOverCurr ?? false { 119 | self.DischargingOverCurrentImage.image = UIImage(systemName: "xmark.shield.fill") 120 | self.DischargingOverCurrentImage.tintColor = .red 121 | } 122 | else { 123 | self.DischargingOverCurrentImage.image = UIImage(systemName: "checkmark.shield.fill") 124 | self.DischargingOverCurrentImage.tintColor = .systemGreen 125 | } 126 | if cmd_basicInformation.protection.ShortCircuit ?? false { 127 | self.ShortcircuitImage.image = UIImage(systemName: "xmark.shield.fill") 128 | self.ShortcircuitImage.tintColor = .red 129 | } 130 | else { 131 | self.ShortcircuitImage.image = UIImage(systemName: "checkmark.shield.fill") 132 | self.ShortcircuitImage.tintColor = .systemGreen 133 | } 134 | if cmd_basicInformation.protection.ICError ?? false { 135 | self.ICErrorImage.image = UIImage(systemName: "xmark.shield.fill") 136 | self.ICErrorImage.tintColor = .red 137 | } 138 | else { 139 | self.ICErrorImage.image = UIImage(systemName: "checkmark.shield.fill") 140 | self.ICErrorImage.tintColor = .systemGreen 141 | } 142 | if cmd_basicInformation.protection.MOSLockIn ?? false { 143 | self.MOSLockInImage.image = UIImage(systemName: "xmark.shield.fill") 144 | self.MOSLockInImage.tintColor = .red 145 | } 146 | else { 147 | self.MOSLockInImage.image = UIImage(systemName: "checkmark.shield.fill") 148 | self.MOSLockInImage.tintColor = .systemGreen 149 | } 150 | } 151 | 152 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 153 | return 37 154 | } 155 | 156 | 157 | } 158 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 11.10.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/SettingController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 11.10.20. 6 | // 7 | 8 | import UIKit 9 | import CoreLocation 10 | 11 | class SettingController: UITableViewController { 12 | 13 | //TODO: Write Userdefaults async 14 | 15 | @IBOutlet weak var tempSegmentControl: UISegmentedControl! 16 | @IBOutlet weak var distanceSegmentControl: UISegmentedControl! 17 | 18 | @IBOutlet weak var demoSwitch: UISwitch! 19 | @IBOutlet weak var backgroundSwitch: UISwitch! 20 | 21 | @IBOutlet weak var gpsAccuracyDetailLabel: UILabel! 22 | 23 | 24 | static var settings = AppSettings.Settings() 25 | 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | self.tableView.delegate = self 30 | } 31 | 32 | override func viewWillAppear(_ animated: Bool) { 33 | loadAndDisplaySettings() 34 | } 35 | 36 | override func viewWillDisappear(_ animated: Bool) { 37 | super.viewWillDisappear(animated) 38 | fileController.saveAppSettings() 39 | } 40 | 41 | func loadAndDisplaySettings() { 42 | DispatchQueue.main.asyncAfter(deadline: .now()) { 43 | SettingController.loadSettings() 44 | 45 | self.demoSwitch.isOn = SettingController.settings.useDemo 46 | self.backgroundSwitch.isOn = SettingController.settings.backgroundUpdating 47 | 48 | self.tempSegmentControl.selectedSegmentIndex = SettingController.settings.thermalUnit.rawValue 49 | self.distanceSegmentControl.selectedSegmentIndex = SettingController.settings.distanceUnit.rawValue 50 | 51 | switch SettingController.settings.gpsUnit { 52 | case .best: 53 | self.gpsAccuracyDetailLabel.text = "Best" 54 | break 55 | case .tenMeters: 56 | self.gpsAccuracyDetailLabel.text = "Ten meters" 57 | break 58 | case .hundretMeters: 59 | self.gpsAccuracyDetailLabel.text = "Hundred meters" 60 | break 61 | case .kilometers: 62 | self.gpsAccuracyDetailLabel.text = "Kilometers" 63 | break 64 | } 65 | } 66 | } 67 | 68 | static func loadSettings() { 69 | print("SettingsController: LoadSettings") 70 | let data = fileController.loadConfigFile(filename: "appSettings") 71 | if data == nil { 72 | print("Could not load Settings...") 73 | fileController.listConfigFiles() 74 | return 75 | } 76 | do { 77 | let newSettings = try JSONDecoder().decode(AppSettings.Settings.self, from: data!) 78 | SettingController.settings = newSettings 79 | } catch { 80 | print(error) 81 | } 82 | } 83 | 84 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 85 | if indexPath.row == 2 && indexPath.section == 1 { 86 | let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) 87 | 88 | alert.addAction(UIAlertAction(title: "Best", style: .default) { _ in 89 | SettingController.settings.gpsAccuracy = kCLLocationAccuracyBest 90 | self.gpsAccuracyDetailLabel.text = "Best" 91 | }) 92 | 93 | alert.addAction(UIAlertAction(title: "10 meters", style: .default) { _ in 94 | SettingController.settings.gpsAccuracy = kCLLocationAccuracyNearestTenMeters 95 | self.gpsAccuracyDetailLabel.text = "Ten meters" 96 | }) 97 | 98 | alert.addAction(UIAlertAction(title: "100 meters", style: .default) { _ in 99 | SettingController.settings.gpsAccuracy = kCLLocationAccuracyHundredMeters 100 | self.gpsAccuracyDetailLabel.text = "Hundred meters" 101 | }) 102 | 103 | alert.addAction(UIAlertAction(title: "1 kilometer", style: .default) { _ in 104 | SettingController.settings.gpsAccuracy = kCLLocationAccuracyKilometer 105 | self.gpsAccuracyDetailLabel.text = "Kilometers" 106 | }) 107 | 108 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in 109 | 110 | }) 111 | present(alert, animated: true) 112 | } 113 | } 114 | 115 | @IBAction func demoSwitched(_ sender: UISwitch) { 116 | SettingController.settings.useDemo = sender.isOn 117 | } 118 | 119 | @IBAction func backgroundSwitched(_ sender: UISwitch) { 120 | SettingController.settings.backgroundUpdating = sender.isOn 121 | } 122 | 123 | @IBAction func thermalChanged(_ sender: UISegmentedControl) { 124 | switch sender.selectedSegmentIndex { 125 | case 0: 126 | SettingController.settings.thermalUnit = .celsius 127 | break 128 | case 1: 129 | SettingController.settings.thermalUnit = .fahrenheit 130 | break 131 | default: 132 | SettingController.settings.thermalUnit = .celsius 133 | } 134 | } 135 | 136 | @IBAction func distanceChanged(_ sender: UISegmentedControl) { 137 | switch sender.selectedSegmentIndex { 138 | case 0: 139 | SettingController.settings.distanceUnit = .kilometers 140 | break 141 | case 1: 142 | SettingController.settings.distanceUnit = .miles 143 | break 144 | default: 145 | SettingController.settings.distanceUnit = .kilometers 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 17.02.21. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | 11 | class AppSettings { 12 | struct Settings: Encodable, Decodable { 13 | enum distanceEnum: Int, Encodable, Decodable { 14 | case kilometers = 0 15 | case miles = 1 16 | } 17 | 18 | enum thermalEnum: Int, Encodable, Decodable { 19 | case fahrenheit = 1 20 | case celsius = 0 21 | } 22 | enum gpsEnum: Int, Encodable, Decodable { 23 | case best = 0 24 | case tenMeters = 1 25 | case hundretMeters = 2 26 | case kilometers = 3 27 | } 28 | 29 | var useDemo = false 30 | var backgroundUpdating = false 31 | var LiontronHidden: Bool? = false 32 | var distanceUnit: distanceEnum = .kilometers 33 | var thermalUnit: thermalEnum = .celsius 34 | var gpsUnit: gpsEnum = .best 35 | var gpsAccuracy: CLLocationAccuracy { 36 | get { 37 | switch gpsUnit { 38 | case .best: 39 | return kCLLocationAccuracyBest 40 | case .tenMeters: 41 | return kCLLocationAccuracyNearestTenMeters 42 | case .hundretMeters: 43 | return kCLLocationAccuracyHundredMeters 44 | case .kilometers: 45 | return kCLLocationAccuracyKilometer 46 | } 47 | } 48 | set { 49 | switch newValue { 50 | case kCLLocationAccuracyBest: 51 | gpsUnit = .best 52 | break 53 | case kCLLocationAccuracyNearestTenMeters: 54 | gpsUnit = .tenMeters 55 | break 56 | case kCLLocationAccuracyHundredMeters: 57 | gpsUnit = .hundretMeters 58 | break 59 | case kCLLocationAccuracyKilometer: 60 | gpsUnit = .kilometers 61 | break 62 | default: 63 | gpsUnit = .best 64 | break 65 | } 66 | } 67 | } 68 | 69 | var refreshTime: Int = 1000 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 11.10.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | print("ViewController: ViewDidLoad()") 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/VoltageInfoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VoltageInfoCell.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin on 13.10.20. 6 | // 7 | 8 | import UIKit 9 | import GradientProgress 10 | 11 | class VoltageInfoCell: UITableViewCell { 12 | 13 | @IBOutlet weak var balancingImage: UIImageView! 14 | @IBOutlet weak var progressView: GradientProgressBar! 15 | @IBOutlet weak var cellLabel: UILabel! 16 | @IBOutlet weak var voltageWidthConstraint: NSLayoutConstraint! 17 | 18 | 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | let accentColor = UIColor(named: "AccentColor")! 22 | if #available(macOS 10.15, *) { 23 | progressView.transform = progressView.transform.scaledBy(x: 1, y: 3) 24 | } 25 | else { 26 | progressView.transform = progressView.transform.scaledBy(x: 1, y: 2) 27 | } 28 | progressView.gradientColors = [accentColor.cgColor, accentColor.cgColor] 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/VoltageInfoProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VoltageInfoProgressView.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 21.11.20. 6 | // 7 | 8 | import MultiProgressView 9 | import UIKit 10 | 11 | @IBDesignable 12 | class LanguageExampleProgressView: MultiProgressView { 13 | 14 | @IBInspectable var language: Int = 0 { 15 | didSet { 16 | titleLabel.text = "" 17 | } 18 | } 19 | 20 | @IBInspectable var percentage: Int = 0 { 21 | didSet { 22 | percentageLabel.text = "\(percentage)%" 23 | } 24 | } 25 | 26 | private let titleLabel: UILabel = { 27 | let label = UILabel() 28 | label.font = UIFont.systemFont(ofSize: 12, weight: .semibold) 29 | label.textColor = .white 30 | return label 31 | }() 32 | 33 | private let percentageLabel: UILabel = { 34 | let label = UILabel() 35 | label.font = UIFont.systemFont(ofSize: 12, weight: .semibold) 36 | return label 37 | }() 38 | 39 | override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | initialize() 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { 45 | super.init(coder: aDecoder) 46 | initialize() 47 | } 48 | 49 | private func initialize() { 50 | setupLabels() 51 | lineCap = .round 52 | titleLabel.isHidden = true 53 | } 54 | 55 | private func setupLabels() { 56 | addSubview(titleLabel) 57 | titleLabel.anchor(left: leftAnchor, paddingLeft: 8) 58 | titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 59 | 60 | addSubview(percentageLabel) 61 | percentageLabel.anchor(right: rightAnchor, paddingRight: 8) 62 | percentageLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 63 | } 64 | 65 | func shouldHideTitle(_ hide: Bool) { 66 | titleLabel.isHidden = hide 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/WiFiInterface.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// WiFiInterface.swift 3 | //// Smart BMS Utility 4 | //// 5 | //// Created by Justin Kühner on 12.10.20. 6 | //// 7 | // 8 | //import UIKit 9 | //import SwiftSocket 10 | // 11 | //class WiFiInterface { 12 | // 13 | // static var broadcastConnection: UDPBroadcastConnection! 14 | // static var connectedIPAddr = "" 15 | // 16 | // static var port: UInt16 = 45874 17 | // static private var setupDone = false 18 | // 19 | // static private var udpClient: UDPClient! 20 | // 21 | // static func setupUDPServer() { 22 | // if !setupDone { 23 | // print("WiFiInterface: setupUDPServer()") 24 | // NotificationCenter.default.addObserver(self, selector: #selector(sendRequest), name: NSNotification.Name("WiFiPacketSendNeeded"), object: nil) 25 | // 26 | // do { 27 | // broadcastConnection = try UDPBroadcastConnection( 28 | // port: 45874 , bindIt: true, 29 | // handler: { [self] (ipAddress: String, port: Int, response: Data) -> Void in 30 | // guard true else { return } 31 | // print("UDP connection received from \(ipAddress):\(port), length: \(response.count)") 32 | // 33 | // if DevicesController.connectionMode == .wifi && ipAddress == self.connectedIPAddr { 34 | // BMSData.dataToBMSReading(bytes: [UInt8](response)) 35 | // } 36 | // else { 37 | // print("Received packet but it is not wanted!") 38 | // } 39 | // }, 40 | // errorHandler: { [self] (error) in 41 | // guard self != nil else { return } 42 | // print("Something went wrong: \(error)") 43 | // }) 44 | // } catch { 45 | // print("WiFiInterface: \(error.localizedDescription)") 46 | // } 47 | // } 48 | // setupDone = true 49 | // } 50 | // 51 | // static func updateClient() { 52 | // udpClient = UDPClient(address: connectedIPAddr, port: Int32(self.port)) 53 | // } 54 | // 55 | // @objc static func sendRequest() { 56 | // let request = bmsRequest() 57 | // print("Sendrequest to \(udpClient.address) on port \(udpClient.port)") 58 | // udpClient.send(data: request.generateBasicInfoRequest()) 59 | // DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 60 | // udpClient.send(data: request.generateVoltageRequest()) 61 | // } 62 | // } 63 | // 64 | //} 65 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/demoDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // demoDevice.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 07.11.20. 6 | // 7 | 8 | 9 | import UIKit 10 | 11 | class demoDevice { 12 | 13 | static var capacity = UInt16.random(in: 1000...2000) 14 | static var rsoc = UInt8(map(x: Int(capacity), in_min: 0, in_max: 8000, out_min: 0, out_max: 100)) 15 | static var capacityOffset: Float = 0.0 16 | 17 | static func setupDemoDevice() { 18 | print("demoDevice: AddObserver()") 19 | NotificationCenter.default.removeObserver(self) 20 | NotificationCenter.default.addObserver(self, selector: #selector(generateData), name: Notification.Name("DemoDeviceNeeded"), object: nil) 21 | if DevicesController.deviceArray.count > 0 { 22 | for i in 0...DevicesController.deviceArray.count-1 { 23 | if DevicesController.deviceArray[i].type == device.connectionType.demo { 24 | return 25 | } 26 | } 27 | } 28 | let tmpDevice = device() 29 | tmpDevice.connected = false 30 | tmpDevice.type = .demo 31 | tmpDevice.settings.deviceUUID = "demo" 32 | tmpDevice.settings.deviceName = "Demo device" 33 | DevicesController.deviceArray.insert(tmpDevice, at: 0) 34 | NotificationCenter.default.post(name: NSNotification.Name("reloadDevices"), object: nil) 35 | fileController.createDeviceDirectory(identifier: "demo") 36 | } 37 | 38 | @objc static func generateData() { 39 | if DevicesController.connectionMode == .demo { 40 | // print("demoDevice: generateData()") 41 | 42 | let voltage = UInt16ToUInt8(data: 0x08E0 - UInt16.random(in: 0...80)) 43 | let cell1 = UInt16ToUInt8(data: 0x0ED6 - UInt16.random(in: 0...125)) 44 | let cell2 = UInt16ToUInt8(data: 0x0ED6 - UInt16.random(in: 0...125)) 45 | let cell3 = UInt16ToUInt8(data: 0x0ED6 - UInt16.random(in: 0...125)) 46 | let cell4 = UInt16ToUInt8(data: 0x0ED6 - UInt16.random(in: 0...125)) 47 | let cell5 = UInt16ToUInt8(data: 0x0ED6 - UInt16.random(in: 0...125)) 48 | let cell6 = UInt16ToUInt8(data: 0x0ED6 - UInt16.random(in: 0...125)) 49 | let current = Int16.random(in: -400...0) - 400 50 | let currentData = Int16ToUInt8(data: current) 51 | 52 | capacityOffset = capacityOffset + (Float(current) / 3600.0) 53 | rsoc = UInt8(map(x: Int(capacity - UInt16(-capacityOffset)), in_min: 0, in_max: 8000, out_min: 0, out_max: 100)) 54 | // print(capacity - UInt16(-capacityOffset)) 55 | let capacityData = UInt16ToUInt8(data: capacity - UInt16(-capacityOffset)) 56 | 57 | 58 | DispatchQueue.main.asyncAfter(deadline: .now()) { 59 | //DD 03 00 1B 08 E0 02 8B 04 41 07 C9 00 02 29 4C 00 00 00 00 00 00 25 37 03 06 02 0B D5 0B CF FA C3 77 60 | let infoData: [UInt8] = [0xDD, 0x03, 0x00, 0x1B, voltage.0, voltage.1, currentData.0, currentData.1, capacityData.0, capacityData.1, 0x17, 0x70, 0x00, 0x02, 0x29, 0x4C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, rsoc, 0x03, 0x06, 0x02, 0x0B, 0xD5, 0x0B, 0xCF, 0xFA, 0xC3, 0x77] 61 | 62 | 63 | BMSData.dataToBMSReading(bytes: infoData) 64 | } 65 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 66 | //DD 04 00 0C 0E D6 0E C4 0E C2 0E C1 0E C2 0E E2 FA DF 77 67 | let infoData: [UInt8] = [0xDD, 0x04, 0x00, 0x0C, cell1.0, cell1.1, cell2.0, cell2.1, cell3.0, cell3.1, cell4.0, cell4.1, cell5.0, cell5.1, cell6.0, cell6.1, 0xFA, 0xDF, 0x77] 68 | BMSData.dataToBMSReading(bytes: infoData) 69 | } 70 | } 71 | } 72 | 73 | static func UInt16ToUInt8(data: UInt16) -> (UInt8, UInt8) { 74 | return (UInt8(data >> 8), UInt8(data & 0x00FF)) 75 | } 76 | static func Int16ToUInt8(data: Int16) -> (UInt8, UInt8) { 77 | let result = byteArray(from: data) 78 | 79 | return (result[0], result[1]) 80 | } 81 | 82 | private static func byteArray(from value: T) -> [UInt8] where T: FixedWidthInteger { 83 | withUnsafeBytes(of: value.bigEndian, Array.init) 84 | } 85 | 86 | static func map(x: Int, in_min: Int, in_max: Int, out_min: Int, out_max: Int) -> Int { 87 | return (Int(x) - in_min) * (out_max - out_min) / (in_max - in_min) + out_min 88 | } 89 | 90 | static func setChecksum(data: [UInt8]) -> [UInt8] { 91 | var datacopy = data 92 | var checksum: UInt16 = 0xFFFF 93 | 94 | for i in 2...datacopy.count-1 { 95 | checksum = checksum - UInt16(datacopy[i]) 96 | } 97 | 98 | print("demoDevice: \(checksum)") 99 | 100 | let checksumBytes = UInt16ToUInt8(data: checksum+1) 101 | datacopy[datacopy.count-3] = checksumBytes.0 102 | datacopy[datacopy.count-2] = checksumBytes.1 103 | 104 | return data 105 | } 106 | static func removeDemoDevice() { 107 | if DevicesController.deviceArray.count > 0 { 108 | for i in 0...DevicesController.deviceArray.count-1 { 109 | if DevicesController.deviceArray[i].type == device.connectionType.demo { 110 | DevicesController.deviceArray.remove(at: i) 111 | return 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/detailCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // detailCell.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 31.10.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class detailCell: UITableViewCell { 11 | 12 | @IBOutlet weak var descriptionLabel: UILabel! 13 | @IBOutlet weak var valueLabel: UILabel! 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/device.swift: -------------------------------------------------------------------------------- 1 | // 2 | // device.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin on 04.11.20. 6 | // 7 | 8 | import CoreBluetooth 9 | 10 | class device { 11 | 12 | public struct deviceSettings: Decodable, Encodable { 13 | var deviceUUID: String = "demo" 14 | var deviceName: String? 15 | var dongleName: String? 16 | var cellCount: Int? 17 | var sensorNames = [String?](repeating: nil, count: 8) 18 | var cellEmptyVoltage: UInt16 = 3000 19 | var cellFullVoltage: UInt16 = 4200 20 | var loggingEnabled: Bool? = false 21 | var gpsLoggingEnabled: Bool? = false 22 | var loggingInterval: Int? = 2 //seconds 23 | var liontronMode: Bool? = false 24 | var autoConnect: Bool? = false 25 | 26 | func getSensorName(index: Int) -> String? { 27 | if index < 8 { 28 | return sensorNames[index] 29 | } 30 | return nil 31 | } 32 | } 33 | 34 | //GENERAL 35 | 36 | var type: connectionType? 37 | public enum connectionType: Int { 38 | case bluetooth = 0 39 | case demo = 1 40 | case disconnected 41 | } 42 | 43 | var settings = deviceSettings() 44 | 45 | var connected = false 46 | var selected = false 47 | 48 | var peakPower = 0.0 49 | var peakCurrent = 0.0 50 | var lowestVoltage = 0.0 51 | var highestVoltage = 0.0 52 | 53 | 54 | 55 | //BLUETOOTH 56 | var waitingForMultiPart = false 57 | var peripheral: CBPeripheral? 58 | var service: CBService? 59 | var RXcharacteristic: CBCharacteristic? //FF01, .read, .notify 60 | var TXcharacteristic: CBCharacteristic? //FF02, .read, .writeWithoutResponse 61 | 62 | func getIdentifier() -> String { 63 | if type == connectionType.bluetooth { 64 | return peripheral?.identifier.uuidString ?? settings.deviceUUID 65 | } 66 | if type == connectionType.demo { 67 | return "demo" 68 | } 69 | return "" 70 | } 71 | 72 | func loadDeviceSettings() { 73 | let uuid = peripheral?.identifier.uuidString ?? "" 74 | if uuid == "" { 75 | print("unknown identifier") 76 | return 77 | } 78 | let newsettings = fileController.getDeviceSettings(deviceUUID: uuid) 79 | if newsettings == nil { 80 | return 81 | } 82 | self.settings = newsettings! 83 | } 84 | 85 | func saveDeviceSettings() { 86 | settings.deviceUUID = peripheral?.identifier.uuidString ?? "demo" 87 | fileController.saveDeviceSettings(deviceSettings: settings) 88 | } 89 | 90 | 91 | 92 | func getName() -> String { 93 | if type == connectionType.bluetooth { 94 | return settings.deviceName ?? "" 95 | } 96 | else if type == connectionType.demo { 97 | return "Demo device" 98 | } 99 | return "" 100 | } 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/deviceCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // deviceCell.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin on 04.11.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class deviceCell: UITableViewCell { 11 | 12 | @IBOutlet weak var titleLabel: UILabel! 13 | @IBOutlet weak var subtitleLabel: UILabel! 14 | } 15 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/editDeviceController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // editDeviceController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin on 05.11.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class editDeviceController: UIViewController { 11 | 12 | static var deviceIndex = -1 13 | var selectedDevice: device! 14 | 15 | @IBOutlet weak var deviceNameLabel: UILabel! 16 | @IBOutlet weak var deviceNameTextField: UITextField! 17 | 18 | override func viewDidLoad() { 19 | if DevicesController.getConnectedDevice() != nil { 20 | selectedDevice = DevicesController.getConnectedDevice() 21 | } 22 | else { 23 | selectedDevice = DevicesController.deviceArray[editDeviceController.deviceIndex] 24 | } 25 | 26 | deviceNameTextField.text = selectedDevice.settings.deviceName 27 | navigationItem.backBarButtonItem = UIBarButtonItem( 28 | title: "Something Else", style: .plain, target: nil, action: nil) 29 | super.viewDidLoad() 30 | } 31 | @IBAction func saveButton(_ sender: Any) { 32 | self.dismiss(animated: true) { [self] in 33 | NotificationCenter.default.post(name: Notification.Name("reloadDevices"), object: nil) 34 | } 35 | } 36 | 37 | override func viewWillDisappear(_ animated: Bool) { 38 | if DevicesController.getConnectedDevice() != nil { 39 | DevicesController.getConnectedDevice()?.settings.deviceName = deviceNameTextField.text 40 | DevicesController.getConnectedDevice()?.saveDeviceSettings() 41 | } 42 | else { 43 | DevicesController.deviceArray[editDeviceController.deviceIndex].settings.deviceName = deviceNameTextField.text 44 | DevicesController.deviceArray[editDeviceController.deviceIndex].saveDeviceSettings() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/editDeviceController2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // editDeviceController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin on 05.11.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class editDeviceController2: UIViewController { 11 | 12 | static var deviceIndex = -1 13 | var selectedDevice: device! 14 | 15 | @IBOutlet weak var deviceNameLabel: UILabel! 16 | @IBOutlet weak var deviceNameTextField: UITextField! 17 | 18 | override func viewDidLoad() { 19 | if DevicesController.getConnectedDevice() != nil { 20 | selectedDevice = DevicesController.getConnectedDevice() 21 | } 22 | else if DevicesController.connectionMode == .demo { 23 | return 24 | } 25 | else { 26 | selectedDevice = DevicesController.deviceArray[editDeviceController.deviceIndex] 27 | } 28 | 29 | deviceNameTextField.text = selectedDevice.settings.deviceName 30 | self.navigationItem.hidesBackButton = true 31 | super.viewDidLoad() 32 | } 33 | @IBAction func saveButton(_ sender: Any) { 34 | self.dismiss(animated: true) { [self] in 35 | NotificationCenter.default.post(name: Notification.Name("reloadDevices"), object: nil) 36 | } 37 | self.navigationController?.popViewController(animated: true) 38 | } 39 | 40 | override func viewWillDisappear(_ animated: Bool) { 41 | if DevicesController.getConnectedDevice() != nil { 42 | DevicesController.getConnectedDevice()?.settings.deviceName = deviceNameTextField.text 43 | DevicesController.getConnectedDevice()?.saveDeviceSettings() 44 | } 45 | else { 46 | DevicesController.deviceArray[editDeviceController.deviceIndex].settings.deviceName = deviceNameTextField.text 47 | DevicesController.deviceArray[editDeviceController.deviceIndex].saveDeviceSettings() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/fileController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // fileController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 10.02.2021. 6 | // 7 | 8 | import FileProvider 9 | 10 | class fileController { 11 | 12 | enum directoryType { 13 | case logs 14 | case config 15 | } 16 | 17 | //Example: /var/mobile/Containers/Data/Application/687DBB06-C45F-47BA-B119-AC9873586403/Documents/logs 18 | static private func getDirectory(dirType: directoryType) -> String { 19 | let docdir = docDirectory() 20 | switch dirType { 21 | case .logs: 22 | return docdir+"/logs" 23 | case .config: 24 | return docdir+"/cfg" 25 | } 26 | } 27 | 28 | static private func docDirectory() -> String { 29 | 30 | guard let docDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else { 31 | return "" 32 | } 33 | return docDirectory 34 | } 35 | 36 | static func createDirectories() { 37 | let fm = FileManager.default 38 | do { 39 | try fm.createDirectory(atPath: getDirectory(dirType: .logs), withIntermediateDirectories: true, attributes: nil) 40 | try fm.createDirectory(atPath: getDirectory(dirType: .config), withIntermediateDirectories: true, attributes: nil) 41 | } catch { 42 | print(error) 43 | } 44 | } 45 | 46 | static func createDeviceDirectory(identifier: String) { 47 | let fm = FileManager.default 48 | do { 49 | let path = getDirectory(dirType: .logs)+"/\(identifier)" 50 | try fm.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) 51 | } catch { 52 | print(error) 53 | } 54 | } 55 | 56 | 57 | static func checkAndCreateDirectory(at path: String) { 58 | // print(path) 59 | let fm = FileManager.default 60 | if !fm.fileExists(atPath: path) { 61 | do { 62 | try fm.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil) 63 | } catch { 64 | print(error) 65 | } 66 | } 67 | else { 68 | //print("Folder already exists") 69 | } 70 | } 71 | 72 | static func writeLogFile(content: String) { 73 | // print("fileController: writeLogFile") 74 | let filename = loggingController.generateFilename() 75 | let uuid = DevicesController.getConnectedDevice()?.getIdentifier() ?? "demo" 76 | let path = getDirectory(dirType: .logs)+"/\(uuid)/\(filename).csv" 77 | let fm = FileManager.default 78 | do { 79 | if !fm.fileExists(atPath: path) { 80 | print("fileController: file does not exist. Creating one") 81 | let header = loggingController.generateFileHeader() 82 | if header == nil { 83 | print("header == nil") 84 | return 85 | } 86 | let newcontent = header!+content 87 | try newcontent.write(toFile: path, atomically: true, encoding: .utf8) 88 | } else { //Appending 89 | let oldcontent = loadLogFile(uuid: uuid, filename: filename) 90 | let newcontent = oldcontent+content 91 | try newcontent.write(toFile: path, atomically: true, encoding: .utf8) 92 | } 93 | } catch { 94 | print("fileController: unable to write to file: \(error)") 95 | } 96 | } 97 | 98 | static func loadLogFile(uuid: String, filename: String) -> String { 99 | let fm = FileManager.default 100 | let path = getDirectory(dirType: .logs)+"/\(uuid)/\(filename).csv" 101 | // print(path) 102 | let data = fm.contents(atPath: path) 103 | if data == nil { 104 | print("loadFile: unable to read file (data == nil)") 105 | return "" 106 | } 107 | return String(data: data!, encoding: .utf8) ?? "" 108 | } 109 | 110 | 111 | static func loadConfigFile(filename: String) -> Data? { 112 | let fm = FileManager.default 113 | let path = getDirectory(dirType: .config)+"/\(filename).json" 114 | return fm.contents(atPath: path) 115 | } 116 | 117 | static func saveDeviceSettings(deviceSettings: device.deviceSettings) { 118 | print("saveDeviceSettings() for uuid \(deviceSettings.deviceUUID)") 119 | do { 120 | let fm = FileManager.default 121 | let path = getDirectory(dirType: .config)+"/\(deviceSettings.deviceUUID).json" 122 | 123 | let jsonEncoder = JSONEncoder() 124 | jsonEncoder.outputFormatting = .prettyPrinted 125 | let data = try jsonEncoder.encode(deviceSettings) 126 | // print(String(data: data, encoding: .utf8)!) 127 | 128 | fm.createFile(atPath: path, contents: data, attributes: nil) 129 | } catch { 130 | print(error) 131 | } 132 | } 133 | 134 | static func getDeviceSettings(deviceUUID: String) -> device.deviceSettings? { 135 | // print("getDeviceSettings() for uuid \(deviceUUID)") 136 | let data = loadConfigFile(filename: deviceUUID) 137 | if data == nil { 138 | print("could not load devicesettings for uuid \(deviceUUID)") 139 | return nil 140 | } 141 | // print(String(data: data!, encoding: .utf8)!) 142 | do { 143 | let deviceSettings = try JSONDecoder().decode(device.deviceSettings.self, from: data!) 144 | return deviceSettings 145 | } catch { 146 | print(error) 147 | return nil 148 | } 149 | } 150 | 151 | 152 | 153 | static func saveAppSettings() { 154 | print("saveAppSettings()") 155 | do { 156 | let jsonEncoder = JSONEncoder() 157 | jsonEncoder.outputFormatting = .prettyPrinted 158 | let data = try jsonEncoder.encode(SettingController.settings) 159 | print(String(data: data, encoding: .utf8)!) 160 | let fm = FileManager.default 161 | let path = getDirectory(dirType: .config)+"/appSettings.json" 162 | // print(path) 163 | fm.createFile(atPath: path, contents: data, attributes: nil) 164 | } catch { 165 | print(error) 166 | } 167 | } 168 | 169 | 170 | static func logCountForFile(uuid: String, filename: String) -> Int { 171 | let count = loadLogFile(uuid: uuid, filename: filename).components(separatedBy: "\n") 172 | return max(0, count.count-1) 173 | } 174 | 175 | 176 | static func countLogDirectories() -> [(String, Int)] { 177 | let fm = FileManager.default 178 | let logDir = getDirectory(dirType: .logs) 179 | var resultArr = [(String, Int)]() 180 | do { 181 | let paths = try fm.contentsOfDirectory(atPath: logDir) 182 | if paths.count > 0 { 183 | for i in 0...paths.count-1 { 184 | let subDirPath = logDir + "/\(paths[i])" 185 | let files = try fm.contentsOfDirectory(atPath: subDirPath) 186 | if files.count > 0 { 187 | resultArr.append((paths[i], files.count)) 188 | } 189 | } 190 | } 191 | return resultArr 192 | } catch { 193 | print(error) 194 | } 195 | 196 | return [] 197 | } 198 | 199 | 200 | static func getLogFiles(uuid: String) -> [String] { 201 | let fm = FileManager.default 202 | do { 203 | var unedited = try fm.contentsOfDirectory(atPath: getDirectory(dirType: .logs)+"/\(uuid)").sorted() 204 | if unedited.count > 0 { 205 | for i in 0...unedited.count-1 { 206 | unedited[i] = unedited[i].replacingOccurrences(of: ".csv", with: "") 207 | } 208 | unedited.sort() 209 | } 210 | return unedited 211 | } catch { 212 | print(error) 213 | } 214 | return [String]() 215 | } 216 | 217 | 218 | static func listConfigFiles() { 219 | let fm = FileManager.default 220 | let configDir = getDirectory(dirType: .config) 221 | do { 222 | let paths = try fm.contentsOfDirectory(atPath: configDir) 223 | if paths.count > 0 { 224 | print("=====docs=====") 225 | for i in 0...paths.count-1 { 226 | print(paths[i]) 227 | var isDir : ObjCBool = false 228 | if fm.fileExists(atPath: configDir+"/"+paths[i], isDirectory: &isDir) { 229 | if isDir.boolValue { 230 | let subPaths = try fm.contentsOfDirectory(atPath: configDir+"/"+paths[i]) 231 | if subPaths.count > 0 { 232 | for i in 0...subPaths.count-1 { 233 | print("\t\(subPaths[i])") 234 | } 235 | } 236 | } 237 | } 238 | } 239 | print("==============") 240 | } 241 | else { 242 | print("Documents directory is empty.") 243 | } 244 | } catch { 245 | print(error) 246 | } 247 | } 248 | 249 | static func listLogFiles() { 250 | let fm = FileManager.default 251 | let logdir = getDirectory(dirType: .logs) 252 | do { 253 | let paths = try fm.contentsOfDirectory(atPath: logdir) 254 | if paths.count > 0 { 255 | print("=====logs=====") 256 | for i in 0...paths.count-1 { 257 | print(paths[i]) 258 | var isDir : ObjCBool = false 259 | if fm.fileExists(atPath: logdir+"/"+paths[i], isDirectory: &isDir) { 260 | if isDir.boolValue { 261 | let subPaths = try fm.contentsOfDirectory(atPath: logdir+"/"+paths[i]) 262 | if subPaths.count > 0 { 263 | for i in 0...subPaths.count-1 { 264 | print("\t\(subPaths[i])") 265 | } 266 | } 267 | } 268 | } 269 | } 270 | print("==============") 271 | } 272 | else { 273 | print("Log directory is empty.") 274 | } 275 | } catch { 276 | print(error) 277 | } 278 | } 279 | 280 | static func clearDirectory() { 281 | let fm = FileManager.default 282 | let docdir = docDirectory() 283 | do { 284 | let items = try fm.contentsOfDirectory(atPath: docdir) 285 | if items.count > 0 { 286 | for i in 0...items.count-1 { 287 | try fm.removeItem(atPath: docdir+"/"+items[i]) 288 | } 289 | } 290 | } catch { 291 | print(error) 292 | } 293 | } 294 | 295 | static func clearLogDirectory() { 296 | let fm = FileManager.default 297 | let docdir = getDirectory(dirType: .logs) 298 | do { 299 | let items = try fm.contentsOfDirectory(atPath: docdir) 300 | if items.count > 0 { 301 | for i in 0...items.count-1 { 302 | try fm.removeItem(atPath: docdir+"/"+items[i]) 303 | } 304 | } 305 | } catch { 306 | print(error) 307 | } 308 | } 309 | 310 | static func clearLogDirectoryForDevice(UUID: String) { 311 | let fm = FileManager.default 312 | let docdir = getDirectory(dirType: .logs)+"/\(UUID)" 313 | do { 314 | let items = try fm.contentsOfDirectory(atPath: docdir) 315 | if items.count > 0 { 316 | for i in 0...items.count-1 { 317 | print("Removing \(docdir)/\(items[i])") 318 | try fm.removeItem(atPath: docdir+"/"+items[i]) 319 | } 320 | } 321 | } catch { 322 | print(error) 323 | } 324 | } 325 | 326 | static func removeLogfile(UUID: String, filename: String) { 327 | let fm = FileManager.default 328 | let logdir = getDirectory(dirType: .logs)+"/\(UUID)/\(filename).csv" 329 | do { 330 | try fm.removeItem(atPath: logdir) 331 | } 332 | catch { 333 | print(error) 334 | } 335 | } 336 | 337 | static func getLogFileDirectory(UUID: String, filename: String) -> String { 338 | let filedir = getDirectory(dirType: .logs)+"/\(UUID)/\(filename).csv" 339 | let fm = FileManager.default 340 | if fm.fileExists(atPath: filedir) { 341 | return filedir 342 | } 343 | return "" 344 | } 345 | 346 | } 347 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/gpsCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // detailCell.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 31.10.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class gpsCell: UITableViewCell { 11 | 12 | @IBOutlet weak var descriptionLabel: UILabel! 13 | @IBOutlet weak var valueLabel: UILabel! 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/listLogFilesController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // listLogFilesController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 18.02.21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | class listLogFilesController: UITableViewController { 12 | 13 | var dirs: [String]! 14 | static var uuid = "" 15 | 16 | override func viewWillAppear(_ animated: Bool) { 17 | super.viewWillAppear(animated) 18 | self.title = fileController.getDeviceSettings(deviceUUID: listLogFilesController.uuid)?.deviceName 19 | dirs = fileController.getLogFiles(uuid: listLogFilesController.uuid) 20 | } 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | self.tableView.delegate = self 25 | self.tableView.dataSource = self 26 | } 27 | 28 | override func numberOfSections(in tableView: UITableView) -> Int { 29 | return 2 30 | } 31 | 32 | 33 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 34 | // print(dirs.count) 35 | return (section == 0) ? 1 : dirs.count 36 | } 37 | 38 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 39 | let cell = tableView.dequeueReusableCell(withIdentifier: "shareCell") as? shareLogCell 40 | if cell == nil { 41 | return UITableViewCell() 42 | } 43 | if indexPath.section == 0 { 44 | cell?.setupDeleteCell() 45 | return cell! 46 | } 47 | else { 48 | cell?.filename = dirs[indexPath.row] 49 | cell?.normalizeCell() 50 | let filename = reformatFilename(filename: dirs[indexPath.row]) 51 | cell?.mainLabel.text = filename 52 | let logcount = fileController.logCountForFile(uuid: listLogFilesController.uuid, filename: dirs[indexPath.row]) 53 | cell?.detailLabel.text = "\(logcount) entr" + ((logcount == 1) ? "y" : "ies") 54 | return cell! 55 | } 56 | } 57 | 58 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 59 | if indexPath.section == 0 { 60 | deleteAllLogFiles() 61 | tableView.reloadData() 62 | } 63 | else { 64 | loggingGraphController.filename = dirs[indexPath.row] 65 | } 66 | tableView.deselectRow(at: indexPath, animated: true) 67 | } 68 | 69 | 70 | override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 71 | if indexPath.section == 1 { 72 | let delete = UIContextualAction(style: .destructive, title: "Delete") { (contextualAction, view, boolValue) in 73 | 74 | let alert = UIAlertController(title: "Are you sure?", message: "You are about to delete the log from \(self.reformatFilename(filename: self.dirs[indexPath.row]))", preferredStyle: .alert) 75 | alert.addAction(UIAlertAction(title: "Yes", style: .destructive, handler: { action in 76 | fileController.removeLogfile(UUID: listLogFilesController.uuid, filename: self.dirs[indexPath.row]) 77 | self.dirs = fileController.getLogFiles(uuid: listLogFilesController.uuid) 78 | tableView.reloadData() 79 | self.dismiss(animated: true, completion: nil) 80 | })) 81 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 82 | self.present(alert, animated: true, completion: nil) 83 | } 84 | delete.backgroundColor = .systemRed 85 | let swipeActions = UISwipeActionsConfiguration(actions: [delete]) 86 | return swipeActions 87 | } 88 | return nil 89 | } 90 | 91 | 92 | func reformatFilename(filename: String) -> String { 93 | let dateFormatter = DateFormatter() 94 | dateFormatter.dateFormat = "yyyyMMdd" 95 | let date = dateFormatter.date(from: filename)! 96 | 97 | dateFormatter.timeStyle = .none 98 | dateFormatter.dateStyle = .medium 99 | dateFormatter.locale = Locale.current 100 | 101 | return dateFormatter.string(from: date) 102 | } 103 | 104 | func deleteAllLogFiles() { 105 | let alert = UIAlertController(title: "Are you sure?", message: "You are about to delete all logs from this device", preferredStyle: .alert) 106 | alert.addAction(UIAlertAction(title: "Yes", style: .destructive, handler: { action in 107 | fileController.clearLogDirectoryForDevice(UUID: listLogFilesController.uuid) 108 | self.dismiss(animated: true, completion: nil) 109 | 110 | })) 111 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 112 | self.present(alert, animated: true, completion: nil) 113 | } 114 | 115 | override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { 116 | let cell = sender as? shareLogCell 117 | return cell?.filename != nil 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/loggingCheckboxCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // loggingCheckboxCell.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 19.02.21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import BEMCheckBox 11 | 12 | class loggingCheckboxCell: UITableViewCell { 13 | 14 | @IBOutlet weak var loggingEnabledCheckbox: BEMCheckBox! 15 | 16 | @IBAction func valueChangd(_ sender: BEMCheckBox) { 17 | DevicesController.getConnectedDevice()?.settings.loggingEnabled = loggingEnabledCheckbox.on 18 | DevicesController.getConnectedDevice()?.saveDeviceSettings() 19 | } 20 | 21 | override func layoutSubviews() { 22 | super.layoutSubviews() 23 | self.loggingEnabledCheckbox.on = DevicesController.getConnectedDevice()?.settings.loggingEnabled ?? false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/loggingController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // loggingController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 06.02.21. 6 | // 7 | 8 | import SwiftCSV 9 | 10 | class loggingController { 11 | 12 | static var lastLogTimestamp = Int64(NSDate().timeIntervalSince1970 * 1000) 13 | 14 | static func generateFileHeader() -> String? { 15 | if (cmd_basicInformation.numberOfCells ?? 0) <= 0 { 16 | return nil 17 | } 18 | var header = "timestampUTC;totalVoltage;chargingCurrentA;disChargingCurrentA;remainingCapacityAh;socPercent;" 19 | if (cmd_basicInformation.numberOfTempSensors ?? 0) > 0 { 20 | for i in 1...cmd_basicInformation.numberOfTempSensors! { 21 | header = header + String(format: "t%d;", i) 22 | } 23 | } 24 | for i in 1...cmd_basicInformation.numberOfCells! { 25 | header = header + String(format: "c%d", i) 26 | if i != cmd_basicInformation.numberOfCells { 27 | header = header + ";" 28 | } 29 | } 30 | return header + "\n" 31 | } 32 | 33 | static private func getNewDataLine() -> String? { 34 | if (cmd_basicInformation.numberOfCells ?? 0) <= 0 { 35 | return nil 36 | } 37 | let date = Date() 38 | let formatter = DateFormatter() 39 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 40 | var line = formatter.string(from: date) + ";" 41 | line += BMSData.convertToString(value: cmd_basicInformation.totalVoltage ?? 0) + ";" 42 | line += String(format: "%.2f", BMSData.getChargingCurrent()) + ";" 43 | line += String(format: "%.2f", BMSData.getDischargingCurrent()) + ";" 44 | line += BMSData.convertToString(value: cmd_basicInformation.residualCapacity ?? 0) + ";" 45 | line += String(format: "%d", cmd_basicInformation.rsoc ?? 0) + ";" 46 | 47 | if (cmd_basicInformation.numberOfTempSensors ?? 0) > 0 { 48 | for i in 0...cmd_basicInformation.numberOfTempSensors!-1 { 49 | line += String(format: "%.1f", cmd_basicInformation.temperatureReadings[Int(i)]) + ";" 50 | } 51 | } 52 | for i in 0...cmd_basicInformation.numberOfCells!-1 { 53 | line += String(format: "%.3f", Double(cmd_voltages.voltageOfCell[Int(i)])/1000.0) 54 | if i != cmd_basicInformation.numberOfCells!-1 { 55 | line = line + ";" 56 | } 57 | } 58 | return line + "\n" 59 | } 60 | 61 | static func generateFilename() -> String { 62 | let date = Date() 63 | let formatter = DateFormatter() 64 | formatter.dateFormat = "yyyyMMdd" 65 | formatter.string(from: date) 66 | return formatter.string(from: date) 67 | } 68 | 69 | static func WriteDataLine() { 70 | let newline = getNewDataLine() 71 | if newline != nil { 72 | fileController.writeLogFile(content: newline!) 73 | lastLogTimestamp = Int64(NSDate().timeIntervalSince1970 * 1000) 74 | } 75 | } 76 | 77 | static func shouldLogCurrentEntry() -> Bool { 78 | let settings = DevicesController.getConnectedDevice()?.settings 79 | if settings?.deviceUUID == "demo" { 80 | return false 81 | } 82 | 83 | let enabled = settings?.loggingEnabled ?? false 84 | let interval = getTimestamp() - lastLogTimestamp >= (settings?.loggingInterval ?? 2)*950 //950 because it would not trigger perfectly on 1000 85 | return enabled && interval 86 | } 87 | 88 | static private func getTimestamp() -> Int64 { 89 | return Int64(Date().timeIntervalSince1970*1000) 90 | } 91 | 92 | static func getLogData(uuid: String, filename: String) -> CSV? { 93 | let loggingText = fileController.loadLogFile(uuid: uuid, filename: filename) 94 | 95 | do { 96 | let csv: CSV = try CSV(string: loggingText, delimiter: ";", loadColumns: true) 97 | return csv 98 | } 99 | catch { 100 | print(error) 101 | } 102 | return nil 103 | } 104 | 105 | struct LogEntry { 106 | var timestampUTC: String 107 | var totalVoltage: String 108 | var chargingCurrent: Double 109 | var dischargingCurrent: Double 110 | var remainingCapacityAh: Double 111 | var socPercent: Int 112 | var temperature: [Double] 113 | var cellVoltage: [Double] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/loggingGraphController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // loggingGraphController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 07.04.21. 6 | // 7 | 8 | import ScrollableGraphView 9 | import SwiftCSV 10 | import ActionSheetPicker_3_0 11 | import LGButton 12 | 13 | class loggingGraphController: UIViewController, ScrollableGraphViewDataSource { 14 | 15 | @IBOutlet weak var graph: ScrollableGraphView! 16 | 17 | static var filename: String? 18 | 19 | var data: CSV? 20 | var linePlot = LinePlot(identifier: "line") 21 | 22 | var referenceLines = ReferenceLines() 23 | 24 | var selectedGraph = "totalVoltage" 25 | 26 | var graphOptions = [(String, String)]() 27 | 28 | @IBOutlet weak var graphSelectionButton: LGButton! 29 | 30 | @IBOutlet weak var stepper: UIStepper! 31 | 32 | override func viewDidLoad() { 33 | print("viewDidLoad") 34 | super.viewDidLoad() 35 | 36 | data = loggingController.getLogData(uuid: listLogFilesController.uuid, filename: loggingGraphController.filename ?? "demo") 37 | 38 | 39 | graph.dataSource = self 40 | setupButtonArrays() 41 | updateColours() 42 | 43 | setupGraph(value: "totalVoltage") 44 | } 45 | 46 | override func viewWillAppear(_ animated: Bool) { 47 | print("viewWillAppear") 48 | super.viewWillAppear(animated) 49 | 50 | } 51 | 52 | func setupButtonArrays() { 53 | if data == nil { 54 | return 55 | } 56 | for head in data!.header { 57 | switch head { 58 | case "timestampUTC": 59 | break 60 | case "totalVoltage": 61 | graphOptions.append(("Total voltage", head)) 62 | case "chargingCurrentA": 63 | graphOptions.append(("Charging current", head)) 64 | case "disChargingCurrentA": 65 | graphOptions.append(("Discharging current", head)) 66 | case "remainingCapacityAh": 67 | graphOptions.append(("Remaining capacity (Ah)", head)) 68 | case "socPercent": 69 | graphOptions.append(("State of charge", head)) 70 | default: 71 | if head.hasPrefix("t") { 72 | let number = parseNumber(value: head) 73 | graphOptions.append(("Temperature \(number)", head)) 74 | } else if head.hasPrefix("c") { 75 | let number = parseNumber(value: head) 76 | graphOptions.append(("Cellvoltage \(number)", head)) 77 | } 78 | } 79 | } 80 | } 81 | 82 | func value(forPlot plot: Plot, atIndex pointIndex: Int) -> Double { 83 | switch(plot.identifier) { 84 | case "line": 85 | let value = data?.namedColumns[selectedGraph] 86 | return Double(value?[pointIndex] ?? "0.0") ?? 0.0 87 | default: 88 | return 0 89 | } 90 | } 91 | 92 | func label(atIndex pointIndex: Int) -> String { 93 | return "" 94 | } 95 | 96 | func numberOfPoints() -> Int { 97 | return data?.namedColumns[selectedGraph]?.count ?? 0 98 | } 99 | 100 | func getMinMax(header: String) -> (Double, Double) { 101 | var minimum = Double.infinity 102 | var maximum = 0.0 103 | 104 | if data != nil { 105 | if (data!.namedColumns[header]?.count ?? 0) > 0 { 106 | for i in 0...data!.namedColumns[header]!.count-1 { 107 | let strValue = data!.namedColumns[header]![i] 108 | let dblValue = Double(strValue) ?? 0.0 109 | minimum = min(minimum, dblValue) 110 | maximum = max(maximum, dblValue) 111 | } 112 | } 113 | } 114 | // print("getMinMax: \(minimum*0.95) \(maximum*1.05)") 115 | return (minimum*0.99, maximum*1.01) 116 | } 117 | 118 | @IBAction func didChangeZoom(_ sender: UIStepper) { 119 | // print(sender.value/4.0) 120 | let zoom = CGFloat(sender.value/4.0) 121 | graph.dataPointSpacing = zoom 122 | 123 | graph.contentOffset = calculateContentOffset(zoom: zoom, contentOffset: graph.contentOffset) 124 | graph.updateContentSize() 125 | graph.reload() 126 | } 127 | 128 | func calculateContentOffset(zoom: CGFloat, contentOffset: CGPoint) -> CGPoint { 129 | let result = CGPoint(x: contentOffset.x/zoom, y: 0) 130 | return result 131 | } 132 | 133 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 134 | super.traitCollectionDidChange(previousTraitCollection) 135 | updateColours() 136 | } 137 | 138 | func updateColours() { 139 | if self.traitCollection.userInterfaceStyle == .dark { 140 | } 141 | else { 142 | 143 | } 144 | graph.backgroundFillColor = .systemBackground 145 | referenceLines.referenceLineColor = .label 146 | referenceLines.dataPointLabelColor = .label 147 | referenceLines.referenceLineLabelColor = .label 148 | linePlot.fillGradientStartColor = .systemOrange 149 | linePlot.fillGradientEndColor = UIColor(named: "accentColor") ?? .systemOrange 150 | graph.reload() 151 | } 152 | 153 | func setupGraph(value: String) { 154 | print("setupGraph: \(value)") 155 | selectedGraph = value 156 | let (min, max) = getMinMax(header: selectedGraph) 157 | print(min, max) 158 | graph.rangeMin = min 159 | graph.rangeMax = max 160 | 161 | graph.shouldAnimateOnStartup = false 162 | graph.dataPointSpacing = 4.0 163 | graph.leftmostPointPadding = 0 164 | graph.rightmostPointPadding = 0 165 | graph.topMargin = 50.0 166 | graph.bottomMargin = 20.0 167 | graph.shouldAdaptRange = false 168 | stepper.value = Double(graph.dataPointSpacing*4) 169 | linePlot.lineStyle = .smooth 170 | linePlot.shouldFill = true 171 | linePlot.fillType = .gradient 172 | linePlot.animationDuration = 0.15 173 | // linePlot2.lineStyle = .smooth 174 | // linePlot2.shouldFill = true 175 | // linePlot2.fillType = .gradient 176 | // linePlot2.animationDuration = 0.15 177 | graph.addPlot(plot: linePlot) 178 | // graph.addPlot(plot: linePlot2) 179 | referenceLines.referenceLineNumberOfDecimalPlaces = 2 180 | graph.addReferenceLines(referenceLines: referenceLines) 181 | graph.updateContentSize() 182 | graph.reload() 183 | } 184 | 185 | @IBAction func graphSelectorButton(_ sender: Any) { 186 | 187 | ActionSheetStringPicker.show(withTitle: "Select graph", rows: filter2DArray(array: graphOptions, index: 0), initialSelection: 0, doneBlock: { (picker, indexes, value) in 188 | let newval = value as? String 189 | if newval == nil { 190 | return 191 | } 192 | self.setupGraph(value: self.getHeaderForDescription(newval!)) 193 | self.graphSelectionButton.titleString = newval! 194 | 195 | }, cancel: { (_) in 196 | return 197 | }, origin: sender) 198 | } 199 | 200 | func parseNumber(value: String) -> Int { 201 | let parsed = value.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() 202 | let result = Int(parsed) 203 | return result ?? 0 204 | } 205 | 206 | func filter2DArray(array: [(String, String)], index: Int) -> [String] { 207 | var result = [String]() 208 | 209 | if index == 0 { 210 | for str in array { 211 | result.append(str.0) 212 | } 213 | } 214 | else if index == 1 { 215 | for str in array { 216 | result.append(str.1) 217 | } 218 | } 219 | return result 220 | } 221 | 222 | func getHeaderForDescription(_ description: String) -> String { 223 | for opt in graphOptions { 224 | if opt.0 == description { 225 | return opt.1 226 | } 227 | } 228 | return "" 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/loggingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // loggingViewController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 06.02.21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import BEMCheckBox 11 | import ActionSheetPicker_3_0 12 | 13 | class loggingViewController: UITableViewController { 14 | 15 | var dirs = [(String, Int)]() 16 | 17 | let logIntervalOptions = ["1 second", "2 seconds", "3 seconds", "4 seconds", "5 seconds", "6 seconds", "7 seconds", "8 seconds", "9 seconds", "10 seconds", "11 seconds", "12 seconds", "13 seconds", "14 seconds", "15 seconds", "16 seconds", "17 seconds", "18 seconds", "19 seconds", "20 seconds", "21 seconds", "22 seconds", "23 seconds", "24 seconds", "25 seconds", "26 seconds", "27 seconds", "28 seconds", "29 seconds", "30 seconds", "31 seconds", "32 seconds", "33 seconds", "34 seconds", "35 seconds", "36 seconds", "37 seconds", "38 seconds", "39 seconds", "40 seconds", "41 seconds", "42 seconds", "43 seconds", "44 seconds", "45 seconds", "46 seconds", "47 seconds", "48 seconds", "49 seconds", "50 seconds", "51 seconds", "52 seconds", "53 seconds", "54 seconds", "55 seconds", "56 seconds", "57 seconds", "58 seconds", "59 seconds", "60 seconds", "61 seconds", "62 seconds", "63 seconds", "64 seconds", "65 seconds", "66 seconds", "67 seconds", "68 seconds", "69 seconds", "70 seconds", "71 seconds", "72 seconds", "73 seconds", "74 seconds", "75 seconds", "76 seconds", "77 seconds", "78 seconds", "79 seconds", "80 seconds", "81 seconds", "82 seconds", "83 seconds", "84 seconds", "85 seconds", "86 seconds", "87 seconds", "88 seconds", "89 seconds", "90 seconds", "91 seconds", "92 seconds", "93 seconds", "94 seconds", "95 seconds", "96 seconds", "97 seconds", "98 seconds", "99 seconds", "100 seconds", "101 seconds", "102 seconds", "103 seconds", "104 seconds", "105 seconds", "106 seconds", "107 seconds", "108 seconds", "109 seconds", "110 seconds", "111 seconds", "112 seconds", "113 seconds", "114 seconds", "115 seconds", "116 seconds", "117 seconds", "118 seconds", "119 seconds", "120 seconds", "121 seconds", "122 seconds", "123 seconds", "124 seconds", "125 seconds", "126 seconds", "127 seconds", "128 seconds", "129 seconds", "130 seconds", "131 seconds", "132 seconds", "133 seconds", "134 seconds", "135 seconds", "136 seconds", "137 seconds", "138 seconds", "139 seconds", "140 seconds", "141 seconds", "142 seconds", "143 seconds", "144 seconds", "145 seconds", "146 seconds", "147 seconds", "148 seconds", "149 seconds", "150 seconds", "151 seconds", "152 seconds", "153 seconds", "154 seconds", "155 seconds", "156 seconds", "157 seconds", "158 seconds", "159 seconds", "160 seconds", "161 seconds", "162 seconds", "163 seconds", "164 seconds", "165 seconds", "166 seconds", "167 seconds", "168 seconds", "169 seconds", "170 seconds", "171 seconds", "172 seconds", "173 seconds", "174 seconds", "175 seconds", "176 seconds", "177 seconds", "178 seconds", "179 seconds", "180 seconds", "181 seconds", "182 seconds", "183 seconds", "184 seconds", "185 seconds", "186 seconds", "187 seconds", "188 seconds", "189 seconds", "190 seconds", "191 seconds", "192 seconds", "193 seconds", "194 seconds", "195 seconds", "196 seconds", "197 seconds", "198 seconds", "199 seconds", "200 seconds", "201 seconds", "202 seconds", "203 seconds", "204 seconds", "205 seconds", "206 seconds", "207 seconds", "208 seconds", "209 seconds", "210 seconds", "211 seconds", "212 seconds", "213 seconds", "214 seconds", "215 seconds", "216 seconds", "217 seconds", "218 seconds", "219 seconds", "220 seconds", "221 seconds", "222 seconds", "223 seconds", "224 seconds", "225 seconds", "226 seconds", "227 seconds", "228 seconds", "229 seconds", "230 seconds", "231 seconds", "232 seconds", "233 seconds", "234 seconds", "235 seconds", "236 seconds", "237 seconds", "238 seconds", "239 seconds", "240 seconds", "241 seconds", "242 seconds", "243 seconds", "244 seconds", "245 seconds", "246 seconds", "247 seconds", "248 seconds", "249 seconds", "250 seconds", "251 seconds", "252 seconds", "253 seconds", "254 seconds", "255 seconds", "256 seconds", "257 seconds", "258 seconds", "259 seconds", "260 seconds", "261 seconds", "262 seconds", "263 seconds", "264 seconds", "265 seconds", "266 seconds", "267 seconds", "268 seconds", "269 seconds", "270 seconds", "271 seconds", "272 seconds", "273 seconds", "274 seconds", "275 seconds", "276 seconds", "277 seconds", "278 seconds", "279 seconds", "280 seconds", "281 seconds", "282 seconds", "283 seconds", "284 seconds", "285 seconds", "286 seconds", "287 seconds", "288 seconds", "289 seconds", "290 seconds", "291 seconds", "292 seconds", "293 seconds", "294 seconds", "295 seconds", "296 seconds", "297 seconds", "298 seconds", "299 seconds", "300 seconds"] 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | self.tableView.dataSource = self 22 | self.tableView.delegate = self 23 | } 24 | 25 | override func viewWillAppear(_ animated: Bool) { 26 | super.viewWillAppear(animated) 27 | 28 | dirs = fileController.countLogDirectories() 29 | self.tableView.reloadData() 30 | } 31 | 32 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 33 | switch section { 34 | case 0: 35 | return "Settings" 36 | case 1: 37 | return "Logs for other devices" 38 | default: 39 | return "" 40 | } 41 | } 42 | 43 | override func numberOfSections(in tableView: UITableView) -> Int { 44 | let deviceUUID = DevicesController.getConnectedDevice()?.getIdentifier() 45 | if dirs.count > 0 { 46 | for i in 0...dirs.count-1 { 47 | if dirs[i].0 != deviceUUID { 48 | return 2 49 | } 50 | } 51 | } 52 | return 1 53 | } 54 | 55 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 56 | switch section { 57 | case 0: 58 | return 3 59 | case 1: 60 | if dirs.count == 0 { 61 | return 0 62 | } 63 | var count = 0 64 | let deviceUUID = DevicesController.getConnectedDevice()?.getIdentifier() ?? "demo" 65 | for i in 0...dirs.count-1 { 66 | if dirs[i].0 != deviceUUID { 67 | count += 1 68 | } 69 | } 70 | return count 71 | default: 72 | return 0 73 | } 74 | } 75 | 76 | 77 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 78 | if indexPath.section == 0 { 79 | switch indexPath.row { 80 | case 0: 81 | let cell = tableView.dequeueReusableCell(withIdentifier: "loggingCheckboxCell") as? loggingCheckboxCell 82 | if cell == nil { 83 | return UITableViewCell() 84 | } 85 | return cell! 86 | case 1: 87 | let cell = tableView.dequeueReusableCell(withIdentifier: "rightDetail") as? rightDetailCell 88 | if cell == nil { 89 | return UITableViewCell() 90 | } 91 | cell!.mainLabel.text = "Log interval" 92 | cell!.detailLabel.text = getIntervalDescription() 93 | return cell! 94 | case 2: 95 | let cell = tableView.dequeueReusableCell(withIdentifier: "rightDetail") as? rightDetailCell 96 | if cell == nil { 97 | return UITableViewCell() 98 | } 99 | cell!.mainLabel.text = "View logs" 100 | let count = getFileCount(for: DevicesController.getConnectedDevice()?.getIdentifier() ?? "demo") 101 | cell!.detailLabel.text = "\(count) item" + ((count == 1) ? "" : "s") 102 | if count == 0 { 103 | cell?.mainLabel.textColor = .secondaryLabel 104 | cell?.accessoryType = .none 105 | cell?.isUserInteractionEnabled = false 106 | cell?.allowSegue = false 107 | } 108 | else { 109 | cell?.mainLabel.textColor = .label 110 | cell?.accessoryType = .disclosureIndicator 111 | cell?.isUserInteractionEnabled = true 112 | cell?.allowSegue = true 113 | } 114 | return cell! 115 | default: 116 | return UITableViewCell() 117 | } 118 | } else if indexPath.section == 1 { 119 | let cell = tableView.dequeueReusableCell(withIdentifier: "rightDetail") as? rightDetailCell 120 | if cell == nil { 121 | return UITableViewCell() 122 | } 123 | let otherdevice = getOtherDeviceUUIDAndFileCount(index: indexPath.row) 124 | let devicesettings = fileController.getDeviceSettings(deviceUUID: otherdevice.0) 125 | cell!.mainLabel.text = devicesettings?.deviceName ?? ((devicesettings?.dongleName ?? "") + "(\(devicesettings?.cellCount ?? 0)S)") 126 | cell!.detailLabel.text = "\(otherdevice.1) item" + (otherdevice.1 == 1 ? "" : "s") 127 | cell?.allowSegue = true 128 | return cell! 129 | } else { 130 | return UITableViewCell() 131 | } 132 | } 133 | 134 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 135 | if indexPath.section == 0 && indexPath.row == 2 { 136 | listLogFilesController.uuid = DevicesController.getConnectedDevice()?.getIdentifier() ?? "demo" 137 | } else if indexPath.section == 1 { 138 | listLogFilesController.uuid = getOtherDeviceUUIDAndFileCount(index: indexPath.row).0 139 | } else if indexPath.section == 0 && indexPath.row == 1 { 140 | let defaultInterval = DevicesController.getConnectedDevice()?.settings.loggingInterval ?? 2 141 | ActionSheetStringPicker.show(withTitle: "Select log interval", rows: logIntervalOptions, initialSelection: defaultInterval-1, doneBlock: { (picker, indexes, value) in 142 | let newval = value as? String 143 | if newval == nil { 144 | return 145 | } 146 | let parsed = newval!.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() 147 | let seconds = Int(parsed) 148 | DevicesController.getConnectedDevice()?.settings.loggingInterval = seconds 149 | DevicesController.getConnectedDevice()?.saveDeviceSettings() 150 | self.tableView.reloadData() 151 | }, cancel: { (_) in 152 | return 153 | }, origin: tableView.cellForRow(at: indexPath)) 154 | return 155 | } else { 156 | return 157 | } 158 | 159 | } 160 | 161 | func getIntervalDescription() -> String { 162 | let logInterval = DevicesController.getConnectedDevice()?.settings.loggingInterval ?? 2 163 | 164 | if logInterval > 59 { 165 | let minutes = Int(floor(Double(logInterval)/60.0)) 166 | let seconds = logInterval % 60 167 | if seconds == 0 { 168 | return "\(minutes)m" 169 | } 170 | return "\(minutes)m, \(seconds)s" 171 | } 172 | return "\(logInterval)s" 173 | } 174 | 175 | func getFileCount(for uuid: String) -> Int { 176 | if dirs.count > 0 { 177 | for i in 0...dirs.count-1 { 178 | if dirs[i].0 == uuid { 179 | return dirs[i].1 180 | } 181 | } 182 | } 183 | return 0 184 | } 185 | 186 | func getOtherDeviceUUIDAndFileCount(index: Int) -> (String, Int) { 187 | let deviceUUID = DevicesController.getConnectedDevice()?.getIdentifier() ?? "demo" 188 | var otherDevices = dirs 189 | if otherDevices.count > 0 { 190 | for i in 0...otherDevices.count-1 { 191 | if otherDevices[i].0 == deviceUUID { 192 | otherDevices.remove(at: i) 193 | break 194 | } 195 | } 196 | } 197 | return otherDevices[index] 198 | } 199 | 200 | override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { 201 | if let cell = sender as? loggingCheckboxCell { 202 | return false 203 | } 204 | else if let cell = sender as? rightDetailCell { 205 | return cell.allowSegue 206 | } 207 | return false 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/moreController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // moreController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 29.11.20. 6 | // 7 | 8 | import UIKit 9 | import BEMCheckBox 10 | 11 | class moreController: UITableViewController { 12 | 13 | @IBOutlet weak var autoConnectCheckbox: BEMCheckBox! 14 | 15 | 16 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 17 | if indexPath.row == 3 && indexPath.section == 0 { 18 | let device = DevicesController.getConnectedDevice()! 19 | device.peakPower = 0.0 20 | device.peakCurrent = 0.0 21 | device.lowestVoltage = 0.0 22 | device.highestVoltage = 0.0 23 | GPSController.topSpeed = 0.0 24 | GPSController.currentSpeed = 0.0 25 | GPSController.efficiency = 0.0 26 | tableView.cellForRow(at: indexPath)?.setSelected(false, animated: true) 27 | } 28 | else if indexPath.row == 1 && indexPath.section == 0 { 29 | self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: nil, action: nil) 30 | } 31 | } 32 | 33 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 34 | if DevicesController.connectionMode == .demo && indexPath.row <= 1 && indexPath.section == 0 { 35 | return 0 36 | } 37 | return super.tableView(tableView, heightForRowAt: indexPath) 38 | } 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | autoConnectCheckbox.on = DevicesController.getConnectedDevice()?.settings.autoConnect ?? false 43 | print("moreController: viewDidLoad(): \(autoConnectCheckbox.on)") 44 | } 45 | 46 | @IBAction func autoconnectChanged(_ sender: BEMCheckBox) { 47 | DevicesController.getConnectedDevice()?.settings.autoConnect = sender.on 48 | DevicesController.getConnectedDevice()?.saveDeviceSettings() 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/rightDetailCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // otherDeviceLogCell.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 19.02.21. 6 | // 7 | import Foundation 8 | import UIKit 9 | 10 | class rightDetailCell: UITableViewCell { 11 | 12 | @IBOutlet weak var mainLabel: UILabel! 13 | @IBOutlet weak var detailLabel: UILabel! 14 | 15 | var allowSegue = false 16 | } 17 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/sensorRenameCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // sensorRenameCell.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 12.11.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class sensorRenameCell: UITableViewCell { 11 | 12 | var index = 0 13 | 14 | @IBOutlet var sensorNameLabel: UILabel! 15 | @IBOutlet var sensorNameTextField: UITextField! 16 | 17 | @IBAction func editingDidEnd(_ sender: UITextField) { 18 | DispatchQueue.main.async { 19 | if self.sensorNameTextField.text == nil || self.sensorNameTextField.text == "" { 20 | DevicesController.getConnectedDevice()?.settings.sensorNames[self.index] = nil 21 | } 22 | else { 23 | DevicesController.getConnectedDevice()?.settings.sensorNames[self.index] = self.sensorNameTextField.text 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/sensorRenameController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // sensorRenameController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 12.11.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class sensorRenameController: UITableViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | self.tableView.delegate = self 15 | self.tableView.dataSource = self 16 | } 17 | 18 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 19 | let cell = tableView.dequeueReusableCell(withIdentifier: "sensorRenameCell", for: indexPath) as! sensorRenameCell 20 | cell.index = indexPath.row 21 | cell.sensorNameLabel.text = "Sensor \(indexPath.row+1):" 22 | cell.sensorNameTextField.text = returnTextFieldName(index: indexPath.row) 23 | 24 | 25 | return cell 26 | } 27 | 28 | override func viewDidDisappear(_ animated: Bool) { 29 | super.viewDidDisappear(animated) 30 | DevicesController.getConnectedDevice()?.saveDeviceSettings() 31 | } 32 | 33 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 34 | return Int(cmd_basicInformation.numberOfTempSensors ?? 0) 35 | } 36 | 37 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 38 | return 45 39 | } 40 | 41 | func returnTextFieldName(index: Int) -> String { 42 | return DevicesController.getConnectedDevice()?.settings.getSensorName(index: index) ?? "Temperature \(index+1)" 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/shareLogCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // shareLogCell.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 19.02.21. 6 | // 7 | 8 | import UIKit 9 | class shareLogCell: UITableViewCell { 10 | 11 | @IBOutlet weak var mainLabel: UILabel! 12 | @IBOutlet weak var detailLabel: UILabel! 13 | 14 | 15 | @IBOutlet weak var btShare: UIButton! 16 | 17 | var filename: String? 18 | 19 | @IBAction func shareButton(_ sender: Any) { 20 | if filename == nil || filename == "" { 21 | return 22 | } 23 | let filepath = NSURL(fileURLWithPath: fileController.getLogFileDirectory(UUID: listLogFilesController.uuid, filename: filename ?? "")) 24 | 25 | let activityViewController = UIActivityViewController(activityItems: [filepath], applicationActivities: nil) 26 | self.window?.rootViewController?.present(activityViewController, animated: true, completion: nil) 27 | } 28 | 29 | func setupDeleteCell() { 30 | self.mainLabel.textColor = .red 31 | self.mainLabel.text = "Clear logs" 32 | self.detailLabel.isHidden = true 33 | self.btShare.isHidden = true 34 | self.accessoryType = .none 35 | } 36 | 37 | func normalizeCell() { 38 | self.mainLabel.textColor = .label 39 | self.detailLabel.isHidden = false 40 | self.btShare.isHidden = false 41 | self.accessoryType = .disclosureIndicator 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Smart BMS Utility/Smart BMS Utility/tabBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // tabBarController.swift 3 | // Smart BMS Utility 4 | // 5 | // Created by Justin Kühner on 07.11.20. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | class tabBarController: UITabBarController, UITabBarControllerDelegate { 12 | 13 | override func viewWillAppear(_ animated: Bool) { 14 | super.viewWillAppear(animated) 15 | self.delegate = self 16 | self.moreNavigationController.navigationBar.isHidden = true 17 | traitCollectionDidChange(nil) 18 | } 19 | 20 | override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { 21 | self.title = item.title 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.5.0 4 | 5 | - Some translations have been revised 6 | - The logging graph has been expanded to include zoom and scroll functions 7 | - Pop-up dialogs have been adapted to the respective platform 8 | - Time format is now adopted by the system 9 | - Performance optimization for logging has been carried out 10 | - Navigation between the tabs has been adjusted 11 | 12 | ## v3.4.2 13 | 14 | JBD: 15 | - Added option to create and remove a bluetooth password 16 | - You can now read and write the configuration when a battery is protected by a bluetooth password 17 | - Changing the password does not cause an error when connecting the next time 18 | - Added option to change max. speed of tachometer 19 | - SOC is now displayed when using serial and parallel connections in multi device view 20 | 21 | ## v3.4.0, v3.4.1 22 | - Updated the UI in some places 23 | - Fix export of CSV files 24 | - You can now trigger notifications and actions based on cellvoltage delta 25 | 26 | JBD: 27 | - We now support password protected bluetooth dongles! 28 | - Implement Calibration (Current, Voltage) 29 | 30 | Daly: 31 | - Increased the maximum value for rated capacity 32 | 33 | ## v3.3.1 [Android only] 34 | - Fix crash on startup 35 | 36 | ## v3.3.0 37 | - Added option to set up notifications and actions once certain events occur 38 | - Fix opening contact form and other sites on android 39 | - Improved logging view 40 | - JBD: 41 | - Fix writing certain configuration parameters that depend on others 42 | 43 | ## v3.2.4 44 | - Exporting files under MacOS fixed 45 | - Daly: 46 | - Checking password has been fixed 47 | 48 | ## v3.2.3 49 | - Fixed some bugs with setting a bms password 50 | - Daly: 51 | - Added unfiltered search to find all dongles and support more bms 52 | 53 | ## v3.2.2 54 | 55 | - Added Chinese and Italian to the supported languages. 56 | - JBD: 57 | - Fixed read configuration for some special batteries. 58 | 59 | ## v3.2.0 60 | 61 | - Configuration profiles added. You can export and import them to get support from your dealer or share them with your friends! 62 | - The version number of the app is now visible on the information page. 63 | - More logging information has been added so we can better help you when you contact us! 64 | - JBD: 65 | - Added compatibility for newer BMS versions 66 | - More crash safety! 67 | - Daly: 68 | - Progress bar will be displayed when writing configuration 69 | - The switch-off delay is now calculated correctly 70 | 71 | ## v3.1.2 72 | 73 | - Devices will now reconnect after connection has been lost 74 | - JBD: Added support for older bluetooth dongles 75 | 76 | ## v3.1.1 77 | 78 | - Multiview scrollable 79 | - Open Device from MultiDeviceView 80 | - Battery Display in overview can be switched between old and new style by clicking 81 | - Daly: Refactored the configuration dropdowns 82 | - JBD: Advanced Protection configuration added 83 | - JBD: Disable configuration editing until first read 84 | 85 | ## v3.1.0 86 | 87 | - We now support the Daly BMS with all the available features! 88 | - New UI to display the battery capacity 89 | - In the Multiconnection screen you have a section to display summed up values 90 | - reorganized app settings 91 | - Changes for JBD: 92 | - display the bms error counts 93 | - reset the bms errors 94 | 95 | ## v3.0.4 96 | 97 | - Added debugging log 98 | - Changed how files are exported / shared 99 | - [iOS + MacOS] Added option to enable background operation 100 | - Minor UI adjustments 101 | - Fixed "Discharge Overcurrent Trigger" value offset by 10 mA 102 | - [Windows] reduced connection times 103 | - Fix Auto-Connect when a device is already connected 104 | 105 | ## v3.0.3 106 | 107 | - Fix wrong keyboard type on device filter 108 | - Minor UI layout and text adjustments 109 | - Added help texts in Configuration View 110 | 111 | ## v3.0.2 112 | 113 | - Added option to filter for BMS names 114 | - Fixed crash on Android 12 115 | 116 | ## v3.0.1 117 | 118 | - Fix estimated Range in GPS View 119 | - Fix text breaking in Overview 120 | - Minor visual improvements 121 | - Added alert when connection to BMS takes longer than expected 122 | - Display warnings and errors from BMS (Overcurrent etc.) 123 | - Added Export Logging Data to CSV file 124 | 125 | ## v3.0.0 126 | 127 | - Rewritten the App in Flutter 128 | - Available for Android, iOS, MacOS and Windows 129 | 130 | ## v2.0.1 131 | 132 | - added back option to export log data to csv 133 | - added back gps view including range calculations 134 | 135 | ## v2.0.0 136 | 137 | - complete recode in SwiftUI 138 | - Added Logging using SwiftUI's new Chart library 139 | - enhanced password features 140 | 141 | ## v1.2.2 142 | 143 | - Fixed charging/discharging buttons sending wrong states after first launch 144 | - Added "pull down to refresh" on the devices tab to clear the list and search for new devices 145 | - Added a graph view for logging (experimental, just a interim solution) 146 | - fixed red texts in the logging files list 147 | - Auto connect to device 148 | - Fixed negative values on GPS page 149 | 150 | ## v1.2.1 151 | 152 | - Fixed some minor crashes 153 | - Fixed capacity settings and overvoltage protection values over 65530 154 | - Added logic that detects when certain features are unavailable and blocks them from beeing used 155 | 156 | ## v1.2.0 157 | 158 | - Added warning for LionTron users 159 | - Option to log BMS data 160 | - Reworked persistent storage backend 161 | - Added "Rename device" in "more" tab, also available in the welcome screen by swiping to the left on a device 162 | - Removed "about & credits" in settings menu 163 | - Fixed charging/discharging button getting stuck 164 | - Fixed broken bluetooth connection after slightly swiping to the right 165 | - Write Data button stays visible after switching tabs 166 | - Added option to log data in background 167 | - Fixed empty devicelist after disconnecting 168 | - improved reconnecting 169 | - Min/Max values are now device specific 170 | - Fixed rare crash on checksum validation 171 | 172 | ## v1.1.1 173 | 174 | - Fixed crashes when higher currents are beeing drawn 175 | - Exit condition for reading and writing data in the configuration page 176 | 177 | ## v1.1.0 178 | 179 | - Added configuration of your bms 180 | - Fixed charge- and runtime calculation 181 | - Smaller design changes 182 | - Inverted current reporting, charging is positive, discharging negative 183 | 184 | ## v1.0.2 185 | 186 | - Fixed crashing when the BMS offered more than 3 temperature sensors 187 | - Fixed crashing when the bluetooth connection dropped 188 | 189 | ## v1.0.1 Release 190 | -------------------------------------------------------------------------------- /compatibility.md: -------------------------------------------------------------------------------- 1 | Please help us maintain this list! Edit this file and make a pull request, or tell us your experience via email or [contact form](https://smartbmsutility.com/contact/)! 2 | 3 | | Battery Manufacturer | BMS used | Compatible? | 4 | |----------------------|----------|-------------------------------------| 5 | | LionTron | JBD | Yes | 6 | | Bulltron | Daly | Yes (may require unfiltered search) | 7 | | Forster Smart Power | JBD? | Yes | 8 | | Vatrer Power | ? | Yes | 9 | | AG Lithium | ? | No | 10 | | LiTime | ? | No | 11 | | Epoch Batteries | ? | ? | 12 | | Robur | ? | Yes | 13 | | PowerQueen | ? | ? | 14 | -------------------------------------------------------------------------------- /img/Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KG-Development/SmartBMSUtility/e8036e548eaf9b2d8eded45409b2beecd9e10342/img/Banner.png -------------------------------------------------------------------------------- /img/app-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KG-Development/SmartBMSUtility/e8036e548eaf9b2d8eded45409b2beecd9e10342/img/app-store.png -------------------------------------------------------------------------------- /img/google-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KG-Development/SmartBMSUtility/e8036e548eaf9b2d8eded45409b2beecd9e10342/img/google-play.png -------------------------------------------------------------------------------- /img/windows-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KG-Development/SmartBMSUtility/e8036e548eaf9b2d8eded45409b2beecd9e10342/img/windows-banner.png --------------------------------------------------------------------------------