macos-collector/Sources/CollectorCore/Wire.swift
105 lines · 3812 bytes
1import Foundation
2
3/// The wire schema version. **Must equal** `signal_schema::SCHEMA_VERSION` on the
4/// Rust side (v3 as of 0.3.0). A daemon rejects frames whose version it does not
5/// understand, so this is a hard cross-language contract.
6public let SCHEMA_VERSION: UInt16 = 3
7
8/// The collector domain. Discriminants **must match** Rust `Source::to_u8`.
9public enum Source: UInt8 {
10 case terminal = 0
11 case git = 1
12 case macos = 2
13 case hardware = 3
14}
15
16/// The enum-constrained metric name. Only the variants this collector emits are
17/// declared here; every discriminant **must match** Rust `SignalName::to_u8`.
18///
19/// The privacy boundary is unchanged for hardware: each of these is an aggregate
20/// `f64` scalar, and none of them carries a tag (all are emitted with
21/// `tag == nil`), exactly as `SignalName::allows_tag` is `false` for them.
22public enum SignalName: UInt8 {
23 case batteryPct = 17 // macos: battery charge percentage [0, 100]
24 case charging = 18 // macos: 1.0 charging, 0.0 not
25 case thermalState = 19 // macos: ProcessInfo thermal state 0..3
26 case batteryDrawW = 27 // hardware: instantaneous battery draw, watts
27 case cpuLoad = 28 // hardware: aggregate CPU busy fraction [0, 1]
28
29 /// Human-readable name, matching the Rust metric names (for `--once`).
30 public var label: String {
31 switch self {
32 case .batteryPct: return "battery_pct"
33 case .charging: return "charging"
34 case .thermalState: return "thermal_state"
35 case .batteryDrawW: return "battery_draw_w"
36 case .cpuLoad: return "cpu_load"
37 }
38 }
39}
40
41/// One flat signal record — the Swift mirror of Rust `Signal`. The only payload
42/// channel is `value: Double` (`f64`); there is deliberately no content field.
43public struct Signal {
44 public let schemaVersion: UInt16
45 public let ts: UInt64
46 public let source: Source
47 public let name: SignalName
48 public let value: Double
49 public let tag: String?
50
51 public init(ts: UInt64, source: Source, name: SignalName, value: Double, tag: String? = nil) {
52 self.schemaVersion = SCHEMA_VERSION
53 self.ts = ts
54 self.source = source
55 self.name = name
56 self.value = value
57 self.tag = tag
58 }
59}
60
61private extension Array where Element == UInt8 {
62 /// Append a fixed-width integer in little-endian order (matches Rust
63 /// `to_le_bytes`).
64 mutating func appendLE<T: FixedWidthInteger>(_ v: T) {
65 var le = v.littleEndian
66 Swift.withUnsafeBytes(of: &le) { append(contentsOf: $0) }
67 }
68}
69
70/// Encode a signal to its length-prefixed wire frame, byte-for-byte identical to
71/// Rust `signal_schema::wire::encode`.
72///
73/// ```text
74/// [u32 body_len]
75/// body = [u16 schema_version][u64 ts][u8 source][u8 name][f64 value]
76/// [u8 tag_present] ( [u16 tag_len][tag_len bytes utf8] )?
77/// ```
78/// All multi-byte integers and the `f64` (via its IEEE-754 bit pattern) are
79/// little-endian.
80public func encode(_ s: Signal) -> [UInt8] {
81 var body: [UInt8] = []
82 body.appendLE(s.schemaVersion) // u16
83 body.appendLE(s.ts) // u64
84 body.append(s.source.rawValue) // u8
85 body.append(s.name.rawValue) // u8
86 body.appendLE(s.value.bitPattern) // f64 as its u64 bit pattern, LE
87 if let tag = s.tag {
88 body.append(1)
89 let bytes = Array(tag.utf8)
90 body.appendLE(UInt16(bytes.count))
91 body.append(contentsOf: bytes)
92 } else {
93 body.append(0)
94 }
95
96 var frame: [UInt8] = []
97 frame.appendLE(UInt32(body.count))
98 frame.append(contentsOf: body)
99 return frame
100}
101
102/// Unix milliseconds, best-effort (matches Rust `now_millis`).
103public func nowMillis() -> UInt64 {
104 UInt64(Date().timeIntervalSince1970 * 1000.0)
105}