Featured image of post HUAWEI Vision Glassを快適に使うためにリバースエンジニアリングした件

HUAWEI Vision Glassを快適に使うためにリバースエンジニアリングした件

目次

HUAWEI Vision Glass

au Smart Glasses(HUAWEI Vision Glass)については前回の記事を参照。

SBSの3D動画を再生したかった

HUAWEI Vision Glassは、Androidアプリに接続した場合のみSBS(Side-by-side)の3D動画を正しく再生できます。SBSってのは、よくあるこんなタイプの映像です。

SBS動画を再生するためには、画面を横長の1枚のディスプレイとして表示させる必要があり、その操作は公式のAndroidアプリからしか行えないのです。

そこで、ゴニョゴニョいじってWindows PCからでも切り替えられるようにしたというのがこの記事の内容となっております。

調べた

詳しくは書きませんが、以下のような機能が含まれるライブラリが使われていました。

  • モードの取得/変更
  • 画面輝度の取得/変更
  • ハードウェア情報の取得
  • 慣性センサー情報の取得

今回は、モードの変更機能だけ再現しています。

動作

通常時は1980×1080のディスプレイとして認識されています。

サイズも普通です。この状態では、左右の目に同じ映像が表示されています。

3Dモードに変更すると、3840×1080として認識されます。

超ワイドなディスプレイになりました。

長すぎ!

使い方

この記事の末尾にあるPowerShellスクリプトの内容をコピーして、PCにテキストファイルとして保存します。PowerShellなので、拡張子を.ps1に変更してください。あとは実行するだけ。ダブルクリックしただけでは実行できなかったり、セキュリティ設定でブロックされてしまったりするかもしれませんので、詳しい方法は各自の環境に合わせて頑張って調べてください。

引数に「2d」または「3d」と入れて実行するとそのモードになります。「./hvg.ps1 3d」とか。

注意点として、Pythonで作りかかったものをAIがPowerShellに移植したので、変なところがあるかもしれません。それから、ショボいスペックのPCや高負荷の状態のときに実行すると、3Dに切り替えようとしても2Dに戻ってきてしまうので、失敗したら再生中の動画を止めるなどして再試行してください。

感想

これで3D映画が見放題と思いきや、3Dのコンテンツを配信しているサービスは案外少ないもんで、SBS対応ともなればさらに狭まってきます。D社やA社の映画はApple Vison Proでしか再生できないとか謎の制限もあって、そんなん誰も見られないだろ!と思うのであった。

私にはかわいそうな投げ売りVision Glassを救う使命があるので、活用策を作るべくまたゴニョゴニョし始めました。乞うご期待。

余談

映画を快適に観る環境について

スマートグラスで快適に映画を観るためにいろいろ試して行きついたところをまとめます。

PC

WindowsなミニPCで、USB-Cが2つ以上あって、そこからDisplayport出力とUSB PD入力ができるものならなんでもよいです。最近はSSDをいっぱい挿せる謎のミニPCを使ってます。

Androidスマホやスマートグラス専用の端末も使うことはできますが、充電に手間がかかったり、自由度が低くなってしまったりといったデメリットが大きいので、据え置きならWindowsがベストだと思います。画面付きのスマホやノートパソコンだと、部屋を暗くして使うときに眩しいのがマイナスポイントになります。

キーボード

Rii K06一択。このRiiというメーカーは、20年近くこの手の小型キーボードばかり作っているようで多数のバリエーションがありますが、K06をおすすめします。他のモデルでは、充電がMicroUSBだとか、乾電池式だとか、加水分解するとか、キー配列がありえないとか、全体的に古臭くて詰めの甘い問題を抱えているので。

Amazonでも取り扱いがあったようですが、基本的にはAliExpressで買うのが安くて早いです。

超高額な日本語キーボード版もあるので、どうしても日本語配列でないと許せない方はAmazonで購入できます。

一つだけ不満な点が、スクロール方向が普通とは逆になってしまっているところ。Windowsの設定で変更できます。ただし、特定のマウスにだけ適用することはできないので、このPCに他のマウスを繋いだら、こんどはそのマウスのスクロールが逆になってしまいます。

製造元について

au Smart GlassesはどっかのODMでHUAWEIとは関係ないんだ!みたいなことを言っている人がいたので、ほんとうにHUAWEI製である証拠を示しておきます。

デバイスの情報を見ると、VID_4817&PID_4242というハードウェアIDが出てきます。

この4817は、他でもないHUAWEIに割り当てられたベンダーIDです。

もちろんHUAWEIはファブレスなので自社生産なわけはないのですが、この商品についてはHUAWEIが設計したものをauが売っているという構図で間違いありません。

3Dコンテンツについて

Xrealのスマートグラスは以前からSBSに対応しているので、Xreal向けに作られた動画やソフトウェアが使える可能性が高いです。YouTubeで「Xreal SBS 3D」、「3840x1080 SBS 3D」、「Full SBS 3D」などで検索すると引っ掛かります。「Xreal Beam Pro」で検索すると、Beam Proで撮影された3D映像が出てきます。

そのほか、一部のiPhoneで空間写真が撮れるらしい、一部の3DSエミュレータがSBS表示に対応しているらしい、など探せばいろいろあるようです。

スクリプト


[CmdletBinding()]
param(
	[ValidateSet('2d','3d')]
	[string]$Set
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Initialize-HidInterop {
	if (-not ('HidInterop.HidDeviceFinder' -as [type])) {
		$typeDefinition = @"
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace HidInterop
{
	public class HidDeviceInfo
	{
		public string DevicePath { get; set; }
		public int InterfaceNumber { get; set; }
		public ushort UsagePage { get; set; }
		public ushort Usage { get; set; }
		public ushort InputReportByteLength { get; set; }
		public ushort OutputReportByteLength { get; set; }
		public ushort VendorId { get; set; }
		public ushort ProductId { get; set; }
	}
	public static class HidDeviceFinder
	{
		public static HidDeviceInfo FindDevice(ushort vendorId, ushort productId, int targetInterfaceNumber, ushort primaryUsagePage, ushort fallbackUsagePage)
		{
			Guid hidGuid;
			NativeMethods.HidD_GetHidGuid(out hidGuid);
			IntPtr infoSet = NativeMethods.SetupDiGetClassDevs(ref hidGuid, null, IntPtr.Zero, NativeMethods.DIGCF_PRESENT | NativeMethods.DIGCF_DEVICEINTERFACE);
			if (infoSet == IntPtr.Zero || infoSet.ToInt64() == -1)
			{
				return null;
			}
			try
			{
				NativeMethods.SP_DEVICE_INTERFACE_DATA interfaceData = new NativeMethods.SP_DEVICE_INTERFACE_DATA();
				interfaceData.cbSize = Marshal.SizeOf(typeof(NativeMethods.SP_DEVICE_INTERFACE_DATA));
				int index = 0;
				HidDeviceInfo bestMatch = null;
				int bestScore = int.MinValue;
				while (NativeMethods.SetupDiEnumDeviceInterfaces(infoSet, IntPtr.Zero, ref hidGuid, index, ref interfaceData))
				{
					index++;
					NativeMethods.SP_DEVINFO_DATA devInfo = new NativeMethods.SP_DEVINFO_DATA();
					devInfo.cbSize = Marshal.SizeOf(typeof(NativeMethods.SP_DEVINFO_DATA));
					NativeMethods.SP_DEVICE_INTERFACE_DETAIL_DATA detailData = new NativeMethods.SP_DEVICE_INTERFACE_DETAIL_DATA();
					detailData.cbSize = (uint)(IntPtr.Size == 8 ? 8 : 4 + Marshal.SystemDefaultCharSize);
					uint requiredSize = 0;
					if (!NativeMethods.SetupDiGetDeviceInterfaceDetail(infoSet, ref interfaceData, ref detailData, (uint)Marshal.SizeOf(detailData), ref requiredSize, ref devInfo))
					{
						continue;
					}
					int interfaceNumber = NativeMethods.GetInterfaceNumber(infoSet, ref devInfo);
					if (interfaceNumber < 0)
					{
						int parsedInterface = ParseInterfaceFromPath(detailData.DevicePath);
						if (parsedInterface >= 0)
						{
							interfaceNumber = parsedInterface;
						}
					}
					using (SafeFileHandle handle = NativeMethods.CreateFile(detailData.DevicePath, NativeMethods.GENERIC_READ | NativeMethods.GENERIC_WRITE, NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE, IntPtr.Zero, NativeMethods.OPEN_EXISTING, NativeMethods.FILE_FLAG_OVERLAPPED, IntPtr.Zero))
					{
						if (handle.IsInvalid)
						{
							continue;
						}
						NativeMethods.HIDD_ATTRIBUTES attributes = NativeMethods.GetAttributes(handle);
						if (attributes.VendorID != vendorId || attributes.ProductID != productId)
						{
							continue;
						}
						NativeMethods.HIDP_CAPS caps = NativeMethods.GetCaps(handle);
						int score = 0;
						if (targetInterfaceNumber >= 0)
						{
							if (interfaceNumber == targetInterfaceNumber)
							{
								score += 2;
							}
							else if (interfaceNumber < 0)
							{
								score += 1;
							}
						}
						else
						{
							score += 1;
						}
						bool primaryMatch = primaryUsagePage != 0 && caps.UsagePage == primaryUsagePage;
						bool fallbackMatch = fallbackUsagePage != 0 && caps.UsagePage == fallbackUsagePage;
						if (primaryUsagePage == 0)
						{
							score += 1;
						}
						else if (primaryMatch)
						{
							score += 2;
						}
						else if (fallbackMatch)
						{
							score += 1;
						}
						if (score > bestScore)
						{
							bestScore = score;
							bestMatch = new HidDeviceInfo
							{
								DevicePath = detailData.DevicePath,
								InterfaceNumber = interfaceNumber,
								UsagePage = caps.UsagePage,
								Usage = caps.Usage,
								InputReportByteLength = caps.InputReportByteLength,
								OutputReportByteLength = caps.OutputReportByteLength,
								VendorId = attributes.VendorID,
								ProductId = attributes.ProductID
							};
						}
					}
				}
				return bestScore >= 0 ? bestMatch : null;
			}
			finally
			{
				NativeMethods.SetupDiDestroyDeviceInfoList(infoSet);
			}
		}
		private static int ParseInterfaceFromPath(string devicePath)
		{
			if (string.IsNullOrEmpty(devicePath))
			{
				return -1;
			}
			const string marker = "mi_";
			int idx = devicePath.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
			if (idx >= 0 && idx + marker.Length + 2 <= devicePath.Length)
			{
				string hex = devicePath.Substring(idx + marker.Length, 2);
				if (int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int value))
				{
					return value;
				}
			}
			return -1;
		}
	}
	public static class NativeMethods
	{
		public const int DIGCF_PRESENT = 0x00000002;
		public const int DIGCF_DEVICEINTERFACE = 0x00000010;
		public const uint GENERIC_READ = 0x80000000;
		public const uint GENERIC_WRITE = 0x40000000;
		public const uint FILE_SHARE_READ = 0x00000001;
		public const uint FILE_SHARE_WRITE = 0x00000002;
		public const uint OPEN_EXISTING = 3;
		public const uint FILE_FLAG_OVERLAPPED = 0x40000000;
		public const uint SPDRP_INTERFACE_NUMBER = 0x0000000E;
		public const int HIDP_STATUS_SUCCESS = 0x00110000;
		[StructLayout(LayoutKind.Sequential)]
		public struct SP_DEVICE_INTERFACE_DATA
		{
			public int cbSize;
			public Guid InterfaceClassGuid;
			public int Flags;
			public IntPtr Reserved;
		}
		[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
		public struct SP_DEVICE_INTERFACE_DETAIL_DATA
		{
			public uint cbSize;
			[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 512)]
			public string DevicePath;
		}
		[StructLayout(LayoutKind.Sequential)]
		public struct SP_DEVINFO_DATA
		{
			public int cbSize;
			public Guid ClassGuid;
			public int DevInst;
			public IntPtr Reserved;
		}
		[StructLayout(LayoutKind.Sequential)]
		public struct HIDD_ATTRIBUTES
		{
			public int Size;
			public ushort VendorID;
			public ushort ProductID;
			public ushort VersionNumber;
		}
		[StructLayout(LayoutKind.Sequential)]
		public struct HIDP_CAPS
		{
			public ushort Usage;
			public ushort UsagePage;
			public ushort InputReportByteLength;
			public ushort OutputReportByteLength;
			public ushort FeatureReportByteLength;
			[MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
			public ushort[] Reserved;
			public ushort NumberLinkCollectionNodes;
			public ushort NumberInputButtonCaps;
			public ushort NumberInputValueCaps;
			public ushort NumberInputDataIndices;
			public ushort NumberOutputButtonCaps;
			public ushort NumberOutputValueCaps;
			public ushort NumberOutputDataIndices;
			public ushort NumberFeatureButtonCaps;
			public ushort NumberFeatureValueCaps;
			public ushort NumberFeatureDataIndices;
		}
		[DllImport("hid.dll")]
		public static extern void HidD_GetHidGuid(out Guid HidGuid);
		[DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
		public static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, string Enumerator, IntPtr hwndParent, int Flags);
		[DllImport("setupapi.dll", SetLastError = true)]
		public static extern bool SetupDiEnumDeviceInterfaces(IntPtr DeviceInfoSet, IntPtr DeviceInfoData, ref Guid InterfaceClassGuid, int MemberIndex, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData);
		[DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
		public static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr DeviceInfoSet, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData, ref SP_DEVICE_INTERFACE_DETAIL_DATA DeviceInterfaceDetailData, uint DeviceInterfaceDetailDataSize, ref uint RequiredSize, ref SP_DEVINFO_DATA DeviceInfoData);
		[DllImport("setupapi.dll", SetLastError = true)]
		public static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);
		[DllImport("setupapi.dll", SetLastError = true)]
		public static extern bool SetupDiGetDeviceRegistryProperty(IntPtr DeviceInfoSet, ref SP_DEVINFO_DATA DeviceInfoData, uint Property, out uint PropertyRegDataType, byte[] PropertyBuffer, uint PropertyBufferSize, out uint RequiredSize);
		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		public static extern SafeFileHandle CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);
		[DllImport("hid.dll", SetLastError = true)]
		public static extern bool HidD_GetAttributes(SafeFileHandle HidDeviceObject, ref HIDD_ATTRIBUTES Attributes);
		[DllImport("hid.dll", SetLastError = true)]
		public static extern bool HidD_GetPreparsedData(SafeFileHandle HidDeviceObject, out IntPtr PreparsedData);
		[DllImport("hid.dll", SetLastError = true)]
		public static extern bool HidD_FreePreparsedData(IntPtr PreparsedData);
		[DllImport("hid.dll", SetLastError = true)]
		public static extern int HidP_GetCaps(IntPtr PreparsedData, out HIDP_CAPS Capabilities);
		[DllImport("hid.dll", SetLastError = true)]
		public static extern bool HidD_SetOutputReport(SafeFileHandle HidDeviceObject, byte[] ReportBuffer, int ReportBufferLength);
		public static SafeFileHandle OpenDevice(string devicePath)
		{
			return CreateFile(devicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, IntPtr.Zero);
		}
		public static HIDD_ATTRIBUTES GetAttributes(SafeFileHandle handle)
		{
			HIDD_ATTRIBUTES attributes = new HIDD_ATTRIBUTES();
			attributes.Size = Marshal.SizeOf(typeof(HIDD_ATTRIBUTES));
			if (!HidD_GetAttributes(handle, ref attributes))
			{
				throw new InvalidOperationException("HidD_GetAttributes に失敗しました。");
			}
			return attributes;
		}
		public static HIDP_CAPS GetCaps(SafeFileHandle handle)
		{
			IntPtr preparsedData;
			if (!HidD_GetPreparsedData(handle, out preparsedData))
			{
				throw new InvalidOperationException("HidD_GetPreparsedData に失敗しました。");
			}
			try
			{
				HIDP_CAPS caps;
				int status = HidP_GetCaps(preparsedData, out caps);
				if (status != HIDP_STATUS_SUCCESS)
				{
					throw new InvalidOperationException("HidP_GetCaps に失敗しました (status 0x" + status.ToString("X") + ")");
				}
				return caps;
			}
			finally
			{
				HidD_FreePreparsedData(preparsedData);
			}
		}
		public static int GetInterfaceNumber(IntPtr infoSet, ref SP_DEVINFO_DATA devInfoData)
		{
			byte[] buffer = new byte[4];
			uint regType;
			uint requiredSize;
			if (SetupDiGetDeviceRegistryProperty(infoSet, ref devInfoData, SPDRP_INTERFACE_NUMBER, out regType, buffer, (uint)buffer.Length, out requiredSize))
			{
				return BitConverter.ToInt32(buffer, 0);
			}
			return -1;
		}
	}
}
"@;
		Add-Type -TypeDefinition $typeDefinition -Language CSharp
	}
}
Initialize-HidInterop
# チェックサムを計算
function Get-Checksum {
	param(
		[byte[]]$Buffer,
		[int]$Length
	)
	$sum = 0
	for ($i = 0; $i -lt $Length; $i++) {
		$sum = ($sum + $Buffer[$i]) -band 0xFF
	}
	return [byte]$sum
}
# 現在の表示モードを取得するコマンド
function New-GetModeCommand {
	$cmd = [byte[]]::new(24)
	$cmd[0] = 0x05
	$cmd[1] = 0xAA
	$cmd[2] = 0x01
	$cmd[3] = 0x01
	$cmd[4] = 0x08
	$cmd[23] = Get-Checksum -Buffer $cmd -Length 23
	return $cmd
}
# 表示モードを設定するコマンド
function New-SetModeCommand {
	param([switch]$Is3D)
	$cmd = [byte[]]::new(24)
	$cmd[0] = 0x05
	$cmd[1] = 0xAA
	$cmd[2] = 0x01
	$cmd[3] = 0x01
	$cmd[4] = 0x19
	$cmd[5] = 0x00
	$cmd[6] = if ($Is3D) { 0x01 } else { 0x00 }
	$cmd[23] = Get-Checksum -Buffer $cmd -Length 23
	return $cmd
}
function Format-BytesToHexString {
	param(
		[byte[]]$Buffer,
		[int]$Length
	)
	if (-not $Buffer) {
		return ''
	}
	$limit = [Math]::Min($Length, $Buffer.Length)
	if ($limit -le 0) {
		return ''
	}
	return -join (0..($limit - 1) | ForEach-Object { $Buffer[$_].ToString('x2') })
}
# コマンドを送信
function Send-HidCommand {
	param(
		[System.IO.FileStream]$Stream,
		[byte[]]$Command,
		[string]$Description,
		[int]$OutputLength
	)
	$hex = Format-BytesToHexString -Buffer $Command -Length $Command.Length
	Write-Host ("{0} コマンド送信: {1}" -f $Description, $hex)
	$primaryLength = if ($OutputLength -gt 0) { [int]$OutputLength } else { $Command.Length }
	if ($primaryLength -lt $Command.Length) {
		$primaryLength = $Command.Length
	}
	if ($Stream -eq $null) {
		throw 'FileStream が初期化されていません。'
	}
	$primaryBuffer = $Command
	if ($primaryLength -ne $Command.Length) {
		$primaryBuffer = [byte[]]::new($primaryLength)
		[Array]::Clear($primaryBuffer, 0, $primaryBuffer.Length)
		[Array]::Copy($Command, 0, $primaryBuffer, 0, [Math]::Min($Command.Length, $primaryBuffer.Length))
	}
	try {
		$Stream.Write($primaryBuffer, 0, $primaryBuffer.Length)
		$Stream.Flush()
		return $true
	} catch {
		Write-Verbose ("メイン書き込みに失敗: {0}" -f $_.Exception.Message)
	}
	$fallbackLength = [Math]::Max($primaryBuffer.Length + 1, $Command.Length + 1)
	if ($OutputLength -gt 0) {
		$fallbackLength = [Math]::Max($fallbackLength, $OutputLength + 1)
	}
	$fallbackBuffer = [byte[]]::new($fallbackLength)
	[Array]::Clear($fallbackBuffer, 0, $fallbackBuffer.Length)
	[Array]::Copy($Command, 0, $fallbackBuffer, 1, [Math]::Min($Command.Length, $fallbackBuffer.Length - 1))
	try {
		$Stream.Write($fallbackBuffer, 0, $fallbackBuffer.Length)
		$Stream.Flush()
		return $true
	} catch {
		Write-Error ("FileStream 経由の送信に失敗しました: {0}" -f $_.Exception.Message)
		return $false
	}
}
# レスポンスを読み取り
function Receive-HidResponse {
	param(
		[System.IO.FileStream]$Stream,
		[int]$TimeoutMs = 2000,
		[int]$Length = 64
	)
	Write-Host '応答を受信しています...'
	$buffer = [byte[]]::new($Length)
	$cts = [System.Threading.CancellationTokenSource]::new()
	$cts.CancelAfter($TimeoutMs)
	try {
		$task = $Stream.ReadAsync($buffer, 0, $buffer.Length, $cts.Token)
		$read = $task.GetAwaiter().GetResult()
		# 読み取ったサイズが実際のバッファより小さい場合に切り詰める
		if ($read -lt $buffer.Length) {
			$trimmed = [byte[]]::new($read)
			[Array]::Copy($buffer, $trimmed, $read)
		} else {
			$trimmed = $buffer
		}
		$hex = Format-BytesToHexString -Buffer $trimmed -Length $trimmed.Length
		Write-Host ("応答({0}バイト): {1}" -f $read, $hex)
		return [pscustomobject]@{ Buffer = $trimmed; Length = $read }
	} catch [System.OperationCanceledException] {
		Write-Host 'タイムアウトしました。'
		return [pscustomobject]@{ Buffer = $null; Length = 0 }
	} catch {
		Write-Error ("応答の読み取りに失敗しました: {0}" -f $_.Exception.Message)
		return [pscustomobject]@{ Buffer = $null; Length = 0 }
	} finally {
		$cts.Dispose()
	}
}
# モード判定
function Parse-ModeResponse {
	param(
		[byte[]]$Buffer,
		[int]$Length
	)
	if (-not $Buffer -or $Length -le 0) {
		return 'データなし'
	}
	for ($i = 0; $i -le $Length - 4; $i++) {
		if ($Buffer[$i] -eq 0x05 -and $Buffer[$i + 1] -eq 0xAA) {
			$remaining = $Length - $i
			if ($remaining -lt 8) {
				return 'データ不足'
			}
			$modeChar = $Buffer[$i + 6]
			$dChar = $Buffer[$i + 7]
			if ($dChar -ne 0x44) {
				return '形式不正'
			}
			switch ($modeChar) {
				0x32 { return '2D' }
				0x33 { return '3D' }
				default { return ('不明(0x{0:X2})' -f $modeChar) }
			}
		}
	}
	return 'ヘッダ未検出'
}
# 表示モード取得/設定
function Invoke-DisplayModeControl {
	param(
		[string]$RequestedMode
	)
	$vendorId = 0x4817
	$productId = 0x4242
	$usagePage = 0xFF00
	$fallbackUsagePage = 0x00FF
	$interfaceNumber = 1
	Write-Host ("VID:0x{0:X4} / PID:0x{1:X4} のデバイスを検索しています..." -f $vendorId, $productId)
	$device = [HidInterop.HidDeviceFinder]::FindDevice([uint16]$vendorId, [uint16]$productId, $interfaceNumber, [uint16]$usagePage, [uint16]$fallbackUsagePage)
	if (-not $device) {
		Write-Host '指定のインターフェース (Interface 1 / Usage Page 0xFF00) が見つかりませんでした。'
		return $false
	}
	Write-Host ("デバイスパスを開きます: {0}" -f $device.DevicePath)
	$handle = [HidInterop.NativeMethods]::OpenDevice($device.DevicePath)
	if ($handle.IsInvalid) {
		throw 'デバイスハンドルを開けませんでした。'
	}
	$bufferSize = [Math]::Max(64, [int]$device.InputReportByteLength)
	$stream = [System.IO.FileStream]::new($handle, [System.IO.FileAccess]::ReadWrite, $bufferSize, $true)
	try {
		Write-Host ("デバイスを開きました (Interface={0}, UsagePage=0x{1:X4})" -f $device.InterfaceNumber, $device.UsagePage)
		if ($RequestedMode) {
			$is3D = $RequestedMode -eq '3d'
			$cmd = New-SetModeCommand -Is3D:$is3D
			$description = ("モード {0} へ設定" -f $RequestedMode.ToUpperInvariant())
			if (Send-HidCommand -Stream $stream -Command $cmd -Description $description -OutputLength $device.OutputReportByteLength) {
				$resp = Receive-HidResponse -Stream $stream
				$status = Parse-ModeResponse -Buffer $resp.Buffer -Length $resp.Length
				Write-Host ("設定コマンド応答: {0}" -f $status)
				Start-Sleep -Milliseconds 500
			} else {
				throw 'モード設定コマンドの送信に失敗しました。'
			}
		}
		$getCmd = New-GetModeCommand
		if (Send-HidCommand -Stream $stream -Command $getCmd -Description 'モード取得' -OutputLength $device.OutputReportByteLength) {
			$resp = Receive-HidResponse -Stream $stream
			$mode = Parse-ModeResponse -Buffer $resp.Buffer -Length $resp.Length
			Write-Host ("現在のモード: {0}" -f $mode)
		} else {
			throw 'モード取得コマンドの送信に失敗しました。'
		}
		return $true
	}
	finally {
		if ($null -ne $stream) {
			$stream.Dispose()
		}
		if ($null -ne $handle) {
			$handle.Dispose()
		}
	}
}
$success = Invoke-DisplayModeControl -RequestedMode $Set
if (-not $success) {
	exit 1
}
Hugo で構築されています。
テーマ StackJimmy によって設計されています。