Last active
September 8, 2025 13:25
-
-
Save pachacamac/ba362aaa4bfce42c27f243a16a1601e3 to your computer and use it in GitHub Desktop.
get lidangle for macbooks call with -c for continuous
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| if [[ "$OSTYPE" != "darwin"* ]]; then | |
| echo "Error: This script only works on macOS" >&2 | |
| exit 1 | |
| fi | |
| if [ ! -x ./lidangler ]; then | |
| echo "Lidangler not found. Compiling..." | |
| cat > .lidangler.c << 'EOF' | |
| #include <IOKit/IOKitLib.h> | |
| #include <IOKit/hid/IOHIDManager.h> | |
| #include <IOKit/hid/IOHIDDevice.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <unistd.h> | |
| #include <string.h> | |
| int get_lid_angle(IOHIDDeviceRef device) { | |
| uint8_t report[8] = {0}; | |
| CFIndex reportLength = sizeof(report); | |
| // Try different report IDs | |
| for (int reportId = 1; reportId <= 3; reportId++) { | |
| IOReturn result = IOHIDDeviceGetReport(device, | |
| kIOHIDReportTypeFeature, | |
| reportId, | |
| report, | |
| &reportLength); | |
| if (result == kIOReturnSuccess && reportLength >= 3) { | |
| // Try to interpret as angle data | |
| uint16_t rawValue = (report[2] << 8) | report[1]; | |
| if (rawValue >= 0 && rawValue <= 36000) { // Reasonable angle range | |
| return rawValue; | |
| } | |
| } | |
| } | |
| return -1; // Error or no valid reading | |
| } | |
| int main(int argc, char *argv[]) { | |
| int continuous_mode = 0; | |
| // Parse command line arguments | |
| if (argc > 1 && strcmp(argv[1], "-c") == 0) { | |
| continuous_mode = 1; | |
| } | |
| IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); | |
| if (!manager) { | |
| return 1; | |
| } | |
| if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { | |
| CFRelease(manager); | |
| return 1; | |
| } | |
| // Look for Apple devices with sensor capabilities | |
| CFDictionaryRef matchingDict = CFDictionaryCreate(kCFAllocatorDefault, | |
| (const void**)&(CFStringRef[]){CFSTR("VendorID")}, | |
| (const void**)&(CFNumberRef[]){CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &(int32_t){0x05AC})}, | |
| 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); | |
| IOHIDManagerSetDeviceMatching(manager, matchingDict); | |
| CFSetRef devices = IOHIDManagerCopyDevices(manager); | |
| if (!devices || CFSetGetCount(devices) == 0) { | |
| if (devices) CFRelease(devices); | |
| CFRelease(matchingDict); | |
| IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); | |
| CFRelease(manager); | |
| return 1; | |
| } | |
| // Try to find a device that responds to feature report requests | |
| const void **deviceArray = malloc(sizeof(void*) * CFSetGetCount(devices)); | |
| CFSetGetValues(devices, deviceArray); | |
| IOHIDDeviceRef targetDevice = NULL; | |
| for (CFIndex i = 0; i < CFSetGetCount(devices); i++) { | |
| IOHIDDeviceRef device = (IOHIDDeviceRef)deviceArray[i]; | |
| if (IOHIDDeviceOpen(device, kIOHIDOptionsTypeNone) == kIOReturnSuccess) { | |
| int angle = get_lid_angle(device); | |
| if (angle >= 0) { | |
| targetDevice = device; | |
| break; | |
| } | |
| IOHIDDeviceClose(device, kIOHIDOptionsTypeNone); | |
| } | |
| } | |
| if (!targetDevice) { | |
| free(deviceArray); | |
| CFRelease(devices); | |
| CFRelease(matchingDict); | |
| IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); | |
| CFRelease(manager); | |
| return 1; | |
| } | |
| if (continuous_mode) { | |
| // Continuous monitoring mode | |
| int lastAngle = -1; | |
| while (1) { | |
| int currentAngle = get_lid_angle(targetDevice); | |
| if (currentAngle >= 0 && currentAngle != lastAngle) { | |
| printf("%d\n", currentAngle); | |
| fflush(stdout); | |
| lastAngle = currentAngle; | |
| } | |
| usleep(100000); // Sleep for 100ms | |
| } | |
| } else { | |
| // Single reading mode | |
| int angle = get_lid_angle(targetDevice); | |
| if (angle >= 0) { | |
| printf("%d\n", angle); | |
| } else { | |
| IOHIDDeviceClose(targetDevice, kIOHIDOptionsTypeNone); | |
| free(deviceArray); | |
| CFRelease(devices); | |
| CFRelease(matchingDict); | |
| IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); | |
| CFRelease(manager); | |
| return 1; | |
| } | |
| } | |
| IOHIDDeviceClose(targetDevice, kIOHIDOptionsTypeNone); | |
| free(deviceArray); | |
| CFRelease(devices); | |
| CFRelease(matchingDict); | |
| IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); | |
| CFRelease(manager); | |
| return 0; | |
| } | |
| EOF | |
| gcc -o lidangler .lidangler.c -framework IOKit -framework CoreFoundation | |
| rm -f .lidangler.c | |
| fi | |
| ./lidangler "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| pip install pyaudio numpy | |
| --- | |
| Lid Angle Monitor with Audio Feedback | |
| Continuously monitors the MacBook lid angle using lidangler.sh and plays | |
| sound blips with frequency based on the current angle. Higher angles produce | |
| higher frequency sounds. | |
| """ | |
| import subprocess | |
| import sys | |
| import time | |
| import threading | |
| import pyaudio | |
| import numpy as np | |
| import signal | |
| import os | |
| class LidAngleMonitor: | |
| def __init__(self): | |
| self.current_angle = None | |
| self.last_angle = None | |
| self.running = True | |
| self.audio_thread = None | |
| self.audio_queue = [] | |
| # Audio parameters | |
| self.sample_rate = 44100 | |
| self.duration = 0.1 # 100ms blip duration | |
| self.volume = 0.3 | |
| # Frequency mapping: angle (0-130) -> frequency (200-2000 Hz) | |
| self.min_freq = 200 | |
| self.max_freq = 2000 | |
| # Initialize PyAudio | |
| self.audio = pyaudio.PyAudio() | |
| # Set up signal handler for graceful shutdown | |
| signal.signal(signal.SIGINT, self.signal_handler) | |
| signal.signal(signal.SIGTERM, self.signal_handler) | |
| def signal_handler(self, signum, frame): | |
| """Handle interrupt signals for graceful shutdown""" | |
| print("\nShutting down...") | |
| self.running = False | |
| def angle_to_frequency(self, angle): | |
| """Convert angle (0-130) to frequency (200-2000 Hz)""" | |
| if angle is None: | |
| return self.min_freq | |
| # Normalize angle to 0-1 range | |
| normalized = min(max(angle / 130.0, 0.0), 1.0) | |
| # Map to frequency range | |
| frequency = self.min_freq + (self.max_freq - self.min_freq) * normalized | |
| return frequency | |
| def generate_tone(self, frequency, duration): | |
| """Generate a sine wave tone""" | |
| samples = int(self.sample_rate * duration) | |
| t = np.linspace(0, duration, samples, False) | |
| wave = np.sin(2 * np.pi * frequency * t) | |
| # Apply envelope to avoid clicks | |
| envelope = np.exp(-t * 8) # Exponential decay | |
| wave = wave * envelope * self.volume | |
| return wave.astype(np.float32) | |
| def play_sound(self, frequency): | |
| """Play a sound blip at the given frequency""" | |
| try: | |
| # Generate the tone | |
| tone = self.generate_tone(frequency, self.duration) | |
| # Open audio stream | |
| stream = self.audio.open( | |
| format=pyaudio.paFloat32, | |
| channels=1, | |
| rate=self.sample_rate, | |
| output=True | |
| ) | |
| # Play the tone | |
| stream.write(tone.tobytes()) | |
| # Clean up | |
| stream.stop_stream() | |
| stream.close() | |
| except Exception as e: | |
| print(f"Error playing sound: {e}", file=sys.stderr) | |
| def monitor_angle(self): | |
| """Monitor lid angle using lidangler.sh -c""" | |
| try: | |
| # Start the lidangler process | |
| process = subprocess.Popen( | |
| ['./lidangler.sh', '-c'], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| text=True, | |
| bufsize=1, | |
| universal_newlines=True | |
| ) | |
| print("Monitoring lid angle... (Press Ctrl+C to stop)") | |
| print("Angle changes will trigger sound blips with frequency based on angle") | |
| print("Higher angles = higher frequency sounds") | |
| print() | |
| while self.running: | |
| # Read a line from the process | |
| line = process.stdout.readline() | |
| if not line: | |
| # Process ended | |
| break | |
| try: | |
| # Parse the angle value | |
| angle = int(line.strip()) | |
| self.current_angle = angle | |
| # Check if angle changed | |
| if self.last_angle is not None and angle != self.last_angle: | |
| frequency = self.angle_to_frequency(angle) | |
| print(f"Angle changed: {self.last_angle} -> {angle} (freq: {frequency:.1f} Hz)") | |
| # Play sound in a separate thread to avoid blocking | |
| sound_thread = threading.Thread( | |
| target=self.play_sound, | |
| args=(frequency,) | |
| ) | |
| sound_thread.daemon = True | |
| sound_thread.start() | |
| self.last_angle = angle | |
| except ValueError: | |
| # Skip invalid lines | |
| continue | |
| except Exception as e: | |
| print(f"Error processing angle data: {e}", file=sys.stderr) | |
| continue | |
| # Clean up the process | |
| process.terminate() | |
| process.wait() | |
| except FileNotFoundError: | |
| print("Error: lidangler.sh not found. Make sure it's in the current directory.", file=sys.stderr) | |
| return False | |
| except Exception as e: | |
| print(f"Error monitoring angle: {e}", file=sys.stderr) | |
| return False | |
| return True | |
| def run(self): | |
| """Main run method""" | |
| # Check if lidangler.sh exists | |
| if not os.path.exists('./lidangler.sh'): | |
| print("Error: lidangler.sh not found in current directory", file=sys.stderr) | |
| return False | |
| # Make sure lidangler.sh is executable | |
| if not os.access('./lidangler.sh', os.X_OK): | |
| print("Making lidangler.sh executable...") | |
| os.chmod('./lidangler.sh', 0o755) | |
| # Start monitoring | |
| success = self.monitor_angle() | |
| # Clean up audio resources | |
| self.audio.terminate() | |
| return success | |
| def main(): | |
| """Main entry point""" | |
| monitor = LidAngleMonitor() | |
| success = monitor.run() | |
| return 0 if success else 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment