@capgo/capacitor-device-info 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,279 @@
1
+ import CoreMotion
2
+ import Darwin
3
+ import Foundation
4
+ import Metal
5
+
6
+ @objc public class DeviceInfo: NSObject {
7
+ private var previousCpuLoad: host_cpu_load_info?
8
+
9
+ @objc public func getInfo() -> [String: Any] {
10
+ var result: [String: Any] = [
11
+ "timestamp": currentTimestamp(),
12
+ "platform": "ios",
13
+ "cpu": cpuInfo(),
14
+ "memory": memoryInfo(),
15
+ "storage": storageInfo(),
16
+ "thermalState": thermalState(),
17
+ "lowPowerMode": ProcessInfo.processInfo.isLowPowerModeEnabled,
18
+ "sensors": onboardSensorsInfo()
19
+ ]
20
+
21
+ if let gpu = gpuInfo() {
22
+ result["gpu"] = gpu
23
+ }
24
+
25
+ return result
26
+ }
27
+
28
+ @objc public func getPluginVersion() -> String {
29
+ return "8.0.0"
30
+ }
31
+
32
+ private func cpuInfo() -> [String: Any] {
33
+ var info: [String: Any] = [
34
+ "cores": ProcessInfo.processInfo.processorCount,
35
+ "activeCores": ProcessInfo.processInfo.activeProcessorCount,
36
+ "architecture": cpuArchitecture(),
37
+ "model": modelIdentifier()
38
+ ]
39
+
40
+ if let usagePercent = cpuUsagePercent() {
41
+ info["usagePercent"] = usagePercent
42
+ }
43
+
44
+ return info
45
+ }
46
+
47
+ private func cpuUsagePercent() -> Double? {
48
+ guard let currentLoad = currentCpuLoad() else {
49
+ return nil
50
+ }
51
+
52
+ defer {
53
+ previousCpuLoad = currentLoad
54
+ }
55
+
56
+ guard let previousLoad = previousCpuLoad else {
57
+ return nil
58
+ }
59
+
60
+ let user = tickDelta(currentLoad.cpu_ticks.0, previousLoad.cpu_ticks.0)
61
+ let system = tickDelta(currentLoad.cpu_ticks.1, previousLoad.cpu_ticks.1)
62
+ let idle = tickDelta(currentLoad.cpu_ticks.2, previousLoad.cpu_ticks.2)
63
+ let nice = tickDelta(currentLoad.cpu_ticks.3, previousLoad.cpu_ticks.3)
64
+ let total = user + system + idle + nice
65
+
66
+ guard total > 0 else {
67
+ return nil
68
+ }
69
+
70
+ return clampPercent((Double(total - idle) / Double(total)) * 100)
71
+ }
72
+
73
+ private func currentCpuLoad() -> host_cpu_load_info? {
74
+ var cpuInfo = host_cpu_load_info()
75
+ var count = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info>.stride / MemoryLayout<integer_t>.stride)
76
+
77
+ let result = withUnsafeMutablePointer(to: &cpuInfo) {
78
+ $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
79
+ host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &count)
80
+ }
81
+ }
82
+
83
+ return result == KERN_SUCCESS ? cpuInfo : nil
84
+ }
85
+
86
+ private func memoryInfo() -> [String: Any] {
87
+ let totalBytes = Double(ProcessInfo.processInfo.physicalMemory)
88
+ var info: [String: Any] = [
89
+ "totalBytes": totalBytes,
90
+ "pressure": "normal"
91
+ ]
92
+
93
+ if let vmInfo = vmMemoryInfo(totalBytes: totalBytes) {
94
+ info.merge(vmInfo) { _, new in new }
95
+ }
96
+
97
+ if let appUsedBytes = appMemoryBytes() {
98
+ info["appUsedBytes"] = appUsedBytes
99
+ }
100
+
101
+ return info
102
+ }
103
+
104
+ private func vmMemoryInfo(totalBytes: Double) -> [String: Any]? {
105
+ var stats = vm_statistics64()
106
+ var count = mach_msg_type_number_t(MemoryLayout<vm_statistics64_data_t>.stride / MemoryLayout<integer_t>.stride)
107
+
108
+ let result = withUnsafeMutablePointer(to: &stats) {
109
+ $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
110
+ host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count)
111
+ }
112
+ }
113
+
114
+ guard result == KERN_SUCCESS else {
115
+ return nil
116
+ }
117
+
118
+ var pageSize: vm_size_t = 0
119
+ host_page_size(mach_host_self(), &pageSize)
120
+ let pageBytes = Double(pageSize)
121
+ let freeBytes = Double(UInt64(stats.free_count) + UInt64(stats.inactive_count)) * pageBytes
122
+ let usedBytes = max(totalBytes - freeBytes, 0)
123
+
124
+ return [
125
+ "freeBytes": freeBytes,
126
+ "usedBytes": usedBytes,
127
+ "usedPercent": percent(usedBytes, totalBytes)
128
+ ]
129
+ }
130
+
131
+ private func appMemoryBytes() -> Double? {
132
+ var info = task_vm_info_data_t()
133
+ var count = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.stride / MemoryLayout<natural_t>.stride)
134
+
135
+ let result = withUnsafeMutablePointer(to: &info) {
136
+ $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
137
+ task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
138
+ }
139
+ }
140
+
141
+ return result == KERN_SUCCESS ? Double(info.phys_footprint) : nil
142
+ }
143
+
144
+ private func storageInfo() -> [String: Any] {
145
+ let url = URL(fileURLWithPath: NSHomeDirectory())
146
+
147
+ do {
148
+ let values = try url.resourceValues(forKeys: [
149
+ .volumeTotalCapacityKey,
150
+ .volumeAvailableCapacityKey,
151
+ .volumeAvailableCapacityForImportantUsageKey
152
+ ])
153
+ let totalBytes = values.volumeTotalCapacity.map(Double.init)
154
+ let importantFreeBytes = values.volumeAvailableCapacityForImportantUsage.map(Double.init)
155
+ let freeBytes = importantFreeBytes ?? values.volumeAvailableCapacity.map(Double.init)
156
+ var info: [String: Any] = [:]
157
+
158
+ if let totalBytes {
159
+ info["totalBytes"] = totalBytes
160
+ }
161
+ if let freeBytes {
162
+ info["freeBytes"] = freeBytes
163
+ }
164
+ if let totalBytes, let freeBytes {
165
+ let usedBytes = max(totalBytes - freeBytes, 0)
166
+ info["usedBytes"] = usedBytes
167
+ info["usedPercent"] = percent(usedBytes, totalBytes)
168
+ }
169
+
170
+ return info
171
+ } catch {
172
+ return [:]
173
+ }
174
+ }
175
+
176
+ private func gpuInfo() -> [String: Any]? {
177
+ guard let device = MTLCreateSystemDefaultDevice() else {
178
+ return nil
179
+ }
180
+
181
+ return [
182
+ "api": "metal",
183
+ "vendor": "Apple",
184
+ "renderer": device.name
185
+ ]
186
+ }
187
+
188
+ private func onboardSensorsInfo() -> [String: Any] {
189
+ let motionManager = CMMotionManager()
190
+ var availableSensors: [[String: Any]] = []
191
+
192
+ if motionManager.isAccelerometerAvailable {
193
+ availableSensors.append(sensorDescriptor(type: "accelerometer", name: "Accelerometer"))
194
+ }
195
+ if motionManager.isGyroAvailable {
196
+ availableSensors.append(sensorDescriptor(type: "gyroscope", name: "Gyroscope"))
197
+ }
198
+ if motionManager.isMagnetometerAvailable {
199
+ availableSensors.append(sensorDescriptor(type: "magnetometer", name: "Magnetometer"))
200
+ }
201
+ if motionManager.isDeviceMotionAvailable {
202
+ availableSensors.append(sensorDescriptor(type: "deviceMotion", name: "Device Motion"))
203
+ }
204
+ if CMAltimeter.isRelativeAltitudeAvailable() {
205
+ availableSensors.append(sensorDescriptor(type: "barometer", name: "Barometer"))
206
+ }
207
+
208
+ return [
209
+ "availableSensors": availableSensors,
210
+ "readings": []
211
+ ]
212
+ }
213
+
214
+ private func sensorDescriptor(type: String, name: String) -> [String: Any] {
215
+ return [
216
+ "type": type,
217
+ "name": name,
218
+ "vendor": "Apple"
219
+ ]
220
+ }
221
+
222
+ private func thermalState() -> String {
223
+ switch ProcessInfo.processInfo.thermalState {
224
+ case .nominal:
225
+ return "nominal"
226
+ case .fair:
227
+ return "fair"
228
+ case .serious:
229
+ return "serious"
230
+ case .critical:
231
+ return "critical"
232
+ @unknown default:
233
+ return "unknown"
234
+ }
235
+ }
236
+
237
+ private func modelIdentifier() -> String {
238
+ var systemInfo = utsname()
239
+ uname(&systemInfo)
240
+
241
+ return withUnsafeBytes(of: &systemInfo.machine) { rawBuffer in
242
+ let pointer = rawBuffer.bindMemory(to: CChar.self).baseAddress
243
+ return pointer.map { String(cString: $0) } ?? "unknown"
244
+ }
245
+ }
246
+
247
+ private func cpuArchitecture() -> String {
248
+ #if arch(arm64)
249
+ return "arm64"
250
+ #elseif arch(arm)
251
+ return "arm"
252
+ #elseif arch(x86_64)
253
+ return "x86_64"
254
+ #elseif arch(i386)
255
+ return "i386"
256
+ #else
257
+ return "unknown"
258
+ #endif
259
+ }
260
+
261
+ private func currentTimestamp() -> Double {
262
+ return Date().timeIntervalSince1970 * 1000
263
+ }
264
+
265
+ private func tickDelta(_ current: natural_t, _ previous: natural_t) -> UInt64 {
266
+ return UInt64(current >= previous ? current - previous : 0)
267
+ }
268
+
269
+ private func percent(_ used: Double, _ total: Double) -> Double {
270
+ guard total > 0 else {
271
+ return 0
272
+ }
273
+ return clampPercent((used / total) * 100)
274
+ }
275
+
276
+ private func clampPercent(_ value: Double) -> Double {
277
+ return min(max(value, 0), 100)
278
+ }
279
+ }
@@ -0,0 +1,157 @@
1
+ import Capacitor
2
+ import Foundation
3
+
4
+ @objc(DeviceInfoPlugin)
5
+ public class DeviceInfoPlugin: CAPPlugin, CAPBridgedPlugin {
6
+ private let pluginVersion = "8.0.0"
7
+ private let defaultIntervalMs = 1000
8
+ private let minimumIntervalMs = 250
9
+
10
+ public let identifier = "DeviceInfoPlugin"
11
+ public let jsName = "DeviceInfo"
12
+ public let pluginMethods: [CAPPluginMethod] = [
13
+ CAPPluginMethod(name: "getInfo", returnType: CAPPluginReturnPromise),
14
+ CAPPluginMethod(name: "startMonitoring", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "stopMonitoring", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "isMonitoring", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "removeAllListeners", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
19
+ ]
20
+
21
+ private let implementation = DeviceInfo()
22
+ private var timer: Timer?
23
+ private var intervalMs: Int?
24
+ private var startedAt: Double?
25
+ private var sampleLimit: Int?
26
+ private var stopAt: Double?
27
+ private var samplesEmitted = 0
28
+
29
+ @objc func getInfo(_ call: CAPPluginCall) {
30
+ call.resolve(implementation.getInfo())
31
+ }
32
+
33
+ @objc func startMonitoring(_ call: CAPPluginCall) {
34
+ stopMonitoringInternal()
35
+
36
+ let effectiveIntervalMs = max(call.getInt("intervalMs") ?? defaultIntervalMs, minimumIntervalMs)
37
+ let now = currentTimestamp()
38
+ intervalMs = effectiveIntervalMs
39
+ startedAt = now
40
+ samplesEmitted = 0
41
+ sampleLimit = positiveInt(call.getInt("sampleCount"))
42
+
43
+ if let durationMs = positiveDouble(call.getDouble("durationMs")) {
44
+ stopAt = now + durationMs
45
+ }
46
+
47
+ if call.getBool("emitImmediately") != false {
48
+ emitSample()
49
+ }
50
+
51
+ if startedAt != nil {
52
+ let timer = Timer(timeInterval: Double(effectiveIntervalMs) / 1000, repeats: true) { [weak self] _ in
53
+ self?.emitSample()
54
+ }
55
+ RunLoop.main.add(timer, forMode: .common)
56
+ self.timer = timer
57
+ }
58
+
59
+ call.resolve([
60
+ "monitoring": startedAt != nil,
61
+ "intervalMs": effectiveIntervalMs,
62
+ "startedAt": now
63
+ ])
64
+ }
65
+
66
+ @objc func stopMonitoring(_ call: CAPPluginCall) {
67
+ stopMonitoringInternal()
68
+ call.resolve(["monitoring": false])
69
+ }
70
+
71
+ @objc func isMonitoring(_ call: CAPPluginCall) {
72
+ var result: [String: Any] = [
73
+ "monitoring": timer != nil
74
+ ]
75
+
76
+ if let intervalMs {
77
+ result["intervalMs"] = intervalMs
78
+ }
79
+ if let startedAt {
80
+ result["startedAt"] = startedAt
81
+ }
82
+ if timer != nil {
83
+ result["samplesEmitted"] = samplesEmitted
84
+ }
85
+
86
+ call.resolve(result)
87
+ }
88
+
89
+ @objc override public func removeAllListeners(_ call: CAPPluginCall) {
90
+ super.removeAllListeners(call)
91
+ }
92
+
93
+ @objc func getPluginVersion(_ call: CAPPluginCall) {
94
+ call.resolve(["version": pluginVersion])
95
+ }
96
+
97
+ deinit {
98
+ stopMonitoringInternal()
99
+ }
100
+
101
+ private func emitSample() {
102
+ guard let startedAt else {
103
+ return
104
+ }
105
+
106
+ var sample = implementation.getInfo()
107
+ samplesEmitted += 1
108
+ let timestamp = sample["timestamp"] as? Double ?? currentTimestamp()
109
+ sample["sequence"] = samplesEmitted
110
+ sample["startedAt"] = startedAt
111
+ sample["elapsedMs"] = timestamp - startedAt
112
+
113
+ notifyListeners("deviceInfoUpdate", data: sample)
114
+
115
+ if shouldStopMonitoring() {
116
+ stopMonitoringInternal()
117
+ }
118
+ }
119
+
120
+ private func shouldStopMonitoring() -> Bool {
121
+ if let sampleLimit, samplesEmitted >= sampleLimit {
122
+ return true
123
+ }
124
+ if let stopAt, currentTimestamp() >= stopAt {
125
+ return true
126
+ }
127
+ return false
128
+ }
129
+
130
+ private func stopMonitoringInternal() {
131
+ timer?.invalidate()
132
+ timer = nil
133
+ intervalMs = nil
134
+ startedAt = nil
135
+ sampleLimit = nil
136
+ stopAt = nil
137
+ samplesEmitted = 0
138
+ }
139
+
140
+ private func positiveInt(_ value: Int?) -> Int? {
141
+ guard let value, value > 0 else {
142
+ return nil
143
+ }
144
+ return value
145
+ }
146
+
147
+ private func positiveDouble(_ value: Double?) -> Double? {
148
+ guard let value, value > 0 else {
149
+ return nil
150
+ }
151
+ return value
152
+ }
153
+
154
+ private func currentTimestamp() -> Double {
155
+ return Date().timeIntervalSince1970 * 1000
156
+ }
157
+ }
@@ -0,0 +1,22 @@
1
+ import XCTest
2
+ @testable import DeviceInfoPlugin
3
+
4
+ class DeviceInfoTests: XCTestCase {
5
+ func testGetInfo() {
6
+ let implementation = DeviceInfo()
7
+ let result = implementation.getInfo()
8
+
9
+ XCTAssertEqual("ios", result["platform"] as? String)
10
+ XCTAssertNotNil(result["cpu"])
11
+ XCTAssertNotNil(result["memory"])
12
+ XCTAssertNotNil(result["storage"])
13
+ XCTAssertNotNil(result["sensors"])
14
+ }
15
+
16
+ func testGetPluginVersion() {
17
+ let implementation = DeviceInfo()
18
+ let result = implementation.getPluginVersion()
19
+
20
+ XCTAssertEqual("8.0.0", result)
21
+ }
22
+ }
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@capgo/capacitor-device-info",
3
+ "version": "8.0.0",
4
+ "description": "Capacitor plugin for reading CPU, memory, GPU, storage, and onboard sensor metrics.",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "CapgoCapacitorDeviceInfo.podspec"
17
+ ],
18
+ "author": "Cap-go <contact@capgo.app>",
19
+ "license": "MPL-2.0",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Cap-go/capacitor-device-info.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/Cap-go/capacitor-device-info/issues"
26
+ },
27
+ "homepage": "https://capgo.app/docs/plugins/device-info/",
28
+ "keywords": [
29
+ "capacitor",
30
+ "plugin",
31
+ "device",
32
+ "metrics",
33
+ "cpu",
34
+ "memory",
35
+ "gpu",
36
+ "storage",
37
+ "sensors",
38
+ "temperature",
39
+ "barometer",
40
+ "capgo"
41
+ ],
42
+ "scripts": {
43
+ "verify": "bun run verify:ios && bun run verify:android && bun run verify:web",
44
+ "verify:ios": "xcodebuild -scheme CapgoCapacitorDeviceInfo -destination generic/platform=iOS",
45
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
46
+ "verify:web": "bun run build",
47
+ "lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint",
48
+ "fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format",
49
+ "eslint": "eslint \"**/*.ts\"",
50
+ "prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
51
+ "swiftlint": "node-swiftlint",
52
+ "docgen": "docgen --api DeviceInfoPlugin --output-readme README.md --output-json dist/docs.json",
53
+ "build": "bun run clean && bun run docgen && tsc && rollup -c rollup.config.mjs",
54
+ "clean": "rimraf ./dist",
55
+ "watch": "tsc --watch",
56
+ "prepublishOnly": "bun run build",
57
+ "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
58
+ },
59
+ "devDependencies": {
60
+ "@capacitor/android": "^8.3.4",
61
+ "@capacitor/cli": "^8.3.4",
62
+ "@capacitor/core": "^8.3.4",
63
+ "@capacitor/docgen": "^0.3.1",
64
+ "@capacitor/ios": "^8.3.4",
65
+ "@eslint/js": "^10.0.1",
66
+ "@ionic/prettier-config": "^4.0.0",
67
+ "@ionic/swiftlint-config": "^2.0.0",
68
+ "@types/node": "^25.9.1",
69
+ "eslint": "^10.4.0",
70
+ "eslint-config-prettier": "^10.1.8",
71
+ "eslint-plugin-import": "^2.32.0",
72
+ "husky": "^9.1.7",
73
+ "prettier": "^3.8.3",
74
+ "prettier-plugin-java": "^2.9.2",
75
+ "prettier-pretty-check": "^0.2.0",
76
+ "rimraf": "^6.1.3",
77
+ "rollup": "^4.60.4",
78
+ "swiftlint": "^2.0.0",
79
+ "typescript": "^6.0.3",
80
+ "typescript-eslint": "^8.59.4"
81
+ },
82
+ "peerDependencies": {
83
+ "@capacitor/core": ">=8.0.0"
84
+ },
85
+ "prettier": "@ionic/prettier-config",
86
+ "swiftlint": "@ionic/swiftlint-config",
87
+ "capacitor": {
88
+ "ios": {
89
+ "src": "ios"
90
+ },
91
+ "android": {
92
+ "src": "android"
93
+ }
94
+ }
95
+ }