Skip to content

Commit 57406a2

Browse files
author
whuan132
committed
Add AIBatteryHelper source code
1 parent 230ed1c commit 57406a2

18 files changed

+1176
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Logger.swift
3+
// AIBattery
4+
//
5+
// Created by whuan132 on 3/31/25.
6+
// © 2025 COLLWEB. All rights reserved.
7+
//
8+
9+
import os
10+
11+
enum LogCategory: String {
12+
case charging = "Charging"
13+
case adapter = "Adapter"
14+
case magsafe = "MagSafe"
15+
case battery = "Battery"
16+
case acPower = "ACPower"
17+
case xpc = "XPC"
18+
case daemon = "Daemon"
19+
case general = "General"
20+
}
21+
22+
/// A centralized logger for AIBatteryHelper using os_log.
23+
enum Logger {
24+
private static let subsystem = "com.collweb.AIBatteryHelper"
25+
26+
static func log(_ message: String, category: LogCategory = .general, type: OSLogType = .default) {
27+
let logger = OSLog(subsystem: subsystem, category: category.rawValue)
28+
os_log("%{public}@", log: logger, type: type, message)
29+
}
30+
31+
static func error(_ message: String, category: LogCategory = .general) {
32+
log(message, category: category, type: .error)
33+
}
34+
35+
static func info(_ message: String, category: LogCategory = .general) {
36+
log(message, category: category, type: .info)
37+
}
38+
39+
static func fault(_ message: String, category: LogCategory = .general) {
40+
log(message, category: category, type: .fault)
41+
}
42+
43+
#if DEBUG
44+
static func debug(_ message: String, category: LogCategory = .general) {
45+
log(message, category: category, type: .debug)
46+
}
47+
#else
48+
static func debug(_: String, category _: LogCategory = .general) {
49+
// Do nothing in release builds
50+
}
51+
#endif
52+
}

AIBatteryHelper/README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# 🔧 AIBatteryHelper
2+
3+
**AIBatteryHelper** is the background daemon component of the AIBattery project. It is responsible for low-level communication with macOS's System Management Controller (SMC), enabling advanced battery management such as charging control, discharge mode, and power state monitoring.
4+
5+
This helper runs as a privileged service and communicates with the main app using `NSXPCConnection`, exposing a safe and extensible API to control hardware behavior.
6+
7+
---
8+
9+
## 📁 Project Structure
10+
11+
AIBatteryHelper/
12+
├── main.swift # Entry point for the daemon process
13+
├── Logging
14+
├── Scripts
15+
├── SMC/
16+
│ ├── Battery/ # Battery-specific control modules
17+
│ ├── Core/ # Low-level AppleSMC interaction
18+
│ ├── Error/ # Custom error definitions
19+
│ └── Power/ # Power source detection (AC)
20+
├── XPC/ # XPC protocol definitions and implementation
21+
22+
---
23+
24+
## 📦 Module Overview
25+
26+
### 🔋 `SMC/Battery/`
27+
28+
Encapsulates battery-related SMC logic, including charging, discharging, MagSafe detection, and adapter status.
29+
30+
- `Battery.swift` – Reads battery state and properties
31+
- `Charging.swift` – Enables/disables charging
32+
- `Magsafe.swift` – Detects MagSafe connection
33+
- `Adapter.swift` – Handles power adapter information
34+
35+
### ⚙️ `SMC/Core/`
36+
37+
Handles low-level SMC communication logic with Apple’s SMC hardware, including register access and binary encoding/decoding.
38+
39+
- `AppleSMC.swift` – Central controller for SMC read/write
40+
- `SMCKeys.swift` – Commonly used SMC keys
41+
- `SMCStructure.swift` – SMC data structure parsing
42+
- `SMCExtensions.swift` – Helpful type extensions and utilities
43+
44+
### `SMC/Error/`
45+
46+
Defines errors that can occur during SMC operations for improved stability and debugging.
47+
48+
- `SMCErrors.swift` – Structured SMC-related errors
49+
50+
### 🔌 `SMC/Power/`
51+
52+
Provides information about AC power connection status.
53+
54+
- `ACPower.swift` – Checks if the Mac is connected to external power
55+
56+
### 📡 `XPC/`
57+
58+
Defines the interprocess communication interface and implementation between the helper tool and the main tray app.
59+
60+
- `BatteryXPCProtocol.swift` – Public protocol exposed to the main app
61+
- `BatteryXPCService.swift` – Internal implementation of the protocol
62+
63+
---
64+
65+
## 🚀 Startup Flow
66+
67+
1. `main.swift` starts and registers the XPC service.
68+
2. The main app connects via `NSXPCConnection`.
69+
3. Battery-related methods are called through the exposed protocol.
70+
4. Internally, SMC access is performed through the Core modules.
71+
72+
---
73+
74+
## ⚠️ Notes
75+
76+
- All SMC operations must be performed in the helper process due to privilege requirements.
77+
- XPC methods should be thread-safe and scoped to single responsibilities.
78+
- AppleSMC access relies on IOKit and may require macOS 13+.
79+
80+
---
81+
82+
## 🛠 Developer Tips
83+
84+
- Always test on real hardware — the macOS simulator does not support SMC interaction.
85+
- Use `os_log` or `log stream` to trace and debug helper-side activity.
86+
- Structure your error handling with `SMCErrors` for better clarity and resilience.
87+
88+
---
89+
90+
## 📄 License
91+
92+
This module is proprietary and intended for internal use only as part of the AIBattery project.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// Adapter.swift
3+
// AIBattery
4+
//
5+
// Created by whuan132 on 3/31/25.
6+
// © 2025 COLLWEB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
// MARK: - AppleSMC Extension – Adapter Control
12+
13+
extension AppleSMC {
14+
/// Checks whether the adapter is currently enabled.
15+
func isAdapterEnabled() throws -> Bool {
16+
Logger.debug("isAdapterEnabled() called")
17+
18+
// Attempt to read the adapter SMC key.
19+
guard let smcVal = getValue(SMCKeys.adapterKey) else {
20+
throw SMCError.noData
21+
}
22+
23+
// Ensure the returned data has exactly 1 byte.
24+
guard smcVal.dataSize == 1 else {
25+
throw SMCError.invalidDataLength(Int(smcVal.dataSize))
26+
}
27+
28+
// In our convention, 0x0 indicates "enabled".
29+
let isEnabled = smcVal.bytes[0] == 0x0
30+
Logger.debug("isAdapterEnabled() returned \(isEnabled)")
31+
return isEnabled
32+
}
33+
34+
/// Enables the power adapter.
35+
func enableAdapter() throws {
36+
Logger.debug("enableAdapter() called")
37+
38+
// Write 0x0 to the adapter key to enable it.
39+
let result = write(SMCKeys.adapterKey, [0x0])
40+
if result != kIOReturnSuccess {
41+
throw SMCError.writeFailed
42+
}
43+
}
44+
45+
/// Disables the power adapter.
46+
func disableAdapter() throws {
47+
Logger.debug("disableAdapter() called")
48+
49+
// Write 0x1 to the adapter key to disable it.
50+
let result = write(SMCKeys.adapterKey, [0x1])
51+
if result != kIOReturnSuccess {
52+
throw SMCError.writeFailed
53+
}
54+
}
55+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// Battery.swift
3+
// AIBattery
4+
//
5+
// Created by whuan132 on 2/15/25.
6+
// © 2025 COLLWEB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
// MARK: - AppleSMC Extension – Battery Charge
12+
13+
extension AppleSMC {
14+
/// Reads the current battery charge percentage (0–100).
15+
func getBatteryCharge() throws -> Int {
16+
Logger.debug("batteryCharge() called")
17+
18+
// Attempt to read the battery charge key from SMC.
19+
guard let smcVal = getValue(SMCKeys.batteryChargeKey) else {
20+
throw SMCError.noData
21+
}
22+
23+
// Expect exactly 1 byte of data.
24+
guard smcVal.dataSize == 1 else {
25+
throw SMCError.invalidDataLength(Int(smcVal.dataSize))
26+
}
27+
28+
// Convert the single byte to Int (assumed range: 0–100).
29+
return Int(smcVal.bytes[0])
30+
}
31+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// Charging.swift
3+
// AIBattery
4+
//
5+
// Created by whuan132 on 3/31/25.
6+
// © 2025 COLLWEB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
// MARK: - AppleSMC Extension – Charging Control
12+
13+
extension AppleSMC {
14+
/// Returns whether battery charging is currently enabled.
15+
func isChargingEnabled() throws -> Bool {
16+
Logger.debug("isChargingEnabled() called")
17+
18+
// Attempt to read ChargingKey1 from SMC.
19+
guard let smcVal = getValue(SMCKeys.chargingKey1) else {
20+
throw SMCError.noData
21+
}
22+
23+
// Charging status should be exactly 1 byte.
24+
guard smcVal.dataSize == 1 else {
25+
throw SMCError.invalidDataLength(Int(smcVal.dataSize))
26+
}
27+
28+
// 0x0 indicates charging is enabled.
29+
let isEnabled = smcVal.bytes[0] == 0x0
30+
Logger.debug("isChargingEnabled() returned \(isEnabled)")
31+
return isEnabled
32+
}
33+
34+
/// Enables battery charging.
35+
func enableCharging() throws {
36+
Logger.debug("enableCharging() called")
37+
38+
// Write 0x0 to ChargingKey1.
39+
let result1 = write(SMCKeys.chargingKey1, [0x0])
40+
if result1 != kIOReturnSuccess {
41+
throw SMCError.writeFailed
42+
}
43+
44+
// Write 0x0 to ChargingKey2.
45+
let result2 = write(SMCKeys.chargingKey2, [0x0])
46+
if result2 != kIOReturnSuccess {
47+
throw SMCError.writeFailed
48+
}
49+
50+
// Also ensure adapter is enabled.
51+
try enableAdapter()
52+
}
53+
54+
/// Disables battery charging.
55+
func disableCharging() throws {
56+
Logger.debug("disableCharging() called")
57+
58+
// Write 0x2 to ChargingKey1.
59+
let result1 = write(SMCKeys.chargingKey1, [0x2])
60+
if result1 != kIOReturnSuccess {
61+
throw SMCError.writeFailed
62+
}
63+
64+
// Write 0x2 to ChargingKey2.
65+
let result2 = write(SMCKeys.chargingKey2, [0x2])
66+
if result2 != kIOReturnSuccess {
67+
throw SMCError.writeFailed
68+
}
69+
}
70+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// Magsafe.swift
3+
// AIBattery
4+
//
5+
// Created by whuan132 on 2/15/25.
6+
// © 2025 COLLWEB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// Represents the state of the MagSafe LED.
12+
enum MagSafeLedState: UInt8, CustomStringConvertible {
13+
case system = 0x00
14+
case off = 0x01
15+
case green = 0x03
16+
case orange = 0x04
17+
case errorOnce = 0x05
18+
case errorPermSlow = 0x06
19+
case errorPermFast = 0x07
20+
case errorPermOff = 0x19
21+
22+
public var description: String {
23+
switch self {
24+
case .system: "system"
25+
case .off: "off"
26+
case .green: "green"
27+
case .orange: "orange"
28+
case .errorOnce: "errorOnce"
29+
case .errorPermSlow: "errorPermSlow"
30+
case .errorPermFast: "errorPermFast"
31+
case .errorPermOff: "errorPermOff"
32+
}
33+
}
34+
}
35+
36+
// MARK: - AppleSMC Extension – MagSafe Control
37+
38+
extension AppleSMC {
39+
/// Sets the MagSafe LED to the specified state.
40+
func setMagSafeLedState(state: MagSafeLedState) throws {
41+
Logger.debug("SetMagSafeLedState(\(state)) called")
42+
let success = write(SMCKeys.magSafeLedKey, [state.rawValue])
43+
if success != kIOReturnSuccess {
44+
throw SMCError.writeFailed
45+
}
46+
}
47+
48+
/// Returns the current MagSafe LED state.
49+
func getMagSafeLedState() throws -> MagSafeLedState {
50+
Logger.debug("magSafeLedState() called")
51+
52+
guard let smcVal = getValue(SMCKeys.magSafeLedKey) else {
53+
throw SMCError.noData
54+
}
55+
guard smcVal.dataSize == 1 else {
56+
throw SMCError.invalidDataLength(Int(smcVal.dataSize))
57+
}
58+
59+
let raw = smcVal.bytes[0]
60+
61+
// Handle known LED states, fallback to .orange if undefined.
62+
if let state = MagSafeLedState(rawValue: raw),
63+
[.off, .green, .orange, .errorOnce, .errorPermSlow].contains(state)
64+
{
65+
Logger.debug("magSafeLedState() returned \(state)")
66+
return state
67+
} else if raw == 0x02 {
68+
Logger.debug("magSafeLedState() returned .green (special case)")
69+
return .green
70+
} else {
71+
Logger.debug("magSafeLedState() returned default (.orange)")
72+
return .orange
73+
}
74+
}
75+
76+
/// Returns true if a MagSafe LED exists on the device.
77+
func checkMagSafeExistence() -> Bool {
78+
getValue(SMCKeys.magSafeLedKey) != nil
79+
}
80+
81+
/// Sets the MagSafe LED to indicate charging (orange) or not charging (green).
82+
func setMagSafeCharging(charging: Bool) throws {
83+
try setMagSafeLedState(state: charging ? .orange : .green)
84+
}
85+
86+
/// Returns true if the MagSafe is currently indicating charging (orange LED).
87+
func isMagSafeCharging() throws -> Bool {
88+
try getMagSafeLedState() == .orange
89+
}
90+
}

0 commit comments

Comments
 (0)