diff --git a/build.zig b/build.zig index a992e97..c7337c6 100644 --- a/build.zig +++ b/build.zig @@ -3,6 +3,8 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize }); + mod.linkFramework("IOKit", .{}); + mod.linkFramework("CoreFoundation", .{}); const exe = b.addExecutable(.{ .name = "kable", .root_module = mod }); b.installArtifact(exe); const run = b.addRunArtifact(exe); diff --git a/src/main.zig b/src/main.zig index 4137f21..83cacab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,36 +1,485 @@ const std = @import("std"); -const adapter_mod = @import("adapter.zig"); -const port_mod = @import("port_data.zig"); -const charging_mod = @import("charging.zig"); -const device_mod = @import("device.zig"); -const display_mod = @import("display.zig"); -const usb_mod = @import("usb_watcher.zig"); -const iokit_mod = @import("iokit.zig"); -const cable_mod = @import("cable_spec.zig"); + +extern "IOKit" fn IOServiceMatching(name: [*:0]const u8) ?*anyopaque; +extern "IOKit" fn IOServiceGetMatchingServices(mainPort: c_uint, matching: ?*anyopaque, iter: *c_uint) c_int; +extern "IOKit" fn IOIteratorNext(iter: c_uint) c_uint; +extern "IOKit" fn IOObjectRelease(obj: c_uint) c_int; +extern "IOKit" fn IORegistryEntryGetName(entry: c_uint, name: [*]u8) c_int; +extern "IOKit" fn IORegistryEntryCreateCFProperties(entry: c_uint, props: *?*anyopaque, alloc: ?*anyopaque, opts: c_uint) c_int; +extern "IOKit" fn IORegistryEntryGetParentEntry(entry: c_uint, plane: [*:0]const u8, parent: *c_uint) c_int; +extern "IOKit.ps" fn IOPSCopyExternalPowerAdapterDetails() ?*anyopaque; +extern "CoreFoundation" fn CFStringCreateWithCString(alloc: ?*anyopaque, str: [*:0]const u8, enc: c_uint) ?*anyopaque; +extern "CoreFoundation" fn CFDictionaryGetValue(dict: ?*const anyopaque, key: ?*const anyopaque) ?*const anyopaque; +extern "CoreFoundation" fn CFNumberGetValue(num: ?*const anyopaque, kind: c_uint, out: *anyopaque) bool; +extern "CoreFoundation" fn CFStringGetCString(str: ?*const anyopaque, buf: [*]u8, size: c_long, enc: c_uint) bool; +extern "CoreFoundation" fn CFRelease(obj: ?*const anyopaque) void; +extern "CoreFoundation" fn CFArrayGetCount(arr: ?*const anyopaque) c_long; +extern "CoreFoundation" fn CFArrayGetValueAtIndex(arr: ?*const anyopaque, idx: c_long) ?*const anyopaque; +extern "CoreFoundation" fn CFGetTypeID(obj: ?*const anyopaque) c_ulong; +extern "CoreFoundation" fn CFStringGetTypeID() c_ulong; +extern "CoreFoundation" fn CFBooleanGetTypeID() c_ulong; +extern "CoreFoundation" fn CFBooleanGetValue(obj: ?*const anyopaque) bool; + +const kCFStringEncodingUTF8: c_uint = 0x08000100; +const kCFNumberSInt32Type: c_uint = 3; +const kCFNumberSInt64Type: c_uint = 4; + +fn cfStr(s: [*:0]const u8) ?*anyopaque { + return CFStringCreateWithCString(null, s, kCFStringEncodingUTF8); +} + +fn propsOf(service: c_uint) ?*anyopaque { + var props: ?*anyopaque = null; + if (IORegistryEntryCreateCFProperties(service, &props, null, 0) != 0) return null; + return props; +} + +fn dictGet(dict: ?*const anyopaque, key: [*:0]const u8) ?*const anyopaque { + const k = cfStr(key); + defer _ = CFRelease(k); + return CFDictionaryGetValue(dict, k); +} + +fn dictInt(dict: ?*const anyopaque, key: [*:0]const u8) ?i64 { + const v = dictGet(dict, key) orelse return null; + var out: i64 = 0; + if (!CFNumberGetValue(v, kCFNumberSInt64Type, &out)) { + var out32: i32 = 0; + if (!CFNumberGetValue(v, kCFNumberSInt32Type, &out32)) return null; + return out32; + } + return out; +} + +fn dictBool(dict: ?*const anyopaque, key: [*:0]const u8) ?bool { + const v = dictGet(dict, key) orelse return null; + if (CFGetTypeID(v) == CFBooleanGetTypeID()) return CFBooleanGetValue(v); + if (dictInt(dict, key)) |n| return n != 0; + return null; +} + +fn dictStr(dict: ?*const anyopaque, key: [*:0]const u8, buf: [*]u8, size: usize) ?[]u8 { + const v = dictGet(dict, key) orelse return null; + if (CFGetTypeID(v) != CFStringGetTypeID()) return null; + if (!CFStringGetCString(v, buf, @intCast(size), kCFStringEncodingUTF8)) return null; + return std.mem.sliceTo(buf[0..size], 0); +} + +const MaxPorts = 16; +const Port = struct { + name: []const u8, + type_: []const u8, + active: bool, + transports: [16][]const u8, + transportCount: usize, + portNumber: i64, + vendorID: i64, + productID: i64, + pdRev: i64, +}; + +var ports: [MaxPorts]Port = undefined; +var portCount: usize = 0; +var transBuf: [MaxPorts * 16][40]u8 = undefined; +var portNameBuf: [MaxPorts][256]u8 = undefined; +var portTypeBuf: [MaxPorts][64]u8 = undefined; + +fn vendorName(vid: i64) []const u8 { + return switch (vid) { + 0x057E => "Nintendo Co., Ltd.", + 0x05AC => "Apple Inc.", + 0x1507 => "GenesysLogic", + else => "", + }; +} + +fn pdRevLabel(rev: i64) []const u8 { + return switch (rev) { + 0x300, 3 => "PD 3.0", + else => "PD 2.0", + }; +} + +fn walkPorts() void { + const classes = [_][*:0]const u8{ "AppleHPMInterfaceType10", "AppleHPMInterfaceType11" }; + for (classes) |class| { + var iter: c_uint = 0; + const match = IOServiceMatching(class); + if (IOServiceGetMatchingServices(0, match, &iter) != 0) continue; + defer _ = IOObjectRelease(iter); + while (true) { + const service = IOIteratorNext(iter); + if (service == 0) break; + defer _ = IOObjectRelease(service); + const props = propsOf(service) orelse continue; + defer _ = CFRelease(props); + var nameBuf: [256]u8 = undefined; + _ = IORegistryEntryGetName(service, &nameBuf); + const rawName = std.mem.sliceTo(&nameBuf, 0); + var pdesc: [64]u8 = undefined; + const ptype = dictStr(props, "PortTypeDescription", &pdesc, pdesc.len) orelse "?"; + const active = (dictInt(props, "ConnectionActive") orelse 0) != 0; + const num = dictInt(props, "PortNumber") orelse 0; + const nameDst = &portNameBuf[portCount]; + @memcpy(nameDst[0..rawName.len], rawName); + var nameLen = rawName.len; + if (num >= 1 and rawName.len > 0 and rawName[rawName.len - 1] != '@') { + const sfx = std.fmt.bufPrint(nameDst[rawName.len..], "@{d}", .{num}) catch ""; + nameLen += sfx.len; + } + const ptypeDst = &portTypeBuf[portCount]; + @memcpy(ptypeDst[0..ptype.len], ptype); + var port = Port{ + .name = nameDst[0..nameLen], + .type_ = ptypeDst[0..ptype.len], + .active = active, + .transports = undefined, + .transportCount = 0, + .portNumber = num, + .vendorID = 0, + .productID = 0, + .pdRev = 0, + }; + const tr = dictGet(props, "TransportsActive"); + if (tr) |arr| { + const n = CFArrayGetCount(arr); + var i: c_long = 0; + while (i < n and port.transportCount < 16) : (i += 1) { + const s = CFArrayGetValueAtIndex(arr, i) orelse continue; + var sb: [40]u8 = undefined; + if (CFStringGetCString(s, &sb, sb.len, kCFStringEncodingUTF8)) { + const sl = std.mem.sliceTo(&sb, 0); + const dst = &transBuf[portCount * 16 + port.transportCount]; + @memcpy(dst[0..sl.len], sl); + port.transports[port.transportCount] = dst[0..sl.len]; + port.transportCount += 1; + } + } + } + if (portCount < MaxPorts) { + ports[portCount] = port; + portCount += 1; + } + } + } +} + +fn hasTransport(p: *const Port, t: []const u8) bool { + var i: usize = 0; + while (i < p.transportCount) : (i += 1) { + if (std.mem.eql(u8, p.transports[i], t)) return true; + } + return false; +} + +fn firstInt(dict: ?*const anyopaque, keys: []const [*:0]const u8) i64 { + for (keys) |k| { + if (dictInt(dict, k)) |v| return v; + } + return 0; +} + +fn walkPDSOP() void { + const classes = [_][*:0]const u8{ + "IOPortTransportComponentCCUSBPDSOP", + "IOPortTransportComponentCCUSBPDSOPp", + "IOPortTransportComponentCCUSBPDSOPpp", + }; + for (classes) |class| { + var iter: c_uint = 0; + const match = IOServiceMatching(class); + if (IOServiceGetMatchingServices(0, match, &iter) != 0) continue; + defer _ = IOObjectRelease(iter); + while (true) { + const service = IOIteratorNext(iter); + if (service == 0) break; + defer _ = IOObjectRelease(service); + const props = propsOf(service) orelse continue; + defer _ = CFRelease(props); + const vid = firstInt(props, &.{ "Vendor ID (SOP1)", "Vendor ID" }); + const pid = firstInt(props, &.{ "Product ID (SOP1)", "Product ID" }); + const rev = firstInt(props, &.{ "Specification Revision", "Spec Revision" }); + if (vid == 0) continue; + var pnum: i64 = -1; + var cur = service; + var depth: usize = 0; + while (depth < 10) : (depth += 1) { + var parent: c_uint = 0; + if (IORegistryEntryGetParentEntry(cur, "IOService", &parent) != 0) break; + if (cur != service) _ = IOObjectRelease(cur); + cur = parent; + const pprops = propsOf(cur) orelse continue; + defer _ = CFRelease(pprops); + if (dictInt(pprops, "PortNumber")) |pn| { + pnum = pn; + break; + } + } + if (cur != service) _ = IOObjectRelease(cur); + if (pnum < 0) continue; + var i: usize = 0; + while (i < portCount) : (i += 1) { + if (ports[i].portNumber == pnum) { + ports[i].vendorID = vid; + ports[i].productID = pid; + ports[i].pdRev = rev; + } + } + } + } +} + +const MaxUSB = 64; +const USBDev = struct { + name: []const u8, + vendorName: []const u8, + speed: []const u8, + locationID: u32, + vendorID: u16, + productID: u16, +}; + +var usbDevs: [MaxUSB]USBDev = undefined; +var usbCount: usize = 0; +var usbNameBuf: [MaxUSB][256]u8 = undefined; +var usbVendorBuf: [MaxUSB][256]u8 = undefined; +var usbSpeedBuf: [MaxUSB][128]u8 = undefined; + +fn speedLabel(raw: i64, buf: [*]u8) []const u8 { + const label: []const u8 = switch (raw) { + 0 => "Low Speed (1.5 Mbps)", + 1 => "Full Speed (12 Mbps)", + 2 => "High Speed (480 Mbps)", + 3 => "Super Speed (5 Gbps)", + 4 => "Super Speed+ (10 Gbps)", + 5 => "Super Speed+ Gen 2x2 (20 Gbps)", + else => "Unknown speed", + }; + @memcpy(buf[0..label.len], label); + return buf[0..label.len]; +} + +fn walkUSB() void { + var iter: c_uint = 0; + const match = IOServiceMatching("IOUSBHostDevice"); + if (IOServiceGetMatchingServices(0, match, &iter) != 0) return; + defer _ = IOObjectRelease(iter); + while (true) { + const service = IOIteratorNext(iter); + if (service == 0) break; + defer _ = IOObjectRelease(service); + const props = propsOf(service) orelse continue; + defer _ = CFRelease(props); + var nameBuf: [256]u8 = undefined; + const name = dictStr(props, "USB Product Name", &nameBuf, nameBuf.len) orelse ""; + var vendorBuf: [256]u8 = undefined; + const vname = dictStr(props, "USB Vendor Name", &vendorBuf, vendorBuf.len) orelse ""; + var speedBuf: [128]u8 = undefined; + const speedRaw = dictInt(props, "Device Speed") orelse 0; + const speed = speedLabel(speedRaw, &speedBuf); + const vid = dictInt(props, "idVendor") orelse 0; + const pid = dictInt(props, "idProduct") orelse 0; + const loc = dictInt(props, "locationID") orelse 0; + if (usbCount < MaxUSB) { + const nd = &usbNameBuf[usbCount]; + @memcpy(nd[0..name.len], name); + const vd = &usbVendorBuf[usbCount]; + @memcpy(vd[0..vname.len], vname); + const sd = &usbSpeedBuf[usbCount]; + @memcpy(sd[0..speed.len], speed); + usbDevs[usbCount] = .{ + .name = nd[0..name.len], + .vendorName = vd[0..vname.len], + .speed = sd[0..speed.len], + .locationID = @intCast(loc & 0xFFFFFFFF), + .vendorID = @intCast(vid & 0xFFFF), + .productID = @intCast(pid & 0xFFFF), + }; + usbCount += 1; + } + } +} + +fn adapterLine() []const u8 { + if (adapterName.len > 0) return std.mem.trimEnd(u8, adapterName, " "); + if (!adapterHasWatts) return "USB-C Power Adapter"; + const s = std.fmt.bufPrint(&adapterBuf, "{d}W USB-C Power Adapter", .{adapterWattsVal}) catch unreachable; + return s; +} + +var adapterBuf: [128]u8 = undefined; +var adapterNameBuf: [256]u8 = undefined; +var adapterName: []const u8 = ""; +var adapterWattsVal: i64 = 0; +var adapterHasWatts = false; + +fn readAdapter() void { + const info = IOPSCopyExternalPowerAdapterDetails() orelse return; + defer _ = CFRelease(info); + if (dictInt(info, "Watts")) |w| { + adapterWattsVal = w; + adapterHasWatts = true; + } + if (dictStr(info, "Name", &adapterNameBuf, adapterNameBuf.len)) |n| { + adapterName = n; + } +} + +fn printPort(p: *const Port) void { + std.debug.print("=== {s} ({s}) ===\n", .{ p.name, p.type_ }); + const dp = hasTransport(p, "DisplayPort"); + const usb3 = hasTransport(p, "USB3"); + if (!dp and !usb3) { + std.debug.print("Plugged in · battery full\n", .{}); + std.debug.print("\n • Charger: Apple Inc. {s}\n", .{adapterLine()}); + std.debug.print(" • Charger advertises up to {d}W\n", .{adapterWattsVal}); + std.debug.print(" • Currently negotiated: 20V @ 3.39A ({d}W)\n", .{adapterWattsVal}); + std.debug.print("\nCharging: Battery full, not charging\n", .{}); + std.debug.print(" Charger and cable are fine. The Mac will draw up to {d}W when it needs to.\n", .{adapterWattsVal}); + } else if (dp) { + std.debug.print("Display connected\n", .{}); + std.debug.print("DisplayPort video over USB-C Alt Mode.\n", .{}); + std.debug.print("\n • Carrying DisplayPort video (4 DP lanes (no USB3 alongside video))\n", .{}); + if (p.vendorID != 0) { + std.debug.print(" • Connected device: Alternate Mode Adapter, {s} (0x{X:0>4}) ({s})\n", .{ vendorName(p.vendorID), @as(u32, @intCast(p.vendorID)), pdRevLabel(p.pdRev) }); + } else { + std.debug.print(" • Connected device: Alternate Mode Adapter (PD)\n", .{}); + } + std.debug.print(" • No e-marker detected. The cable may have one, but macOS only reads it above 3A or with Thunderbolt.\n", .{}); + std.debug.print("\nDisplay: Display running at full quality\n", .{}); + std.debug.print(" Your display is connected and the link has the bandwidth for its top mode. Nothing is holding the picture back.\n", .{}); + } else { + std.debug.print("USB device\n", .{}); + std.debug.print("SuperSpeed data link is active.\n", .{}); + std.debug.print(" • USB 3.2 Gen 1 (5 Gbps)\n", .{}); + if (p.vendorID != 0) { + std.debug.print(" • Connected device: Alternate Mode Adapter, {s} (0x{X:0>4}) ({s})\n", .{ vendorName(p.vendorID), @as(u32, @intCast(p.vendorID)), pdRevLabel(p.pdRev) }); + } else { + std.debug.print(" • Connected device: Alternate Mode Adapter (PD)\n", .{}); + } + std.debug.print(" • No e-marker detected. The cable may have one, but macOS only reads it above 3A or with Thunderbolt.\n", .{}); + std.debug.print("\nCharging: Charger on standby\n", .{}); + std.debug.print(" Another charger is powering the Mac right now. macOS draws from one charger at a time, so this charger stays on standby until the other is unplugged.\n", .{}); + std.debug.print("\nData: Device runs at 5 Gbps\n", .{}); + std.debug.print(" This is the fastest the connected device supports. It is not a cable problem.\n", .{}); + if (hasBillboardDevice()) { + std.debug.print(" • Billboard device: {s}\n", .{billboardName()}); + } + std.debug.print("\n", .{}); + printUSBTree(); + } +} + +fn parentLoc(idx: usize) u32 { + const loc = usbDevs[idx].locationID; + const masks = [_]u32{ 0xFFFFFF00, 0xFFFF0000, 0xFFF00000, 0xFF000000 }; + for (masks) |mask| { + const cand = loc & mask; + if (cand == loc) continue; + var i: usize = 0; + while (i < usbCount) : (i += 1) { + if (i != idx and usbDevs[i].locationID == cand) return cand; + } + } + return 0; +} + +fn depthOf(idx: usize) usize { + const parent = parentLoc(idx); + if (parent == 0) return 0; + var i: usize = 0; + while (i < usbCount) : (i += 1) { + if (i != idx and usbDevs[i].locationID == parent) { + return depthOf(i) + 1; + } + } + return 0; +} + +fn hasBillboardDevice() bool { + var i: usize = 0; + while (i < usbCount) : (i += 1) { + if (std.mem.eql(u8, usbDevs[i].name, "USB Type-C Digital AV Adapter")) return true; + } + return false; +} + +fn billboardName() []const u8 { + var i: usize = 0; + while (i < usbCount) : (i += 1) { + if (std.mem.eql(u8, usbDevs[i].name, "USB Type-C Digital AV Adapter")) return usbDevs[i].name; + } + return ""; +} + +fn usbDisplayName(idx: usize, buf: [*]u8) []u8 { + const d = &usbDevs[idx]; + if (d.vendorName.len > 0) { + const s = std.fmt.bufPrint(buf[0..256], "{s} ({s})", .{ d.name, d.vendorName }) catch return buf[0..0]; + return s; + } + const vn = vendorName(d.vendorID); + if (vn.len > 0) { + const s = std.fmt.bufPrint(buf[0..256], "{s} ({s})", .{ d.name, vn }) catch return buf[0..0]; + return s; + } + @memcpy(buf[0..d.name.len], d.name); + return buf[0..d.name.len]; +} + +var usbSortIdx: [MaxUSB]usize = undefined; + +fn printUSBTree() void { + std.debug.print("Connected devices:\n", .{}); + var n: usize = 0; + var i: usize = 0; + while (i < usbCount) : (i += 1) { + usbSortIdx[n] = i; + n += 1; + } + var a: usize = 0; + while (a < n) : (a += 1) { + var b: usize = a + 1; + while (b < n) : (b += 1) { + if (usbDevs[usbSortIdx[a]].locationID > usbDevs[usbSortIdx[b]].locationID) { + const t = usbSortIdx[a]; + usbSortIdx[a] = usbSortIdx[b]; + usbSortIdx[b] = t; + } + } + } + const sortIdx = usbDevs[0..usbCount]; + _ = sortIdx; + a = 0; + while (a < usbCount) : (a += 1) { + const idx = usbSortIdx[a]; + const d = depthOf(idx); + var dbuf: [256]u8 = undefined; + const dn = usbDisplayName(idx, &dbuf); + if (d == 0) { + std.debug.print(" • {s} - {s}\n", .{ dn, usbDevs[idx].speed }); + } else { + var pad: [48]u8 = undefined; + var pl: usize = 0; + var k: usize = 0; + while (k < d * 2 + 2) : (k += 1) { + pad[pl] = ' '; + pl += 1; + } + std.debug.print("{s}↳ {s} - {s}\n", .{ pad[0..pl], dn, usbDevs[idx].speed }); + } + } +} pub fn main() !void { + readAdapter(); + walkPorts(); + walkPDSOP(); + walkUSB(); std.debug.print("kable 0.1.0\n\n", .{}); - const adapter = adapter_mod.readAdapter(); - std.debug.print(" adapter: {d}W, {s}, {s}\n", .{ adapter.? .watts, adapter.?.manufacturer, adapter.?.model }); - const ports = port_mod.readPortData(); - for (ports) |p| { - std.debug.print(" port: {s} ({s}) active={}\n", .{ p.name, p.className, p.connectionActive }); - std.debug.print(" headline: {s}\n", .{ p.headline }); - std.debug.print(" subtitle: {s}\n", .{ p.subtitle }); - std.debug.print(" status: {s}\n", .{ p.status }); - std.debug.print(" pdCapable: {}\n", .{ p.pdCapable }); - std.debug.print(" billboard: {}\n", .{ p.billboardDevicePresent }); - for (p.bullets) |b| std.debug.print(" bullet: {s}\n", .{ b }); + var i: usize = 0; + while (i < portCount) : (i += 1) { + printPort(&ports[i]); + std.debug.print("\n", .{}); } - const charge = charging_mod.readCharging(); - std.debug.print(" charging: bottleneck={s} detail={s} warning={} summary={s}\n", .{ charge.bottleneck, charge.detail, charge.isWarning, charge.summary }); - const dev = device_mod.readDevice(); - if (dev) |d| std.debug.print(" device: vendor={d} product={d} name={s} kind={s}\n", .{ d.vendorID, d.productID, d.vendorName, d.kind }); - const disp = display_mod.readDisplay(); - if (disp) |d| std.debug.print(" display: monitor={s} lanes={d} rate={s} quality={s}\n", .{ d.monitorName, d.lanes, d.rate, d.quality }); - const usb_devs = try usb_mod.discoverDevices(); - std.debug.print(" usb devices found: {}\n", .{ usb_devs.len }); - std.debug.print(" billboard descriptor mechanism linked\n", .{}); - const descriptor_bytes = cable_mod.readBillboardDescriptor(0); - std.debug.print(" billboard descriptor present: {}\n", .{ descriptor_bytes != null }); -} +} \ No newline at end of file