diff --git a/cscore/BUILD.bazel b/cscore/BUILD.bazel index 8533fde451..c0ee389d32 100644 --- a/cscore/BUILD.bazel +++ b/cscore/BUILD.bazel @@ -46,6 +46,7 @@ objc_library( "Foundation", "CoreMedia", "CoreVideo", + "IOKit", ], tags = ["manual"], deps = [ diff --git a/cscore/CMakeLists.txt b/cscore/CMakeLists.txt index 3e96d42719..af5d17aa5b 100644 --- a/cscore/CMakeLists.txt +++ b/cscore/CMakeLists.txt @@ -21,7 +21,7 @@ if(APPLE) cscore PROPERTIES LINK_FLAGS - "-framework CoreFoundation -framework AVFoundation -framework Foundation -framework CoreMedia -framework CoreVideo" + "-framework CoreFoundation -framework AVFoundation -framework Foundation -framework CoreMedia -framework CoreVideo -framework IOKit" ) elseif(MSVC) target_sources(cscore PRIVATE ${cscore_windows_src}) diff --git a/cscore/src/main/native/objcpp/UsbCameraImpl.h b/cscore/src/main/native/objcpp/UsbCameraImpl.h index 86b1166b05..68c9078acc 100644 --- a/cscore/src/main/native/objcpp/UsbCameraImpl.h +++ b/cscore/src/main/native/objcpp/UsbCameraImpl.h @@ -12,6 +12,8 @@ #include #include +#include + #include "SourceImpl.h" namespace cs { @@ -88,8 +90,34 @@ class UsbCameraImpl : public SourceImpl { UsbCameraImplObjc* cppGetObjc() { return m_objc; } + int CreatePropertyPublic(std::string_view name, std::function()> newFunc) { + return CreateProperty(name, newFunc); + } + + PropertyImpl* GetPropertyPublic(int property) { + return GetProperty(property); + } + + void NotifyPropertyCreatedPublic(int propIndex, PropertyImpl& prop) { + NotifyPropertyCreated(propIndex, prop); + } + + void UpdatePropertyValuePublic(int property, bool setString, int value, std::string_view valueStr) { + UpdatePropertyValue(property, setString, value, valueStr); + } + + wpi::mutex& GetMutex() { return m_mutex; } + + // Property cache accessors + wpi::StringMap& GetPropertyCache() { return m_propertyCache; } + wpi::StringMap& GetPropertyAutoCache() { return m_propertyAutoCache; } + private: UsbCameraImplObjc* m_objc; std::vector m_platformModes; + + // Property caches + wpi::StringMap m_propertyCache; + wpi::StringMap m_propertyAutoCache; }; } // namespace cs diff --git a/cscore/src/main/native/objcpp/UsbCameraImplObjc.h b/cscore/src/main/native/objcpp/UsbCameraImplObjc.h index c173e80011..b4f7626ae9 100644 --- a/cscore/src/main/native/objcpp/UsbCameraImplObjc.h +++ b/cscore/src/main/native/objcpp/UsbCameraImplObjc.h @@ -5,11 +5,39 @@ #pragma once #import -#import "UsbCameraDelegate.h" + #include #include + +#import "UsbCameraDelegate.h" +#import "UvcControlImpl.h" + #include "cscore_cpp.h" +// Quirk: exposure auto is 3 for on, 1 for off +#define kPropertyAutoExposureOn 3 +#define kPropertyAutoExposureOff 1 + +// Property names +#define kPropertyBrightness "brightness" +#define kPropertyWhiteBalance "white_balance_temperature" +#define kPropertyExposure "raw_exposure_time_absolute" +#define kPropertyContrast "raw_contrast" +#define kPropertySaturation "raw_saturation" +#define kPropertySharpness "raw_sharpness" +#define kPropertyGain "gain" +#define kPropertyGamma "gamma" +#define kPropertyHue "raw_hue" +#define kPropertyFocus "focus_absolute" +#define kPropertyZoom "zoom" +#define kPropertyBackLightCompensation "backlight_compensation" +#define kPropertyPowerLineFrequency "power_line_frequency" + +// Auto property names +#define kPropertyAutoExposure "exposure_auto" +#define kPropertyAutoWhiteBalance "white_balance_automatic" +#define kPropertyAutoFocus "focus_auto" + namespace cs { class UsbCameraImpl; } @@ -30,6 +58,7 @@ class UsbCameraImpl; @property(nonatomic) AVCaptureDevice* videoDevice; @property(nonatomic) AVCaptureDeviceInput* videoInput; @property(nonatomic) UsbCameraDelegate* callback; +@property(nonatomic) UvcControlImpl* uvcControl; @property(nonatomic) AVCaptureVideoDataOutput* videoOutput; @property(nonatomic) AVCaptureSession* session; @@ -68,4 +97,8 @@ class UsbCameraImpl; - (void)getCameraName:(std::string*)name; - (void)setNewCameraPath:(std::string_view*)path; +- (void)deviceCacheProperties; +- (void)cacheProperty:(uint32_t)propID withName:(NSString *)name; +- (void)cacheAutoProperty:(uint32_t)propID withName:(NSString *)baseName; + @end diff --git a/cscore/src/main/native/objcpp/UsbCameraImplObjc.mm b/cscore/src/main/native/objcpp/UsbCameraImplObjc.mm index 5d43de7f32..1db2ccbc30 100644 --- a/cscore/src/main/native/objcpp/UsbCameraImplObjc.mm +++ b/cscore/src/main/native/objcpp/UsbCameraImplObjc.mm @@ -2,12 +2,14 @@ // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. -#import "UsbCameraImplObjc.h" -#include "UsbCameraImpl.h" +#include #pragma GCC diagnostic ignored "-Wunused-parameter" +#import "UsbCameraImplObjc.h" + #include "Notifier.h" #include "Log.h" +#include "UsbCameraImpl.h" template inline void NamedLog(UsbCameraImplObjc* objc, unsigned int level, @@ -104,44 +106,300 @@ using namespace cs; name:AVCaptureDeviceWasDisconnectedNotification object:nil]; [self deviceConnect]; + [self deviceCacheProperties]; }); } +- (BOOL)getEnabledWithProperty:(int)property withValue:(int)value { + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + return false; + } + + // There is room for quirk handling improvement here, but I will leave it + // for now. + if (property == sharedThis->GetPropertyIndex(kPropertyAutoExposure)) { + return value == kPropertyAutoExposureOn; + } + + return value != 0; +} + + +- (int)clampToPercent:(int)value { + if (value < 0) { + return 0; + } + if (value > 100) { + return 100; + } + return value; +} + +- (int)percentageToRaw:(int)propID percentage:(int)percentage min:(int)min max:(int)max { + if (min == max) { + return min; + } + + return min + (max - min) * percentage / 100; +} + +- (BOOL)isPercentageProperty:(int)propID { + return propID == CAPPROPID_BRIGHTNESS || + propID == CAPPROPID_CONTRAST || + propID == CAPPROPID_SATURATION || + propID == CAPPROPID_HUE || + propID == CAPPROPID_SHARPNESS || + propID == CAPPROPID_GAIN; +} + // Property functions - (void)setProperty:(int)property withValue:(int)value status:(CS_Status*)status { + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + *status = CS_INVALID_HANDLE; + return; + } + + // Make sure properties are cached + if (!self.propertiesCached) { + [self deviceCacheProperties]; + } + + // Get the property name from the property index + wpi::SmallString<128> nameBuf; + std::string_view propName = sharedThis->GetPropertyName(property, nameBuf, status); + if (*status != 0) { + OBJCERROR("Failed to get property name for index {}", property); + return; + } + + std::string nameStr(propName); + + // Check if it's an auto property + auto& propertyAutoCache = sharedThis->GetPropertyAutoCache(); + auto autoIt = propertyAutoCache.find(nameStr); + if (autoIt != propertyAutoCache.end()) { + uint32_t propID = autoIt->second; + bool enabled = [self getEnabledWithProperty:property withValue:value]; + dispatch_async_and_wait(self.sessionQueue, ^{ + if (self.uvcControl == nil) { + *status = CS_INVALID_PROPERTY; + return; + } + + if (![self.uvcControl setAutoProperty:propID enabled:enabled status:status]) { + OBJCERROR("Failed to set auto property {} to {}", + nameStr, enabled); + return; + } + + // Update property value + sharedThis->UpdatePropertyValuePublic(property, false, value, {}); + }); + return; + } + + // Handle regular property + auto& propertyCache = sharedThis->GetPropertyCache(); + auto it = propertyCache.find(nameStr); + if (it == propertyCache.end()) { + OBJCERROR("Property not found in cache: {}", nameStr); + *status = CS_INVALID_PROPERTY; + return; + } + + uint32_t propID = it->second; + + dispatch_async_and_wait(self.sessionQueue, ^{ + if (self.uvcControl == nil) { + *status = CS_INVALID_PROPERTY; + return; + } + + // Get the property implementation to access its limits + const PropertyImpl* prop = sharedThis->GetPropertyPublic(property); + if (!prop) { + *status = CS_INVALID_PROPERTY; + return; + } + + + int32_t realValue = value; + if ([self isPercentageProperty:propID]) { + // Clamp to 0-100 + realValue = [self clampToPercent:realValue]; + + // Scale to min/max + realValue = [self percentageToRaw:propID percentage:realValue min:prop->minimum max:prop->maximum]; + } + + if (![self.uvcControl setProperty:propID withValue:realValue status:status]) { + OBJCERROR("Failed to set property {} to value {}", nameStr, realValue); + return; + } + + // Update property value in the container + sharedThis->UpdatePropertyValuePublic(property, false, value, {}); + }); } + - (void)setStringProperty:(int)property withValue:(std::string_view*)value status:(CS_Status*)status { + *status = CS_INVALID_PROPERTY; + return; } // Standard common camera properties - (void)setBrightness:(int)brightness status:(CS_Status*)status { - *status = CS_INVALID_PROPERTY; + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + *status = CS_INVALID_HANDLE; + return; + } + + // Make sure properties are cached + if (!self.propertiesCached) { + [self deviceCacheProperties]; + } + + // Get the property index and set it + int prop = sharedThis->GetPropertyIndex(kPropertyBrightness); + sharedThis->SetProperty(prop, brightness, status); } + - (int)getBrightness:(CS_Status*)status { - *status = CS_INVALID_PROPERTY; - return 0; + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + *status = CS_INVALID_HANDLE; + return 0; + } + + // Make sure properties are cached + if (!self.propertiesCached) { + [self deviceCacheProperties]; + } + + // Get the property index and its value + int prop = sharedThis->GetPropertyIndex(kPropertyBrightness); + return sharedThis->GetProperty(prop, status); } + - (void)setWhiteBalanceAuto:(CS_Status*)status { - *status = CS_INVALID_PROPERTY; + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + *status = CS_INVALID_HANDLE; + return; + } + + // Make sure properties are cached + if (!self.propertiesCached) { + [self deviceCacheProperties]; + } + + int prop = sharedThis->GetPropertyIndex(kPropertyAutoWhiteBalance); + sharedThis->SetProperty(prop, 1, status); } + - (void)setWhiteBalanceHoldCurrent:(CS_Status*)status { - *status = CS_INVALID_PROPERTY; + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + *status = CS_INVALID_HANDLE; + return; + } + + // Make sure properties are cached + if (!self.propertiesCached) { + [self deviceCacheProperties]; + } + + int prop = sharedThis->GetPropertyIndex(kPropertyAutoWhiteBalance); + sharedThis->SetProperty(prop, 0, status); } + - (void)setWhiteBalanceManual:(int)value status:(CS_Status*)status { - *status = CS_INVALID_PROPERTY; + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + *status = CS_INVALID_HANDLE; + return; + } + + // Make sure properties are cached + if (!self.propertiesCached) { + [self deviceCacheProperties]; + } + + // First disable auto white balance + int autoProp = sharedThis->GetPropertyIndex(kPropertyAutoWhiteBalance); + sharedThis->SetProperty(autoProp, 0, status); + if (*status != 0) { + return; + } + + // Then set the white balance value + int prop = sharedThis->GetPropertyIndex(kPropertyWhiteBalance); + sharedThis->SetProperty(prop, value, status); } + - (void)setExposureAuto:(CS_Status*)status { - *status = CS_INVALID_PROPERTY; + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + *status = CS_INVALID_HANDLE; + return; + } + + // Make sure properties are cached + if (!self.propertiesCached) { + [self deviceCacheProperties]; + } + + // Set the auto exposure property to enabled (1) + int prop = sharedThis->GetPropertyIndex(kPropertyAutoExposure); + sharedThis->SetProperty(prop, kPropertyAutoExposureOn, status); } + - (void)setExposureHoldCurrent:(CS_Status*)status { - *status = CS_INVALID_PROPERTY; + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + *status = CS_INVALID_HANDLE; + return; + } + + // Make sure properties are cached + if (!self.propertiesCached) { + [self deviceCacheProperties]; + } + + // Set the auto exposure property to disabled (0) + int prop = sharedThis->GetPropertyIndex(kPropertyAutoExposure); + sharedThis->SetProperty(prop, kPropertyAutoExposureOff, status); } + - (void)setExposureManual:(int)value status:(CS_Status*)status { - *status = CS_INVALID_PROPERTY; + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + *status = CS_INVALID_HANDLE; + return; + } + + // Make sure properties are cached + if (!self.propertiesCached) { + [self deviceCacheProperties]; + } + + // First disable auto exposure + int autoProp = sharedThis->GetPropertyIndex(kPropertyAutoExposure); + sharedThis->SetProperty(autoProp, kPropertyAutoExposureOff, status); + if (*status != 0) { + return; + } + + // Then set the exposure value + int prop = sharedThis->GetPropertyIndex(kPropertyExposure); + sharedThis->SetProperty(prop, value, status); } - (bool)setVideoMode:(const cs::VideoMode&)mode status:(CS_Status*)status { @@ -295,10 +553,144 @@ using namespace cs; // All above are called from C++, must always dispatch to loop +// Property caching methods - (void)deviceCacheProperties { - if (self.session == nil) { - return; - } + if (self.uvcControl == nil) { + return; + } + + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + OBJCERROR("Cannot cache properties: UsbCameraImpl not available"); + return; + } + + // Cache basic properties + [self cacheProperty:CAPPROPID_BRIGHTNESS withName:@kPropertyBrightness]; + [self cacheProperty:CAPPROPID_WHITEBALANCE withName:@kPropertyWhiteBalance]; + [self cacheProperty:CAPPROPID_EXPOSURE withName:@kPropertyExposure]; + [self cacheProperty:CAPPROPID_CONTRAST withName:@kPropertyContrast]; + [self cacheProperty:CAPPROPID_SATURATION withName:@kPropertySaturation]; + [self cacheProperty:CAPPROPID_SHARPNESS withName:@kPropertySharpness]; + [self cacheProperty:CAPPROPID_GAIN withName:@kPropertyGain]; + [self cacheProperty:CAPPROPID_GAMMA withName:@kPropertyGamma]; + [self cacheProperty:CAPPROPID_HUE withName:@kPropertyHue]; + [self cacheProperty:CAPPROPID_FOCUS withName:@kPropertyFocus]; + [self cacheProperty:CAPPROPID_ZOOM withName:@kPropertyZoom]; + [self cacheProperty:CAPPROPID_BACKLIGHTCOMP withName:@kPropertyBackLightCompensation]; + [self cacheProperty:CAPPROPID_POWERLINEFREQ withName:@kPropertyPowerLineFrequency]; + + // Cache auto properties + [self cacheAutoProperty:CAPPROPID_EXPOSURE withName:@kPropertyAutoExposure]; + [self cacheAutoProperty:CAPPROPID_WHITEBALANCE withName:@kPropertyAutoWhiteBalance]; + [self cacheAutoProperty:CAPPROPID_FOCUS withName:@kPropertyAutoFocus]; + + self.propertiesCached = true; +} + +- (void)cacheProperty:(uint32_t)propID withName:(NSString *)name { + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + OBJCERROR("Cannot cache property: UsbCameraImpl not available"); + return; + } + + if (self.uvcControl == nil) { + OBJCWARNING("Cannot cache property {}: UVC control not initialized", [name UTF8String]); + return; + } + + // Get property limits + int32_t minimum = 0, maximum = 0, defaultValue = 0; + int32_t value = defaultValue; + CS_Status status; + + std::string nameStr = std::string([name UTF8String]); + + // Get the property limits + if (![self.uvcControl getPropertyLimits:propID + min:&minimum + max:&maximum + defValue:&defaultValue + status:&status]) { + OBJCWARNING("Failed to get property limits for {}", nameStr); + return; + } + + // Get current value + if (![self.uvcControl getProperty:propID withValue:&value status:&status]) { + value = defaultValue; + OBJCWARNING("Failed to get current value for {}: {}", + nameStr, value); + return; + } + + // Create property + auto& propertyCache = sharedThis->GetPropertyCache(); + propertyCache[nameStr] = propID; + + // Create the property implementation + std::unique_ptr prop; + prop = std::make_unique(nameStr); + prop->propKind = CS_PROP_INTEGER; + prop->value = value; + prop->minimum = minimum; + prop->maximum = maximum; + prop->step = 1; // Most camera properties use a step of 1 + prop->defaultValue = defaultValue; + + // Add the property to the container + std::scoped_lock lock(sharedThis->GetMutex()); + int ndx = sharedThis->CreatePropertyPublic(nameStr, [&] { return std::move(prop); }); + + // Notify that property has been created + sharedThis->NotifyPropertyCreatedPublic(ndx, *sharedThis->GetPropertyPublic(ndx)); +} + +- (void)cacheAutoProperty:(uint32_t)propID withName:(NSString *)baseName { + auto sharedThis = self.cppImpl.lock(); + if (!sharedThis) { + OBJCERROR("Cannot cache auto property: UsbCameraImpl not available"); + return; + } + + if (self.uvcControl == nil) { + OBJCWARNING("Cannot cache auto property {}: UVC control not initialized", [baseName UTF8String]); + return; + } + + // Build auto mode property name + std::string nameStr = std::string([baseName UTF8String]); + + // Get current auto mode status + bool enabled = false; + CS_Status status = 0; + + if(![self.uvcControl getAutoProperty:propID enabled:&enabled status:&status]) { + OBJCWARNING("Failed to get auto property {}", nameStr); + return; + } + + // Create property + std::unique_ptr prop; + prop = std::make_unique(nameStr); + prop->propKind = CS_PROP_BOOLEAN; + prop->value = enabled ? 1 : 0; + prop->minimum = 0; + prop->maximum = 1; + prop->step = 1; + prop->defaultValue = 0; // Default is manual mode + + // Add property to container + std::scoped_lock lock(sharedThis->GetMutex()); + int ndx = sharedThis->CreatePropertyPublic(nameStr, [&] { return std::move(prop); }); + + // Notify property created + sharedThis->NotifyPropertyCreatedPublic(ndx, *sharedThis->GetPropertyPublic(ndx)); + + // Map property name to ID + auto& propertyAutoCache = sharedThis->GetPropertyAutoCache(); + propertyAutoCache[nameStr] = propID; } static cs::VideoMode::PixelFormat FourCCToPixelFormat(FourCharCode fourcc) { @@ -459,6 +851,53 @@ static cs::VideoMode::PixelFormat FourCCToPixelFormat(FourCharCode fourcc) { self.deviceValid = true; } +- (CMTime)findNearestFrameDuration:(int)fps { + if (self.currentFormat == nil) { + return CMTimeMake(1, fps); + } + + NSArray* frameRates = self.currentFormat.videoSupportedFrameRateRanges; + if (frameRates.count == 0) { + return CMTimeMake(1, fps); + } + + // Find the nearest frame duration + CMTime nearestDuration = CMTimeMake(1, fps); + double minDiff = DBL_MAX; + + for (AVFrameRateRange* range in frameRates) { + CMTime minDuration = range.minFrameDuration; + CMTime maxDuration = range.maxFrameDuration; + + // Calculate frame duration for current fps + CMTime targetDuration = CMTimeMake(1, fps); + + // Check if within range + if (CMTimeCompare(targetDuration, minDuration) >= 0 && + CMTimeCompare(targetDuration, maxDuration) <= 0) { + return targetDuration; + } + + // Calculate difference with min value + double minDiffValue = fabs(CMTimeGetSeconds(targetDuration) - CMTimeGetSeconds(minDuration)); + if (minDiffValue < minDiff) { + minDiff = minDiffValue; + nearestDuration = minDuration; + } + + // Calculate difference with max value + double maxDiffValue = fabs(CMTimeGetSeconds(targetDuration) - CMTimeGetSeconds(maxDuration)); + if (maxDiffValue < minDiff) { + minDiff = maxDiffValue; + nearestDuration = maxDuration; + } + } + + OBJCDEBUG("Nearest fps: {}", nearestDuration.timescale / static_cast(nearestDuration.value)); + + return nearestDuration; +} + - (bool)deviceStreamOn { if (self.streaming) { return false; @@ -476,9 +915,9 @@ static cs::VideoMode::PixelFormat FourCCToPixelFormat(FourCharCode fourcc) { } if (self.currentFPS != 0) { self.videoDevice.activeVideoMinFrameDuration = - CMTimeMake(1, self.currentFPS); + [self findNearestFrameDuration:self.currentFPS]; self.videoDevice.activeVideoMaxFrameDuration = - CMTimeMake(1, self.currentFPS); + [self findNearestFrameDuration:self.currentFPS]; } [self.videoDevice unlockForConfiguration]; } else { @@ -574,6 +1013,16 @@ static cs::VideoMode::PixelFormat FourCCToPixelFormat(FourCharCode fourcc) { goto err; } + CS_Status status; + self.uvcControl = [UvcControlImpl createFromAVCaptureDevice:self.videoDevice status:&status]; + if (self.uvcControl == nil) { + OBJCWARNING("Failed to initialize UVC control for camera: {}", status); + } else { + OBJCINFO("UVC control initialized successfully"); + } + + self.uvcControl.cppImpl = self.cppImpl; + self.callback = [[UsbCameraDelegate alloc] init]; if (self.callback == nil) { OBJCWARNING("Creating Camera Callback failed"); diff --git a/cscore/src/main/native/objcpp/UvcControlImpl.h b/cscore/src/main/native/objcpp/UvcControlImpl.h new file mode 100644 index 0000000000..aea335775c --- /dev/null +++ b/cscore/src/main/native/objcpp/UvcControlImpl.h @@ -0,0 +1,129 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#import +#import +#import + +#include +#include + +#import "UsbCameraDelegate.h" + +#include "cscore_cpp.h" + +// Status code definition +#define CS_UVC_STATUS_ERROR -3001 +#define CS_UVC_STATUS_DEVICE_DISCONNECTED -3002 + +// UVC control selector definitions +#define UVC_INPUT_TERMINAL_ID 0x01 + +// Camera terminal control selectors +#define CT_AE_MODE_CONTROL 0x02 +#define CT_AE_PRIORITY_CONTROL 0x03 +#define CT_EXPOSURE_TIME_ABSOLUTE_CONTROL 0x04 +#define CT_EXPOSURE_TIME_RELATIVE_CONTROL 0x05 +#define CT_FOCUS_ABSOLUTE_CONTROL 0x06 +#define CT_FOCUS_RELATIVE_CONTROL 0x07 +#define CT_FOCUS_AUTO_CONTROL 0x08 +#define CT_ZOOM_ABSOLUTE_CONTROL 0x0B +#define CT_ZOOM_RELATIVE_CONTROL 0x0C + +// Processing unit control selectors +#define PU_BACKLIGHT_COMPENSATION_CONTROL 0x01 +#define PU_BRIGHTNESS_CONTROL 0x02 +#define PU_CONTRAST_CONTROL 0x03 +#define PU_GAIN_CONTROL 0x04 +#define PU_POWER_LINE_FREQUENCY_CONTROL 0x05 +#define PU_HUE_CONTROL 0x06 +#define PU_SATURATION_CONTROL 0x07 +#define PU_SHARPNESS_CONTROL 0x08 +#define PU_GAMMA_CONTROL 0x09 +#define PU_WHITE_BALANCE_TEMPERATURE_CONTROL 0x0A +#define PU_WHITE_BALANCE_TEMPERATURE_AUTO_CONTROL 0x0B +#define PU_WHITE_BALANCE_COMPONENT_CONTROL 0x0C +#define PU_WHITE_BALANCE_COMPONENT_AUTO_CONTROL 0x0D +#define PU_HUE_AUTO_CONTROL 0x10 +#define PU_CONTRAST_AUTO_CONTROL 0x13 + +// Camera request error code +#define VC_REQUEST_ERROR_CODE_CONTROL 0x02 + +// UVC control interface definitions +#define UVC_CONTROL_INTERFACE_CLASS 14 +#define UVC_CONTROL_INTERFACE_SUBCLASS 1 + +// UVC control request types +#define UVC_SET_CUR 0x01 +#define UVC_GET_CUR 0x81 +#define UVC_GET_MIN 0x82 +#define UVC_GET_MAX 0x83 +#define UVC_GET_RES 0x84 +#define UVC_GET_INFO 0x86 +#define UVC_GET_DEF 0x87 + +// Camera property ID definitions +#define CAPPROPID_EXPOSURE 1 +#define CAPPROPID_FOCUS 2 +#define CAPPROPID_ZOOM 3 +#define CAPPROPID_WHITEBALANCE 4 +#define CAPPROPID_GAIN 5 +#define CAPPROPID_BRIGHTNESS 6 +#define CAPPROPID_CONTRAST 7 +#define CAPPROPID_SATURATION 8 +#define CAPPROPID_GAMMA 9 +#define CAPPROPID_HUE 10 +#define CAPPROPID_SHARPNESS 11 +#define CAPPROPID_BACKLIGHTCOMP 12 +#define CAPPROPID_POWERLINEFREQ 13 +#define CAPPROPID_LAST 14 + +namespace cs { +class UsbCameraImpl; +} + +@interface UvcControlImpl : NSObject + +@property(nonatomic) IOUSBInterfaceInterface190** controlInterface; +@property(nonatomic) uint32_t processingUnitID; +@property(nonatomic) std::weak_ptr cppImpl; + +// Create from AVCaptureDevice ++ (instancetype)createFromAVCaptureDevice:(AVCaptureDevice*)device status:(CS_Status*)status; + +// Initialize with USB vendor/product/location +- (instancetype)initWithVendorId:(uint16_t)vid + productId:(uint16_t)pid + location:(uint32_t)location + status:(CS_Status*)status; + +- (void)dealloc; + +// Basic property control +- (bool)setProperty:(uint32_t)propID + withValue:(int32_t)value + status:(CS_Status*)status; +- (bool)getProperty:(uint32_t)propID + withValue:(int32_t*)value + status:(CS_Status*)status; + +// Auto mode control +- (bool)setAutoProperty:(uint32_t)propID + enabled:(bool)enabled + status:(CS_Status*)status; +- (bool)getAutoProperty:(uint32_t)propID + enabled:(bool*)enabled + status:(CS_Status*)status; + +// Property range query +- (bool)getPropertyLimits:(uint32_t)propID + min:(int32_t*)min + max:(int32_t*)max + defValue:(int32_t*)defValue + status:(CS_Status*)status; + +@end \ No newline at end of file diff --git a/cscore/src/main/native/objcpp/UvcControlImpl.mm b/cscore/src/main/native/objcpp/UvcControlImpl.mm new file mode 100644 index 0000000000..05b7229a78 --- /dev/null +++ b/cscore/src/main/native/objcpp/UvcControlImpl.mm @@ -0,0 +1,773 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +// Copyright (c) 2017 Jason von Nieda, Niels Moseley +// +// The MIT License (MIT) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +#import "UvcControlImpl.h" + +#include "Log.h" +#include "UsbCameraImpl.h" + +template +inline void NamedLog(UvcControlImpl* objc, unsigned int level, + const char* file, unsigned int line, const S& format, + Args&&... args) { + auto sharedThis = objc.cppImpl.lock(); + if (!sharedThis) { + return; + } + + wpi::Logger& logger = sharedThis->objcGetLogger(); + std::string_view name = sharedThis->GetName(); + + if (logger.HasLogger() && level >= logger.min_level()) { + cs::NamedLogV(logger, level, file, line, name, format, + fmt::make_format_args(args...)); + } +} + +#define UVCLOG(level, format, ...) \ + NamedLog(self, level, __FILE__, __LINE__, \ + format __VA_OPT__(, ) __VA_ARGS__) + +#define UVCERROR(format, ...) \ + UVCLOG(::wpi::WPI_LOG_ERROR, format __VA_OPT__(, ) __VA_ARGS__) +#define UVCWARNING(format, ...) \ + UVCLOG(::wpi::WPI_LOG_WARNING, format __VA_OPT__(, ) __VA_ARGS__) +#define UVCINFO(format, ...) \ + UVCLOG(::wpi::WPI_LOG_INFO, format __VA_OPT__(, ) __VA_ARGS__) + +#ifdef NDEBUG +#define UVCDEBUG(format, ...) \ + do { \ + } while (0) +#define UVCDEBUG1(format, ...) \ + do { \ + } while (0) +#define UVCDEBUG2(format, ...) \ + do { \ + } while (0) +#define UVCDEBUG3(format, ...) \ + do { \ + } while (0) +#define UVCDEBUG4(format, ...) \ + do { \ + } while (0) +#else +#define UVCDEBUG(format, ...) \ + UVCLOG(::wpi::WPI_LOG_DEBUG, format __VA_OPT__(, ) __VA_ARGS__) +#define UVCDEBUG1(format, ...) \ + UVCLOG(::wpi::WPI_LOG_DEBUG1, format __VA_OPT__(, ) __VA_ARGS__) +#define UVCDEBUG2(format, ...) \ + UVCLOG(::wpi::WPI_LOG_DEBUG2, format __VA_OPT__(, ) __VA_ARGS__) +#define UVCDEBUG3(format, ...) \ + UVCLOG(::wpi::WPI_LOG_DEBUG3, format __VA_OPT__(, ) __VA_ARGS__) +#define UVCDEBUG4(format, ...) \ + UVCLOG(::wpi::WPI_LOG_DEBUG4, format __VA_OPT__(, ) __VA_ARGS__) +#endif + +// USB descriptor for UVC processing unit +struct ProcessingUnitDescriptor +{ + uint8_t bLength; + uint8_t bDescriptorType; // CS_INTERFACE 0x24 + uint8_t bDescriptorSubtype; // VC_PROCESSING_UNIT 0x05 + uint8_t bUnitID; +}; + +struct propertyInfo_t +{ + uint32_t selector; // selector ID + uint32_t unit; // unit (==0 for INPUT TERMINA:, ==1 for PROCESSING UNIT) + uint32_t length; // length (bytes) +}; + +/** The order of the propertyInfo structure must + be the same as the PROPID numbers in the + openpnp-capture.h header */ +const propertyInfo_t propertyInfo[] = +{ + {0,0,0}, + {CT_EXPOSURE_TIME_ABSOLUTE_CONTROL , 0, 4}, + {CT_FOCUS_ABSOLUTE_CONTROL , 0, 2}, + {CT_ZOOM_ABSOLUTE_CONTROL , 0, 2}, + {PU_WHITE_BALANCE_TEMPERATURE_CONTROL, 1, 2}, + {PU_GAIN_CONTROL , 1, 2}, + {PU_BRIGHTNESS_CONTROL , 1, 2}, + {PU_CONTRAST_CONTROL , 1, 2}, + {PU_SATURATION_CONTROL , 1, 2}, + {PU_GAMMA_CONTROL , 1, 2}, + {PU_HUE_CONTROL , 1, 2}, + {PU_SHARPNESS_CONTROL , 1, 2}, + {PU_BACKLIGHT_COMPENSATION_CONTROL , 1, 2}, + {PU_POWER_LINE_FREQUENCY_CONTROL , 1, 1} +}; + +@implementation UvcControlImpl { + IOUSBDeviceInterface** _deviceInterface; +} + + ++ (instancetype)createFromAVCaptureDevice:(AVCaptureDevice*)device status:(CS_Status*)status { + if (!device) { + NSLog(@"UVC: device is nil"); + *status = CS_UVC_STATUS_ERROR; + return nil; + } + + NSError* error = nil; + NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:@"^UVC\\s+Camera\\s+VendorID\\_([0-9]+)\\s+ProductID\\_([0-9]+)$" + options:0 + error:&error]; + if (error) { + NSLog(@"UVC: failed to create regex: %@", error); + *status = CS_UVC_STATUS_ERROR; + return nil; + } + + NSString* modelID = [device valueForKey:@"modelID"]; + if (!modelID) { + NSLog(@"UVC: modelID is nil"); + *status = CS_UVC_STATUS_ERROR; + return nil; + } + + NSTextCheckingResult* match = [regex firstMatchInString:modelID + options:0 + range:NSMakeRange(0, modelID.length)]; + if (!match || match.numberOfRanges != 3) { + NSLog(@"UVC: modelID regex match failed"); + *status = CS_UVC_STATUS_ERROR; + return nil; + } + + NSString* vendorIDStr = [modelID substringWithRange:[match rangeAtIndex:1]]; + NSString* productIDStr = [modelID substringWithRange:[match rangeAtIndex:2]]; + uint16_t vendorID = (uint16_t)strtoul([vendorIDStr UTF8String], NULL, 10); + uint16_t productID = (uint16_t)strtoul([productIDStr UTF8String], NULL, 10); + + uint32_t locationID = 0; + CFMutableDictionaryRef dict = IOServiceMatching(kIOUSBDeviceClassName); + CFDictionarySetValue(dict, CFSTR("idVendor"), (__bridge CFNumberRef)@(vendorID)); + CFDictionarySetValue(dict, CFSTR("idProduct"), (__bridge CFNumberRef)@(productID)); + + io_iterator_t iter = 0; + kern_return_t ioResult = IOServiceGetMatchingServices(kIOMainPortDefault, dict, &iter); + if (ioResult == kIOReturnSuccess) { + io_service_t usbDevice = IOIteratorNext(iter); + while (usbDevice != 0) { + CFTypeRef locationIDRef = IORegistryEntryCreateCFProperty(usbDevice, + CFSTR("locationID"), + kCFAllocatorDefault, + 0); + if (locationIDRef) { + locationID = [(__bridge NSNumber*)locationIDRef unsignedIntValue]; + CFRelease(locationIDRef); + NSString* uniqueID = [device valueForKey:@"uniqueID"]; + NSString* locationIDHex = [NSString stringWithFormat:@"0x%x", locationID]; + if ([uniqueID hasPrefix:locationIDHex]) { + IOObjectRelease(usbDevice); + break; + } + } + IOObjectRelease(usbDevice); + usbDevice = IOIteratorNext(iter); + } + IOObjectRelease(iter); + } + + UvcControlImpl *instance = [[UvcControlImpl alloc] initWithVendorId:vendorID + productId:productID + location:locationID + status:status]; + if (!instance) { + NSLog(@"UVC: failed to create UvcControlImpl, status=%d", *status); + } + return instance; +} + +- (instancetype)initWithVendorId:(uint16_t)vid + productId:(uint16_t)pid + location:(uint32_t)location + status:(CS_Status*)status { + self = [super init]; + if (self) { + // UVCINFO("Initializing with VID: 0x{:04X}, PID: 0x{:04X}, Location: 0x{:08X}", vid, pid, location); + _deviceInterface = [self findDevice:vid productId:pid location:location]; + if (_deviceInterface == nullptr) { + // UVCWARNING("Failed to find device"); + *status = CS_UVC_STATUS_DEVICE_DISCONNECTED; + return nil; + } + + _processingUnitID = [self getProcessingUnitID:_deviceInterface]; + + _controlInterface = [self createControlInterface:_deviceInterface]; + + if (_controlInterface == nullptr) { + // UVCWARNING("Failed to create control interface"); + *status = CS_UVC_STATUS_DEVICE_DISCONNECTED; + return nil; + } + } + return self; +} + +- (void)dealloc { + if (_controlInterface != nullptr) { + (*_controlInterface)->USBInterfaceClose(_controlInterface); + (*_controlInterface)->Release(_controlInterface); + } + if (_deviceInterface != nullptr) { + (*_deviceInterface)->Release(_deviceInterface); + } +} + +- (IOUSBDeviceInterface**)findDevice:(uint16_t)vid + productId:(uint16_t)pid + location:(uint32_t)location { + + CFMutableDictionaryRef dict = IOServiceMatching(kIOUSBDeviceClassName); + + io_iterator_t serviceIterator; + kern_return_t result = IOServiceGetMatchingServices((mach_port_t)NULL, dict, &serviceIterator); + if (result != kIOReturnSuccess) { + UVCERROR("findDevice: IOServiceGetMatchingServices failed: {}", result); + return nullptr; + } + + io_service_t device; + while((device = IOIteratorNext(serviceIterator)) != 0) { + IOUSBDeviceInterface **deviceInterface = nullptr; + IOCFPlugInInterface **plugInInterface = nullptr; + SInt32 score; + + kern_return_t result = IOCreatePlugInInterfaceForService( + device, kIOUSBDeviceUserClientTypeID, + kIOCFPlugInInterfaceID, &plugInInterface, &score); + + if ((result != kIOReturnSuccess) || (plugInInterface == nullptr)) { + UVCERROR("findDevice: Camera control error: {}", result); + IOObjectRelease(device); + continue; + } + + HRESULT hr = (*plugInInterface)->QueryInterface(plugInInterface, + CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID), + (LPVOID*)&deviceInterface); + + if (hr || (deviceInterface == nullptr)) { + (*plugInInterface)->Release(plugInInterface); + IOObjectRelease(device); + UVCERROR("findDevice: QueryInterface failed"); + continue; + } + + uint16_t vendorID, productID; + uint32_t locationID; + result = (*deviceInterface)->GetDeviceVendor(deviceInterface, &vendorID); + result = (*deviceInterface)->GetDeviceProduct(deviceInterface, &productID); + result = (*deviceInterface)->GetLocationID(deviceInterface, &locationID); + + // if 'location' is zero, we won't match on location + // to achieve this, we simply set locationID to zero. + if (location == 0) { + locationID = 0; + } + + if ((vendorID == vid) && (productID == pid) && (locationID == location)) { + (*plugInInterface)->Release(plugInInterface); + IOObjectRelease(device); + IOObjectRelease(serviceIterator); + return deviceInterface; + } + + (*deviceInterface)->Release(deviceInterface); + (*plugInInterface)->Release(plugInInterface); + IOObjectRelease(device); + } + + IOObjectRelease(serviceIterator); + return nullptr; +} + +- (uint32_t)getProcessingUnitID:(IOUSBDeviceInterface**)dev { + IOReturn kr; + IOUSBConfigurationDescriptorPtr configDesc; + + kr = (*dev)->GetConfigurationDescriptorPtr(dev, 0, &configDesc); + if (kr) { + return 0; + } + + UVCDEBUG4("USB descriptor:"); + UVCDEBUG4(" length = 0x{:08X}", configDesc->bLength); + UVCDEBUG4(" type = 0x{:08X}", configDesc->bDescriptorType); + UVCDEBUG4(" totalLen = 0x{:08X}", configDesc->wTotalLength); + UVCDEBUG4(" interfaces = 0x{:08X}", configDesc->bNumInterfaces); + + uint32_t idx = 0; + uint8_t *ptr = (uint8_t*)configDesc; + + // Search for VIDEO/CONTROL interface descriptor + // Class=14, Subclass=1, Protocol=0 + // and find the processing unit, if available.. + // DescriptorType 0x24, DescriptorSubType 0x5 + + IOUSBInterfaceDescriptor *iface = NULL; + ProcessingUnitDescriptor *pud = NULL; + bool inVideoControlInterfaceDescriptor = false; + while(idx < configDesc->wTotalLength) { + IOUSBDescriptorHeader *hdr = (IOUSBDescriptorHeader *)&ptr[idx]; + switch(hdr->bDescriptorType) + { + case 0x05: // Endpoint descriptor ID + break; + case 0x02: // Configuration descriptor ID + break; + case 0x04: // Interface descriptor ID + iface = (IOUSBInterfaceDescriptor*)&ptr[idx]; + if ((iface->bInterfaceClass == 14) && + (iface->bInterfaceSubClass == 1) && + (iface->bInterfaceProtocol == 0)) + { + inVideoControlInterfaceDescriptor = true; + } + else + { + inVideoControlInterfaceDescriptor = false; + } + break; + case 0x24: // class-specific ID + pud = (ProcessingUnitDescriptor*)&ptr[idx]; + if (inVideoControlInterfaceDescriptor) + { + if (pud->bDescriptorSubtype == 0x05) + { + return pud->bUnitID; + } + } + break; + default: + break; + } + idx += hdr->bLength; + } + + return 0; +} + +- (IOUSBInterfaceInterface190**)createControlInterface:(IOUSBDeviceInterface**)deviceInterface { + IOUSBInterfaceInterface190 **controlInterface; + + io_iterator_t interfaceIterator; + IOUSBFindInterfaceRequest interfaceRequest; + interfaceRequest.bInterfaceClass = UVC_CONTROL_INTERFACE_CLASS; + interfaceRequest.bInterfaceSubClass = UVC_CONTROL_INTERFACE_SUBCLASS; + interfaceRequest.bInterfaceProtocol = kIOUSBFindInterfaceDontCare; + interfaceRequest.bAlternateSetting = kIOUSBFindInterfaceDontCare; + + IOReturn result = (*deviceInterface)->CreateInterfaceIterator(deviceInterface, + &interfaceRequest, &interfaceIterator); + + if (result != kIOReturnSuccess) { + return nullptr; + } + + io_service_t usbInterface; + if ((usbInterface = IOIteratorNext(interfaceIterator)) != 0) { + IOCFPlugInInterface **plugInInterface = nullptr; + SInt32 score; + + kern_return_t kr = IOCreatePlugInInterfaceForService(usbInterface, + kIOUSBInterfaceUserClientTypeID, + kIOCFPlugInInterfaceID, + &plugInInterface, + &score); + + kr = IOObjectRelease(usbInterface); + if ((kr != kIOReturnSuccess) || !plugInInterface) { + UVCERROR("createControlInterface: cannot create plug-in {:08X}", + kr); + return nullptr; + } + + HRESULT hr = (*plugInInterface)->QueryInterface(plugInInterface, + CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID), + (LPVOID*) &controlInterface); + + (*plugInInterface)->Release(plugInInterface); + + if (hr || !controlInterface) { + UVCERROR("createControlInterface: cannot create device interface {:08X}", + result); + return nullptr; + } + + UVCDEBUG3("createControlInterface: created control interface"); + return controlInterface; + } + return nullptr; +} + +- (bool)sendControlRequest:(IOUSBDevRequest)req { + if (_controlInterface == nullptr) { + UVCERROR("control interface is NULL"); + return false; + } + + kern_return_t kr; + if (@available(macOS 12.0, *)) { + // macOS 12 doesn't like if we're trying to open USB interface here... + } else { + kr = (*_controlInterface)->USBInterfaceOpen(_controlInterface); + if (kr != kIOReturnSuccess) { + UVCERROR("USBInterfaceOpen failed with error: 0x{:08X}", kr); + return false; + } + } + + kr = (*_controlInterface)->ControlRequest(_controlInterface, 0, &req); + if (kr != kIOReturnSuccess) { + // IOKIT error code + #define err_get_system(err) (((err)>>26)&0x3f) + #define err_get_sub(err) (((err)>>14)&0xfff) + #define err_get_code(err) ((err)&0x3fff) + + uint32_t code = err_get_code(kr); + uint32_t sys = err_get_system(kr); + uint32_t sub = err_get_sub(kr); + + switch(kr) + { + case kIOUSBUnknownPipeErr: + UVCERROR("Pipe ref not recognised"); + break; + case kIOUSBTooManyPipesErr: + UVCERROR("Too many pipes"); + break; + case kIOUSBEndpointNotFound: + UVCERROR("Endpoint not found"); + break; + case kIOUSBConfigNotFound: + UVCERROR("USB configuration not found"); + break; + case kIOUSBPipeStalled: + //Note: we don't report this as an error as this happens when + // an unsupported or locked property is set. + UVCDEBUG("Pipe has stalled, error needs to be cleared"); + break; + case kIOUSBInterfaceNotFound: + UVCERROR("USB control interface not found"); + break; + default: + UVCERROR("ControlRequest failed (KR=sys:sub:code) = {:02Xh}:{:03Xh}:{:04Xh}", + sys, sub, code); + break; + } + + if (@available(macOS 12.0, *)) { + // macOS 12 doesn't like if we're trying to close USB interface here... + } else { + kr = (*_controlInterface)->USBInterfaceClose(_controlInterface); + if (kr != kIOReturnSuccess) { + UVCERROR("USBInterfaceClose failed"); + } + } + return false; + } + + if (@available(macOS 12.0, *)) { + // macOS 12 doesn't like if we're trying to close USB interface here either... + } else { + kr = (*_controlInterface)->USBInterfaceClose(_controlInterface); + + if (kr != kIOReturnSuccess) { + UVCERROR("USBInterfaceClose failed"); + } + } + + return true; +} + +- (bool)setData:(uint32_t)selector unit:(uint32_t)unit length:(uint32_t)length data:(int32_t)data { + IOUSBDevRequest req; + req.bmRequestType = USBmakebmRequestType((UInt8)kUSBOut, (UInt8)kUSBClass, (UInt8)kUSBInterface); + req.bRequest = UVC_SET_CUR; + req.wValue = (selector << 8); + req.wIndex = (unit << 8); + req.wLength = length; + req.wLenDone = 0; + req.pData = &data; + return [self sendControlRequest:req]; +} + +- (bool)getData:(uint32_t)selector unit:(uint32_t)unit length:(uint32_t)length data:(int32_t*)data { + IOUSBDevRequest req; + req.bmRequestType = USBmakebmRequestType((UInt8)kUSBIn, (UInt8)kUSBClass, (UInt8)kUSBInterface); + req.bRequest = UVC_GET_CUR; + req.wValue = (selector << 8); + req.wIndex = (unit << 8); + req.wLength = length; + req.wLenDone = 0; + req.pData = data; + return [self sendControlRequest:req]; +} + +- (bool)getMaxData:(uint32_t)selector unit:(uint32_t)unit length:(uint32_t)length data:(int32_t*)data { + IOUSBDevRequest req; + *data = 0; + req.bmRequestType = USBmakebmRequestType((UInt8)kUSBIn, (UInt8)kUSBClass, (UInt8)kUSBInterface); + req.bRequest = UVC_GET_MAX; + req.wValue = (selector << 8); + req.wIndex = (unit << 8); + req.wLength = length; + req.wLenDone = 0; + req.pData = data; + return [self sendControlRequest:req]; +} + +- (bool)getMinData:(uint32_t)selector unit:(uint32_t)unit length:(uint32_t)length data:(int32_t*)data { + IOUSBDevRequest req; + *data = 0; + req.bmRequestType = USBmakebmRequestType((UInt8)kUSBIn, (UInt8)kUSBClass, (UInt8)kUSBInterface); + req.bRequest = UVC_GET_MIN; + req.wValue = (selector << 8); + req.wIndex = (unit << 8); + req.wLength = length; + req.wLenDone = 0; + req.pData = data; + return [self sendControlRequest:req]; +} + +- (bool)getDefault:(uint32_t)selector unit:(uint32_t)unit length:(uint32_t)length data:(int32_t*)data { + IOUSBDevRequest req; + *data = 0; + req.bmRequestType = USBmakebmRequestType((UInt8)kUSBIn, (UInt8)kUSBClass, (UInt8)kUSBInterface); + req.bRequest = UVC_GET_DEF; + req.wValue = (selector << 8); + req.wIndex = (unit << 8); + req.wLength = length; + req.wLenDone = 0; + req.pData = data; + return [self sendControlRequest:req]; +} + +- (bool)getInfo:(uint32_t)selector unit:(uint32_t)unit data:(uint32_t*)data { + IOUSBDevRequest req; + *data = 0; + req.bmRequestType = USBmakebmRequestType((UInt8)kUSBIn, (UInt8)kUSBClass, (UInt8)kUSBInterface); + req.bRequest = UVC_GET_INFO; + req.wValue = (selector << 8); + req.wIndex = (unit << 8); + req.wLength = 1; + req.wLenDone = 0; + req.pData = data; + return [self sendControlRequest:req]; +} + +- (bool)setProperty:(uint32_t)propID withValue:(int32_t)value status:(CS_Status*)status { + if (_controlInterface == nullptr) { + UVCERROR("control interface is NULL"); + *status = CS_UVC_STATUS_DEVICE_DISCONNECTED; + return false; + } + + bool ok = false; + if (propID < CAPPROPID_LAST) { + uint32_t unit = (propertyInfo[propID].unit == 0) ? UVC_INPUT_TERMINAL_ID : _processingUnitID; + ok = [self setData:propertyInfo[propID].selector unit:unit length:propertyInfo[propID].length data:value]; + if (!ok) { + UVCWARNING("Failed to set property {}", propID); + } + } else { + UVCWARNING("Invalid property ID: {}", propID); + } + return ok; +} + +- (bool)getProperty:(uint32_t)propID withValue:(int32_t*)value status:(CS_Status*)status { + if (_controlInterface == nullptr) { + UVCERROR("control interface is NULL"); + *status = CS_UVC_STATUS_DEVICE_DISCONNECTED; + return false; + } + + bool ok = false; + if (propID < CAPPROPID_LAST) { + uint32_t unit = (propertyInfo[propID].unit == 0) ? UVC_INPUT_TERMINAL_ID : _processingUnitID; + ok = [self getData:propertyInfo[propID].selector unit:unit length:propertyInfo[propID].length data:value]; + + switch(propertyInfo[propID].length) { + case 2: + *value = static_cast(*value); + break; + case 1: + *value = static_cast(*value); + break; + default: + break; + } + if (!ok) { + UVCWARNING("Failed to get property {}", propID); + } + } else { + UVCWARNING("Invalid property ID: {}", propID); + } + return ok; +} + +- (bool)setAutoProperty:(uint32_t)propID enabled:(bool)enabled status:(CS_Status*)status { + if (_controlInterface == nullptr) { + UVCERROR("control interface is NULL"); + *status = CS_UVC_STATUS_DEVICE_DISCONNECTED; + return false; + } + + int32_t value = enabled ? 1 : 0; + switch(propID) { + case CAPPROPID_EXPOSURE: + return [self setData:CT_AE_MODE_CONTROL unit:UVC_INPUT_TERMINAL_ID length:1 data:enabled ? 0x8 : 0x1]; + case CAPPROPID_WHITEBALANCE: + return [self setData:PU_WHITE_BALANCE_TEMPERATURE_AUTO_CONTROL unit:_processingUnitID length:1 data:value]; + case CAPPROPID_FOCUS: + return [self setData:CT_FOCUS_AUTO_CONTROL unit:UVC_INPUT_TERMINAL_ID length:1 data:value]; + default: + return false; + } +} + +- (bool)getAutoProperty:(uint32_t)propID enabled:(bool*)enabled status:(CS_Status*)status { + if (_controlInterface == nullptr) { + UVCERROR("control interface is NULL"); + *status = CS_UVC_STATUS_DEVICE_DISCONNECTED; + return false; + } + + int32_t value; + + switch(propID) { + case CAPPROPID_EXPOSURE: + if ([self getData:CT_AE_MODE_CONTROL unit:UVC_INPUT_TERMINAL_ID length:1 data:&value]) { + // value = 1 -> manual mode + // 2 -> auto mode (I haven't seen this in the wild) + // 4 -> shutter priority mode (haven't seen this) + // 8 -> aperature prioritry mode (seen this used) + value &= 0xFF; + *enabled = (value==1) ? false : true; + return true; + } + return false; + case CAPPROPID_WHITEBALANCE: + if ([self getData:PU_WHITE_BALANCE_TEMPERATURE_AUTO_CONTROL unit:_processingUnitID length:1 data:&value]) { + value &= 0xFF; + *enabled = (value==1) ? true : false; + UVCDEBUG3("White balance auto mode: {}", *enabled ? "enabled" : "disabled"); + return true; + } + return false; + case CAPPROPID_FOCUS: + if ([self getData:CT_FOCUS_AUTO_CONTROL unit:UVC_INPUT_TERMINAL_ID length:1 data:&value]) { + value &= 0xFF; + *enabled = (value==1) ? true : false; + UVCDEBUG3("Focus auto mode: {}", *enabled ? "enabled" : "disabled"); + return true; + } + return false; + default: + UVCWARNING("Unsupported auto property ID: {}", propID); + return false; + } +} + +- (bool)getPropertyLimits:(uint32_t)propID min:(int32_t*)min max:(int32_t*)max defValue:(int32_t*)defValue status:(CS_Status*)status { + if (_controlInterface == nullptr) { + *status = CS_UVC_STATUS_DEVICE_DISCONNECTED; + return false; + } + + bool ok = true; + if (propID < CAPPROPID_LAST) { + uint32_t unit = (propertyInfo[propID].unit == 0) ? UVC_INPUT_TERMINAL_ID : _processingUnitID; + + if (![self getMinData:propertyInfo[propID].selector unit:unit length:propertyInfo[propID].length data:min]) { + ok = false; + } + + if (![self getMaxData:propertyInfo[propID].selector unit:unit length:propertyInfo[propID].length data:max]) { + ok = false; + } + + if (![self getDefault:propertyInfo[propID].selector unit:unit length:propertyInfo[propID].length data:defValue]) { + ok = false; + } + + switch(propertyInfo[propID].length) { + case 2: + *min = static_cast(*min); + *max = static_cast(*max); + *defValue = static_cast(*defValue); + break; + case 1: + *min = static_cast(*min); + *max = static_cast(*max); + *defValue = static_cast(*defValue); + break; + default: + break; + } + } else { + UVCWARNING("getPropertyLimits: property ID out of bounds"); + ok = false; + } + return ok; +} + +- (void)reportCapabilities:(uint32_t)selector unit:(uint32_t)unit { + uint32_t info; + [self getInfo:selector unit:unit data:&info]; + if (info & 0x01) { + UVCDEBUG4("GET "); + } + if (info & 0x02) { + UVCDEBUG4("SET "); + } + if (info & 0x04) { + UVCDEBUG4("DISABLED "); + } + if (info & 0x08) { + UVCDEBUG4("AUTO-UPD "); + } + if (info & 0x10) { + UVCDEBUG4("ASYNC "); + } + if (info & 0x20) { + UVCDEBUG4("DISCOMMIT"); + } + UVCDEBUG4(""); +} + +@end \ No newline at end of file diff --git a/shared/config.gradle b/shared/config.gradle index adca10481e..540ed9bd32 100644 --- a/shared/config.gradle +++ b/shared/config.gradle @@ -32,6 +32,13 @@ nativeUtils.enableSourceLink() nativeUtils.wpi.addMacMinimumVersionArg() +nativeUtils.platformConfigs.each { + if (it.name.contains('osx')) { + it.linker.getArgs().add('-framework') + it.linker.getArgs().add('IOKit') + } +} + // Compress debug info on Linux nativeUtils.platformConfigs.each { if (it.name.contains('linux')) {