This commit is contained in:
samhenrigold
2025-09-06 15:07:16 -04:00
parent f78027e498
commit e964eac899
16 changed files with 372 additions and 35 deletions

View File

@@ -246,13 +246,19 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = CG56CG5WCQ;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SELECTED_FILES = readonly;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
@@ -265,6 +271,12 @@
PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.LidAngleSensor;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
};
@@ -275,13 +287,19 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = CG56CG5WCQ;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SELECTED_FILES = readonly;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
@@ -294,6 +312,12 @@
PRODUCT_BUNDLE_IDENTIFIER = gold.samhenri.LidAngleSensor;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
};

View File

@@ -9,6 +9,7 @@
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (strong, nonatomic) NSWindow *window;
@end

View File

@@ -6,27 +6,154 @@
//
#import "AppDelegate.h"
#import "LidAngleSensor.h"
@interface AppDelegate ()
@property (strong) IBOutlet NSWindow *window;
@property (strong, nonatomic) LidAngleSensor *lidSensor;
@property (strong, nonatomic) NSTextField *angleLabel;
@property (strong, nonatomic) NSTextField *statusLabel;
@property (strong, nonatomic) NSTimer *updateTimer;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
[self createWindow];
[self initializeLidSensor];
[self startUpdatingDisplay];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
[self.updateTimer invalidate];
[self.lidSensor stopLidAngleUpdates];
}
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
return YES;
}
- (void)createWindow {
// Create the main window
NSRect windowFrame = NSMakeRect(100, 100, 400, 300);
self.window = [[NSWindow alloc] initWithContentRect:windowFrame
styleMask:NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable
backing:NSBackingStoreBuffered
defer:NO];
[self.window setTitle:@"MacBook Lid Angle Sensor"];
[self.window makeKeyAndOrderFront:nil];
[self.window center];
// Create the content view
NSView *contentView = [[NSView alloc] initWithFrame:windowFrame];
[self.window setContentView:contentView];
// Create title label
NSTextField *titleLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 220, 300, 40)];
[titleLabel setStringValue:@"MacBook Lid Angle Sensor"];
[titleLabel setFont:[NSFont boldSystemFontOfSize:18]];
[titleLabel setBezeled:NO];
[titleLabel setDrawsBackground:NO];
[titleLabel setEditable:NO];
[titleLabel setSelectable:NO];
[titleLabel setAlignment:NSTextAlignmentCenter];
[contentView addSubview:titleLabel];
// Create angle display label
self.angleLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 150, 300, 50)];
[self.angleLabel setStringValue:@"Initializing..."];
[self.angleLabel setFont:[NSFont monospacedSystemFontOfSize:24 weight:NSFontWeightMedium]];
[self.angleLabel setBezeled:NO];
[self.angleLabel setDrawsBackground:NO];
[self.angleLabel setEditable:NO];
[self.angleLabel setSelectable:NO];
[self.angleLabel setAlignment:NSTextAlignmentCenter];
[self.angleLabel setTextColor:[NSColor systemBlueColor]];
[contentView addSubview:self.angleLabel];
// Create status label
self.statusLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 100, 300, 30)];
[self.statusLabel setStringValue:@"Detecting sensor..."];
[self.statusLabel setFont:[NSFont systemFontOfSize:14]];
[self.statusLabel setBezeled:NO];
[self.statusLabel setDrawsBackground:NO];
[self.statusLabel setEditable:NO];
[self.statusLabel setSelectable:NO];
[self.statusLabel setAlignment:NSTextAlignmentCenter];
[self.statusLabel setTextColor:[NSColor secondaryLabelColor]];
[contentView addSubview:self.statusLabel];
// Create info label
NSTextField *infoLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(50, 30, 300, 60)];
[infoLabel setStringValue:@"This app reads the MacBook's internal lid angle sensor.\n0° = Closed, ~180° = Fully Open"];
[infoLabel setFont:[NSFont systemFontOfSize:12]];
[infoLabel setBezeled:NO];
[infoLabel setDrawsBackground:NO];
[infoLabel setEditable:NO];
[infoLabel setSelectable:NO];
[infoLabel setAlignment:NSTextAlignmentCenter];
[infoLabel setTextColor:[NSColor tertiaryLabelColor]];
[contentView addSubview:infoLabel];
}
- (void)initializeLidSensor {
self.lidSensor = [[LidAngleSensor alloc] init];
if (self.lidSensor.isAvailable) {
[self.statusLabel setStringValue:@"Sensor detected - Reading angle..."];
[self.statusLabel setTextColor:[NSColor systemGreenColor]];
} else {
[self.statusLabel setStringValue:@"Lid angle sensor not available on this device"];
[self.statusLabel setTextColor:[NSColor systemRedColor]];
[self.angleLabel setStringValue:@"Not Available"];
[self.angleLabel setTextColor:[NSColor systemRedColor]];
}
}
- (void)startUpdatingDisplay {
// Update the display every 100ms for smooth real-time updates
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(updateAngleDisplay)
userInfo:nil
repeats:YES];
}
- (void)updateAngleDisplay {
if (!self.lidSensor.isAvailable) {
return;
}
double angle = [self.lidSensor lidAngle];
if (angle == -2.0) {
[self.angleLabel setStringValue:@"Read Error"];
[self.angleLabel setTextColor:[NSColor systemOrangeColor]];
[self.statusLabel setStringValue:@"Failed to read sensor data"];
[self.statusLabel setTextColor:[NSColor systemOrangeColor]];
} else {
[self.angleLabel setStringValue:[NSString stringWithFormat:@"%.1f°", angle]];
[self.angleLabel setTextColor:[NSColor systemBlueColor]];
// Provide contextual status based on angle
NSString *status;
if (angle < 5.0) {
status = @"Lid is closed";
} else if (angle < 45.0) {
status = @"Lid slightly open";
} else if (angle < 90.0) {
status = @"Lid partially open";
} else if (angle < 135.0) {
status = @"Lid mostly open";
} else {
status = @"Lid fully open";
}
[self.statusLabel setStringValue:status];
[self.statusLabel setTextColor:[NSColor secondaryLabelColor]];
}
}
@end

View File

@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17150" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24123.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17150"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@@ -11,12 +10,8 @@
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="">
<connections>
<outlet property="window" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
@@ -680,19 +675,5 @@
</items>
<point key="canvasLocation" x="200" y="121"/>
</menu>
<window title="LidAngleSensor" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="335" y="390" width="480" height="360"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="480" height="360"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="top-28-M4D"/>
</connections>
<point key="canvasLocation" x="200" y="400"/>
</window>
</objects>
</document>

View File

@@ -0,0 +1,51 @@
//
// LidAngleSensor.h
// LidAngleSensor
//
// Created by Sam on 2025-09-06.
//
#import <Foundation/Foundation.h>
#import <IOKit/hid/IOHIDManager.h>
#import <IOKit/hid/IOHIDDevice.h>
/**
* LidAngleSensor provides access to the MacBook's internal lid angle sensor.
*
* This class interfaces with the HID device that reports the angle between
* the laptop lid and base, providing real-time angle measurements in degrees.
*
* Device Specifications (discovered through reverse engineering):
* - Apple device: VID=0x05AC, PID=0x8104
* - HID Usage: Sensor page (0x0020), Orientation usage (0x008A)
* - Data format: 16-bit angle value in centidegrees (0.01° resolution)
* - Range: 0-360 degrees
*/
@interface LidAngleSensor : NSObject
@property (nonatomic, assign, readonly) IOHIDDeviceRef hidDevice;
@property (nonatomic, assign, readonly) BOOL isAvailable;
/**
* Initialize and connect to the lid angle sensor.
* @return Initialized sensor instance, or nil if sensor not available
*/
- (instancetype)init;
/**
* Read the current lid angle.
* @return Angle in degrees (0-360), or -2.0 if read failed
*/
- (double)lidAngle;
/**
* Start lid angle monitoring (called automatically in init).
*/
- (void)startLidAngleUpdates;
/**
* Stop lid angle monitoring and release resources.
*/
- (void)stopLidAngleUpdates;
@end

View File

@@ -0,0 +1,153 @@
//
// LidAngleSensor.m
// LidAngleSensor
//
// Created by Sam on 2025-09-06.
//
#import "LidAngleSensor.h"
@interface LidAngleSensor ()
@property (nonatomic, assign) IOHIDDeviceRef hidDevice;
@end
@implementation LidAngleSensor
- (instancetype)init {
self = [super init];
if (self) {
_hidDevice = [self findLidAngleSensor];
if (_hidDevice) {
IOHIDDeviceOpen(_hidDevice, kIOHIDOptionsTypeNone);
NSLog(@"[LidAngleSensor] Successfully initialized lid angle sensor");
} else {
NSLog(@"[LidAngleSensor] Failed to find lid angle sensor");
}
}
return self;
}
- (void)dealloc {
[self stopLidAngleUpdates];
}
- (BOOL)isAvailable {
return _hidDevice != NULL;
}
- (IOHIDDeviceRef)findLidAngleSensor {
IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
if (!manager) {
NSLog(@"[LidAngleSensor] Failed to create IOHIDManager");
return NULL;
}
if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
NSLog(@"[LidAngleSensor] Failed to open IOHIDManager");
CFRelease(manager);
return NULL;
}
// Use Generic Desktop + Mouse criteria to find candidate devices
// This broader search allows us to find the lid sensor among other HID devices
NSDictionary *matchingDict = @{
@"UsagePage": @(0x0001), // Generic Desktop
@"Usage": @(0x0003), // Mouse
};
IOHIDManagerSetDeviceMatching(manager, (__bridge CFDictionaryRef)matchingDict);
CFSetRef devices = IOHIDManagerCopyDevices(manager);
IOHIDDeviceRef device = NULL;
if (devices && CFSetGetCount(devices) > 0) {
NSLog(@"[LidAngleSensor] Found %ld devices, looking for sensor...", CFSetGetCount(devices));
const void **deviceArray = malloc(sizeof(void*) * CFSetGetCount(devices));
CFSetGetValues(devices, deviceArray);
// Search for the specific lid angle sensor device
// Discovered through reverse engineering: Apple device with Sensor page
for (CFIndex i = 0; i < CFSetGetCount(devices); i++) {
IOHIDDeviceRef currentDevice = (IOHIDDeviceRef)deviceArray[i];
CFNumberRef vendorID = IOHIDDeviceGetProperty(currentDevice, CFSTR("VendorID"));
CFNumberRef productID = IOHIDDeviceGetProperty(currentDevice, CFSTR("ProductID"));
CFNumberRef usagePage = IOHIDDeviceGetProperty(currentDevice, CFSTR("PrimaryUsagePage"));
CFNumberRef usage = IOHIDDeviceGetProperty(currentDevice, CFSTR("PrimaryUsage"));
int vid = 0, pid = 0, up = 0, u = 0;
if (vendorID) CFNumberGetValue(vendorID, kCFNumberIntType, &vid);
if (productID) CFNumberGetValue(productID, kCFNumberIntType, &pid);
if (usagePage) CFNumberGetValue(usagePage, kCFNumberIntType, &up);
if (usage) CFNumberGetValue(usage, kCFNumberIntType, &u);
// Target the specific lid angle sensor device
// VID=0x05AC (Apple), PID=0x8104, UsagePage=0x0020 (Sensor), Usage=0x008A (Orientation)
if (vid == 0x05AC && pid == 0x8104 && up == 0x0020 && u == 0x008A) {
device = (IOHIDDeviceRef)CFRetain(currentDevice);
NSLog(@"[LidAngleSensor] Found lid angle sensor device: VID=0x%04X, PID=0x%04X", vid, pid);
break;
}
}
free(deviceArray);
}
if (devices) CFRelease(devices);
IOHIDManagerClose(manager, kIOHIDOptionsTypeNone);
CFRelease(manager);
return device;
}
- (double)lidAngle {
if (!_hidDevice) {
return -2.0; // Device not available
}
// Read lid angle using discovered parameters:
// Feature Report Type 2, Report ID 1, returns 3 bytes with 16-bit angle in centidegrees
uint8_t report[8] = {0};
CFIndex reportLength = sizeof(report);
IOReturn result = IOHIDDeviceGetReport(_hidDevice,
kIOHIDReportTypeFeature, // Type 2
1, // Report ID 1
report,
&reportLength);
if (result == kIOReturnSuccess && reportLength >= 3) {
// Data format: [report_id, angle_low, angle_high]
// Example: [01 72 00] = 0x7201 centidegrees = 291.85 degrees
uint16_t rawValue = *(uint16_t*)(report);
double angle = rawValue * 0.01; // Convert centidegrees to degrees
return angle;
}
return -2.0;
}
- (void)startLidAngleUpdates {
if (!_hidDevice) {
_hidDevice = [self findLidAngleSensor];
if (_hidDevice) {
NSLog(@"[LidAngleSensor] Starting lid angle updates");
IOHIDDeviceOpen(_hidDevice, kIOHIDOptionsTypeNone);
} else {
NSLog(@"[LidAngleSensor] Lid angle sensor is not supported");
}
}
}
- (void)stopLidAngleUpdates {
if (_hidDevice) {
NSLog(@"[LidAngleSensor] Stopping lid angle updates");
IOHIDDeviceClose(_hidDevice, kIOHIDOptionsTypeNone);
CFRelease(_hidDevice);
_hidDevice = NULL;
}
}
@end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.