├── .gitmodules ├── LICENSE ├── README.md ├── downloads └── MonitorToolHook.7z └── src ├── MonitorToolHook ├── Hook │ ├── Hook.csproj │ ├── HookCommunicationChannel.cs │ └── HookMain.cs ├── Monitor │ ├── App.config │ ├── Monitor.csproj │ └── Program.cs └── MonitorToolHook.sln └── kvmutil ├── friendlyname.py ├── kvmutil.py ├── linux ├── pbp.swap.input.sh ├── pbp.swap.video.sh ├── pbp.switch.onoff.sh ├── profile.sidebyside.sh ├── switch2usbc.profile.full.sh └── switch2usbc.sh ├── requirements.txt └── win ├── linux.profile.sidebyside.bat └── win.wrapper.bat /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "kvmutil/ddcci"] 2 | path = src/kvmutil/ddcci 3 | url = https://github.com/Informatic/python-ddcci 4 | branch = master 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # KVM utilities (DELL) 3 | Utility to work with the integrated hardware KVM switch of modern monitors. 4 | > *Currently the focus of this tool is the DELL U4919DW* 5 | 6 | ## Components 7 | The tool consists of two components 8 | 1. KVM Util: python based tool to send commands to the KVM switch and control it 9 | 2. MonitorTool Hook: reverse engineering tool for Windows closed source tools of the monitor manufacturers. It allows to spy on them to gather the VCP commands they write and read on the display 10 | 11 | ## KVM Util 12 | ### Prerequisites 13 | > Installed Python 3 of course. 14 | #### a) Windows 15 | - installed external python package *monitorcontrol* *(see to https://monitorcontrol.readthedocs.io/en/latest/ & https://github.com/newAM/monitorcontrol)*: 16 | `pip3 install monitorcontrol` 17 | 18 | #### b) Linux 19 | - External git repository cloned into `kvm\ddcci` 20 | - Installed Python package *python-smbus* `pip3 install smbus` 21 | - Installed ddcutil for `pbpswitch` commands (see https://www.ddcutil.com) 22 | `sudo apt-get install ddcutil` 23 | - User must have access to the i2c devices `/dev/i2c-X`: e.g. add user to group i2c 24 | 25 | ### Usage 26 | > First determine the device ID of the display. You can use `ddccontrol -p` on Linux. On Windows it's the 0-based index of the attached monitors. 27 | 28 | **Get the *src* folder**, navigate to *src\kvmutil* and execute kvmutil.py. 29 | ``` 30 | kvmutil.py [-h] deviceid {inputselect,pbp,pbpsubinputselect,pbpswitch,pbpswitch2,pbpswap} ... 31 | ``` 32 | #### Usage examples 33 | In the sub folders of `kvmutil` there are some examples of real usages of the tool in Windows (`kvmutil\win`) and Linux (`kvmutil\linux`) 34 | 35 | ## MonitorTool Hook 36 | The tool is written in C# and uses the EasyHook framework *(see https://easyhook.github.io/)*. 37 | 38 | You can download the latest/nightly build: https://raw.githubusercontent.com/ScriptGod1337/kvm/master/downloads/MonitorToolHook.7z 39 | Usage: `Monitor.exe MonitorToolToSpy` 40 | For example for DELL `Monitor.exe "C:\Program Files (x86)\Dell\Dell Display Manager\ddm.exe"` 41 | 42 | **Important**: the monitor must be the parent process of the program to hook - e.g. by doing a fresh start of it. *This means close Dell Display Manager prior starting the monitor* 43 | 44 | The monitor logs the VCP commands send by the program to the console. You can execute a functionality of the monitor manufacture tool and find out which VCP command with which parameter was written or read 45 | ``` 46 | Monitor.exe "C:\Program Files (x86)\Dell\Dell Display Manager\ddm.exe" 47 | ... 48 | **read OK GetVCPFeatureAndVCPFeatureReply_Hook 0x0 0xE5 0x0 0xFF 49 | **read OK GetVCPFeatureAndVCPFeatureReply_Hook 0x0 0xE7 0xFF20 0xFFAA 50 | **read OK GetVCPFeatureAndVCPFeatureReply_Hook 0x0 0xE8 0x12 0xFFFF 51 | **read OK GetVCPFeatureAndVCPFeatureReply_Hook 0x0 0xE9 0x0 0xFF 52 | ... 53 | **write SetcVCPFeature 0x0 0xE9 0x24 54 | ... 55 | ``` 56 | 57 | ***These values can be used in the KVM Util or in custom scripts*** 58 | 59 | ### Run the code locally 60 | 1. Download the code or clone the repo 61 | 2. Copy the EasyHook dll's over from the download linked above into the `src\MonitorToolHook\Monitor` folder 62 | 3. Open the solution file 63 | 4. Go to the properties page of `Monitor.csproj` and add the location of `ddm.exe` to the Debug settings 'command line arguments': `"C:\Program Files (x86)\Dell\Dell Display Manager\ddm.exe"` 64 | 5. Run the Monitor project (set as startup project) 65 | -------------------------------------------------------------------------------- /downloads/MonitorToolHook.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScriptGod1337/kvm/3ff3319228e74a2c2727107b0fc5deaac74fd20b/downloads/MonitorToolHook.7z -------------------------------------------------------------------------------- /src/MonitorToolHook/Hook/Hook.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {AC75B3B2-F659-46FC-A44F-90EA3D894BD2} 8 | Library 9 | Properties 10 | Hook 11 | Hook 12 | v4.7.2 13 | 512 14 | true 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | PreserveNewest 51 | 52 | 53 | PreserveNewest 54 | 55 | 56 | PreserveNewest 57 | 58 | 59 | PreserveNewest 60 | 61 | 62 | PreserveNewest 63 | 64 | 65 | PreserveNewest 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/MonitorToolHook/Hook/HookCommunicationChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Hook { 8 | public class HookCommunicationChannel : MarshalByRefObject { 9 | 10 | public void Ping() { 11 | Console.WriteLine("Ping -> Pong"); 12 | } 13 | 14 | public void IsInstalled(int clientPID) { 15 | Console.WriteLine("HookCommunicationChannel has injected into process {0}", clientPID); 16 | } 17 | 18 | public void ReportMessages(params string[] messages) { 19 | foreach(var msg in messages) { 20 | Console.WriteLine(msg); 21 | } 22 | } 23 | 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MonitorToolHook/Hook/HookMain.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Concurrent; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Hook { 7 | public class HookMain : EasyHook.IEntryPoint { 8 | private HookCommunicationChannel channel; 9 | private List hooks = new List(); 10 | private BlockingCollection msgQueue = new BlockingCollection(); 11 | 12 | public HookMain(EasyHook.RemoteHooking.IContext context, string channelName) { 13 | channel = EasyHook.RemoteHooking.IpcConnectClient(channelName); 14 | } 15 | 16 | public void Run(EasyHook.RemoteHooking.IContext context, string channelName) { 17 | channel.IsInstalled(EasyHook.RemoteHooking.GetCurrentProcessId()); 18 | 19 | hooks.Add(HookAPI("dxva2.dll", "GetVCPFeatureAndVCPFeatureReply", new GetVCPFeatureAndVCPFeatureReply_Delegate(GetVCPFeatureAndVCPFeatureReply_Hook))); 20 | hooks.Add(HookAPI("dxva2.dll", "SetVCPFeature", new SetVCPFeature_Delegate(SetVCPFeature_Hook))); 21 | 22 | channel.ReportMessages("Hook active. Starting application..."); 23 | EasyHook.RemoteHooking.WakeUpProcess(); 24 | 25 | try { 26 | while (true) { 27 | String msg; 28 | if (msgQueue.TryTake(out msg, TimeSpan.FromMilliseconds(5000))) { 29 | channel.ReportMessages(msg); 30 | } else { 31 | channel.Ping(); 32 | } 33 | } 34 | } finally { 35 | foreach (var hook in hooks) { 36 | hook.Dispose(); 37 | } 38 | EasyHook.LocalHook.Release(); 39 | } 40 | } 41 | 42 | [DllImport("kernel32.dll", SetLastError = true)] 43 | private static extern IntPtr LoadLibrary(string dllName); 44 | 45 | private EasyHook.LocalHook HookAPI(String dllName, String Method, Delegate InNewProck) { 46 | // ensure thate the dll is loaded 47 | LoadLibrary(dllName); 48 | 49 | // hock 50 | var hook = EasyHook.LocalHook.Create( 51 | EasyHook.LocalHook.GetProcAddress(dllName, Method), 52 | InNewProck, this); 53 | hook.ThreadACL.SetExclusiveACL(new Int32[] { 0 }); 54 | 55 | msgQueue.Add(String.Format("Hooked {0} {1}", dllName, Method)); 56 | return hook; 57 | } 58 | 59 | #region GetVCPFeatureAndVCPFeatureReply 60 | [DllImport("dxva2.dll", SetLastError = true)] 61 | private static extern Boolean GetVCPFeatureAndVCPFeatureReply(IntPtr hMonitor, byte bVCPCode, IntPtr pvct, IntPtr pdwCurrentValue, IntPtr pdwMaximumValue); 62 | 63 | [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] 64 | private delegate Boolean GetVCPFeatureAndVCPFeatureReply_Delegate(IntPtr hMonitor, byte bVCPCode, IntPtr pvct, IntPtr pdwCurrentValue, IntPtr pdwMaximumValue); 65 | 66 | private Boolean GetVCPFeatureAndVCPFeatureReply_Hook(IntPtr hMonitor, byte bVCPCode, IntPtr pvct, IntPtr pdwCurrentValue, IntPtr pdwMaximumValue) { 67 | Boolean result = GetVCPFeatureAndVCPFeatureReply(hMonitor, bVCPCode, pvct, pdwCurrentValue, pdwMaximumValue); 68 | if (result) { 69 | msgQueue.Add(String.Format("**read OK GetVCPFeatureAndVCPFeatureReply_Hook 0x{0:X} 0x{1:X} 0x{2:X} 0x{3:X}", hMonitor, bVCPCode, 70 | Marshal.ReadInt32(pdwCurrentValue), Marshal.ReadInt32(pdwMaximumValue))); 71 | } else { 72 | msgQueue.Add(String.Format("**read NOK GetVCPFeatureAndVCPFeatureReply_Hook 0x{0:X} 0x{1:X}", hMonitor, bVCPCode)); 73 | } 74 | return result; 75 | } 76 | #endregion 77 | 78 | #region SetVCPFeature 79 | [DllImport("dxva2.dll", SetLastError = true)] 80 | private static extern Boolean SetVCPFeature(IntPtr hMonitor, byte bVCPCode, UInt32 dwNewValue); 81 | 82 | [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] 83 | private delegate Boolean SetVCPFeature_Delegate(IntPtr hMonitor, byte bVCPCode, UInt32 dwNewValue); 84 | 85 | private Boolean SetVCPFeature_Hook(IntPtr hMonitor, byte bVCPCode, UInt32 dwNewValue) { 86 | msgQueue.Add(String.Format("**write SetcVCPFeature 0x{0:X} 0x{1:X} 0x{2:X}", hMonitor, bVCPCode, dwNewValue)); 87 | return SetVCPFeature(hMonitor, bVCPCode, dwNewValue); 88 | } 89 | #endregion 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/MonitorToolHook/Monitor/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/MonitorToolHook/Monitor/Monitor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {33720CB8-1014-431E-9BB7-2E3645CF74FC} 8 | Exe 9 | Monitor 10 | Monitor 11 | v4.7.2 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | PreserveNewest 58 | 59 | 60 | PreserveNewest 61 | 62 | 63 | PreserveNewest 64 | 65 | 66 | PreserveNewest 67 | 68 | 69 | PreserveNewest 70 | 71 | 72 | PreserveNewest 73 | 74 | 75 | 76 | 77 | {ac75b3b2-f659-46fc-a44f-90ea3d894bd2} 78 | Hook 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/MonitorToolHook/Monitor/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Runtime.Remoting; 5 | using System.Reflection; 6 | 7 | namespace Monitor { 8 | class Program { 9 | static void Main(string[] args) { 10 | string injectionLibrary = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Hook.dll"); 11 | 12 | string channelName = null; 13 | EasyHook.RemoteHooking.IpcCreateServer(ref channelName, WellKnownObjectMode.Singleton); 14 | 15 | int targetPID; 16 | EasyHook.RemoteHooking.CreateAndInject( 17 | args[0], "", 18 | 0, 19 | EasyHook.InjectionOptions.DoNotRequireStrongName, 20 | injectionLibrary, injectionLibrary, 21 | out targetPID, channelName 22 | ); 23 | 24 | Console.WriteLine("Created process {0}...", targetPID); 25 | 26 | Process.GetProcessById(targetPID).WaitForExit(); 27 | 28 | Console.WriteLine("Press enter to exit..."); 29 | Console.ReadLine(); 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/MonitorToolHook/MonitorToolHook.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29926.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monitor", "Monitor\Monitor.csproj", "{33720CB8-1014-431E-9BB7-2E3645CF74FC}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hook", "Hook\Hook.csproj", "{AC75B3B2-F659-46FC-A44F-90EA3D894BD2}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7F5E42C0-F44E-42C6-8C0E-398D9B78A187}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {33720CB8-1014-431E-9BB7-2E3645CF74FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {33720CB8-1014-431E-9BB7-2E3645CF74FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {33720CB8-1014-431E-9BB7-2E3645CF74FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {33720CB8-1014-431E-9BB7-2E3645CF74FC}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {AC75B3B2-F659-46FC-A44F-90EA3D894BD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {AC75B3B2-F659-46FC-A44F-90EA3D894BD2}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {AC75B3B2-F659-46FC-A44F-90EA3D894BD2}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {AC75B3B2-F659-46FC-A44F-90EA3D894BD2}.Release|Any CPU.Build.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ExtensibilityGlobals) = postSolution 31 | SolutionGuid = {DFF2E81F-42C5-4AE9-BAFA-493AB69AC1F1} 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /src/kvmutil/friendlyname.py: -------------------------------------------------------------------------------- 1 | from monitorcontrol import get_monitors 2 | 3 | import ctypes 4 | from ctypes.wintypes import ( 5 | BOOL, USHORT, UINT, DWORD, ULONG, LONG, LARGE_INTEGER, 6 | WCHAR, 7 | POINTL, RECT, HMONITOR, 8 | ) 9 | 10 | # createGDIName2FriendlyName 11 | #============================================================== 12 | QDC_ALL_PATHS = 1 13 | QDC_ONLY_ACTIVE_PATHS = 2 14 | DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE = 1 15 | DISPLAYCONFIG_MODE_INFO_TYPE_TARGET = 2 16 | DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME = 1 17 | DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME = 2 18 | # --- generic 19 | # --------------------------- 20 | class LUID(ctypes.Structure): 21 | _fields_ = [ 22 | ('LowPart', DWORD), 23 | ('HighPart', LONG), 24 | ] 25 | class DISPLAYCONFIG_RATIONAL(ctypes.Structure): 26 | _fields_ = [ 27 | ('Numerator', UINT), 28 | ('Denominator', UINT), 29 | ] 30 | class DISPLAYCONFIG_2DREGION(ctypes.Structure): 31 | _fields_ = [ 32 | ('cx', UINT), 33 | ('cz', UINT), 34 | ] 35 | # --- DISPLAYCONFIG_PATH_INFO 36 | # --------------------------- 37 | class DISPLAYCONFIG_PATH_SOURCE_INFO(ctypes.Structure): 38 | _fields_ = [ 39 | ('adapterId', LUID), 40 | ('id', UINT), 41 | ('modeInfoIdx', UINT), 42 | ('statusFlags', UINT), 43 | ] 44 | class DISPLAYCONFIG_PATH_TARGET_INFO(ctypes.Structure): 45 | _fields_ = [ 46 | ('adapterId', LUID), 47 | ('id', UINT), 48 | ('modeInfoIdx', UINT), 49 | ('outputTechnology', UINT), 50 | ('rotation', UINT), 51 | ('scaling', UINT), 52 | ('refreshRate', DISPLAYCONFIG_RATIONAL), 53 | ('scanLineOrdering', UINT), 54 | ('targetAvailable', BOOL), 55 | ('statusFlags', UINT), 56 | ] 57 | class DISPLAYCONFIG_PATH_INFO(ctypes.Structure): 58 | _fields_ = [ 59 | ("sourceInfo", DISPLAYCONFIG_PATH_SOURCE_INFO), 60 | ("targetInfo", DISPLAYCONFIG_PATH_TARGET_INFO), 61 | ('statusFlags', UINT), 62 | ] 63 | # --- DISPLAYCONFIG_MODE_INFO 64 | # --------------------------- 65 | class DISPLAYCONFIG_VIDEO_SIGNAL_INFO(ctypes.Structure): 66 | _fields_ = [ 67 | ("pixelRate", LARGE_INTEGER), 68 | ("hSyncFreq", DISPLAYCONFIG_RATIONAL), 69 | ("vSyncFreq", DISPLAYCONFIG_RATIONAL), 70 | ("activeSize", DISPLAYCONFIG_2DREGION), 71 | ("totalSize", DISPLAYCONFIG_2DREGION), 72 | ("videoStandard", UINT), # union with AdditionalSignalInfo ignored 73 | ("scanLineOrdering", UINT), 74 | ] 75 | class DISPLAYCONFIG_SOURCE_MODE(ctypes.Structure): 76 | _fields_ = [ 77 | ("width", UINT), 78 | ("height", UINT), 79 | ("pixelFormat", UINT), 80 | ("position", POINTL), 81 | ] 82 | class DISPLAYCONFIG_MODE_INFO_DUMMYUNIONNAME(ctypes.Union): 83 | _fields_ = [ 84 | ("target", DISPLAYCONFIG_VIDEO_SIGNAL_INFO), 85 | ("sourceMode", DISPLAYCONFIG_SOURCE_MODE), 86 | # desktopImageInfo if union ignored 87 | ] 88 | class DISPLAYCONFIG_MODE_INFO(ctypes.Structure): 89 | _fields_ = [ 90 | ("infoType", UINT), 91 | ("id", UINT), 92 | ("adapterId", LUID), 93 | ("mode", DISPLAYCONFIG_MODE_INFO_DUMMYUNIONNAME), 94 | ] 95 | # ------------------------ 96 | class DISPLAYCONFIG_DEVICE_INFO_HEADER(ctypes.Structure): 97 | _fields_ = [ 98 | ("type", UINT), 99 | ("size", UINT), 100 | ("adapterId", LUID), 101 | ("id", UINT), 102 | ] 103 | 104 | class DISPLAYCONFIG_SOURCE_DEVICE_NAME(ctypes.Structure): 105 | _fields_ = [ 106 | ("header", DISPLAYCONFIG_DEVICE_INFO_HEADER), 107 | ("viewGDIName", WCHAR * 32), 108 | ] 109 | 110 | class DISPLAYCONFIG_TARGET_DEVICE_NAME(ctypes.Structure): 111 | _fields_ = [ 112 | ("header", DISPLAYCONFIG_DEVICE_INFO_HEADER), 113 | ("flags", UINT), 114 | ("outputTechnology", UINT), 115 | ("edidManufactureId", USHORT), 116 | ("edidProductCodeId", USHORT), 117 | ("connectorInstance", UINT), 118 | ("monitorFriendlyDeviceName", WCHAR * 64), 119 | ("monitorDevicePath", WCHAR * 128), 120 | ] 121 | # ------------------------ 122 | # reads \\.\DISPLAYx for an adapter 123 | def getGDINameFromSource(adapterId: LUID, sourceId: UINT): 124 | request = DISPLAYCONFIG_SOURCE_DEVICE_NAME() 125 | 126 | request.header.size = ctypes.sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME) 127 | request.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME 128 | request.header.adapterId = adapterId 129 | request.header.id = sourceId 130 | winAPIResult = ctypes.windll.user32.DisplayConfigGetDeviceInfo(ctypes.byref(request)) 131 | if winAPIResult != 0: raise Exception("DisplayConfigGetDeviceInfo failed %d" % winAPIResult) 132 | 133 | return request.viewGDIName 134 | 135 | # readsDisplayFriendlyName for an adapter 136 | def getFriendlyNameFromTarget(adapterId: LUID, sourceId: UINT): 137 | request = DISPLAYCONFIG_TARGET_DEVICE_NAME() 138 | 139 | request.header.size = ctypes.sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME) 140 | request.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME 141 | request.header.adapterId = adapterId 142 | request.header.id = sourceId 143 | winAPIResult = ctypes.windll.user32.DisplayConfigGetDeviceInfo(ctypes.byref(request)) 144 | if winAPIResult != 0: raise Exception("DisplayConfigGetDeviceInfo failed %d" % winAPIResult) 145 | 146 | return request.monitorFriendlyDeviceName 147 | 148 | # queries all known display information for all adapters 149 | def readDisplayModes(): 150 | countPathElements = DWORD() 151 | countModeElements = DWORD() 152 | 153 | # get buffer necessary and allocated 154 | winAPIResult = ctypes.windll.user32.GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, ctypes.byref(countPathElements), ctypes.byref(countModeElements)) 155 | if winAPIResult != 0: raise Exception("GetDisplayConfigBufferSizes failed %d" % winAPIResult) 156 | displayPaths = (DISPLAYCONFIG_PATH_INFO * countPathElements.value)() 157 | displayModes = (DISPLAYCONFIG_MODE_INFO * countModeElements.value)() 158 | 159 | # query data 160 | winAPIResult = ctypes.windll.user32.QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, 161 | ctypes.byref(countPathElements), displayPaths, 162 | ctypes.byref(countModeElements), displayModes, 163 | 0) 164 | if winAPIResult != 0: raise Exception("QueryDisplayConfig failed %d" % winAPIResult) 165 | 166 | return [displayModes[n] for n in range(0, countModeElements.value)] 167 | 168 | # creates map \\.\DISPLAYx to DisplyFreindlyName 169 | def createGDIName2FriendlyName(): 170 | displayModes = readDisplayModes() 171 | 172 | adapterKeys = set() 173 | adapterKey2GDIName = dict() 174 | adapterKey2FriendlyName = dict() 175 | for mode in displayModes: 176 | adapterKey = "%d-%d" % (mode.adapterId.LowPart, mode.adapterId.HighPart) 177 | adapterKeys.add(adapterKey) 178 | 179 | if mode.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE: 180 | GDIName = getGDINameFromSource(mode.adapterId, mode.id) 181 | adapterKey2GDIName[adapterKey] = GDIName 182 | elif mode.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_TARGET: 183 | friendlyName = getFriendlyNameFromTarget(mode.adapterId, mode.id) 184 | adapterKey2FriendlyName[adapterKey] = friendlyName 185 | else: raise Exception("Invalid infoType %d" % mode.infoType) 186 | 187 | gdiName2FriendlyName = dict() 188 | for adapterKey in adapterKeys: 189 | friendlyName = adapterKey2FriendlyName[adapterKey] 190 | gdiName = adapterKey2GDIName[adapterKey] 191 | 192 | gdiName2FriendlyName[gdiName] = friendlyName 193 | 194 | return gdiName2FriendlyName 195 | 196 | # findMonitorIndexByFriendlyName 197 | #============================================================== 198 | class MONITORINFOEXW(ctypes.Structure): 199 | _fields_ = [ 200 | ("cbSize", ULONG), 201 | ("rcMonitor", RECT), 202 | ("rcWork", RECT), 203 | ("dwFlags", ULONG), 204 | ("szDevice", WCHAR * 32), 205 | ] 206 | 207 | # reads \\.\DISPLAYx for monitor ID for HMONITOR 208 | def readGDIName(hmonitor: HMONITOR): 209 | monitorinfo = MONITORINFOEXW() 210 | monitorinfo.cbSize = ctypes.sizeof(MONITORINFOEXW) 211 | winAPIResult = ctypes.windll.user32.GetMonitorInfoW(hmonitor, ctypes.byref(monitorinfo)) 212 | if winAPIResult != 1: raise Exception("GetMonitorInfoW failed %d" % winAPIResult) 213 | 214 | return monitorinfo.szDevice 215 | 216 | def createGDIName2MonitorIndex(): 217 | gdiName2MonitorResult = dict() 218 | 219 | monitors = get_monitors() 220 | for n in range(0, monitors.count): 221 | hmonitor = monitors[n].vcp.hmonitor 222 | gdiName = readGDIName(hmonitor) 223 | gdiName2MonitorResult[n] = gdiName 224 | 225 | return gdiName2MonitorResult 226 | 227 | def findMonitorIndexByFriendlyName(friendlyName: str): 228 | gdiName2FriendlyName = createGDIName2FriendlyName() 229 | 230 | monitors = get_monitors() 231 | for n in range(0, len(monitors)): 232 | hmonitor = monitors[n].vcp.hmonitor 233 | gdiName = readGDIName(hmonitor) 234 | 235 | currentFriendlyName = gdiName2FriendlyName[gdiName] 236 | if friendlyName.casefold() == currentFriendlyName.casefold(): return n 237 | 238 | return None -------------------------------------------------------------------------------- /src/kvmutil/kvmutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import platform 3 | from enum import Enum, auto 4 | import argparse 5 | import time 6 | 7 | def isOSWin(): 8 | return platform.system().lower() == "windows" 9 | def isOsLinux(): 10 | return platform.system().lower().startswith("linux") 11 | 12 | if isOSWin(): from monitorcontrol import get_monitors 13 | elif isOsLinux(): 14 | from ddcci.ddcci import DDCCIDevice 15 | import subprocess 16 | import re 17 | else: raise Exception("Unsupported OS '%s'" % platform.system()) 18 | 19 | # Defintion of DDC/CI codes 20 | # ====================================================================================================================== 21 | class VCPCode(Enum): 22 | # see VESA Monitor Control Command Set Standard v2.21, page 81 - (e.g. https://milek7.pl/ddcbacklight/mccs.pdf) 23 | InputSelect = 0x60 24 | 25 | # DELL (U4919DW?) specific 26 | DELL_U4919DW_PBP_SwapVideo = 0xe5 27 | DELL_U4919DW_PBP_SwapInput = 0xe7 28 | DELL_U4919DW_PBP_SubInput = 0xe8 29 | DELL_U4919DW_PBP_Mode = 0xe9 30 | UNKNOWN = 0x00 31 | 32 | class InputName(Enum): 33 | HDMI1 = auto() 34 | HDMI2 = auto() 35 | DisplayPort1 = auto() 36 | USBC_DELL_U4919DW = auto() 37 | 38 | class PBPCmd(Enum): 39 | ON = auto() 40 | OFF = auto() 41 | SWAP= auto() 42 | 43 | VCPCodeValues = { 44 | VCPCode.InputSelect: { 45 | # see VESA Monitor Control Command Set Standard v2.21, page 81 - (e.g. https://milek7.pl/ddcbacklight/mccs.pdf) 46 | InputName.HDMI1: 0x11, 47 | InputName.HDMI2: 0x12, 48 | InputName.DisplayPort1: 0x0f, 49 | 50 | # DELL (U4919DW?) specific 51 | InputName.USBC_DELL_U4919DW: 0x1B1B, 52 | }, 53 | VCPCode.DELL_U4919DW_PBP_SwapVideo: { 54 | PBPCmd.SWAP: 0xf000, 55 | }, 56 | VCPCode.DELL_U4919DW_PBP_SwapInput: { 57 | PBPCmd.SWAP: 0xff00, 58 | }, 59 | VCPCode.DELL_U4919DW_PBP_SubInput: { 60 | InputName.HDMI1: 0x11, 61 | InputName.HDMI2: 0x12, 62 | InputName.DisplayPort1: 0x0f, 63 | InputName.USBC_DELL_U4919DW: 0x1b, 64 | }, 65 | VCPCode.DELL_U4919DW_PBP_Mode: { 66 | PBPCmd.ON: 0x24, 67 | PBPCmd.OFF: 0x00, 68 | } 69 | } 70 | # ====================================================================================================================== 71 | 72 | # Defintion of commands for cmdline 73 | # ====================================================================================================================== 74 | class VCPCommand: 75 | def __init__(self, help, helpOptions, nargs, options: dict): 76 | self.help = help 77 | self.helpOptions = helpOptions 78 | self.options = options 79 | self.nargs = nargs 80 | 81 | class VCPWriteCommand: 82 | def __init__(self, code: VCPCode, value: int): 83 | self.code = code 84 | self.value = value 85 | 86 | def __repr__(self): 87 | return "VCPWriteCommand(code=0x%02x value=0x%04x)" % (self.code.value, self.value) 88 | 89 | vcpWriteCmds = { 90 | "InputSelect": VCPCommand( 91 | "Change input source selection", 92 | "Input source to switch to", 93 | None, { 94 | InputName.HDMI1.name.lower(): VCPWriteCommand(VCPCode.InputSelect, VCPCodeValues[VCPCode.InputSelect][InputName.HDMI1]), 95 | InputName.HDMI2.name.lower(): VCPWriteCommand(VCPCode.InputSelect, VCPCodeValues[VCPCode.InputSelect][InputName.HDMI2]), 96 | InputName.DisplayPort1.name.lower(): VCPWriteCommand(VCPCode.InputSelect, VCPCodeValues[VCPCode.InputSelect][InputName.DisplayPort1]), 97 | 98 | # DELL U4919DW specific 99 | InputName.USBC_DELL_U4919DW.name.lower():VCPWriteCommand(VCPCode.InputSelect, VCPCodeValues[VCPCode.InputSelect][InputName.USBC_DELL_U4919DW]), 100 | }, 101 | ), 102 | "PBP": VCPCommand( 103 | "Change PBP mode options", 104 | "PBP command", 105 | "+", { 106 | "on": VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_Mode, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_Mode][PBPCmd.ON]), 107 | "off": VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_Mode, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_Mode][PBPCmd.OFF]), 108 | "swapvideo":VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_SwapVideo, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_SwapVideo][PBPCmd.SWAP]), 109 | "swapinput":VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_SwapInput, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_SwapInput][PBPCmd.SWAP]), 110 | }, 111 | ), 112 | "PBPSubInputSelect": VCPCommand( 113 | "Change the input source of the PBP sub picture", 114 | "Input source to switch to", 115 | None, { 116 | InputName.HDMI1.name.lower(): VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_SubInput, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_SubInput][InputName.HDMI1]), 117 | InputName.HDMI2.name.lower(): VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_SubInput, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_SubInput][InputName.HDMI2]), 118 | InputName.DisplayPort1.name.lower(): VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_SubInput, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_SubInput][InputName.DisplayPort1]), 119 | 120 | # DELL U4919DW specific 121 | InputName.USBC_DELL_U4919DW.name.lower():VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_SubInput, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_SubInput][InputName.USBC_DELL_U4919DW]), 122 | } 123 | ), 124 | } 125 | # ====================================================================================================================== 126 | def parseDeviceID(deviceID: str): 127 | # if number use it directly 128 | if deviceID.isnumeric(): 129 | try: return int(deviceID) 130 | except ValueError: raise Exception("If deviceID is a number it must be an int") 131 | 132 | # Only supports Windows 133 | if not isOSWin(): raise Exception("Display friendly name only supported on Windows") 134 | 135 | from friendlyname import findMonitorIndexByFriendlyName 136 | index = findMonitorIndexByFriendlyName(deviceID) 137 | if index is None: raise Exception("Display with friendly name '%s' not found" % deviceID) 138 | print("Found display name '%s' at ID=%d" % (deviceID, index)) 139 | 140 | return index 141 | # ====================================================================================================================== 142 | def openDevice(deviceID: int): 143 | print("Accessing device with ID=%i..." % deviceID) 144 | if isOSWin(): 145 | device = get_monitors()[deviceID] 146 | device.open() 147 | elif isOsLinux(): 148 | device = DDCCIDevice(deviceID) 149 | else: raise Exception("Unsupported OS '%s'" % platform.system()) 150 | 151 | device.deviceID = deviceID 152 | return device 153 | 154 | def readVCPValue(device, code: VCPCode): 155 | print("Reading VPCCode=0x%02x..." % code.value) 156 | if isOSWin(): 157 | readValue = device.vcp.get_vcp_feature(code.value)[0] 158 | elif isOsLinux(): 159 | # device.read() seems not to work -> use external ddcutil 160 | cmdOutput = str(subprocess.check_output(["ddcutil", 161 | "-b", str(device.deviceID), 162 | "getvcp", "0x%02x" % code.value])) 163 | 164 | match = re.match(r".*sh=0x(\w{2}).*sl=0x(\w{2})", cmdOutput) 165 | if match == None: raise Exception("invalid output from ddcutil '%s'" % cmdOutput) 166 | 167 | readValue = int(match.group(1), 16) << 8 | int(match.group(2), 16) 168 | print("...ddcutil getvcp 0x%04x" % readValue) 169 | 170 | print("...got 0x%04x" % readValue) 171 | return readValue 172 | 173 | def writeVCPValue(device, vcpWriteCmd: VCPWriteCommand): 174 | print("Sending %s..." % vcpWriteCmd) 175 | if isOSWin(): device.vcp.set_vcp_feature(vcpWriteCmd.code.value, vcpWriteCmd.value) 176 | elif isOsLinux(): device.write(vcpWriteCmd.code.value, vcpWriteCmd.value) 177 | else: raise Exception() 178 | 179 | def executeVCPCmd(device, cmdName, parameter): 180 | vcpWriteCmd = vcpWriteCmds[cmdName].options[parameter] 181 | print("Executing the command %s=%s..." % (cmdName, parameter)) 182 | 183 | writeVCPValue(device, vcpWriteCmd) 184 | 185 | print("...done") 186 | 187 | def switchPBP(device): 188 | currentPBPValue = readVCPValue(device, VCPCode.DELL_U4919DW_PBP_Mode) 189 | if currentPBPValue == VCPCodeValues[VCPCode.DELL_U4919DW_PBP_Mode][PBPCmd.ON]: pbpMode = PBPCmd.ON 190 | elif currentPBPValue == VCPCodeValues[VCPCode.DELL_U4919DW_PBP_Mode][PBPCmd.OFF]: pbpMode = PBPCmd.OFF 191 | else: raise Exception("Unknown PBPMode %s" % currentPBPValue) 192 | print("Current PBPMode=%s" % pbpMode) 193 | 194 | if pbpMode == PBPCmd.OFF: 195 | print("Switching PBP on...") 196 | writeVCPValue(device, VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_Mode, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_Mode][PBPCmd.ON])) 197 | return PBPCmd.ON 198 | elif pbpMode == PBPCmd.ON: 199 | print("Switching PBP off...") 200 | writeVCPValue(device, VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_Mode, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_Mode][PBPCmd.OFF])) 201 | return PBPCmd.OFF 202 | 203 | def switchPBPWithSub(device, subInputSource): 204 | if switchPBP(device) == PBPCmd.ON: 205 | print("PBP is on -> setting sub input...") 206 | 207 | if isOSWin(): 208 | time.sleep(3.5) 209 | # Re-open display 210 | device = openDevice(device.deviceID) 211 | elif isOsLinux(): time.sleep(3) 212 | 213 | executeVCPCmd(device, "PBPSubInputSelect", subInputSource) 214 | else: print("PBP is off -> skip setting sub input") 215 | 216 | def swapPBP(device): 217 | writeVCPValue(device, 218 | VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_SwapVideo, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_SwapVideo][PBPCmd.SWAP])) 219 | 220 | time.sleep(0.5) 221 | 222 | writeVCPValue(device, 223 | VCPWriteCommand(VCPCode.DELL_U4919DW_PBP_SwapInput, VCPCodeValues[VCPCode.DELL_U4919DW_PBP_SwapInput][PBPCmd.SWAP])) 224 | 225 | if __name__ == "__main__": 226 | argParser = argparse.ArgumentParser() 227 | argParser.add_argument("device", help="i2c bus ID of the device (Linux: e.g. determined via 'ddccontrol -p' || Windows: index in global display list or display friendly name)") 228 | # --- 229 | subParsers = argParser.add_subparsers(help="command categorie") 230 | for cmdName, cmdInfo in vcpWriteCmds.items(): 231 | subParser = subParsers.add_parser(cmdName.lower(), help=cmdInfo.help) 232 | subParser.add_argument(cmdName, help=cmdInfo.helpOptions, choices=cmdInfo.options.keys(), nargs=cmdInfo.nargs) 233 | # === 234 | subParser = subParsers.add_parser("pbpswitch", help="LOGIC: Switches PBP mode on/off") 235 | subParser.set_defaults(pbpswitch='do') 236 | # - 237 | subParser = subParsers.add_parser("pbpswitch2", help="LOGIC: Switches PBP mode on/off + selects input of the sub picture") 238 | subParser.add_argument("pbpswitch2", help="Input source to set sub input to", choices=vcpWriteCmds["PBPSubInputSelect"].options.keys()) 239 | # - 240 | subParser = subParsers.add_parser("pbpswap", help="LOGIC: Swaps PBP video input source + USB input remains at the same video input") 241 | subParser.set_defaults(pbpswap='do') 242 | # === 243 | args = argParser.parse_args() 244 | 245 | # open device 246 | deviceID = parseDeviceID(args.device) 247 | deviceObj = openDevice(deviceID) 248 | 249 | cmdExecuted = False 250 | 251 | # get commands & parameters 252 | for cmdName in vcpWriteCmds.keys(): 253 | if hasattr(args, cmdName): 254 | selectedParams = getattr(args, cmdName) 255 | if isinstance(selectedParams, list): 256 | # multiple parameter 257 | cmdExecuted = True 258 | for selectedParam in selectedParams: executeVCPCmd(deviceObj, cmdName, selectedParam) 259 | elif not selectedParams is None: 260 | cmdExecuted = True 261 | executeVCPCmd(deviceObj, cmdName, selectedParams) # on parameter 262 | 263 | # PBP switch/wap 264 | if hasattr(args, "pbpswitch"): 265 | switchPBP(deviceObj) 266 | cmdExecuted = True 267 | elif hasattr(args, "pbpswitch2"): 268 | switchPBPWithSub(deviceObj, args.pbpswitch2) 269 | cmdExecuted = True 270 | elif hasattr(args, "pbpswap"): 271 | swapPBP(deviceObj) 272 | cmdExecuted = True 273 | 274 | if not cmdExecuted: 275 | # no commands selected 276 | argParser.print_usage() 277 | exit(1) 278 | -------------------------------------------------------------------------------- /src/kvmutil/linux/pbp.swap.input.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | cd $(dirname $(realpath "$0"))/.. 3 | ./kvmutil.py 3 pbp swapinput 4 | -------------------------------------------------------------------------------- /src/kvmutil/linux/pbp.swap.video.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | cd $(dirname $(realpath "$0"))/.. 3 | ./kvmutil.py 3 pbpswap 4 | -------------------------------------------------------------------------------- /src/kvmutil/linux/pbp.switch.onoff.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | cd $(dirname $(realpath "$0"))/.. 3 | ./kvmutil.py 3 pbpswitch2 usbc_dell_4919dw 4 | -------------------------------------------------------------------------------- /src/kvmutil/linux/profile.sidebyside.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | cd $(dirname $(realpath "$0"))/.. 3 | ./kvmutil.py 3 inputselect hdmi1 4 | sleep 0.5 5 | ./kvmutil.py 3 pbp on 6 | sleep 0.5 7 | ./kvmutil.py 3 pbpsubinputselect hdmi2 8 | xrandr --output HDMI-1 --mode 2560x1440_55 --left-of HDMI-2 9 | xrandr --output HDMI-2 --mode 2560x1440_55 --primary 10 | -------------------------------------------------------------------------------- /src/kvmutil/linux/switch2usbc.profile.full.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | cd $(dirname $(realpath "$0"))/.. 3 | ./kvmutil.py 3 inputselect usbc_dell_u4919dw 4 | sleep 1 5 | ./kvmutil.py 3 pbp off 6 | -------------------------------------------------------------------------------- /src/kvmutil/linux/switch2usbc.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | cd $(dirname $(realpath "$0"))/.. 3 | ./kvmutil.py 3 inputselect usbc_dell_u4919dw 4 | -------------------------------------------------------------------------------- /src/kvmutil/requirements.txt: -------------------------------------------------------------------------------- 1 | monitorcontrol>=1.7.2 2 | smbus -------------------------------------------------------------------------------- /src/kvmutil/win/linux.profile.sidebyside.bat: -------------------------------------------------------------------------------- 1 | call win.wrapper.bat inputselect hdmi1 2 | timeout 1 3 | call win.wrapper.bat pbp on 4 | timeout 1 5 | call win.wrapper.bat pbpsubinputselect hdmi2 6 | -------------------------------------------------------------------------------- /src/kvmutil/win/win.wrapper.bat: -------------------------------------------------------------------------------- 1 | #set DISPLAY=0 2 | set DISPLAY="DELL U4919DW" 3 | python ../kvmutil.py %DISPLAY% %* --------------------------------------------------------------------------------