├── .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 |
5 |
6 |
7 |
8 | 
9 | 
10 | 
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 | | [
](https://play.google.com/store/apps/details?id=com.nearix.smart_bms_utility) | [
](https://apps.apple.com/de/app/apple-store/id1540178292) | [
](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
--------------------------------------------------------------------------------