diff --git a/LidAngleSensor.xcodeproj/project.pbxproj b/LidAngleSensor.xcodeproj/project.pbxproj index 36f66e1..eb7e710 100644 --- a/LidAngleSensor.xcodeproj/project.pbxproj +++ b/LidAngleSensor.xcodeproj/project.pbxproj @@ -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; }; diff --git a/LidAngleSensor/AppDelegate.h b/LidAngleSensor/AppDelegate.h index 5953a63..667496c 100644 --- a/LidAngleSensor/AppDelegate.h +++ b/LidAngleSensor/AppDelegate.h @@ -9,6 +9,7 @@ @interface AppDelegate : NSObject +@property (strong, nonatomic) NSWindow *window; @end diff --git a/LidAngleSensor/AppDelegate.m b/LidAngleSensor/AppDelegate.m index 7546e17..9e94430 100644 --- a/LidAngleSensor/AppDelegate.m +++ b/LidAngleSensor/AppDelegate.m @@ -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 diff --git a/LidAngleSensor/Base.lproj/MainMenu.xib b/LidAngleSensor/Base.lproj/MainMenu.xib index 63b54cf..d1e65ee 100644 --- a/LidAngleSensor/Base.lproj/MainMenu.xib +++ b/LidAngleSensor/Base.lproj/MainMenu.xib @@ -1,8 +1,7 @@ - + - - + @@ -11,12 +10,8 @@ - - - - - - + + @@ -680,19 +675,5 @@ - - - - - - - - - - - - - - diff --git a/LidAngleSensor/LidAngleSensor.h b/LidAngleSensor/LidAngleSensor.h new file mode 100644 index 0000000..5aba3be --- /dev/null +++ b/LidAngleSensor/LidAngleSensor.h @@ -0,0 +1,51 @@ +// +// LidAngleSensor.h +// LidAngleSensor +// +// Created by Sam on 2025-09-06. +// + +#import +#import +#import + +/** + * 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 diff --git a/LidAngleSensor/LidAngleSensor.m b/LidAngleSensor/LidAngleSensor.m new file mode 100644 index 0000000..61c85f4 --- /dev/null +++ b/LidAngleSensor/LidAngleSensor.m @@ -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 diff --git a/LidAngleSensor/sfx/CREAK_LOOP.wav b/LidAngleSensor/sfx/CREAK_LOOP.wav new file mode 100644 index 0000000..e1f968b Binary files /dev/null and b/LidAngleSensor/sfx/CREAK_LOOP.wav differ diff --git a/LidAngleSensor/sfx/GRAIN_MED1.wav b/LidAngleSensor/sfx/GRAIN_MED1.wav new file mode 100644 index 0000000..443dabc Binary files /dev/null and b/LidAngleSensor/sfx/GRAIN_MED1.wav differ diff --git a/LidAngleSensor/sfx/GRAIN_MED2.wav b/LidAngleSensor/sfx/GRAIN_MED2.wav new file mode 100644 index 0000000..4dad921 Binary files /dev/null and b/LidAngleSensor/sfx/GRAIN_MED2.wav differ diff --git a/LidAngleSensor/sfx/GRAIN_SHARP1.wav b/LidAngleSensor/sfx/GRAIN_SHARP1.wav new file mode 100644 index 0000000..0709269 Binary files /dev/null and b/LidAngleSensor/sfx/GRAIN_SHARP1.wav differ diff --git a/LidAngleSensor/sfx/GRAIN_SHARP2.wav b/LidAngleSensor/sfx/GRAIN_SHARP2.wav new file mode 100644 index 0000000..a85d503 Binary files /dev/null and b/LidAngleSensor/sfx/GRAIN_SHARP2.wav differ diff --git a/LidAngleSensor/sfx/GRAIN_SHARP3.wav b/LidAngleSensor/sfx/GRAIN_SHARP3.wav new file mode 100644 index 0000000..4859e37 Binary files /dev/null and b/LidAngleSensor/sfx/GRAIN_SHARP3.wav differ diff --git a/LidAngleSensor/sfx/GRAIN_SHARP4.wav b/LidAngleSensor/sfx/GRAIN_SHARP4.wav new file mode 100644 index 0000000..8bdf61e Binary files /dev/null and b/LidAngleSensor/sfx/GRAIN_SHARP4.wav differ diff --git a/LidAngleSensor/sfx/GRAIN_SML1.wav b/LidAngleSensor/sfx/GRAIN_SML1.wav new file mode 100644 index 0000000..5af52f0 Binary files /dev/null and b/LidAngleSensor/sfx/GRAIN_SML1.wav differ diff --git a/LidAngleSensor/sfx/GRAIN_SML2.wav b/LidAngleSensor/sfx/GRAIN_SML2.wav new file mode 100644 index 0000000..afd79d3 Binary files /dev/null and b/LidAngleSensor/sfx/GRAIN_SML2.wav differ diff --git a/LidAngleSensor/sfx/GRAIN_SML3.wav b/LidAngleSensor/sfx/GRAIN_SML3.wav new file mode 100644 index 0000000..1018ee9 Binary files /dev/null and b/LidAngleSensor/sfx/GRAIN_SML3.wav differ