hammerspoon/extensions/streamdeck/HSStreamDeckDevice.m

374 lines
13 KiB
Objective-C

//
// HSStreamDeckDevice.m
// Hammerspoon
//
// Created by Chris Jones on 06/09/2017.
// Copyright © 2017 Hammerspoon. All rights reserved.
//
#import "HSStreamDeckDevice.h"
@interface HSStreamDeckDevice ()
@property (nonatomic, copy) NSString *serialNumber;
@end
@implementation HSStreamDeckDevice
- (id)initWithDevice:(IOHIDDeviceRef)device manager:(id)manager {
self = [super init];
if (self) {
self.device = device;
self.isValid = YES;
self.manager = manager;
self.buttonCallbackRef = LUA_NOREF;
self.selfRefCount = 0;
self.buttonStateCache = [[NSMutableArray alloc] init];
// These defaults are not necessary, all base classes will override them, but if we miss something, these are chosen to try and provoke a crash where possible, so we notice the lack of an override.
self.imageCodec = STREAMDECK_CODEC_UNKNOWN;
self.deckType = @"Unknown";
self.keyColumns = -1;
self.keyRows = -1;
self.imageFlipX = NO;
self.imageFlipY = NO;
self.imageAngle = 0;
self.simpleReportLength = 0;
self.reportLength = 0;
self.reportHeaderLength = 0;
self.dataKeyOffset = 0;
self.resetCommand = nil;
self.setBrightnessCommand = nil;
self.serialNumberCommand = 0;
self.firmwareVersionCommand = 0;
self.firmwareReadOffset = 0;
self.serialNumberReadOffset = 0;
serialNumberCache = nil;
//NSLog(@"Added new Stream Deck device %p with IOKit device %p from manager %p", (__bridge void *)self, (void*)self.device, (__bridge void *)self.manager);
}
return self;
}
- (void)invalidate {
self.isValid = NO;
}
- (void)initialiseCaches {
for (int i = 0; i <= self.keyCount; i++) {
[self.buttonStateCache setObject:@0 atIndexedSubscript:i];
}
[self cacheSerialNumber];
}
- (IOReturn)deviceWriteSimpleReport:(NSData *)command {
if (self.simpleReportLength == 0) {
[LuaSkin logError:@"Initialising Stream Deck device with no simple report length defined"];
return kIOReturnInternalError;
}
NSMutableData *reportData = [NSMutableData dataWithLength:self.simpleReportLength];
[reportData replaceBytesInRange:NSMakeRange(0, command.length) withBytes:command.bytes];
return [self deviceWrite:reportData];
}
- (IOReturn)deviceWrite:(NSData *)report {
const uint8_t *rawBytes = (const uint8_t*)report.bytes;
return IOHIDDeviceSetReport(self.device, kIOHIDReportTypeFeature, rawBytes[0], rawBytes, report.length);
}
- (NSData *)deviceRead:(int)resultLength reportID:(CFIndex)reportID readOffset:(NSUInteger)readOffset {
CFIndex reportLength = resultLength + readOffset;
uint8_t *report = malloc(reportLength);
//NSLog(@"deviceRead: expecting resultLength %d, calculated report length %ld", resultLength, (long)reportLength);
IOHIDDeviceGetReport(self.device, kIOHIDReportTypeFeature, reportID, report, &reportLength);
char *c_data = (char *)(report + readOffset);
NSData *dataRaw = [NSData dataWithBytes:c_data length:resultLength];
free(report);
NSMutableData *data = [NSMutableData dataWithLength:0];
[dataRaw enumerateByteRangesUsingBlock:^(const void * _Nonnull bytes, NSRange byteRange, BOOL * _Nonnull stop) {
NSUInteger copyLength = byteRange.length;
for (NSUInteger i = 0; i < byteRange.length; i++) {
if (((const uint8_t*)bytes)[i] == 0x00) {
copyLength = i + 1;
break;
}
}
[data appendBytes:bytes length:copyLength-1];
}];
return data;
}
- (int)transformKeyIndex:(int)sourceKey {
//NSLog(@"transformKeyIndex: returning %d unmodified", sourceKey);
return sourceKey;
}
- (void)deviceDidSendInput:(NSArray*)newButtonStates {
//NSLog(@"Got an input event from device: %p: button:%@ isDown:%@", (__bridge void*)self, button, isDown);
if (!self.isValid) {
return;
}
LuaSkin *skin = [LuaSkin sharedWithState:NULL];
_lua_stackguard_entry(skin.L);
if (![skin checkGCCanary:self.lsCanary]) {
_lua_stackguard_exit(skin.L);
return;
}
if (self.buttonCallbackRef == LUA_NOREF || self.buttonCallbackRef == LUA_REFNIL) {
[skin logError:@"hs.streamdeck received a button input, but no callback has been set. See hs.streamdeck:buttonCallback()"];
return;
}
//NSLog(@"buttonStateCache: %@", self.buttonStateCache);
//NSLog(@"newButtonStates: %@", newButtonStates);
for (int button=1; button <= self.keyCount; button++) {
if (![self.buttonStateCache[button] isEqual:newButtonStates[button]]) {
[skin pushLuaRef:streamDeckRefTable ref:self.buttonCallbackRef];
[skin pushNSObject:self];
lua_pushinteger(skin.L, button);
lua_pushboolean(skin.L, ((NSNumber*)(newButtonStates[button])).boolValue);
[skin protectedCallAndError:@"hs.streamdeck:buttonCallback" nargs:3 nresults:0];
self.buttonStateCache[button] = newButtonStates[button];
}
}
_lua_stackguard_exit(skin.L);
}
- (BOOL)setBrightness:(int)brightness {
if (!self.isValid) {
return NO;
}
if (!self.setBrightnessCommand) {
NSException *exception = [NSException exceptionWithName:@"HSStreamDeckDeviceUnimplemented"
reason:@"setBrightness method not implemented"
userInfo:nil];
[exception raise];
return NO;
}
NSMutableData *brightnessCommand = [self.setBrightnessCommand mutableCopy];
[brightnessCommand replaceBytesInRange:NSMakeRange(self.setBrightnessCommand.length - 1, 1) withBytes:&brightness];
IOReturn res = [self deviceWriteSimpleReport:brightnessCommand];
return res == kIOReturnSuccess;
}
- (void)reset {
if (!self.isValid) {
return;
}
if (!self.resetCommand) {
NSException *exception = [NSException exceptionWithName:@"HSStreamDeckDeviceUnimplemented"
reason:@"resetCommand bytes not set, or reset method not overridden"
userInfo:nil];
[exception raise];
return;
}
IOReturn res = [self deviceWriteSimpleReport:self.resetCommand];
if (res != kIOReturnSuccess) {
NSLog(@"hs.streamdeck:reset() failed on %@ (%@)", self.deckType, self.serialNumber);
}
}
- (NSString*)getSerialNumber {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
if (!serialNumberCache) {
// This shouldn't be necessary, since we cache the serial number when the device is initialised, but just in case
serialNumberCache = [self cacheSerialNumber];
}
return serialNumberCache;
#pragma clang diagnostic pop
}
- (NSString *)cacheSerialNumber {
if (!self.isValid) {
return nil;
}
if (self.serialNumberCommand == 0) {
NSException *exception = [NSException exceptionWithName:@"HSStreamDeckDeviceUnimplemented"
reason:@"serialNumberCommand not set, or cacheSerialNumber method not overridden"
userInfo:nil];
[exception raise];
return nil;
}
NSData *serialNumberData = [self deviceRead:self.simpleReportLength reportID:self.serialNumberCommand readOffset:self.serialNumberReadOffset];
NSString *serialNumber = [[NSString alloc] initWithData:serialNumberData
encoding:NSUTF8StringEncoding];
return serialNumber;
}
- (NSString*)firmwareVersion {
if (!self.isValid) {
return nil;
}
if (self.firmwareVersionCommand == 0) {
NSException *exception = [NSException exceptionWithName:@"HSStreamDeckDeviceUnimplemented"
reason:@"firmwareVersionCOmmand not set, or firmwareVersion method not implemented"
userInfo:nil];
[exception raise];
}
NSString *firmwareVersion = [[NSString alloc] initWithData:[self deviceRead:self.simpleReportLength reportID:self.firmwareVersionCommand readOffset:self.firmwareReadOffset]
encoding:NSUTF8StringEncoding];
return firmwareVersion;
}
- (int)getKeyCount {
return self.keyColumns * self.keyRows;
}
- (void)clearImage:(int)button {
[self setColor:[NSColor blackColor] forButton:button];
}
- (void)setColor:(NSColor *)color forButton:(int)button {
if (!self.isValid) {
return;
}
NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(self.imageWidth, self.imageHeight)];
[image lockFocus];
[color drawSwatchInRect:NSMakeRect(0, 0, self.imageWidth, self.imageHeight)];
[image unlockFocus];
[self setImage:image forButton:button];
}
- (void)setImage:(NSImage *)image forButton:(int)button {
if (!self.isValid) {
return;
}
NSImage *renderImage;
// Unconditionally resize the image
NSImage *sourceImage = [image copy];
NSSize newSize = NSMakeSize(self.imageWidth, self.imageHeight);
renderImage = [[NSImage alloc] initWithSize: newSize];
[renderImage lockFocus];
[sourceImage setSize: newSize];
[[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
[sourceImage drawAtPoint:NSZeroPoint fromRect:CGRectMake(0, 0, newSize.width, newSize.height) operation:NSCompositingOperationCopy fraction:1.0];
[renderImage unlockFocus];
if (![image isValid]) {
[LuaSkin logError:@"image is invalid"];
}
if (![renderImage isValid]) {
[LuaSkin logError:@"Invalid image passed to hs.streamdeck:setImage() (renderImage)"];
// return;
}
// Both of these functions are no-ops if there are no rotations or flips required, so we'll call them unconditionally
renderImage = [renderImage imageRotated:self.imageAngle];
renderImage = [renderImage flipImage:self.imageFlipX vert:self.imageFlipY];
NSData *data = nil;
switch (self.imageCodec) {
case STREAMDECK_CODEC_BMP:
data = [renderImage bmpData];
break;
case STREAMDECK_CODEC_JPEG:
data = [renderImage jpegData];
break;
case STREAMDECK_CODEC_UNKNOWN:
[LuaSkin logError:@"Unknown image codec for hs.streamdeck device"];
break;
}
// Writing the image to hardware is a device-specific operation, so hand it off to our subclasses
[self deviceWriteImage:data button:[self transformKeyIndex:button]];
}
- (void)deviceWriteImage:(NSData *)data button:(int)button {
NSException *exception = [NSException exceptionWithName:@"HSStreamDeckDeviceUnimplemented"
reason:@"deviceWriteImage method not implemented"
userInfo:nil];
[exception raise];
}
- (void)deviceV2WriteImage:(NSData *)data button:(int)button {
uint8_t reportHeader[] = {0x02, // Report ID
0x07, // Unknown (always seems to be 7)
button - 1, // Deck button to set
0x00, // Final page bool
0x00, // Some kind of encoding of the length of the current page
0x00, // Some other kind of encoding of the current page length
0x00, // Some kind of encoding of the page number
0x00 // Some other kind of encoding of the page number
};
// The v2 Stream Decks needs images sent in slices no more than 1016 bytes + the report header
int maxPayloadLength = self.reportLength - self.reportHeaderLength;
int bytesRemaining = (int)data.length;
int bytesSent = 0;
int pageNumber = 0;
const uint8_t *imageBuf = data.bytes;
IOReturn result;
while (bytesRemaining > 0) {
int thisPageLength = MIN(bytesRemaining, maxPayloadLength);
bytesSent = pageNumber * maxPayloadLength;
// Set our current page number
reportHeader[6] = pageNumber & 0xFF;
reportHeader[7] = pageNumber >> 8;
// Set our current page length
reportHeader[4] = thisPageLength & 0xFF;
reportHeader[5] = thisPageLength >> 8;
// Set if we're the last page of data
if (bytesRemaining <= maxPayloadLength) reportHeader[3] = 1;
NSMutableData *report = [NSMutableData dataWithLength:self.reportLength];
[report replaceBytesInRange:NSMakeRange(0, self.reportHeaderLength)
withBytes:reportHeader];
[report replaceBytesInRange:NSMakeRange(self.reportHeaderLength, thisPageLength)
withBytes:imageBuf+bytesSent
length:thisPageLength];
result = IOHIDDeviceSetReport(self.device,
kIOHIDReportTypeOutput,
reportHeader[0],
report.bytes,
(int)report.length);
if (result != kIOReturnSuccess) {
NSLog(@"WARNING: writing an image with hs.streamdeck encountered a failure on page %d: %d", pageNumber, result);
}
bytesRemaining = bytesRemaining - thisPageLength;
pageNumber++;
}
}
@end