Skip to content

Instantly share code, notes, and snippets.

@adthom
Last active February 14, 2026 14:31
Show Gist options
  • Select an option

  • Save adthom/b703078806adeb71fe860929df0bd4c1 to your computer and use it in GitHub Desktop.

Select an option

Save adthom/b703078806adeb71fe860929df0bd4c1 to your computer and use it in GitHub Desktop.
Teams BYOD Rooms/Spaces Manual Device Discovery Script
<#
.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
}
}
@adthom
Copy link
Author

adthom commented Oct 10, 2024

Fix Merge Logic for Existing Results - v1.6

Versioned Link

  • Corrected the logic for merging $PreviousResults that do not have conflicts into $MergedResults

@adthom
Copy link
Author

adthom commented Oct 10, 2024

Fix Conflict Detection Logic and Merge Logic for Existing Results - v1.7

Versioned Link

  • Corrected the logic for identifying conflicts in $ExistingDevices
  • Corrected the logic for merging $PreviousResults that do not have conflicts into $MergedResults

@adthom
Copy link
Author

adthom commented Oct 10, 2024

Update Script for Improved Readability and Maintainability - v2.0

Versioned Link

  • Refactored the script to improve readability and maintainability.
  • Replaced the $CurrentDevices initialization with a function call to GetCurrentDevices.
  • Added new functions: AddNativeHIDType, GetCurrentDevices, InvokeWMIQuery, GetMonitorId, GetEDID, MonitorIsExpectedOutputType, Get-MonitorVIDFromEDID, Get-MonitorPIDFromEDID, Get-MonitorEDIDDescriptor, MergeResults, BuildDeviceHierarchy, BuildSerialNumberLookup, GetCurrentContents, and ExportData.
  • Improved the logic for gathering and processing device information.
  • Enhanced the merging logic for $MediaDevices and $MonitorDevices into $DataToExport.
  • Fixed potential bugs related to device information retrieval and data aggregation.
  • Improved the handling of conflicts and merging results when updating the CSV file.
  • Added detailed comments and region markers for better code organization.
  • Replaced inline file reading and writing logic with calls to GetCurrentContents and ExportData.
  • Improved error handling for file operations to provide better user feedback.

@adthom
Copy link
Author

adthom commented Dec 20, 2024

Remove Unused Group ID field - v2.1

Versioned Link

  • Removed the unused Group ID field from the CSV file and the script.

@HSteindl
Copy link

HSteindl commented Jun 4, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment