├── .gitignore ├── Makefile ├── .github └── workflows │ └── swift.yml ├── Package.swift ├── LICENSE ├── Sources └── bclm_loop │ ├── persist.swift │ ├── main.swift │ └── SMC.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .swiftpm 3 | /.build 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | Package.resolved 8 | 9 | # Release files 10 | config.json 11 | bclm_loop.zip 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prefix ?= /usr/local 2 | bindir = $(prefix)/bin 3 | 4 | build: 5 | swift build -c release --disable-sandbox --arch arm64 6 | strip .build/release/bclm_loop 7 | 8 | install: build 9 | mkdir -p "$(bindir)" 10 | install ".build/release/bclm_loop" "$(bindir)" 11 | 12 | uninstall: 13 | rm -rf "$(bindir)/bclm_loop" 14 | 15 | clean: 16 | rm -rf .build 17 | 18 | .PHONY: build install uninstall clean 19 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Build 17 | run: make build 18 | 19 | - uses: actions/upload-artifact@v4 20 | with: 21 | name: bclm_loop 22 | path: .build/release/bclm_loop 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "bclm_loop", 8 | dependencies: [ 9 | .package(url: "https://github.com/apple/swift-argument-parser", from: "0.1.0") 10 | ], 11 | targets: [ 12 | .target( 13 | name: "bclm_loop", 14 | dependencies: [ 15 | .product(name: "ArgumentParser", package: "swift-argument-parser") 16 | ]), 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 lslqtz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/bclm_loop/persist.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let launchctl = "/bin/launchctl" 4 | let plist = "com.lslqtz.bclm_loop.plist" 5 | let plist_path = "/Library/LaunchDaemons/\(plist)" 6 | 7 | struct Preferences: Codable { 8 | var Label: String 9 | var RunAtLoad: Bool 10 | var KeepAlive: Bool 11 | var ProgramArguments: [String] 12 | } 13 | 14 | func persist(_ enable: Bool) -> Bool { 15 | if isPersistent() && enable { 16 | return false 17 | } 18 | if !isPersistent() && !enable { 19 | return false 20 | } 21 | 22 | let process = Process() 23 | let pipe = Pipe() 24 | 25 | var load: String 26 | if enable { 27 | load = "load" 28 | } else { 29 | load = "unload" 30 | } 31 | 32 | process.launchPath = launchctl 33 | process.arguments = [load, plist_path] 34 | process.standardOutput = pipe 35 | process.standardError = pipe 36 | 37 | process.launch() 38 | 39 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 40 | let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) 41 | 42 | if output != nil && !output!.isEmpty { 43 | print(output!) 44 | } 45 | 46 | return true 47 | } 48 | 49 | func isPersistent() -> Bool { 50 | let process = Process() 51 | let pipe = Pipe() 52 | 53 | process.launchPath = launchctl 54 | process.arguments = ["list"] 55 | process.standardOutput = pipe 56 | process.standardError = pipe 57 | 58 | process.launch() 59 | 60 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 61 | let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) 62 | 63 | if output != nil && output!.contains(plist) { 64 | return true 65 | } else { 66 | return false 67 | } 68 | } 69 | 70 | func updatePlist(targetBatteryLevel: Int, targetBatteryMargin: Int) -> Bool { 71 | let preferences = 72 | Preferences( 73 | Label: plist, 74 | RunAtLoad: true, 75 | KeepAlive: true, 76 | ProgramArguments: [ 77 | Bundle.main.executablePath! as String, 78 | "loop", 79 | String(targetBatteryLevel), 80 | String(targetBatteryMargin), 81 | ] 82 | ) 83 | 84 | let path = URL(fileURLWithPath: plist_path) 85 | 86 | let encoder = PropertyListEncoder() 87 | encoder.outputFormat = .xml 88 | 89 | do { 90 | let data = try encoder.encode(preferences) 91 | try data.write(to: path) 92 | return true 93 | } catch { 94 | print(error) 95 | } 96 | 97 | return false 98 | } 99 | 100 | func removePlist() -> Bool { 101 | do { 102 | try FileManager.default.removeItem(at: URL(fileURLWithPath: plist_path)) 103 | return true 104 | } catch { 105 | print(error) 106 | } 107 | 108 | return false 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BCLM_Loop 2 | 3 | bclm_loop is a background looping utility that maintains the battery level of Apple Silicon based Mac computers. This project was inspired by several battery management solutions, including Apple's own battery health management. 4 | 5 | The purpose of limiting the battery's max charge is to prolong battery health and to prevent damage to the battery. Various sources show that the optimal charge range for operation of lithium-ion batteries is between 40% and 80%, commonly referred to as the 40-80 rule [[1]](https://www.apple.com/batteries/why-lithium-ion/)[[2]](https://www.eeworldonline.com/why-you-should-stop-fully-charging-your-smartphone-now/)[[3]](https://www.csmonitor.com/Technology/Tech/2014/0103/40-80-rule-New-tip-for-extending-battery-life). This project is especially helpful to people who leave their Macs on the charger all day, every day. 6 | 7 | To use it, Apple Optimized Battery Charging must be turned off. It will first try to use firmware based battery level limits on supported firmware. 8 | 9 | If the current battery level is higher than the target battery level, please manually discharge it to the target battery level or lower, otherwise it may only stay at the current battery level. This tool does not implement forced discharge because it may make the system unable to recognize the power adapter status. 10 | 11 | When the battery is no longer charging (for any reason, including but not limited to reaching the target battery level or insufficient power from the power adapter), MagSafe LED will turn green, which may be inconsistent with system behavior (which only turn green when fully charged). 12 | 13 | It only supports Apple Silicon based Mac computers. macOS 26 and above may not be supported! 14 | 15 | This project was forked from upstream (https://github.com/zackelia/bclm). 16 | 17 | ## Installation 18 | 19 | ### Brew 20 | 21 | ``` 22 | $ brew tap lslqtz/formulae 23 | $ brew install bclm_loop 24 | ``` 25 | 26 | ### From Source 27 | 28 | ``` 29 | $ make build 30 | $ sudo make install 31 | ``` 32 | 33 | ### From Releases 34 | 35 | ``` 36 | $ unzip bclm_loop.zip 37 | $ sudo mkdir -p /usr/local/bin 38 | $ sudo cp bclm_loop /usr/local/bin/bclm_loop 39 | $ sudo xattr -c /usr/local/bin/bclm_loop 40 | $ sudo chmod +x /usr/local/bin/bclm_loop 41 | ``` 42 | 43 | ## Usage 44 | 45 | ``` 46 | $ bclm_loop 47 | OVERVIEW: Battery Charge Level Max (BCLM) Utility. 48 | 49 | USAGE: bclm_loop 50 | 51 | OPTIONS: 52 | --version Show the version. 53 | -h, --help Show help information. 54 | 55 | SUBCOMMANDS: 56 | loop Loop bclm on target battery level (Default: 80%). 57 | persist Persists bclm loop service. 58 | unpersist Unpersists bclm loop service. 59 | 60 | See 'bclm_loop help ' for detailed help. 61 | ``` 62 | 63 | The program must be run as root. 64 | 65 | ## Migrate 66 | 67 | If you are migrating from upstream bclm or older version (ver < 1.0) of bclm_loop. 68 | 69 | ``` 70 | $ sudo bclm unpersist 71 | $ sudo rm -f /Library/LaunchDaemon/com.zackelia.bclm_loop.plist 72 | ``` 73 | 74 | ## Persistence 75 | 76 | It can run in the background to maintain battery levels. This command will create a new plist in `/Library/LaunchDaemons` and load it via `launchctl`. 77 | 78 | ``` 79 | $ sudo bclm_loop persist 80 | ``` 81 | 82 | Likewise, this command can also unpersist by unloading the service and removing the plist. 83 | 84 | ``` 85 | $ sudo bclm_loop unpersist 86 | ``` 87 | -------------------------------------------------------------------------------- /Sources/bclm_loop/main.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | import IOKit.ps 4 | import IOKit.pwr_mgt 5 | 6 | var defaultTargetBatteryLevel = 80 7 | var defaultBatteryLevelMargin = 5 8 | var targetBatteryLevelRange = [5, 95] 9 | var targetBatteryMarginRange = [2, 30] 10 | var chargeNowFilePath = "/tmp/bclm_loop.chargeNow" 11 | var chargeNowFileCreationTimeMaxInterval: Int64 = 12 12 | var chargeNow = false 13 | 14 | var operateMode = 0 15 | var isMagSafeSupported = false 16 | 17 | var chwa_key = SMCKit.getKey("CHWA", type: DataTypes.UInt8) // Removed in macOS Sequoia. 18 | var ch0b_key = SMCKit.getKey("CH0B", type: DataTypes.UInt8) 19 | var ch0c_key = SMCKit.getKey("CH0C", type: DataTypes.UInt8) 20 | var ch0i_key = SMCKit.getKey("CH0I", type: DataTypes.UInt8) 21 | var chte_key = SMCKit.getKey("CHTE", type: DataTypes.UInt32) // Added in macOS Tahoe. 22 | var ch0j_key = SMCKit.getKey("CH0J", type: DataTypes.UInt8) // Added in macOS Tahoe. 23 | var aclc_key = SMCKit.getKey("ACLC", type: DataTypes.UInt8) 24 | 25 | var chwa_bytes_unlimit: SMCBytes = ( 26 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 27 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 28 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 29 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 30 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 31 | ) 32 | var chwa_bytes_limit: SMCBytes = ( 33 | UInt8(1), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 34 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 35 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 36 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 37 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 38 | ) 39 | var chte_bytes_unlimit: SMCBytes = ( 40 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 41 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 42 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 43 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 44 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 45 | ) 46 | var chte_bytes_limit: SMCBytes = ( 47 | UInt8(1), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 48 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 49 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 50 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 51 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 52 | ) 53 | var ch0x_bytes_unlimit: SMCBytes = ( 54 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 55 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 56 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 57 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 58 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 59 | ) 60 | var ch0x_bytes_limit: SMCBytes = ( 61 | UInt8(2), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 62 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 63 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 64 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 65 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 66 | ) 67 | var ch0i_bytes_charge: SMCBytes = ( 68 | UInt8(1), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 69 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 70 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 71 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 72 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 73 | ) 74 | var ch0i_bytes_discharge: SMCBytes = ( 75 | UInt8(1), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 76 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 77 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 78 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 79 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 80 | ) 81 | var ch0j_bytes_charge: SMCBytes = ( 82 | UInt8(1), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 83 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 84 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 85 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 86 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 87 | ) 88 | var ch0j_bytes_discharge: SMCBytes = ( 89 | UInt8(1), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 90 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 91 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 92 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 93 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 94 | ) 95 | var aclc_bytes_green: SMCBytes = ( 96 | UInt8(3), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 97 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 98 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 99 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 100 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 101 | ) 102 | var aclc_bytes_red: SMCBytes = ( 103 | UInt8(4), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 104 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 105 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 106 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 107 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 108 | ) 109 | var aclc_bytes_disable: SMCBytes = ( 110 | UInt8(1), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 111 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 112 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 113 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 114 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 115 | ) 116 | var aclc_bytes_unknown: SMCBytes = ( 117 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 118 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 119 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 120 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 121 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 122 | ) 123 | 124 | func CheckPermission() throws { 125 | guard getuid() == 0 else { 126 | throw ValidationError("Must run as root.") 127 | } 128 | } 129 | 130 | func CheckPlatform() throws { 131 | #if arch(x86_64) 132 | throw ValidationError("Only support Apple Silicon.") 133 | #endif 134 | } 135 | 136 | func CheckTargetBatteryLevel(targetBatteryLevel: Int) throws { 137 | guard targetBatteryLevel >= targetBatteryLevelRange[0] && targetBatteryLevel <= targetBatteryLevelRange[1] else { 138 | throw ValidationError("Value must be between \(targetBatteryLevelRange[0]) and \(targetBatteryLevelRange[1]).") 139 | } 140 | } 141 | 142 | func CheckTargetBatteryMargin(targetBatteryLevel: Int, targetBatteryMargin: Int) throws { 143 | guard targetBatteryMargin >= targetBatteryMarginRange[0] && targetBatteryMargin <= targetBatteryMarginRange[1] else { 144 | throw ValidationError("Value must be between \(targetBatteryMarginRange[0]) and \(targetBatteryMarginRange[1]).") 145 | } 146 | 147 | guard (targetBatteryLevel - targetBatteryMargin) >= targetBatteryLevelRange[0] else { 148 | throw ValidationError("The value (\(targetBatteryLevel)-\(targetBatteryMargin)) must be greater than or equal to \(targetBatteryLevelRange[0]).") 149 | } 150 | } 151 | 152 | func GetCurrentTimestamp() -> Int64 { 153 | return Int64(ceil(Date().timeIntervalSince1970)) 154 | } 155 | 156 | func CheckChargeNowFile() -> Bool { 157 | do { 158 | let chargeNowFileContent = try String(contentsOfFile: chargeNowFilePath, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) 159 | try FileManager.default.removeItem(atPath: chargeNowFilePath) 160 | 161 | let chargeNowFileCreationTimestamp = Int64(chargeNowFileContent) 162 | let currentTimestamp = GetCurrentTimestamp() 163 | if chargeNowFileCreationTimestamp != nil && (chargeNowFileCreationTimestamp! + chargeNowFileCreationTimeMaxInterval) >= currentTimestamp { 164 | return true 165 | } 166 | 167 | print("Bad chargeNow file content. (chargeNowFileContent: \(chargeNowFileContent), chargeNowFileCreationTimestamp: \(chargeNowFileCreationTimestamp != nil ? String(chargeNowFileCreationTimestamp!) : "nil"), currentTimestamp: \(String(currentTimestamp)))") 168 | } catch { 169 | let realError = error as NSError 170 | if realError.code != NSFileReadNoSuchFileError { 171 | print(realError.localizedDescription) 172 | } 173 | } 174 | 175 | return false 176 | } 177 | 178 | func SetChargeNowFile(status: Bool) -> Bool { 179 | try? FileManager.default.removeItem(atPath: chargeNowFilePath) 180 | 181 | if !status || FileManager.default.createFile(atPath: chargeNowFilePath, contents: String(GetCurrentTimestamp()).data(using: .utf8)) { 182 | return true 183 | } 184 | 185 | return false 186 | } 187 | 188 | func AllowChargeNow(status: Bool) -> Bool { 189 | if !chargeNow && status { 190 | chargeNow = true 191 | } else if chargeNow && !status { 192 | chargeNow = false 193 | } else { 194 | return false 195 | } 196 | 197 | return true 198 | } 199 | 200 | struct BCLMLoop: ParsableCommand { 201 | static let configuration = CommandConfiguration( 202 | commandName: "bclm_loop", 203 | abstract: "Battery Charge Level Max Loop (BCLM_Loop) Utility.", 204 | version: "1.0b9", 205 | subcommands: [Loop.self, ChargeNow.self, Persist.self, Unpersist.self]) 206 | 207 | struct Loop: ParsableCommand { 208 | static let configuration = CommandConfiguration( 209 | abstract: "Loop bclm on target battery level. (Default: \(defaultTargetBatteryLevel)%)") 210 | 211 | @Argument(help: "The value to set (\(targetBatteryLevelRange[0])-\(targetBatteryLevelRange[1])). Firmware-based battery level limits are not supported if not set to \(defaultTargetBatteryLevel).") 212 | var targetBatteryLevel: Int = defaultTargetBatteryLevel 213 | 214 | @Argument(help: "The value to set (\(targetBatteryMarginRange[0])-\(targetBatteryMarginRange[1])). Firmware-based battery level limits are not supported if not set to \(defaultBatteryLevelMargin).") 215 | var targetBatteryMargin: Int = defaultBatteryLevelMargin 216 | 217 | func validate() throws { 218 | try CheckPermission() 219 | try CheckPlatform() 220 | try CheckTargetBatteryLevel(targetBatteryLevel: targetBatteryLevel) 221 | try CheckTargetBatteryMargin(targetBatteryLevel: targetBatteryLevel, targetBatteryMargin: targetBatteryMargin) 222 | } 223 | 224 | func CheckFirmwareSupport() -> Bool { 225 | if targetBatteryLevel != defaultTargetBatteryLevel || targetBatteryMargin != defaultBatteryLevelMargin { 226 | return false 227 | } 228 | 229 | if #available(macOS 14.8.1, *) { 230 | return false 231 | } 232 | 233 | do { 234 | _ = try SMCKit.readData(chwa_key) 235 | } catch { 236 | return false 237 | } 238 | 239 | return true 240 | } 241 | 242 | func GetSoftwareOperateMode() -> Int { 243 | if #available(macOS 15.7, *) { 244 | return 1 245 | } 246 | 247 | if #available(macOS 14.8.1, *) { 248 | if #unavailable(macOS 15.0) { 249 | return 1 250 | } 251 | } 252 | 253 | do { 254 | _ = try SMCKit.readData(ch0b_key) 255 | _ = try SMCKit.readData(ch0c_key) 256 | } catch { 257 | return 1 258 | } 259 | 260 | return 0 261 | } 262 | 263 | func CheckMagSafeSupport() -> Bool { 264 | do { 265 | _ = try SMCKit.readData(aclc_key) 266 | } catch { 267 | return false 268 | } 269 | 270 | return true 271 | } 272 | 273 | func AllowCharging(status: Bool) throws { 274 | if operateMode == 10 { 275 | if status { 276 | try SMCKit.writeData(chwa_key, data: chwa_bytes_unlimit) 277 | } else { 278 | try SMCKit.writeData(chwa_key, data: chwa_bytes_limit) 279 | } 280 | return 281 | } 282 | 283 | if operateMode == 1 { 284 | if status { 285 | try SMCKit.writeData(chte_key, data: chte_bytes_unlimit) 286 | } else { 287 | try SMCKit.writeData(chte_key, data: chte_bytes_limit) 288 | } 289 | return 290 | } 291 | 292 | if status { 293 | try SMCKit.writeData(ch0b_key, data: ch0x_bytes_unlimit) 294 | try SMCKit.writeData(ch0c_key, data: ch0x_bytes_unlimit) 295 | } else { 296 | try SMCKit.writeData(ch0b_key, data: ch0x_bytes_limit) 297 | try SMCKit.writeData(ch0c_key, data: ch0x_bytes_limit) 298 | } 299 | } 300 | 301 | func ForceDischarging(status: Bool) throws { 302 | if operateMode == 10 { 303 | return 304 | } 305 | 306 | if operateMode == 1 { 307 | if status { 308 | try SMCKit.writeData(ch0j_key, data: ch0j_bytes_discharge) 309 | } else { 310 | try SMCKit.writeData(ch0j_key, data: ch0j_bytes_charge) 311 | } 312 | return 313 | } 314 | 315 | if status { 316 | try SMCKit.writeData(ch0i_key, data: ch0i_bytes_discharge) 317 | } else { 318 | try SMCKit.writeData(ch0i_key, data: ch0i_bytes_charge) 319 | } 320 | } 321 | 322 | func ChangeMagSafeLED(color: String) throws { 323 | if !isMagSafeSupported { 324 | return 325 | } 326 | 327 | switch color { 328 | case "Green": 329 | try SMCKit.writeData(aclc_key, data: aclc_bytes_green) 330 | break 331 | case "Red": 332 | try SMCKit.writeData(aclc_key, data: aclc_bytes_red) 333 | break 334 | case "Disable": 335 | try SMCKit.writeData(aclc_key, data: aclc_bytes_disable) 336 | break 337 | case "Unknown": fallthrough 338 | default: 339 | try SMCKit.writeData(aclc_key, data: aclc_bytes_unknown) 340 | break 341 | } 342 | } 343 | 344 | func run() { 345 | print("bclm_loop has started...") 346 | 347 | var pmStatus : IOReturn? = nil 348 | var assertionID = IOPMAssertionID(0) 349 | let reasonForActivity = "bclm_loop - Prevent sleep before charging limit is reached." 350 | let maxTryCount = 3 351 | var lastLimit = false 352 | var lastLimitCheckCount = 0 353 | var lastACPower : Bool? = nil 354 | var lastCharging : Bool? = nil 355 | var lastChargingCheckCount = 0 356 | 357 | do { 358 | try SMCKit.open() 359 | print("SMC has opened!") 360 | 361 | if CheckFirmwareSupport() { 362 | operateMode = 10 363 | print("Use firmware-based battery level limits, operateMode: \(operateMode).") 364 | } else { 365 | operateMode = GetSoftwareOperateMode() 366 | print("Use software-based battery level limits, operateMode: \(operateMode).") 367 | } 368 | 369 | if CheckMagSafeSupport() { 370 | isMagSafeSupported = true 371 | print("Enabled MagSafe control.") 372 | } else { 373 | isMagSafeSupported = false 374 | print("Disabled MagSafe control.") 375 | } 376 | 377 | SMCKit.close() 378 | print("SMC has closed!") 379 | } catch { 380 | print(error) 381 | return 382 | } 383 | 384 | signal(SIGUSR1) { _ in 385 | _ = AllowChargeNow(status: true) 386 | print("Received SIGUSR1 signal, enabled chargeNow.") 387 | } 388 | 389 | while true { 390 | let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue() 391 | let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array 392 | let chargeState = sources[0]["Power Source State"] as? String 393 | let isACPower : Bool? = (chargeState == "AC Power") ? true : (chargeState == "Battery Power" ? false : nil) 394 | let isCharging = sources[0]["Is Charging"] as? Bool 395 | let currentBattLevelInt = Int((sources[0]["Current Capacity"] as? Int) ?? -1) 396 | //let maxBattLevelInt = Int((sources[0]["Max Capacity"] as? Int) ?? -1) 397 | 398 | // Avoid failure by repeating maxTryCount times, and avoid opening SMC each time to affect performance. 399 | var needLimit = true 400 | 401 | if chargeState != nil && currentBattLevelInt >= 0 { 402 | if isACPower == true { 403 | if CheckChargeNowFile() { 404 | _ = AllowChargeNow(status: true) 405 | print("Detect chargeNow file, enabled chargeNow.") 406 | } 407 | // If already in battery level limit, some margin (targetBatteryMargin) is required to release the battery level limit. 408 | if chargeNow || (!lastLimit && currentBattLevelInt < targetBatteryLevel) || (lastLimit && currentBattLevelInt < (targetBatteryLevel - targetBatteryMargin)) { 409 | needLimit = false 410 | } 411 | } else if chargeNow { 412 | _ = AllowChargeNow(status: false) 413 | print("AC power is disconnected, disabled chargeNow.") 414 | } 415 | } 416 | if lastLimit != needLimit { 417 | print("Limit status will be changed. (Current: \(String(needLimit)), Last: \(String(lastLimit)))") 418 | 419 | lastLimit = needLimit 420 | lastLimitCheckCount = 1 421 | } else { 422 | lastLimitCheckCount += 1 423 | } 424 | 425 | if isACPower != nil && lastACPower != isACPower { 426 | lastACPower = isACPower 427 | lastCharging = nil 428 | } 429 | 430 | if lastCharging != isCharging { 431 | var isChargingStr = "nil" 432 | if isCharging != nil { 433 | isChargingStr = String(isCharging!) 434 | } 435 | var lastChargingStr = "nil" 436 | if lastCharging != nil { 437 | lastChargingStr = String(lastCharging!) 438 | } 439 | print("Charging status has changed! (Current: \(isChargingStr), Last: \(lastChargingStr))") 440 | 441 | lastCharging = isCharging 442 | lastChargingCheckCount = 1 443 | } else if isCharging != nil { 444 | lastChargingCheckCount += 1 445 | } 446 | 447 | // If each function has been repeated maxTryCount times, skip check. 448 | if lastLimitCheckCount <= maxTryCount || lastChargingCheckCount <= maxTryCount { 449 | do { 450 | try SMCKit.open() 451 | print("SMC has opened!") 452 | 453 | // Change charging status (If current charging status is known). 454 | if needLimit == true { 455 | try AllowCharging(status: false) 456 | //try ForceDischarging(status: true) 457 | print("Limit status has changed! (Limit)") 458 | 459 | // A: The battery is "full", sleep will no longer be prevented (If currently prevented). 460 | // B: No charger connected, sleep will no longer be prevented (If currently prevented), but charging is limited by default to prevent charging to 100% when disconnected from charger and sleeping. 461 | if pmStatus != nil && IOPMAssertionRelease(assertionID) == kIOReturnSuccess { 462 | pmStatus = nil 463 | assertionID = IOPMAssertionID(0) 464 | } 465 | } else if needLimit == false { 466 | try AllowCharging(status: true) 467 | //try ForceDischarging(status: false) 468 | print("Limit status has changed! (Unlimit)") 469 | 470 | // The battery is not "full", sleep will be prevented (If not currently prevented). 471 | if pmStatus == nil { 472 | pmStatus = IOPMAssertionCreateWithName(kIOPMAssertionTypePreventSystemSleep as CFString, UInt32(kIOPMAssertionLevelOn), reasonForActivity as CFString, &assertionID) 473 | if pmStatus != kIOReturnSuccess { 474 | pmStatus = nil 475 | assertionID = IOPMAssertionID(0) 476 | print("Failed to prevent sleep.") 477 | } 478 | } 479 | } 480 | 481 | // Change MagSafe LED status. 482 | if isMagSafeSupported { 483 | if isCharging == false { 484 | try ChangeMagSafeLED(color: "Green") 485 | print("MagSafe LED status has changed! (Full)") 486 | } else if isCharging == true { 487 | try ChangeMagSafeLED(color: "Red") 488 | print("MagSafe LED status has changed! (Charging)") 489 | } else { 490 | try ChangeMagSafeLED(color: "Unknown") 491 | print("MagSafe LED status has changed! (Unknown)") 492 | } 493 | } 494 | 495 | SMCKit.close() 496 | print("SMC has closed!") 497 | } catch { 498 | print(error) 499 | } 500 | } 501 | 502 | sleep(2) 503 | } 504 | } 505 | } 506 | 507 | struct ChargeNow: ParsableCommand { 508 | static let configuration = CommandConfiguration( 509 | commandName: "chargeNow", 510 | abstract: "Send a command to bclm_loop to fully charge now. (Only available if bclm_loop is running and currently charging)") 511 | 512 | func validate() throws { 513 | try CheckPlatform() 514 | } 515 | 516 | func run() { 517 | if SetChargeNowFile(status: true) { 518 | print("The command has been sent. If bclm_loop is running and currently charging, it should respond quickly.") 519 | } 520 | } 521 | } 522 | 523 | struct Persist: ParsableCommand { 524 | static let configuration = CommandConfiguration( 525 | abstract: "Persists bclm loop service.") 526 | 527 | @Argument(help: "The value to set (\(targetBatteryLevelRange[0])-\(targetBatteryLevelRange[1])). Firmware-based battery level limits are not supported if not set to \(defaultTargetBatteryLevel).") 528 | var targetBatteryLevel: Int = defaultTargetBatteryLevel 529 | 530 | @Argument(help: "The value to set (\(targetBatteryMarginRange[0])-\(targetBatteryMarginRange[1])). Firmware-based battery level limits are not supported if not set to \(defaultBatteryLevelMargin).") 531 | var targetBatteryMargin: Int = defaultBatteryLevelMargin 532 | 533 | func validate() throws { 534 | try CheckPermission() 535 | try CheckPlatform() 536 | try CheckTargetBatteryLevel(targetBatteryLevel: targetBatteryLevel) 537 | try CheckTargetBatteryMargin(targetBatteryLevel: targetBatteryLevel, targetBatteryMargin: targetBatteryMargin) 538 | } 539 | 540 | func run() { 541 | if persist(false) { 542 | print("Already persisting! Re-persist...\n") 543 | _ = removePlist() 544 | } 545 | 546 | if updatePlist(targetBatteryLevel: targetBatteryLevel, targetBatteryMargin: targetBatteryMargin) && persist(true) { 547 | print("Success!") 548 | } 549 | } 550 | } 551 | 552 | struct Unpersist: ParsableCommand { 553 | static let configuration = CommandConfiguration( 554 | abstract: "Unpersists bclm loop service.") 555 | 556 | func validate() throws { 557 | try CheckPermission() 558 | try CheckPlatform() 559 | } 560 | 561 | func run() { 562 | if !persist(false) { 563 | fputs("Already not persisting!\n", stderr) 564 | } 565 | 566 | if removePlist() { 567 | print("Success!") 568 | } 569 | } 570 | } 571 | } 572 | 573 | BCLMLoop.main() 574 | -------------------------------------------------------------------------------- /Sources/bclm_loop/SMC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMC.swift 3 | // SMCKit 4 | // 5 | // The MIT License 6 | // 7 | // Copyright (C) 2014-2017 beltex 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import IOKit 28 | import Foundation 29 | 30 | //------------------------------------------------------------------------------ 31 | // MARK: Type Aliases 32 | //------------------------------------------------------------------------------ 33 | 34 | // http://stackoverflow.com/a/22383661 35 | 36 | /// Floating point, unsigned, 14 bits exponent, 2 bits fraction 37 | public typealias FPE2 = (UInt8, UInt8) 38 | 39 | /// Floating point, signed, 7 bits exponent, 8 bits fraction 40 | public typealias SP78 = (UInt8, UInt8) 41 | 42 | public typealias SMCBytes = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, 43 | UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, 44 | UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, 45 | UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, 46 | UInt8, UInt8, UInt8, UInt8) 47 | 48 | //------------------------------------------------------------------------------ 49 | // MARK: Standard Library Extensions 50 | //------------------------------------------------------------------------------ 51 | 52 | extension UInt32 { 53 | 54 | init(fromBytes bytes: (UInt8, UInt8, UInt8, UInt8)) { 55 | // TODO: Broken up due to "Expression was too complex" error as of 56 | // Swift 4. 57 | 58 | let byte0 = UInt32(bytes.0) << 24 59 | let byte1 = UInt32(bytes.1) << 16 60 | let byte2 = UInt32(bytes.2) << 8 61 | let byte3 = UInt32(bytes.3) 62 | 63 | self = byte0 | byte1 | byte2 | byte3 64 | } 65 | } 66 | 67 | extension Bool { 68 | 69 | init(fromByte byte: UInt8) { 70 | self = byte == 1 ? true : false 71 | } 72 | } 73 | 74 | public extension Int { 75 | 76 | init(fromFPE2 bytes: FPE2) { 77 | self = (Int(bytes.0) << 6) + (Int(bytes.1) >> 2) 78 | } 79 | 80 | func toFPE2() -> FPE2 { 81 | return (UInt8(self >> 6), UInt8((self << 2) ^ ((self >> 6) << 8))) 82 | } 83 | } 84 | 85 | extension Double { 86 | 87 | init(fromSP78 bytes: SP78) { 88 | // FIXME: Handle second byte 89 | let sign = bytes.0 & 0x80 == 0 ? 1.0 : -1.0 90 | self = sign * Double(bytes.0 & 0x7F) // AND to mask sign bit 91 | } 92 | } 93 | 94 | // Thanks to Airspeed Velocity for the great idea! 95 | // http://airspeedvelocity.net/2015/05/22/my-talk-at-swift-summit/ 96 | public extension FourCharCode { 97 | 98 | init(fromString str: String) { 99 | precondition(str.count == 4) 100 | 101 | self = str.utf8.reduce(0) { sum, character in 102 | return sum << 8 | UInt32(character) 103 | } 104 | } 105 | 106 | init(fromStaticString str: StaticString) { 107 | precondition(str.utf8CodeUnitCount == 4) 108 | 109 | self = str.withUTF8Buffer { buffer in 110 | // TODO: Broken up due to "Expression was too complex" error as of 111 | // Swift 4. 112 | 113 | let byte0 = UInt32(buffer[0]) << 24 114 | let byte1 = UInt32(buffer[1]) << 16 115 | let byte2 = UInt32(buffer[2]) << 8 116 | let byte3 = UInt32(buffer[3]) 117 | 118 | return byte0 | byte1 | byte2 | byte3 119 | } 120 | } 121 | 122 | func toString() -> String { 123 | return String(describing: UnicodeScalar(self >> 24 & 0xff)!) + 124 | String(describing: UnicodeScalar(self >> 16 & 0xff)!) + 125 | String(describing: UnicodeScalar(self >> 8 & 0xff)!) + 126 | String(describing: UnicodeScalar(self & 0xff)!) 127 | } 128 | } 129 | 130 | //------------------------------------------------------------------------------ 131 | // MARK: Defined by AppleSMC.kext 132 | //------------------------------------------------------------------------------ 133 | 134 | /// Defined by AppleSMC.kext 135 | /// 136 | /// This is the predefined struct that must be passed to communicate with the 137 | /// AppleSMC driver. While the driver is closed source, the definition of this 138 | /// struct happened to appear in the Apple PowerManagement project at around 139 | /// version 211, and soon after disappeared. It can be seen in the PrivateLib.c 140 | /// file under pmconfigd. Given that it is C code, this is the closest 141 | /// translation to Swift from a type perspective. 142 | /// 143 | /// ### Issues 144 | /// 145 | /// * Padding for struct alignment when passed over to C side 146 | /// * Size of struct must be 80 bytes 147 | /// * C array's are bridged as tuples 148 | /// 149 | /// http://www.opensource.apple.com/source/PowerManagement/PowerManagement-211/ 150 | public struct SMCParamStruct { 151 | 152 | /// I/O Kit function selector 153 | public enum Selector: UInt8 { 154 | case kSMCHandleYPCEvent = 2 155 | case kSMCReadKey = 5 156 | case kSMCWriteKey = 6 157 | case kSMCGetKeyFromIndex = 8 158 | case kSMCGetKeyInfo = 9 159 | } 160 | 161 | /// Return codes for SMCParamStruct.result property 162 | public enum Result: UInt8 { 163 | case kSMCSuccess = 0 164 | case kSMCError = 1 165 | case kSMCKeyNotFound = 132 166 | } 167 | 168 | public struct SMCVersion { 169 | var major: CUnsignedChar = 0 170 | var minor: CUnsignedChar = 0 171 | var build: CUnsignedChar = 0 172 | var reserved: CUnsignedChar = 0 173 | var release: CUnsignedShort = 0 174 | } 175 | 176 | public struct SMCPLimitData { 177 | var version: UInt16 = 0 178 | var length: UInt16 = 0 179 | var cpuPLimit: UInt32 = 0 180 | var gpuPLimit: UInt32 = 0 181 | var memPLimit: UInt32 = 0 182 | } 183 | 184 | public struct SMCKeyInfoData { 185 | /// How many bytes written to SMCParamStruct.bytes 186 | var dataSize: IOByteCount32 = 0 187 | 188 | /// Type of data written to SMCParamStruct.bytes. This lets us know how 189 | /// to interpret it (translate it to human readable) 190 | var dataType: UInt32 = 0 191 | 192 | var dataAttributes: UInt8 = 0 193 | } 194 | 195 | /// FourCharCode telling the SMC what we want 196 | var key: UInt32 = 0 197 | 198 | var vers = SMCVersion() 199 | 200 | var pLimitData = SMCPLimitData() 201 | 202 | var keyInfo = SMCKeyInfoData() 203 | 204 | /// Padding for struct alignment when passed over to C side 205 | var padding: UInt16 = 0 206 | 207 | /// Result of an operation 208 | var result: UInt8 = 0 209 | 210 | var status: UInt8 = 0 211 | 212 | /// Method selector 213 | var data8: UInt8 = 0 214 | 215 | var data32: UInt32 = 0 216 | 217 | /// Data returned from the SMC 218 | var bytes: SMCBytes = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 219 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 220 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 221 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 222 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 223 | UInt8(0), UInt8(0)) 224 | } 225 | 226 | //------------------------------------------------------------------------------ 227 | // MARK: SMC Client 228 | //------------------------------------------------------------------------------ 229 | 230 | /// SMC data type information 231 | public struct DataTypes { 232 | 233 | /// Fan information struct 234 | public static let FDS = 235 | DataType(type: FourCharCode(fromStaticString: "{fds"), size: 16) 236 | public static let Flag = 237 | DataType(type: FourCharCode(fromStaticString: "flag"), size: 1) 238 | /// See type aliases 239 | public static let FPE2 = 240 | DataType(type: FourCharCode(fromStaticString: "fpe2"), size: 2) 241 | /// See type aliases 242 | public static let SP78 = 243 | DataType(type: FourCharCode(fromStaticString: "sp78"), size: 2) 244 | public static let UInt8 = 245 | DataType(type: FourCharCode(fromStaticString: "ui8 "), size: 1) 246 | public static let UInt32 = 247 | DataType(type: FourCharCode(fromStaticString: "ui32"), size: 4) 248 | } 249 | 250 | public struct SMCKey { 251 | let code: FourCharCode 252 | let info: DataType 253 | } 254 | 255 | public struct DataType: Equatable { 256 | let type: FourCharCode 257 | let size: UInt32 258 | } 259 | 260 | public func ==(lhs: DataType, rhs: DataType) -> Bool { 261 | return lhs.type == rhs.type && lhs.size == rhs.size 262 | } 263 | 264 | /// Apple System Management Controller (SMC) user-space client for Intel-based 265 | /// Macs. Works by talking to the AppleSMC.kext (kernel extension), the closed 266 | /// source driver for the SMC. 267 | public struct SMCKit { 268 | 269 | public enum SMCError: Error { 270 | 271 | /// AppleSMC driver not found 272 | case driverNotFound 273 | 274 | /// Failed to open a connection to the AppleSMC driver 275 | case failedToOpen 276 | 277 | /// This SMC key is not valid on this machine 278 | case keyNotFound(code: String) 279 | 280 | /// Requires root privileges 281 | case notPrivileged 282 | 283 | /// Fan speed must be > 0 && <= fanMaxSpeed 284 | case unsafeFanSpeed 285 | 286 | /// https://developer.apple.com/library/mac/qa/qa1075/_index.html 287 | /// 288 | /// - parameter kIOReturn: I/O Kit error code 289 | /// - parameter SMCResult: SMC specific return code 290 | case unknown(kIOReturn: kern_return_t, SMCResult: UInt8) 291 | } 292 | 293 | /// Connection to the SMC driver 294 | fileprivate static var connection: io_connect_t = 0 295 | 296 | /// Open connection to the SMC driver. This must be done first before any 297 | /// other calls 298 | public static func open() throws { 299 | let service = IOServiceGetMatchingService(kIOMasterPortDefault, 300 | IOServiceMatching("AppleSMC")) 301 | 302 | if service == 0 { throw SMCError.driverNotFound } 303 | 304 | let result = IOServiceOpen(service, mach_task_self_, 0, 305 | &SMCKit.connection) 306 | IOObjectRelease(service) 307 | 308 | if result != kIOReturnSuccess { throw SMCError.failedToOpen } 309 | } 310 | 311 | /// Close connection to the SMC driver 312 | @discardableResult 313 | public static func close() -> Bool { 314 | let result = IOServiceClose(SMCKit.connection) 315 | return result == kIOReturnSuccess ? true : false 316 | } 317 | 318 | /// Get information about a key 319 | public static func keyInformation(_ key: FourCharCode) throws -> DataType { 320 | var inputStruct = SMCParamStruct() 321 | 322 | inputStruct.key = key 323 | inputStruct.data8 = SMCParamStruct.Selector.kSMCGetKeyInfo.rawValue 324 | 325 | let outputStruct = try callDriver(&inputStruct) 326 | 327 | return DataType(type: outputStruct.keyInfo.dataType, 328 | size: outputStruct.keyInfo.dataSize) 329 | } 330 | 331 | /// Get information about the key at index 332 | public static func keyInformationAtIndex(_ index: Int) throws -> 333 | FourCharCode { 334 | var inputStruct = SMCParamStruct() 335 | 336 | inputStruct.data8 = SMCParamStruct.Selector.kSMCGetKeyFromIndex.rawValue 337 | inputStruct.data32 = UInt32(index) 338 | 339 | let outputStruct = try callDriver(&inputStruct) 340 | 341 | return outputStruct.key 342 | } 343 | 344 | public static func getKey(_ code: String, type: DataType) -> SMCKey { 345 | let key = SMCKey(code: FourCharCode(fromString: code), info: type) 346 | return key 347 | } 348 | 349 | /// Read data of a key 350 | public static func readData(_ key: SMCKey) throws -> SMCBytes { 351 | var inputStruct = SMCParamStruct() 352 | 353 | inputStruct.key = key.code 354 | inputStruct.keyInfo.dataSize = UInt32(key.info.size) 355 | inputStruct.data8 = SMCParamStruct.Selector.kSMCReadKey.rawValue 356 | 357 | let outputStruct = try callDriver(&inputStruct) 358 | 359 | return outputStruct.bytes 360 | } 361 | 362 | /// Write data for a key 363 | public static func writeData(_ key: SMCKey, data: SMCBytes) throws { 364 | var inputStruct = SMCParamStruct() 365 | 366 | inputStruct.key = key.code 367 | inputStruct.bytes = data 368 | inputStruct.keyInfo.dataSize = UInt32(key.info.size) 369 | inputStruct.data8 = SMCParamStruct.Selector.kSMCWriteKey.rawValue 370 | 371 | _ = try callDriver(&inputStruct) 372 | } 373 | 374 | /// Make an actual call to the SMC driver 375 | public static func callDriver(_ inputStruct: inout SMCParamStruct, 376 | selector: SMCParamStruct.Selector = .kSMCHandleYPCEvent) 377 | throws -> SMCParamStruct { 378 | assert(MemoryLayout.stride == 80, "SMCParamStruct size is != 80") 379 | 380 | var outputStruct = SMCParamStruct() 381 | let inputStructSize = MemoryLayout.stride 382 | var outputStructSize = MemoryLayout.stride 383 | 384 | let result = IOConnectCallStructMethod(SMCKit.connection, 385 | UInt32(selector.rawValue), 386 | &inputStruct, 387 | inputStructSize, 388 | &outputStruct, 389 | &outputStructSize) 390 | 391 | switch (result, outputStruct.result) { 392 | case (kIOReturnSuccess, SMCParamStruct.Result.kSMCSuccess.rawValue): 393 | return outputStruct 394 | case (kIOReturnSuccess, SMCParamStruct.Result.kSMCKeyNotFound.rawValue): 395 | throw SMCError.keyNotFound(code: inputStruct.key.toString()) 396 | case (kIOReturnNotPrivileged, _): 397 | throw SMCError.notPrivileged 398 | default: 399 | throw SMCError.unknown(kIOReturn: result, 400 | SMCResult: outputStruct.result) 401 | } 402 | } 403 | } 404 | 405 | //------------------------------------------------------------------------------ 406 | // MARK: General 407 | //------------------------------------------------------------------------------ 408 | 409 | extension SMCKit { 410 | 411 | /// Get all valid SMC keys for this machine 412 | public static func allKeys() throws -> [SMCKey] { 413 | let count = try keyCount() 414 | var keys = [SMCKey]() 415 | 416 | for i in 0 ..< count { 417 | let key = try keyInformationAtIndex(i) 418 | let info = try keyInformation(key) 419 | keys.append(SMCKey(code: key, info: info)) 420 | } 421 | 422 | return keys 423 | } 424 | 425 | /// Get the number of valid SMC keys for this machine 426 | public static func keyCount() throws -> Int { 427 | let key = SMCKey(code: FourCharCode(fromStaticString: "#KEY"), 428 | info: DataTypes.UInt32) 429 | 430 | let data = try readData(key) 431 | return Int(UInt32(fromBytes: (data.0, data.1, data.2, data.3))) 432 | } 433 | 434 | /// Is this key valid on this machine? 435 | public static func isKeyFound(_ code: FourCharCode) throws -> Bool { 436 | do { 437 | _ = try keyInformation(code) 438 | } catch SMCError.keyNotFound { return false } 439 | 440 | return true 441 | } 442 | } 443 | 444 | //------------------------------------------------------------------------------ 445 | // MARK: Temperature 446 | //------------------------------------------------------------------------------ 447 | 448 | /// The list is NOT exhaustive. In addition, the names of the sensors may not be 449 | /// mapped to the correct hardware component. 450 | /// 451 | /// ### Sources 452 | /// 453 | /// * powermetrics(1) 454 | /// * https://www.apple.com/downloads/dashboard/status/istatpro.html 455 | /// * https://github.com/hholtmann/smcFanControl 456 | /// * https://github.com/jedda/OSX-Monitoring-Tools 457 | /// * http://www.opensource.apple.com/source/net_snmp/ 458 | /// * http://www.parhelia.ch/blog/statics/k3_keys.html 459 | public struct TemperatureSensors { 460 | 461 | public static let AMBIENT_AIR_0 = TemperatureSensor(name: "AMBIENT_AIR_0", 462 | code: FourCharCode(fromStaticString: "TA0P")) 463 | public static let AMBIENT_AIR_1 = TemperatureSensor(name: "AMBIENT_AIR_1", 464 | code: FourCharCode(fromStaticString: "TA1P")) 465 | // Via powermetrics(1) 466 | public static let CPU_0_DIE = TemperatureSensor(name: "CPU_0_DIE", 467 | code: FourCharCode(fromStaticString: "TC0F")) 468 | public static let CPU_0_DIODE = TemperatureSensor(name: "CPU_0_DIODE", 469 | code: FourCharCode(fromStaticString: "TC0D")) 470 | public static let CPU_0_HEATSINK = TemperatureSensor(name: "CPU_0_HEATSINK", 471 | code: FourCharCode(fromStaticString: "TC0H")) 472 | public static let CPU_0_PROXIMITY = 473 | TemperatureSensor(name: "CPU_0_PROXIMITY", 474 | code: FourCharCode(fromStaticString: "TC0P")) 475 | public static let ENCLOSURE_BASE_0 = 476 | TemperatureSensor(name: "ENCLOSURE_BASE_0", 477 | code: FourCharCode(fromStaticString: "TB0T")) 478 | public static let ENCLOSURE_BASE_1 = 479 | TemperatureSensor(name: "ENCLOSURE_BASE_1", 480 | code: FourCharCode(fromStaticString: "TB1T")) 481 | public static let ENCLOSURE_BASE_2 = 482 | TemperatureSensor(name: "ENCLOSURE_BASE_2", 483 | code: FourCharCode(fromStaticString: "TB2T")) 484 | public static let ENCLOSURE_BASE_3 = 485 | TemperatureSensor(name: "ENCLOSURE_BASE_3", 486 | code: FourCharCode(fromStaticString: "TB3T")) 487 | public static let GPU_0_DIODE = TemperatureSensor(name: "GPU_0_DIODE", 488 | code: FourCharCode(fromStaticString: "TG0D")) 489 | public static let GPU_0_HEATSINK = TemperatureSensor(name: "GPU_0_HEATSINK", 490 | code: FourCharCode(fromStaticString: "TG0H")) 491 | public static let GPU_0_PROXIMITY = 492 | TemperatureSensor(name: "GPU_0_PROXIMITY", 493 | code: FourCharCode(fromStaticString: "TG0P")) 494 | public static let HDD_PROXIMITY = TemperatureSensor(name: "HDD_PROXIMITY", 495 | code: FourCharCode(fromStaticString: "TH0P")) 496 | public static let HEATSINK_0 = TemperatureSensor(name: "HEATSINK_0", 497 | code: FourCharCode(fromStaticString: "Th0H")) 498 | public static let HEATSINK_1 = TemperatureSensor(name: "HEATSINK_1", 499 | code: FourCharCode(fromStaticString: "Th1H")) 500 | public static let HEATSINK_2 = TemperatureSensor(name: "HEATSINK_2", 501 | code: FourCharCode(fromStaticString: "Th2H")) 502 | public static let LCD_PROXIMITY = TemperatureSensor(name: "LCD_PROXIMITY", 503 | code: FourCharCode(fromStaticString: "TL0P")) 504 | public static let MEM_SLOT_0 = TemperatureSensor(name: "MEM_SLOT_0", 505 | code: FourCharCode(fromStaticString: "TM0S")) 506 | public static let MEM_SLOTS_PROXIMITY = 507 | TemperatureSensor(name: "MEM_SLOTS_PROXIMITY", 508 | code: FourCharCode(fromStaticString: "TM0P")) 509 | public static let MISC_PROXIMITY = TemperatureSensor(name: "MISC_PROXIMITY", 510 | code: FourCharCode(fromStaticString: "Tm0P")) 511 | public static let NORTHBRIDGE = TemperatureSensor(name: "NORTHBRIDGE", 512 | code: FourCharCode(fromStaticString: "TN0H")) 513 | public static let NORTHBRIDGE_DIODE = 514 | TemperatureSensor(name: "NORTHBRIDGE_DIODE", 515 | code: FourCharCode(fromStaticString: "TN0D")) 516 | public static let NORTHBRIDGE_PROXIMITY = 517 | TemperatureSensor(name: "NORTHBRIDGE_PROXIMITY", 518 | code: FourCharCode(fromStaticString: "TN0P")) 519 | public static let ODD_PROXIMITY = TemperatureSensor(name: "ODD_PROXIMITY", 520 | code: FourCharCode(fromStaticString: "TO0P")) 521 | public static let PALM_REST = TemperatureSensor(name: "PALM_REST", 522 | code: FourCharCode(fromStaticString: "Ts0P")) 523 | public static let PWR_SUPPLY_PROXIMITY = 524 | TemperatureSensor(name: "PWR_SUPPLY_PROXIMITY", 525 | code: FourCharCode(fromStaticString: "Tp0P")) 526 | public static let THUNDERBOLT_0 = TemperatureSensor(name: "THUNDERBOLT_0", 527 | code: FourCharCode(fromStaticString: "TI0P")) 528 | public static let THUNDERBOLT_1 = TemperatureSensor(name: "THUNDERBOLT_1", 529 | code: FourCharCode(fromStaticString: "TI1P")) 530 | 531 | public static let all = [AMBIENT_AIR_0.code: AMBIENT_AIR_0, 532 | AMBIENT_AIR_1.code: AMBIENT_AIR_1, 533 | CPU_0_DIE.code: CPU_0_DIE, 534 | CPU_0_DIODE.code: CPU_0_DIODE, 535 | CPU_0_HEATSINK.code: CPU_0_HEATSINK, 536 | CPU_0_PROXIMITY.code: CPU_0_PROXIMITY, 537 | ENCLOSURE_BASE_0.code: ENCLOSURE_BASE_0, 538 | ENCLOSURE_BASE_1.code: ENCLOSURE_BASE_1, 539 | ENCLOSURE_BASE_2.code: ENCLOSURE_BASE_2, 540 | ENCLOSURE_BASE_3.code: ENCLOSURE_BASE_3, 541 | GPU_0_DIODE.code: GPU_0_DIODE, 542 | GPU_0_HEATSINK.code: GPU_0_HEATSINK, 543 | GPU_0_PROXIMITY.code: GPU_0_PROXIMITY, 544 | HDD_PROXIMITY.code: HDD_PROXIMITY, 545 | HEATSINK_0.code: HEATSINK_0, 546 | HEATSINK_1.code: HEATSINK_1, 547 | HEATSINK_2.code: HEATSINK_2, 548 | MEM_SLOT_0.code: MEM_SLOT_0, 549 | MEM_SLOTS_PROXIMITY.code: MEM_SLOTS_PROXIMITY, 550 | PALM_REST.code: PALM_REST, 551 | LCD_PROXIMITY.code: LCD_PROXIMITY, 552 | MISC_PROXIMITY.code: MISC_PROXIMITY, 553 | NORTHBRIDGE.code: NORTHBRIDGE, 554 | NORTHBRIDGE_DIODE.code: NORTHBRIDGE_DIODE, 555 | NORTHBRIDGE_PROXIMITY.code: NORTHBRIDGE_PROXIMITY, 556 | ODD_PROXIMITY.code: ODD_PROXIMITY, 557 | PWR_SUPPLY_PROXIMITY.code: PWR_SUPPLY_PROXIMITY, 558 | THUNDERBOLT_0.code: THUNDERBOLT_0, 559 | THUNDERBOLT_1.code: THUNDERBOLT_1] 560 | } 561 | 562 | public struct TemperatureSensor { 563 | public let name: String 564 | public let code: FourCharCode 565 | } 566 | 567 | public enum TemperatureUnit { 568 | case celius 569 | case fahrenheit 570 | case kelvin 571 | 572 | public static func toFahrenheit(_ celius: Double) -> Double { 573 | // https://en.wikipedia.org/wiki/Fahrenheit#Definition_and_conversions 574 | return (celius * 1.8) + 32 575 | } 576 | 577 | public static func toKelvin(_ celius: Double) -> Double { 578 | // https://en.wikipedia.org/wiki/Kelvin 579 | return celius + 273.15 580 | } 581 | } 582 | 583 | extension SMCKit { 584 | 585 | public static func allKnownTemperatureSensors() throws -> 586 | [TemperatureSensor] { 587 | var sensors = [TemperatureSensor]() 588 | 589 | for sensor in TemperatureSensors.all.values { 590 | if try isKeyFound(sensor.code) { sensors.append(sensor) } 591 | } 592 | 593 | return sensors 594 | } 595 | 596 | public static func allUnknownTemperatureSensors() throws -> [TemperatureSensor] { 597 | let keys = try allKeys() 598 | 599 | return keys.filter { $0.code.toString().hasPrefix("T") && 600 | $0.info == DataTypes.SP78 && 601 | TemperatureSensors.all[$0.code] == nil } 602 | .map { TemperatureSensor(name: "Unknown", code: $0.code) } 603 | } 604 | 605 | /// Get current temperature of a sensor 606 | public static func temperature(_ sensorCode: FourCharCode, 607 | unit: TemperatureUnit = .celius) throws -> Double { 608 | let data = try readData(SMCKey(code: sensorCode, info: DataTypes.SP78)) 609 | 610 | let temperatureInCelius = Double(fromSP78: (data.0, data.1)) 611 | 612 | switch unit { 613 | case .celius: 614 | return temperatureInCelius 615 | case .fahrenheit: 616 | return TemperatureUnit.toFahrenheit(temperatureInCelius) 617 | case .kelvin: 618 | return TemperatureUnit.toKelvin(temperatureInCelius) 619 | } 620 | } 621 | } 622 | 623 | //------------------------------------------------------------------------------ 624 | // MARK: Fan 625 | //------------------------------------------------------------------------------ 626 | 627 | public struct Fan { 628 | // TODO: Should we start the fan id from 1 instead of 0? 629 | public let id: Int 630 | public let name: String 631 | public let minSpeed: Int 632 | public let maxSpeed: Int 633 | } 634 | 635 | extension SMCKit { 636 | 637 | public static func allFans() throws -> [Fan] { 638 | let count = try fanCount() 639 | var fans = [Fan]() 640 | 641 | for i in 0 ..< count { 642 | fans.append(try SMCKit.fan(i)) 643 | } 644 | 645 | return fans 646 | } 647 | 648 | public static func fan(_ id: Int) throws -> Fan { 649 | let name = try fanName(id) 650 | let minSpeed = try fanMinSpeed(id) 651 | let maxSpeed = try fanMaxSpeed(id) 652 | return Fan(id: id, name: name, minSpeed: minSpeed, maxSpeed: maxSpeed) 653 | } 654 | 655 | /// Number of fans this machine has. All Intel based Macs, except for the 656 | /// 2015 MacBook (8,1), have at least 1 657 | public static func fanCount() throws -> Int { 658 | let key = SMCKey(code: FourCharCode(fromStaticString: "FNum"), 659 | info: DataTypes.UInt8) 660 | 661 | let data = try readData(key) 662 | return Int(data.0) 663 | } 664 | 665 | public static func fanName(_ id: Int) throws -> String { 666 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)ID"), 667 | info: DataTypes.FDS) 668 | let data = try readData(key) 669 | 670 | // The last 12 bytes of '{fds' data type, a custom struct defined by the 671 | // AppleSMC.kext that is 16 bytes, contains the fan name 672 | let c1 = String(UnicodeScalar(data.4)) 673 | let c2 = String(UnicodeScalar(data.5)) 674 | let c3 = String(UnicodeScalar(data.6)) 675 | let c4 = String(UnicodeScalar(data.7)) 676 | let c5 = String(UnicodeScalar(data.8)) 677 | let c6 = String(UnicodeScalar(data.9)) 678 | let c7 = String(UnicodeScalar(data.10)) 679 | let c8 = String(UnicodeScalar(data.11)) 680 | let c9 = String(UnicodeScalar(data.12)) 681 | let c10 = String(UnicodeScalar(data.13)) 682 | let c11 = String(UnicodeScalar(data.14)) 683 | let c12 = String(UnicodeScalar(data.15)) 684 | 685 | let name = c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9 + c10 + c11 + c12 686 | 687 | let characterSet = CharacterSet.whitespaces 688 | return name.trimmingCharacters(in: characterSet) 689 | } 690 | 691 | public static func fanCurrentSpeed(_ id: Int) throws -> Int { 692 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)Ac"), 693 | info: DataTypes.FPE2) 694 | 695 | let data = try readData(key) 696 | return Int(fromFPE2: (data.0, data.1)) 697 | } 698 | 699 | public static func fanMinSpeed(_ id: Int) throws -> Int { 700 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)Mn"), 701 | info: DataTypes.FPE2) 702 | 703 | let data = try readData(key) 704 | return Int(fromFPE2: (data.0, data.1)) 705 | } 706 | 707 | public static func fanMaxSpeed(_ id: Int) throws -> Int { 708 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)Mx"), 709 | info: DataTypes.FPE2) 710 | 711 | let data = try readData(key) 712 | return Int(fromFPE2: (data.0, data.1)) 713 | } 714 | 715 | /// Requires root privileges. By minimum we mean that OS X can interject and 716 | /// raise the fan speed if needed, however it will not go below this. 717 | /// 718 | /// WARNING: You are playing with hardware here, BE CAREFUL. 719 | /// 720 | /// - Throws: Of note, `SMCKit.SMCError`'s `UnsafeFanSpeed` and `NotPrivileged` 721 | public static func fanSetMinSpeed(_ id: Int, speed: Int) throws { 722 | let maxSpeed = try fanMaxSpeed(id) 723 | if speed <= 0 || speed > maxSpeed { throw SMCError.unsafeFanSpeed } 724 | 725 | let data = speed.toFPE2() 726 | let bytes: SMCBytes = (data.0, data.1, UInt8(0), UInt8(0), UInt8(0), UInt8(0), 727 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 728 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 729 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 730 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 731 | UInt8(0), UInt8(0)) 732 | 733 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)Mn"), 734 | info: DataTypes.FPE2) 735 | 736 | try writeData(key, data: bytes) 737 | } 738 | } 739 | 740 | //------------------------------------------------------------------------------ 741 | // MARK: Miscellaneous 742 | //------------------------------------------------------------------------------ 743 | 744 | public struct batteryInfo { 745 | public let batteryCount: Int 746 | public let isACPresent: Bool 747 | public let isBatteryPowered: Bool 748 | public let isBatteryOk: Bool 749 | public let isCharging: Bool 750 | } 751 | 752 | extension SMCKit { 753 | 754 | public static func isOpticalDiskDriveFull() throws -> Bool { 755 | // TODO: Should we catch key not found? That just means the machine 756 | // doesn't have an ODD. Returning false though is not fully correct. 757 | // Maybe we could throw a no ODD error instead? 758 | let key = SMCKey(code: FourCharCode(fromStaticString: "MSDI"), 759 | info: DataTypes.Flag) 760 | 761 | let data = try readData(key) 762 | return Bool(fromByte: data.0) 763 | } 764 | 765 | public static func batteryInformation() throws -> batteryInfo { 766 | let batteryCountKey = 767 | SMCKey(code: FourCharCode(fromStaticString: "BNum"), 768 | info: DataTypes.UInt8) 769 | let batteryPoweredKey = 770 | SMCKey(code: FourCharCode(fromStaticString: "BATP"), 771 | info: DataTypes.Flag) 772 | let batteryInfoKey = 773 | SMCKey(code: FourCharCode(fromStaticString: "BSIn"), 774 | info: DataTypes.UInt8) 775 | 776 | let batteryCountData = try readData(batteryCountKey) 777 | let batteryCount = Int(batteryCountData.0) 778 | 779 | let isBatteryPoweredData = try readData(batteryPoweredKey) 780 | let isBatteryPowered = Bool(fromByte: isBatteryPoweredData.0) 781 | 782 | let batteryInfoData = try readData(batteryInfoKey) 783 | let isCharging = batteryInfoData.0 & 1 == 1 ? true : false 784 | let isACPresent = (batteryInfoData.0 >> 1) & 1 == 1 ? true : false 785 | let isBatteryOk = (batteryInfoData.0 >> 6) & 1 == 1 ? true : false 786 | 787 | return batteryInfo(batteryCount: batteryCount, isACPresent: isACPresent, 788 | isBatteryPowered: isBatteryPowered, 789 | isBatteryOk: isBatteryOk, 790 | isCharging: isCharging) 791 | } 792 | } 793 | --------------------------------------------------------------------------------