-
-
Save adthom/b703078806adeb71fe860929df0bd4c1 to your computer and use it in GitHub Desktop.
| <# | |
| .SYNOPSIS | |
| Queries and exports attached peripheral information | |
| .DESCRIPTION | |
| Queries and exports attached peripheral information for use in Microsoft Teams Bring Your Own Device (BYOD) | |
| data entry in the Pro Management Portal (PMP) for enhanced reporting | |
| .EXAMPLE | |
| .\Get-TeamsBYODSpaceDevices.ps1 | |
| .NOTES | |
| Version: 2.1 | |
| Author: andthom@microsoft.com, rowille@microsoft.com | |
| Creation Date: March 2024 | |
| #> | |
| ## Disclaimer | |
| # (c)2024 Microsoft Corporation. All rights reserved. This document is provided "as-is." Information and views expressed in this document, | |
| # including URL and other Internet Web site references, may change without notice. You bear the risk of using it. | |
| # This document does not provide you with any legal rights to any intellectual property in any Microsoft product. | |
| # You may copy and use this document for your internal, reference purposes. You may modify this document for your internal purposes. | |
| [CmdletBinding()] | |
| param() | |
| function AddNativeHIDType { | |
| [CmdletBinding()] | |
| param() | |
| end { | |
| try { | |
| $null = [Native.HID+DeviceInfo]::new() | |
| } | |
| catch { | |
| #Region TypeDefinition | |
| $Signature = @' | |
| const uint FILE_SHARE_READ = 0x00000001; | |
| const uint FILE_SHARE_WRITE = 0x00000002; | |
| const uint OPEN_EXISTING = 0x00000003; | |
| const uint GENERIC_READ = 0x80000000; | |
| const uint GENERIC_WRITE = 0x40000000; | |
| const uint FILE_FLAG_OVERLAPPED = 0x40000000; | |
| const int ERROR_INVALID_USER_BUFFER = 0x6F8; | |
| [DllImport("kernel32", SetLastError = true)] | |
| static extern SafeFileHandle CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); | |
| [DllImport("kernel32", SetLastError = true)] | |
| static extern bool CloseHandle(IntPtr hObject); | |
| internal struct HIDD_ATTRIBUTES | |
| { | |
| public int Size; | |
| public ushort VendorID; | |
| public ushort ProductID; | |
| public ushort VersionNumber; | |
| public static HIDD_ATTRIBUTES Create() | |
| { | |
| return new HIDD_ATTRIBUTES() | |
| { | |
| Size = Marshal.SizeOf(typeof(HIDD_ATTRIBUTES)) | |
| }; | |
| } | |
| } | |
| static SafeFileHandle GetDeviceHandle(string lpFileName) | |
| { | |
| var hDevice = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, IntPtr.Zero); | |
| if (Marshal.GetLastWin32Error() != 0) | |
| { | |
| return SafeFileHandle.Null; | |
| } | |
| return hDevice; | |
| } | |
| [DllImport("hid", SetLastError = true)] | |
| static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, out HIDD_ATTRIBUTES attributes); | |
| [DllImport("hid", SetLastError = true, CharSet = CharSet.Unicode)] | |
| static extern bool HidD_GetSerialNumberString(SafeFileHandle hidDeviceObject, StringBuilder buffer, int bufferLength); | |
| static string HIDGetSerialNumberString(SafeFileHandle hDevice) | |
| { | |
| var builder = new StringBuilder(256); | |
| while (builder.Capacity < 4094) | |
| { | |
| if (HidD_GetSerialNumberString(hDevice, builder, builder.Capacity)) | |
| return builder.ToString(); | |
| if (Marshal.GetLastWin32Error() != ERROR_INVALID_USER_BUFFER) | |
| break; | |
| builder.Capacity *= 2; | |
| } | |
| return null; | |
| } | |
| public static DeviceInfo GetHidDevice(string devicePath) | |
| { | |
| using (var hDevice = GetDeviceHandle(devicePath)) | |
| { | |
| if (hDevice.IsInvalid) | |
| return null; | |
| var attributes = HIDD_ATTRIBUTES.Create(); | |
| HidD_GetAttributes(hDevice, out attributes); | |
| if (Marshal.GetLastWin32Error() != 0) | |
| return null; | |
| return new DeviceInfo | |
| { | |
| SerialNumber = HIDGetSerialNumberString(hDevice), | |
| VendorId = attributes.VendorID, | |
| ProductId = attributes.ProductID, | |
| }; | |
| } | |
| } | |
| public class DeviceInfo | |
| { | |
| public string SerialNumber { get; set; } | |
| public ushort VendorId { get; set; } | |
| public ushort ProductId { get; set; } | |
| } | |
| internal static readonly IntPtr INVALID_HANDLE = new IntPtr(-1); | |
| public class SafeFileHandle : SafeHandle | |
| { | |
| public static readonly SafeFileHandle Invalid = new SafeFileHandle(); | |
| public static readonly SafeFileHandle Null = new SafeFileHandle(IntPtr.Zero, false); | |
| public SafeFileHandle() | |
| : base(INVALID_HANDLE, true) | |
| { | |
| } | |
| public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle = true) | |
| : base(INVALID_HANDLE, ownsHandle) | |
| { | |
| this.SetHandle(preexistingHandle); | |
| } | |
| public override bool IsInvalid { get { return this.handle == INVALID_HANDLE || this.handle == IntPtr.Zero; } } | |
| protected override bool ReleaseHandle() { return CloseHandle(this.handle); } | |
| } | |
| '@ | |
| #EndRegion TypeDefinition | |
| Add-Type -MemberDefinition $Signature -Name HID -Namespace Native -UsingNamespace System.Text | |
| } | |
| } | |
| } | |
| function GetCurrentDevices { | |
| [CmdletBinding()] | |
| [OutputType([Collections.Generic.HashSet[string]])] | |
| param() | |
| end { | |
| Write-Output ([Collections.Generic.HashSet[string]]::new([string[]]@((Get-PnpDevice -PresentOnly).DeviceID), [StringComparer]::OrdinalIgnoreCase)) -NoEnumerate | |
| } | |
| } | |
| function Get-DeviceProperties { | |
| [CmdletBinding()] | |
| [OutputType([PSCustomObject])] | |
| param( | |
| [Parameter(Mandatory = $true, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] | |
| [string] | |
| $DeviceID, | |
| [hashtable] | |
| $Properties = @{ | |
| 'DEVPKEY_NAME' = 'Name' | |
| 'DEVPKEY_Device_BusReportedDeviceDesc' = 'NameFromBus' | |
| 'DEVPKEY_Device_Service' = 'Service' | |
| 'DEVPKEY_Device_Class' = 'PNPClass' | |
| 'DEVPKEY_Device_Parent' = 'ParentID' | |
| } | |
| ) | |
| process { | |
| $Instance = [Management.ManagementObject]::new('Win32_PnPEntity.DeviceID="{0}"' -f $DeviceID.Replace('\', '\\')) | |
| $params = $Instance.GetMethodParameters('GetDeviceProperties') | |
| $params['devicePropertyKeys'] = [string[]]$Properties.Keys | |
| $result = $Instance.InvokeMethod('GetDeviceProperties', $params, $null) | |
| $objHash = [ordered]@{ | |
| DeviceID = $DeviceID | |
| } | |
| foreach ($property in $result['deviceProperties']) { | |
| $name = $property['KeyName'] | |
| if ([string]::IsNullOrEmpty($name)) { | |
| $name = $property['key'] | |
| } | |
| $objHash[$Properties[$name]] = $property['Data'] | |
| } | |
| [PSCustomObject]$objHash | |
| } | |
| } | |
| function InvokeWMIQuery { | |
| [CmdletBinding()] | |
| [OutputType([Management.ManagementBaseObject])] | |
| param( | |
| [Parameter(Mandatory,ValueFromPipeline)] | |
| [string] | |
| $QueryString, | |
| [string] | |
| $Scope = 'root/WMI' | |
| ) | |
| $query = [Management.ObjectQuery]::new('WQL', $QueryString) | |
| $searcher = [Management.ManagementObjectSearcher]::new([Management.ManagementScope]::new($Scope), $query) | |
| Write-Verbose "Running '$QueryString'" | |
| try { $searcher.Get() } catch {} finally { if ($searcher) { $searcher.Dispose() } } | |
| } | |
| function GetMonitorId { | |
| [CmdletBinding()] | |
| [OutputType([byte[]])] | |
| param( | |
| [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] | |
| [Alias('DeviceID')] | |
| [string] | |
| $InstanceName | |
| ) | |
| process { | |
| Write-Verbose "Running GetMonitorId for '$InstanceName'" | |
| 'SELECT * FROM WmiMonitorDescriptorMethods WHERE Active = TRUE AND InstanceName LIKE ''{0}%''' -f $InstanceName.Replace('\', '\\') | | |
| InvokeWMIQuery | Select-Object -ExpandProperty InstanceName -First 1 | |
| } | |
| } | |
| function GetEDID { | |
| [CmdletBinding()] | |
| [OutputType([byte[]])] | |
| param( | |
| [Parameter(Mandatory,ValueFromPipelineByPropertyName, ValueFromPipeline)] | |
| [Alias('DeviceID')] | |
| [string] | |
| $InstanceName | |
| ) | |
| process { | |
| Write-Verbose "Running GetEDID for '$InstanceName'" | |
| $EDID = 'SELECT * FROM WmiMonitorDescriptorMethods WHERE Active = TRUE AND InstanceName LIKE ''{0}%''' -f $InstanceName.Replace('\', '\\') | InvokeWMIQuery | Select-Object -First 1 | |
| if (!$EDID) { | |
| return $null | |
| } | |
| $inParams = $EDID.GetMethodParameters('WmiGetMonitorRawEEdidV1Block') | |
| $RawEDID = $null | |
| for ($i = 0; $i -lt 256; $i++) { | |
| $inParams.BlockId = $i | |
| try { | |
| $RawEDID = $EDID.InvokeMethod('WmiGetMonitorRawEEdidV1Block', $inParams, $null) | Where-Object { $_.BlockType -eq 1 } | Select-Object -ExpandProperty BlockContent | |
| } | |
| catch { break } | |
| if ($RawEDID -and $RawEDID.Length -gt 0) { | |
| break | |
| } | |
| } | |
| if (!$RawEDID -or $RawEDID.Length -eq 0) { | |
| return $null | |
| } | |
| Write-Output $RawEDID -NoEnumerate | |
| } | |
| } | |
| function MonitorIsExpectedOutputType { | |
| [CmdletBinding()] | |
| [OutputType([bool])] | |
| param( | |
| [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] | |
| [Alias('DeviceID')] | |
| [string] | |
| $InstanceName | |
| ) | |
| process { | |
| Write-Verbose "Running MonitorIsExpectedOutputType for '$InstanceName'" | |
| $GoodOutputType = @('SELECT VideoOutputTechnology FROM WmiMonitorConnectionParams WHERE InstanceName LIKE ''{0}%''' -f $InstanceName.Replace('\', '\\') | | |
| InvokeWMIQuery | Where-Object { $_.VideoOutputTechnology -notin @(6, 11, 13, 0x80000000) }) | |
| return $GoodOutputType.Count -gt 0 | |
| } | |
| } | |
| function Get-MonitorVIDFromEDID { | |
| [CmdletBinding()] | |
| [OutputType([UInt16])] | |
| param( | |
| [Parameter(Mandatory)] | |
| [byte[]] | |
| $EDID | |
| ) | |
| end { | |
| if ($EDID.Length -ne 128) { | |
| return $null | |
| } | |
| # value is big-endian, so need to reverse the 2 bytes | |
| [BitConverter]::ToUInt16([byte[]]($EDID[9],$EDID[8]),0) | |
| } | |
| } | |
| function Get-MonitorPIDFromEDID { | |
| [CmdletBinding()] | |
| [OutputType([UInt16])] | |
| param( | |
| [Parameter(Mandatory)] | |
| [byte[]] | |
| $EDID | |
| ) | |
| end { | |
| if ($EDID.Length -ne 128) { | |
| return $null | |
| } | |
| [BitConverter]::ToUInt16($EDID,10) | |
| } | |
| } | |
| function Get-MonitorEDIDDescriptor { | |
| [CmdletBinding()] | |
| [OutputType([string])] | |
| param( | |
| [Parameter(Mandatory)] | |
| [byte[]] | |
| $EDID, | |
| [Parameter(Mandatory)] | |
| [UInt32] | |
| $Descriptor | |
| ) | |
| end { | |
| if ($EDID.Length -ne 128) { | |
| return $null | |
| } | |
| [String]::new([char[]]@(& { | |
| $i = 0 | |
| while ($i -lt ($EDID.Length - 4) -and [BitConverter]::ToUInt32($EDID,$i) -ne $Descriptor) { $i++ } | |
| if ($i -lt ($EDID.Length - 4)) { | |
| $i += 4 | |
| while ($i -lt $EDID.Length -and $EDID[$i] -ne 0xa) { | |
| if ($EDID[$i] -ne 0) { [char]$EDID[$i] } | |
| $i++ | |
| } | |
| } | |
| })) | |
| } | |
| } | |
| function MergeResults { | |
| [CmdletBinding()] | |
| param( | |
| [Collections.IEnumerable] | |
| $NewDevices, | |
| [Collections.IEnumerable] | |
| $ExistingDevices | |
| ) | |
| begin { | |
| function Get-ConflictKey { | |
| [CmdletBinding()] | |
| [OutputType([string])] | |
| param( | |
| [Parameter(Mandatory,ValueFromPipeline)] | |
| [PSObject] | |
| $InputObject | |
| ) | |
| process { | |
| [string]::Join('_',$InputObject.'Vendor ID', $InputObject.'Product ID', $InputObject.'Serial Number') | |
| } | |
| } | |
| function Get-RunKey { | |
| [CmdletBinding()] | |
| [OutputType([string])] | |
| param( | |
| [Parameter(Mandatory,ValueFromPipeline)] | |
| [PSObject] | |
| $InputObject | |
| ) | |
| process { | |
| [string]::Join('_',$InputObject.Account, $InputObject.'Display Name') | |
| } | |
| } | |
| } | |
| end { | |
| $NewDeviceKeys = @($NewDevices | Get-ConflictKey) | |
| $Conflicts = $ExistingDevices | Where-Object { $NewDeviceKeys -contains (Get-ConflictKey $_) } | |
| $ExistingResults = [Collections.Generic.Dictionary[string, Collections.Generic.List[PSCustomObject]]]@{} | |
| $ExistingDevices | ForEach-Object { | |
| $Key = Get-RunKey $_ | |
| if (!$ExistingResults.ContainsKey($Key)) { $ExistingResults[$Key] = @() } | |
| $ExistingResults[$Key].Add($_) | |
| } | |
| $MergedResults = [Collections.Generic.List[PSCustomObject]]@() | |
| foreach ($Key in $ExistingResults.Keys) { | |
| $PreviousRun = $ExistingResults[$Key] | |
| $PreviousRunDevicesWithSerialNumber = @($PreviousRun | Where-Object { ![string]::IsNullOrEmpty($_.'Serial Number') }) | |
| $PreviousRunHasNoConflict = @($PreviousRun | Where-Object { $_ -in $Conflicts }).Count -eq 0 | |
| $PreviousRunHasUniqueSerialNumber = !$PreviousRunHasNoConflict -and @($PreviousRunDevicesWithSerialNumber | Where-Object { $_ -notin $Conflicts }).Count -gt 0 | |
| $PreviousRunIsInvalid = $PreviousRunHasUniqueSerialNumber -and @($PreviousRunDevicesWithSerialNumber | Where-Object { $_ -in $Conflicts }).Count -gt 0 | |
| if ($PreviousRunIsInvalid) { | |
| # If the run has a unique serial number, but at least 1 device with a serial number that is in conflict, then it is an invalid run | |
| Write-Warning 'The current execution contains differing data from prior conflicting run!' | |
| } | |
| if ($PreviousRunHasNoConflict -or ($PreviousRunHasUniqueSerialNumber -and !$PreviousRunIsInvalid)) { | |
| # If the previous run has no conflicts, it should be included in the output | |
| # If the run has at least one unique serial number, and no conflicting devices with a serial number, it should be included in the output | |
| foreach ($item in $PreviousRun) { | |
| $MergedResults.Add($item) | |
| } | |
| } else { | |
| Write-Warning "The data below will be removed from the output!`n$($PreviousRun | Format-Table -AutoSize | Out-String)" | |
| } | |
| } | |
| # All devices from the current run should be included at the end of the output | |
| foreach ($item in $NewDevices) { | |
| $MergedResults.Add($item) | |
| } | |
| $MergedResults | Write-Output | |
| } | |
| } | |
| function BuildDeviceHierarchy { | |
| [CmdletBinding()] | |
| param( | |
| [Collections.IEnumerable] | |
| $DeviceSubset, | |
| [Collections.IEnumerable] | |
| $AllDevices | |
| ) | |
| end { | |
| $Stack = [Collections.Generic.Stack[object]]$DeviceSubset | |
| $ProcessedIDs = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) | |
| while ($Stack.Count) { | |
| $CurrentDevice = $Stack.Pop() | |
| if (!$ProcessedIDs.Add($CurrentDevice.DeviceID)) { | |
| continue | |
| } | |
| $Children = $AllDevices.Where({ $CurrentDevice.DeviceID -eq $_.ParentID }) | |
| $CurrentDevice | Add-Member -NotePropertyName Children -NotePropertyValue $Children | |
| foreach ($Child in $CurrentDevice.Children) { | |
| $Stack.Push($Child) | |
| $AllDevices = $AllDevices.Where({ $_.DeviceID -ne $Child.DeviceID }) | |
| } | |
| } | |
| } | |
| } | |
| $script:DeviceSerialNumbers = @{} | |
| function BuildSerialNumberLookup { | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory,ValueFromPipeline)] | |
| [string] | |
| $InputObject | |
| ) | |
| begin { | |
| $script:DeviceSerialNumbers.Clear() | |
| AddNativeHIDType | |
| } | |
| process { | |
| $HIDInfo = [Native.HID]::GetHidDevice('\\?\' + $InputObject.Replace('\', '#') + '#{4d1e55b2-f16f-11cf-88cb-001111000030}') | |
| if (!$HIDInfo) { | |
| return | |
| } | |
| $Lookup = [string]::Join('-',$HIDInfo.VendorId,$HIDInfo.ProductId) | |
| if ($DeviceSerialNumbers.ContainsKey($Lookup) -and $DeviceSerialNumbers[$Lookup] -ne $HIDInfo.SerialNumber) { | |
| Write-Warning "Duplicate device found with Vendor ID: $($HIDInfo.VendorId) and Product ID: $($HIDInfo.ProductId). Changing serial number from $($DeviceSerialNumbers[$Lookup]) to $($HIDInfo.SerialNumber)." | |
| } | |
| $DeviceSerialNumbers[$Lookup] = $HIDInfo.SerialNumber | |
| } | |
| } | |
| function GetCurrentContents { | |
| [CmdletBinding()] | |
| param( | |
| [string] | |
| $OutputFilePath | |
| ) | |
| end { | |
| while ($true) { | |
| try { | |
| $Contents = @(Import-Csv -Path $OutputFilePath -ErrorAction Stop) | |
| Write-Output $Contents -NoEnumerate | |
| break | |
| } | |
| catch [IO.IOException] { | |
| if (!(Test-Path $OutputFilePath)) { | |
| Write-Output @() -NoEnumerate | |
| break | |
| } | |
| Write-Warning "The file $OutputFilePath is currently in use. Please close the file and press Enter to retry." | |
| $null = Read-Host | |
| } | |
| } | |
| } | |
| } | |
| function ExportData { | |
| [CmdletBinding()] | |
| param( | |
| [Collections.IEnumerable] | |
| $DataToExport, | |
| [string] | |
| $OutputFilePath | |
| ) | |
| end { | |
| while ($true) { | |
| try { | |
| $DataToExport | Export-Csv -Path $OutputFilePath -NoTypeInformation -ErrorAction Stop | |
| break | |
| } | |
| catch [IO.IOException] { | |
| Write-Warning "The file $OutputFilePath is currently in use. Please close the file and press Enter to retry." | |
| $null = Read-Host | |
| } | |
| } | |
| } | |
| } | |
| $MONITOR_SERIAL_NUMBER_DESCRIPTOR = 4278190080 # 0x0,0x0,0x0,0xff | |
| $MONITOR_NAME_DESCRIPTOR = 4227858432 # 0x0,0x0,0x0,0xfc | |
| $USB_DEVICEID_PATTERN = 'VID_(?<vid>[0-9A-F]{4})&PID_(?<pid>[0-9A-F]{4})' | |
| $DEBUG_RUN = $env:RUN_FOR_ALL_DEVICES -and $env:RUN_FOR_ALL_DEVICES -ne '0' | |
| $MediaDevices = [Collections.Generic.List[PSCustomObject]]@() | |
| $CurrentDevices = GetCurrentDevices | |
| while ($true) { | |
| Write-Host 'Please Connect The BYOD Room or Workspace Devices Now' | |
| $InputSeconds = Read-Host 'Press Enter When Ready' | |
| if (![int]::TryParse($InputSeconds,[ref]0) -or $InputSeconds -le 0) { | |
| $InputSeconds = 15 | |
| } | |
| Write-Host "Waiting $InputSeconds seconds for devices to be ready..." | |
| Start-Sleep -Seconds $InputSeconds | |
| Write-Host 'Checking for new devices...' | |
| $NewDevices = GetCurrentDevices | |
| $NewDevices.ExceptWith($CurrentDevices) | |
| if (!$NewDevices.Count) { | |
| Write-Host 'No new devices found, Are you sure you connected the devices when prompted?' -ForegroundColor Red | |
| if (!$DEBUG_RUN) { return } | |
| $NewDevices = $CurrentDevices | |
| } | |
| $MediaDevices.Clear() | |
| Write-Host 'Getting device information...' | |
| $UPN = $null | |
| $DisplayName = $null | |
| while (!$UPN) { | |
| $UPN = Read-Host 'Enter the BYOD Room or Workspace Account''s User Principal Name (UPN)' | |
| } | |
| $DisplayName = Read-Host 'Enter the BYOD Room or Workspace Account''s Display Name (default: none)' | |
| if (!$Local:OutputFilePath -or !(Test-Path $OutputFilePath)) { | |
| $DefaultFilePath = [Environment]::GetFolderPath('Desktop') | |
| $OutputFilePath = Read-Host "Enter the folder path where the PERIPHERALS.csv file will be saved (default: $DefaultFilePath)" | |
| if (!$OutputFilePath) { | |
| $OutputFilePath = $DefaultFilePath | |
| } | |
| $OutputFilePath = [IO.Path]::Combine($OutputFilePath, 'PERIPHERALS.csv') | |
| } | |
| else { | |
| Write-Host "Using the existing file $OutputFilePath to store the data." | |
| } | |
| Write-Host 'Gathering peripheral data for the newly connected devices...' | |
| $Devices = @($NewDevices | Get-DeviceProperties) | |
| $USBCompositeDevices = @($Devices | Where-Object { $_.PNPClass -in 'USB' }) | |
| $MonitorDevices = @($Devices | Where-Object { $_.PNPClass -in 'Monitor' }) | |
| BuildDeviceHierarchy -DeviceSubset $USBCompositeDevices -AllDevices $Devices | |
| Write-Host 'Processing the discovered peripheral data...' | |
| $NewDevices | BuildSerialNumberLookup | |
| $USBCompositeDevices | | |
| Where-Object { $_.Children | Where-Object { | |
| $_.PNPClass -in @('Image', 'Camera') -or ($_.Children | Where-Object { $_.PNPClass -eq 'AudioEndpoint' }) | |
| } } | | |
| ForEach-Object { | |
| $Device = $_ | |
| if ($NewDevices.Contains($Device.DeviceID)) { $null = $NewDevices.Remove($Device.DeviceID) } | |
| if ($Device.DeviceID -notmatch $USB_DEVICEID_PATTERN) { | |
| continue | |
| } | |
| $DeviceVID = [Int32]::Parse($Matches['vid'], [Globalization.NumberStyles]::AllowHexSpecifier) | |
| $DevicePID = [Int32]::Parse($Matches['pid'], [Globalization.NumberStyles]::AllowHexSpecifier) | |
| $DeviceSerialNumber = $DeviceSerialNumbers["$DeviceVID-$DevicePID"] | |
| $Camera = @($Device.Children | Where-Object { $_.PNPClass -in @('Camera', 'Image') } | Sort-Object DeviceID) | |
| $Audio = @($Device.Children | Where-Object { $_.Children | Where-Object { $_.PNPClass -eq 'AudioEndpoint' } } | Sort-Object DeviceID) | |
| $AudioEndpoints = @($Audio.Children | Where-Object { $_.PNPClass -eq 'AudioEndpoint' -and $_.DeviceID } | | |
| ForEach-Object { [PSCustomObject]@{Id = $_.DeviceID.Split('\', 3)[2]; Name = $_.Name } }) | |
| $Speaker = @($AudioEndpoints | Where-Object { $_.Id.StartsWith('{0.0.0.') } | Sort-Object Id) | |
| $Microphone = @($AudioEndpoints | Where-Object { $_.Id.StartsWith('{0.0.1.') } | Sort-Object Id) | |
| if (!$Camera -and !$Speaker -and !$Microphone) { | |
| continue | |
| } | |
| $DeviceType = & { | |
| if ($Speaker -and !$Microphone -and !$Camera) { | |
| return 'Speaker' | |
| } | |
| if (!$Speaker -and $Microphone -and !$Camera) { | |
| return 'Microphone' | |
| } | |
| if (!$Speaker -and !$Microphone -and $Camera) { | |
| return 'Camera' | |
| } | |
| if ($Speaker -and $Microphone -and !$Camera) { | |
| return 'CompositeAudioDevice' | |
| } | |
| if ($Speaker -and $Microphone -and $Camera) { | |
| return 'CompositeAudioVideoDevice' | |
| } | |
| return 'CompositeVideoDevice' | |
| } | |
| $DeviceName = switch ($DeviceType) { | |
| 'Speaker' { $Speaker[0].Name } | |
| 'Microphone' { $Microphone[0].Name } | |
| 'Camera' { $Camera[0].Name } | |
| 'CompositeAudioDevice' { $(if (![string]::IsNullOrEmpty($Device.NameFromBus)) { $Device.NameFromBus } else { $Microphone[0].Name }) } | |
| 'CompositeVideoDevice' { 'Composite - ' + $(if (![string]::IsNullOrEmpty($Device.NameFromBus)) { $Device.NameFromBus } else { $Camera[0].Name }) } | |
| 'CompositeAudioVideoDevice' { $(if (![string]::IsNullOrEmpty($Device.NameFromBus)) { $Device.NameFromBus } else { $Camera[0].Name }) } | |
| default { $(if (![string]::IsNullOrEmpty($Device.NameFromBus)) { $Device.NameFromBus } else { $Device.Name }) } | |
| } | |
| $MediaDevice = [PSCustomObject][ordered]@{ | |
| Account = "$UPN" | |
| 'Display Name' = "$DisplayName" | |
| 'Product ID' = "$DevicePID" | |
| 'Vendor ID' = "$DeviceVID" | |
| 'Serial Number' = "$DeviceSerialNumber" | |
| 'Peripheral Name' = "$DeviceName" | |
| 'Peripheral Type' = "$DeviceType" | |
| } | |
| $MediaDevices.Add($MediaDevice) | |
| } | |
| $MonitorDevices | Where-Object { $_.DeviceID } | | |
| ForEach-Object { | |
| $Device = $_ | |
| if ($NewDevices.Contains($_.DeviceID)) { $null = $NewDevices.Remove($_.DeviceID) } | |
| $RawEDID = $Device | GetEDID | |
| if (!$RawEDID) { | |
| Write-Host "Failed to retrieve EDID information for $($Device.DeviceID)." | |
| continue | |
| } | |
| if (!($Device | GetMonitorId | MonitorIsExpectedOutputType)) { | |
| Write-Host "$($Device.DeviceID) is not of the desired type." | |
| continue | |
| } | |
| $DevicePID = Get-MonitorPIDFromEDID $RawEDID | |
| $DeviceVID = Get-MonitorVIDFromEDID $RawEDID | |
| $DeviceSerialNumber = Get-MonitorEDIDDescriptor $RawEDID $MONITOR_SERIAL_NUMBER_DESCRIPTOR | |
| $DeviceName = Get-MonitorEDIDDescriptor $RawEDID $MONITOR_NAME_DESCRIPTOR | |
| $MediaDevice = [PSCustomObject][ordered]@{ | |
| Account = "$UPN" | |
| 'Display Name' = "$DisplayName" | |
| 'Product ID' = "$DevicePID" | |
| 'Vendor ID' = "$DeviceVID" | |
| 'Serial Number' = "$DeviceSerialNumber" | |
| 'Peripheral Name' = "$DeviceName" | |
| 'Peripheral Type' = 'Screen' | |
| } | |
| $MediaDevices.Add($MediaDevice) | |
| } | |
| $DataToExport = @($MediaDevices | Where-Object { $_.'Peripheral Type' -notin @('Unknown', 'Camera') }) | |
| if ((Test-Path $OutputFilePath)) { | |
| $CurrentContents = GetCurrentContents $OutputFilePath | |
| $DataToExport = @(MergeResults -NewDevices $DataToExport -ExistingDevices $CurrentContents) | |
| } | |
| # Update the CSV file | |
| ExportData $DataToExport $OutputFilePath | |
| Write-Host "Your discovered peripheral data has been collected and exported to $OutputFilePath." -ForegroundColor Green | |
| Write-Host 'Here is a preview of the results that were exported:' | |
| $DataToExport | Format-Table -AutoSize | |
| $Continue = Read-Host 'Are you finished collecting BYOD Rooms or Workspaces? (default: no)' | |
| if ($Continue -and $Continue[0] -eq 'y') { | |
| break | |
| } | |
| } |
Hello Andy!
Thanks for your great script!
Just came across a special monitor, whose EDID looks ok, but seemingly is not. https://www.edidreader.com/ does not complain and shows a serial no. of 96. Likewise PCs being hooked up to this monitor dont report any troubles.
However, this very script is not the only one complaining (https://people.freedesktop.org/~imirkin/edid-decode/ also barks). so maybe the EDID is indeed broken.
Anyhow, maybe you will want to catch this one: Your script extracts a serial number of 0xC2 0xA0 out of the EDID and into the CSV, so 2 bytes but none of them is readable. Excel even tries to interpret this and shows some funny character, but most likely this is not what we want to transfer as a serial no. over to the Rooms Portal. ;-) Maybe you want to filter out any non-printable characters before writing to the CSV. Or simply ignore the device if the Serial No. does not make sense.
Thanks for consideration.
This is the EDID in question:
00,FF,FF,FF,FF,FF,FF,00,0D,AE,00,16,60,00,00,00,01,21,01,03,80,1E,13,78,2E,EE,91,A3,54,4C,99,26,
0F,50,54,21,08,00,D1,00,B3,00,95,00,81,00,01,01,01,01,01,01,01,01,9C,68,00,A0,A0,40,29,60,30,20,
35,00,2D,BC,10,00,00,1A,00,00,00,FC,00,44,69,73,70,6C,61,79,0A,20,20,20,20,20,00,00,00,FF,00,0A,
20,20,20,20,20,20,20,20,20,20,20,20,00,00,00,FD,00,30,3F,1E,6E,28,00,0A,20,20,20,20,20,20,01,C4,
02,03,31,F2,45,90,01,02,03,04,E2,00,D5,E3,05,C0,00,23,09,7F,07,83,01,00,00,67,03,0C,00,10,00,18,
3C,E6,06,05,01,73,73,00,68,1A,00,00,01,01,30,3C,E6,6A,5E,00,A0,A0,A0,29,50,30,20,35,00,2D,BC,10,
00,00,1A,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,23
Remove Unused Group ID field - v2.1
Versioned Link