├── .gitattributes ├── .gitignore ├── HIDInterface.sln ├── LICENSE ├── README.md ├── TestUSBInterface ├── App.config ├── Program.cs └── TestUSBInterface.csproj └── USBInterface ├── ArrayHelpers.cs ├── DeviceScanner.cs ├── HidApi.cs ├── ReportEventArgs.cs ├── USBDevice.cs └── USBInterface.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userprefs 8 | *.sln.docstates 9 | .editorconfig 10 | *.dll 11 | 12 | # Build results 13 | [Dd]ebug/ 14 | [Dd]ebugPublic/ 15 | [Rr]elease/ 16 | x64/ 17 | build/ 18 | bld/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | 22 | # MSTest test Results 23 | [Tt]est[Rr]esult*/ 24 | [Bb]uild[Ll]og.* 25 | 26 | #NUNIT 27 | *.VisualState.xml 28 | TestResult.xml 29 | 30 | # Build Results of an ATL Project 31 | [Dd]ebugPS/ 32 | [Rr]eleasePS/ 33 | dlldata.c 34 | 35 | *_i.c 36 | *_p.c 37 | *_i.h 38 | *.ilk 39 | *.meta 40 | *.obj 41 | *.pch 42 | *.pdb 43 | *.pgc 44 | *.pgd 45 | *.rsp 46 | *.sbr 47 | *.tlb 48 | *.tli 49 | *.tlh 50 | *.tmp 51 | *.tmp_proj 52 | *.log 53 | *.vspscc 54 | *.vssscc 55 | .builds 56 | *.pidb 57 | *.svclog 58 | *.scc 59 | 60 | # Chutzpah Test files 61 | _Chutzpah* 62 | 63 | # Visual C++ cache files 64 | ipch/ 65 | *.aps 66 | *.ncb 67 | *.opensdf 68 | *.sdf 69 | *.cachefile 70 | 71 | # Visual Studio profiler 72 | *.psess 73 | *.vsp 74 | *.vspx 75 | 76 | # TFS 2012 Local Workspace 77 | $tf/ 78 | 79 | # Guidance Automation Toolkit 80 | *.gpState 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper*/ 84 | *.[Rr]e[Ss]harper 85 | *.DotSettings.user 86 | 87 | # JustCode is a .NET coding addin-in 88 | .JustCode 89 | 90 | # TeamCity is a build add-in 91 | _TeamCity* 92 | 93 | # DotCover is a Code Coverage Tool 94 | *.dotCover 95 | 96 | # NCrunch 97 | *.ncrunch* 98 | _NCrunch_* 99 | .*crunch*.local.xml 100 | 101 | # MightyMoose 102 | *.mm.* 103 | AutoTest.Net/ 104 | 105 | # Web workbench (sass) 106 | .sass-cache/ 107 | 108 | # Installshield output folder 109 | [Ee]xpress/ 110 | 111 | # DocProject is a documentation generator add-in 112 | DocProject/buildhelp/ 113 | DocProject/Help/*.HxT 114 | DocProject/Help/*.HxC 115 | DocProject/Help/*.hhc 116 | DocProject/Help/*.hhk 117 | DocProject/Help/*.hhp 118 | DocProject/Help/Html2 119 | DocProject/Help/html 120 | 121 | # Click-Once directory 122 | publish/ 123 | 124 | # Publish Web Output 125 | *.[Pp]ublish.xml 126 | *.azurePubxml 127 | 128 | # NuGet Packages Directory 129 | packages/ 130 | ## TODO: If the tool you use requires repositories.config uncomment the next line 131 | #!packages/repositories.config 132 | 133 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 134 | # This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented) 135 | !packages/build/ 136 | 137 | # Windows Azure Build Output 138 | csx/ 139 | *.build.csdef 140 | 141 | # Windows Store app package directory 142 | AppPackages/ 143 | 144 | # Others 145 | sql/ 146 | *.Cache 147 | ClientBin/ 148 | [Ss]tyle[Cc]op.* 149 | ~$* 150 | *~ 151 | *.dbmdl 152 | *.dbproj.schemaview 153 | *.pfx 154 | *.publishsettings 155 | node_modules/ 156 | 157 | # RIA/Silverlight projects 158 | Generated_Code/ 159 | 160 | # Backup & report files from converting an old project file to a newer 161 | # Visual Studio version. Backup files are not needed, because we have git ;-) 162 | _UpgradeReport_Files/ 163 | Backup*/ 164 | UpgradeLog*.XML 165 | UpgradeLog*.htm 166 | 167 | # SQL Server files 168 | *.mdf 169 | *.ldf 170 | 171 | # Business Intelligence projects 172 | *.rdl.data 173 | *.bim.layout 174 | *.bim_*.settings 175 | 176 | # Microsoft Fakes 177 | FakesAssemblies/ 178 | 179 | # ========================= 180 | # Operating System Files 181 | # ========================= 182 | 183 | # OSX 184 | # ========================= 185 | 186 | .DS_Store 187 | .AppleDouble 188 | .LSOverride 189 | 190 | # Icon must ends with two \r. 191 | Icon 192 | 193 | # Thumbnails 194 | ._* 195 | 196 | # Files that might appear on external disk 197 | .Spotlight-V100 198 | .Trashes 199 | 200 | # Windows 201 | # ========================= 202 | 203 | # Windows image file caches 204 | Thumbs.db 205 | ehthumbs.db 206 | 207 | # Folder config file 208 | Desktop.ini 209 | 210 | # Recycle Bin used on file shares 211 | $RECYCLE.BIN/ 212 | 213 | # Windows Installer files 214 | *.cab 215 | *.msi 216 | *.msm 217 | *.msp 218 | -------------------------------------------------------------------------------- /HIDInterface.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2003 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "USBInterface", "USBInterface\USBInterface.csproj", "{30432D4A-0128-48E7-ADCD-249D70323611}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUSBInterface", "TestUSBInterface\TestUSBInterface.csproj", "{E69C9EE3-BCA8-4E5D-8EFA-7EBD75B1087A}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {30432D4A-0128-48E7-ADCD-249D70323611}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {30432D4A-0128-48E7-ADCD-249D70323611}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {30432D4A-0128-48E7-ADCD-249D70323611}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {30432D4A-0128-48E7-ADCD-249D70323611}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {E69C9EE3-BCA8-4E5D-8EFA-7EBD75B1087A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {E69C9EE3-BCA8-4E5D-8EFA-7EBD75B1087A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {E69C9EE3-BCA8-4E5D-8EFA-7EBD75B1087A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {E69C9EE3-BCA8-4E5D-8EFA-7EBD75B1087A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {FBCD496A-267D-40A5-8C67-F452E0F47DBD} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HIDInterface 2 | C# Wrapper of HIDAPI from signal11, wrapper multiplatform, used for interfaction with generic HID Devices, USB or bluetooth 3 | 4 | This is a fork of a project that appeared to be abandoned by its creator. 5 | 6 | The code is completely rewritten. 7 | 8 | It also includes some ideas from the code found in this SO question: 9 | http://stackoverflow.com/questions/15368376/options-for-hidapi-on-os-x 10 | 11 | 12 | # Getting started 13 | ```csharp 14 | class Program 15 | { 16 | 17 | public static void handle(object s, USBInterface.ReportEventArgs a) 18 | { 19 | Console.WriteLine(string.Join(", ", a.Data)); 20 | } 21 | 22 | public static void enter(object s, EventArgs a) 23 | { 24 | Console.WriteLine("device arrived"); 25 | } 26 | public static void exit(object s, EventArgs a) 27 | { 28 | Console.WriteLine("device removed"); 29 | } 30 | 31 | static void Main(string[] args) 32 | { 33 | // setup a scanner before hand 34 | DeviceScanner scanner = new DeviceScanner(0x4d8, 0x3f); 35 | scanner.DeviceArrived += enter; 36 | scanner.DeviceRemoved += exit; 37 | scanner.StartAsyncScan(); 38 | Console.WriteLine("asd"); 39 | 40 | // this should probably happen in enter() function 41 | try 42 | { 43 | // this can all happen inside a using(...) statement 44 | USBDevice dev = new USBDevice(0x4d8, 0x3f, null, false, 31); 45 | 46 | Console.WriteLine(dev.Description()); 47 | 48 | // add handle for data read 49 | dev.InputReportArrivedEvent += handle; 50 | // after adding the handle start reading 51 | dev.StartAsyncRead(); 52 | // can add more handles at any time 53 | dev.InputReportArrivedEvent += handle; 54 | 55 | // write some data 56 | byte[] data = new byte[32]; 57 | data[0] = 0x00; 58 | data[1] = 0x23; 59 | dev.Write(data); 60 | 61 | dev.Dispose(); 62 | } 63 | catch (Exception e) 64 | { 65 | Console.WriteLine(e.Message); 66 | } 67 | Console.ReadKey(); 68 | } 69 | } 70 | 71 | ``` 72 | 73 | ## Common question 74 | 75 | 76 | 77 | ### Using HIDAPI, how can you query the raw report descriptor? 78 | 79 | This is from stack overflow: 80 | http://stackoverflow.com/questions/17706853/using-hidapi-how-can-you-query-the-raw-report-descriptor 81 | 82 | Answer: HIDAPI does not provide functions for getting or parsing the report descriptor. 83 | Since HIDAPI is for talking to a custom devices, these devices will likely contain all 84 | or mostly vendor-defined report items anyway. 85 | 86 | So this implies that you should not use hidapi to work with printers/keyboards whose discriptors are not know beforehand. 87 | For that you should look into the libusb project. 88 | 89 | ### Why is it called USBInterface if its a hid wrapper and can not really work with generic usb? 90 | Thats what the original maintainer called it. One day I might change the name to HIDInterface. 91 | 92 | ## Gotchas 93 | 94 | First of make sure your application is the same bitness as the compiled dll (32-bit aka 86) / (64-bit aka x64) 95 | 96 | Because of the naming differences for hidapi dynamic library on all the platforms, this wrapper uses 97 | "hidapi" as the dll name and let windows or mono try to handle extension resolution. But unfortunately 98 | this means that it is your job to put either hidapi.dll (windows) or hidapi.so (linux) file in 99 | the solution directory together with USBInterface.dll (this library). 100 | 101 | When you think you have set everything up the simplest way to check that things are working is to call DeviceScanner.ScanOnce function. 102 | 103 | When setting this up on linux remember that linux by default requiers root permission to read and write to device. The DeviceScanner class should work regardless of root access. 104 | 105 | On linux you would probably want to set up a udev rule so that you device is automatically remounted and at members of some group (like 'devices') can access the device without root. 106 | 107 | 108 | When reading on windows, even if Report IDs are not used, during read function 109 | windows still sticks in the first command byte with 0x00 value. This must concern 110 | you _only_ if you pass a custom reportLen to the Read function. 111 | For reports that dont use Report IDs, you should set the DefaultInputReportLength in the constructor and 112 | this difference of windows to other systems will be handled tansparently and consistently. 113 | 114 | (to me this ironically seems pretty logical, because the output and feature reports make 115 | use of this command byte, would have been easier to just include it everywhere, BUT this is not by the USB HID spec,) 116 | 117 | 118 | According to the USB spec no item length should be larger than 32 bits (where item length is Report Size * Report Count) 119 | so on linux there is a redundant check in hid-code.c that goes something like this: 120 | http://lxr.free-electrons.com/source/drivers/hid/hid-core.c#L390 121 | 122 | Over the years the value has changed from 32 -> 96 (or 92, cannot remember) -> 128, becasue the 123 | reality is: there are uncompliant USB devices! If you are connecting a hid device wich fails with 124 | `invalid report_size` error, you can increase that limit and recompile your kernel, then it will work. 125 | And file a bug too! Because that check is there just for protocol reasons and since kernel devs agreed 126 | that 32 does not work, it is then completely redundant! 127 | 128 | 129 | And when your device is opened with USBDevice the DeviceScanner will think that the device is disconnected, 130 | because it is busy. The moral is: the DeviceScanner is not perfect. 131 | 132 | 133 | 134 | If you have a gui app and you attach an event handler to USBDevice events make sure you use Invoke or BeginInvoke in your handler function. 135 | like so: 136 | ```csharp 137 | // some place assign the handler 138 | dev.InputReportReceivedHandler += this.DataHandler; 139 | 140 | // .... 141 | 142 | private void DataHandler(object sender, InputReportEventArgs args) 143 | { 144 | if (this.InvokeRequired) 145 | { 146 | try 147 | { 148 | this.Invoke(new Action(DataHandler), sender, args); 149 | } 150 | catch (Exception e) 151 | { 152 | Console.WriteLine(e.Message); 153 | } 154 | return; 155 | } 156 | 157 | // body of your method 158 | } 159 | ``` 160 | 161 | # How to setup udev rule 162 | You want to do this so that your programs can access the device without having to use sudo. 163 | 164 | So first you should run lsusb to get device id 165 | ``` 166 | $ sudo lsusb 167 | Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 168 | Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 169 | Bus 004 Device 013: ID 1234:5678 MyDevice 170 | ``` 171 | 172 | Since we are not working with just usb, but with hid devices we must be more specific. 173 | The best way to find the path to your hid device is to run the hidtest program. 174 | If you installed hidapi from the repo and hence dont have the hidtest program, 175 | my advice is: now is a good time to download the source. Because you want to know 176 | the exact path as hidapi sees it. 177 | 178 | Anyway the hidtest program will list all hid devices and their paths such as 179 | ``` 180 | sudo ./hidtest-hidraw 181 | 182 | Device Found 183 | type: 1234 5678 184 | path: /dev/hidraw3 185 | serial_number: 0 186 | Manufacturer: Nice Guy 187 | Product: MyDevice from Nice Guy 188 | Release: 100 189 | Interface: 0 190 | ``` 191 | 192 | Now you ask udev what it knows about the device and you put in the "/dev/hidraw3": 193 | ``` 194 | sudo udevadm info -a -p $(udevadm info -q path -n /dev/hidraw3) 195 | ``` 196 | 197 | From its output you build a udev rule and after reboot all should work. 198 | Important: when building the file make sure to query `SUBSYSTEM=="hidraw"` and not `"usb"` 199 | Oh and `SUBSYSTEM` != `SUBSYSTEMS` <- watch the trailing `S` 200 | Example rule file: 201 | ``` 202 | sudo cat udev/rules.d/xy-mydevice.rules 203 | SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", GROUP="john", MODE="0660" 204 | ``` 205 | 206 | Then to test you can run `udevadm monitor` in one console to view all device paths 207 | and in another: 208 | ``` 209 | # check your udev rule 210 | usedadm test $(udevadm info -q path -n /dev/hidraw3) 211 | # force reread config 212 | udevadm control --reload 213 | # you should re-plug the device to be extra sure 214 | udevadm trigger 215 | ``` 216 | 217 | 218 | # Important things for development 219 | 220 | ## How to understand HID 221 | 222 | You will need to use some sort of a usb sniffer to at the very least check out the various types of reports. 223 | On windows I can recommend USBlyzer. 224 | For linux I can recommend wireshark (it has some plugin for usb sniffing) 225 | 226 | THen if there is only one website you will read about HID this should be it: 227 | http://www.usbmadesimple.co.uk/ums_5.htm 228 | 229 | And you can find various HID related specs here: 230 | http://www.usb.org/developers/hidpage 231 | 232 | Both of the above are extremely good sources. 233 | 234 | Talks about report descriptors, same as the specification but in normal english: 235 | http://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/ 236 | 237 | On linux by default lsusb is unable to show you the report descriptor even with sudo. Before that you need to unbind the device. But hid devices are not different to usual usb devices and they must be unbound from the hid-generic driver using their HID ID. 238 | 239 | source for example: http://unix.stackexchange.com/questions/12005/how-to-use-linux-kernel-driver-bind-unbind-interface-for-usb-hid-devices#13035 240 | 241 | Example: dmesg shows: 242 | ``` 243 | hid-generic 0003:046D:C229.0036: input,hiddev0,hidraw4: USB HID v1.11 Keypad [Logitech G19 Gaming Keyboard] on usb-0000:00:13.2-3.2/input1 244 | ``` 245 | Then you use 0003:046D:C229.0036 as HID device id and 246 | ``` 247 | echo -n "0003:046D:C229.0036" > /sys/bus/hid/drivers/hid-generic/unbind 248 | ``` 249 | To bind back just reinsert device or 250 | ``` 251 | echo -n "0003:046D:C229.0036" > /sys/bus/hid/drivers/hid-generic/bind 252 | ``` 253 | 254 | 255 | 256 | 257 | ## How to work with unamanged code and import unamanaged DLL 258 | 259 | 260 | 1) You must use: 261 | 262 | ``` 263 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 264 | ``` 265 | 266 | Also look into setting the [CharSet = CharSet.Ansi] when dealing with strings strings. 267 | 268 | 2) See how the C# types map to C types. 269 | Primitive types map readily! 270 | (int, short, ushort) 271 | The only issue might be that long in C++ is 32-bit, so you should define all such instances as int in your C# code 272 | 273 | 3) passing strings: 274 | ```` 275 | int takes_string(char* s); 276 | int takes_string(const char* s); 277 | int takes_string(const unsigned char* s); 278 | int takes_string(wchar_t* ws); 279 | ``` 280 | (and other similar variations...); 281 | 282 | For all of them you should use: 283 | ``` 284 | [DLLImport("...")] 285 | private static extern int takes_string([In] string s) 286 | ``` 287 | 288 | The [In] attribute tells the marshaler that this is passed into a function and not returned. 289 | 290 | 4) Getting strings back: 291 | ``` 292 | int fills_string(char* pBuffer); 293 | int fills_string(wchar_t* pBuffer); 294 | ``` 295 | 296 | For these ones use: 297 | ``` 298 | [DLLImport("...")] 299 | private static extern int fills_string(StringBuilder sb); 300 | ``` 301 | 302 | Call it like this in c# (use the StringBuilder rather than string class on c# side): 303 | 304 | ``` 305 | var sb = new StringBuilder(255); 306 | fills_string(sb); 307 | Console.WriteLine(sb.ToString()) 308 | ``` 309 | 310 | You use StringBuilder with char* and string with const char*. 311 | 312 | Refer to the table for how C# types map onto C types: 313 | https://msdn.microsoft.com/en-us/library/fzhhdwae(v=vs.110).aspx 314 | 315 | 316 | Auto-marshaling: 317 | This is code without auto-marshaling: 318 | in C++ i have a function: 319 | 320 | ``` 321 | void GetImage(void* ptr) 322 | { 323 | .... 324 | } 325 | ``` 326 | 327 | and in C#: 328 | 329 | ``` 330 | [DllImport("libPylonInterface.so")] 331 | private static extern void GetImage(IntPtr ptr); 332 | 333 | public static void GetImage(out byte[] arr) 334 | { 335 | //allocating unmanaged memory for byte array 336 | IntPtr ptr = Marshal.AllocHGlobal (_width * _height); 337 | //and "copying" image data to this pointer 338 | GetImage (ptr); 339 | 340 | arr = new byte[_width * _height]; 341 | //copying from unmanaged to managed memory 342 | Marshal.Copy (ptr, arr, 0, _width * _height); 343 | Marshal.FreeHGlobal(ptr); 344 | } 345 | ``` 346 | 347 | So we have to do many memory manipulations in c#. 348 | 349 | Now this is with auto-marshaling: 350 | 351 | ``` 352 | [DllImport("libPylonInterface.so")] 353 | private static extern void GetImage([Out] byte[] arr); 354 | 355 | .... 356 | 357 | arr = new byte[_width * _height]; 358 | // now the arr is filled with data 359 | GetImage(arr); 360 | ``` 361 | 362 | This also avoids the second memory copy because the marshaller will pin the managed array and pass its address to the unmanaged code. 363 | The unmanaged code can then populate the managed memory directly. 364 | 365 | 366 | Manually managing memory in C#: 367 | If for example you must pass a double** to the C function. One way to handle that is to manually allocate and marshal unmanaged memory. 368 | You cannot use a double[,] because that simply does not map to double*[]. 369 | Then in C# code you will have things like this (to setup double** shich is to be passed to a C function): 370 | 371 | ``` 372 | IntPtr[] CreateUnmanagedArrays(double[][] arr) 373 | { 374 | IntPtr[] result = new IntPtr[arr.Length]; 375 | for (int i=0; idata = (unsigned char*)_strdup("hello"); 421 | (*outData)->len = 5; 422 | return 0; 423 | } 424 | 425 | void API_Free(DataStruct** pp) 426 | { 427 | free((*pp)->data); 428 | delete *pp; 429 | *pp = NULL; 430 | } 431 | 432 | ``` 433 | 434 | The C# code to access those functions are as follows: 435 | 436 | ``` 437 | [StructLayout(LayoutKind.Sequential)] 438 | struct DataStruct 439 | { 440 | public IntPtr data; 441 | public int len; 442 | }; 443 | 444 | [DllImport("ReadFile.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] 445 | unsafe private static extern int API_ReadFile([MarshalAs(UnmanagedType.LPWStr)]string filename, DataStruct** outData); 446 | 447 | [DllImport("ReadFile.dll", CallingConvention = CallingConvention.Cdecl)] 448 | unsafe private static extern void API_Free(DataStruct** handle); 449 | 450 | unsafe static int ReadFile(string filename, out byte[] buffer) 451 | { 452 | DataStruct* outData; 453 | int result = API_ReadFile(filename, &outData); 454 | buffer = new byte[outData->len]; 455 | Marshal.Copy((IntPtr)outData->data, buffer, 0, outData->len); 456 | API_Free(&outData); 457 | return result; 458 | } 459 | 460 | static void Main(string[] args) 461 | { 462 | byte[] buffer; 463 | ReadFile("test.txt", out buffer); 464 | foreach (byte ch in buffer) 465 | { 466 | Console.Write("{0} ", ch); 467 | } 468 | Console.Write("\n"); 469 | } 470 | ``` 471 | 472 | 473 | Whenever you can just use byte[] arrays for marshalling, otherwise you run into all sorts of troubles such as: 474 | The C code: 475 | 476 | ``` 477 | typedef struct s_parameterStuct 478 | { 479 | int count; 480 | char name[ 128 ]; 481 | } parameterStruct; 482 | ``` 483 | 484 | And the c# code: 485 | 486 | ``` 487 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] 488 | public class parameterStuct 489 | { 490 | public int count; 491 | 492 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] 493 | public char[] name; 494 | } 495 | ``` 496 | 497 | But since char is 2 bytes in c# the SizeConst for name should be 64 (becasue 64 of these occupy 128 bytes). 498 | 499 | 500 | Further links: 501 | http://stackoverflow.com/questions/17620396/tutorial-needed-on-invoking-unmanaged-dll-from-c-net 502 | 503 | 504 | 505 | 506 | 507 | -------------------------------------------------------------------------------- /TestUSBInterface/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /TestUSBInterface/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using USBInterface; 7 | 8 | namespace TestUSBInterface 9 | { 10 | class Program 11 | { 12 | 13 | public static void handle(object s, USBInterface.ReportEventArgs a) 14 | { 15 | Console.WriteLine(string.Join(", ", a.Data)); 16 | } 17 | 18 | public static void enter(object s, EventArgs a) 19 | { 20 | Console.WriteLine("device arrived"); 21 | } 22 | public static void exit(object s, EventArgs a) 23 | { 24 | Console.WriteLine("device removed"); 25 | } 26 | 27 | static void Main(string[] args) 28 | { 29 | // setup a scanner before hand 30 | DeviceScanner scanner = new DeviceScanner(0x4d8, 0x3f); 31 | scanner.DeviceArrived += enter; 32 | scanner.DeviceRemoved += exit; 33 | scanner.StartAsyncScan(); 34 | Console.WriteLine("asd"); 35 | 36 | // this should probably happen in enter() function 37 | try 38 | { 39 | // this can all happen inside a using(...) statement 40 | USBDevice dev = new USBDevice(0x4d8, 0x3f, null, false, 31); 41 | 42 | Console.WriteLine(dev.Description()); 43 | 44 | // add handle for data read 45 | dev.InputReportArrivedEvent += handle; 46 | // after adding the handle start reading 47 | dev.StartAsyncRead(); 48 | // can add more handles at any time 49 | dev.InputReportArrivedEvent += handle; 50 | 51 | // write some data 52 | byte[] data = new byte[32]; 53 | data[0] = 0x00; 54 | data[1] = 0x23; 55 | dev.Write(data); 56 | 57 | dev.Dispose(); 58 | } 59 | catch (Exception e) 60 | { 61 | Console.WriteLine(e.Message); 62 | } 63 | Console.ReadKey(); 64 | } 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /TestUSBInterface/TestUSBInterface.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.0 4 | Exe 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /USBInterface/ArrayHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace USBInterface 7 | { 8 | public class ArrayHelpers 9 | { 10 | public void StreamWriteChar(char c, byte[] stream, int index) 11 | { 12 | if (index < stream.Length) 13 | { 14 | stream[index] = (byte)c; 15 | index++; 16 | } 17 | } 18 | 19 | public void StreamWriteInt16(ushort num, byte[] stream, int index) 20 | { 21 | if (index + 1 < stream.Length) 22 | { 23 | stream[index] = (byte)((num >> 8) & 0xff); 24 | index++; 25 | stream[index] = (byte)(num & 0xff); 26 | index++; 27 | } 28 | } 29 | 30 | public void StreamWriteInt32(uint num, byte[] stream, int index) 31 | { 32 | if (index + 3 < stream.Length) 33 | { 34 | stream[index] = (byte)((num >> 24) & 0xff); 35 | index++; 36 | stream[index] = (byte)((num >> 16) & 0xff); 37 | index++; 38 | stream[index] = (byte)((num >> 8) & 0xff); 39 | index++; 40 | stream[index] = (byte)(num&0xff); 41 | index++; 42 | } 43 | } 44 | 45 | public char StreamReadChar(byte[] stream, int index) 46 | { 47 | char ret = '\0'; 48 | if (index < stream.Length) 49 | { 50 | ret = (char)stream[index]; 51 | index++; 52 | } 53 | return ret; 54 | } 55 | 56 | public ushort StreamReadInt16(byte[] stream, int index) 57 | { 58 | ushort ret = 0; 59 | if (index + 1 < stream.Length) 60 | { 61 | ret |= (ushort)(stream[index] << 8); 62 | index++; 63 | ret |= (ushort)(stream[index]); 64 | index++; 65 | } 66 | return ret; 67 | } 68 | 69 | public uint StreamReadInt32(byte[] stream, int index) 70 | { 71 | uint ret = 0; 72 | if (index + 3 < stream.Length) 73 | { 74 | ret |= (uint)(stream[index] << 24); 75 | index++; 76 | ret |= (uint)(stream[index] << 16); 77 | index++; 78 | ret |= (uint)(stream[index] << 8); 79 | index++; 80 | ret |= (uint)(stream[index]); 81 | index++; 82 | } 83 | return ret; 84 | } 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /USBInterface/DeviceScanner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | 8 | namespace USBInterface 9 | { 10 | public class DeviceScanner 11 | { 12 | public event EventHandler DeviceArrived; 13 | public event EventHandler DeviceRemoved; 14 | 15 | public bool isDeviceConnected 16 | { 17 | get { return deviceConnected; } 18 | } 19 | 20 | // for async reading 21 | private object syncLock = new object(); 22 | private Thread scannerThread; 23 | private volatile bool asyncScanOn = false; 24 | 25 | private volatile bool deviceConnected = false; 26 | 27 | private int scanIntervalMillisecs = 10; 28 | public int ScanIntervalInMillisecs 29 | { 30 | get { lock (syncLock) { return scanIntervalMillisecs; } } 31 | set { lock (syncLock) { scanIntervalMillisecs = value; } } 32 | } 33 | 34 | public bool isScanning 35 | { 36 | get { return asyncScanOn; } 37 | } 38 | 39 | private ushort vendorId; 40 | private ushort productId; 41 | 42 | // Use this class to monitor when your devices connects. 43 | // Note that scanning for device when it is open by another process will return FALSE 44 | // even though the device is connected (because the device is unavailiable) 45 | public DeviceScanner(ushort VendorID, ushort ProductID, int scanIntervalMillisecs = 100) 46 | { 47 | vendorId = VendorID; 48 | productId = ProductID; 49 | ScanIntervalInMillisecs = scanIntervalMillisecs; 50 | } 51 | 52 | // scanning for device when it is open by another process will return false 53 | public static bool ScanOnce(ushort vid, ushort pid) 54 | { 55 | return HidApi.hid_enumerate(vid, pid) != IntPtr.Zero; 56 | } 57 | 58 | public void StartAsyncScan() 59 | { 60 | // Build the thread to listen for reads 61 | if (asyncScanOn) 62 | { 63 | // dont run more than one thread 64 | return; 65 | } 66 | asyncScanOn = true; 67 | scannerThread = new Thread(ScanLoop); 68 | scannerThread.Name = "HidApiAsyncDeviceScanThread"; 69 | scannerThread.Start(); 70 | } 71 | 72 | public void StopAsyncScan() 73 | { 74 | asyncScanOn = false; 75 | } 76 | 77 | private void ScanLoop() 78 | { 79 | var culture = CultureInfo.InvariantCulture; 80 | Thread.CurrentThread.CurrentCulture = culture; 81 | Thread.CurrentThread.CurrentUICulture = culture; 82 | 83 | // The read has a timeout parameter, so every X milliseconds 84 | // we check if the user wants us to continue scanning. 85 | while (asyncScanOn) 86 | { 87 | try 88 | { 89 | IntPtr device_info = HidApi.hid_enumerate(vendorId, productId); 90 | bool device_on_bus = device_info != IntPtr.Zero; 91 | // freeing the enumeration releases the device, 92 | // do it as soon as you can, so we dont block device from others 93 | HidApi.hid_free_enumeration(device_info); 94 | if (device_on_bus && ! deviceConnected) 95 | { 96 | // just found new device 97 | deviceConnected = true; 98 | if (DeviceArrived != null) 99 | { 100 | DeviceArrived(this, EventArgs.Empty); 101 | } 102 | } 103 | if (! device_on_bus && deviceConnected) 104 | { 105 | // just lost device connection 106 | deviceConnected = false; 107 | if (DeviceRemoved != null) 108 | { 109 | DeviceRemoved(this, EventArgs.Empty); 110 | } 111 | } 112 | } 113 | catch (Exception e) 114 | { 115 | // stop scan, user can manually restart again with StartAsyncScan() 116 | asyncScanOn = false; 117 | } 118 | // when read 0 bytes, sleep and read again 119 | Thread.Sleep(ScanIntervalInMillisecs); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /USBInterface/HidApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace USBInterface 8 | { 9 | internal class HidApi 10 | { 11 | #region Native Methods 12 | 13 | // On windows for system installed: hidapi.dll 14 | // On linux for system installed: "libhidapi-hidraw" or "libhidapi-libusb" 15 | // unfortunately there is no way simple to automatically 16 | // find the library on all platforms becasue of different 17 | // naming conventions. 18 | // Just use hidapi and expect users to supply it in same folder as .exe 19 | public const string DLL_FILE_NAME = "hidapi"; 20 | 21 | /// Return Type: int 22 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 23 | public static extern int hid_init(); 24 | 25 | 26 | /// Return Type: int 27 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 28 | public static extern int hid_exit(); 29 | 30 | /// Return Type: hid_device_info* 31 | ///vendor_id: unsigned short 32 | ///product_id: unsigned short 33 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 34 | public static extern IntPtr hid_enumerate(ushort vendor_id, ushort product_id); 35 | 36 | /// Return Type: void 37 | ///devs: struct hid_device_info* 38 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 39 | public static extern void hid_free_enumeration(IntPtr devs); 40 | 41 | /// Return Type: hid_device* 42 | ///vendor_id: unsigned short 43 | ///product_id: unsigned short 44 | ///serial_number: wchar_t* 45 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 46 | public static extern IntPtr hid_open(ushort vendor_id, ushort product_id, [In] string serial_number); 47 | 48 | 49 | /// Return Type: hid_device* 50 | ///path: char* 51 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 52 | public static extern IntPtr hid_open_path([In] string path); 53 | 54 | 55 | /// Return Type: int 56 | ///device: hid_device* 57 | ///data: unsigned char* 58 | ///length: size_t->unsigned int 59 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 60 | public static extern int hid_write(IntPtr device, [In] byte[] data, uint length); 61 | 62 | 63 | /// Return Type: int 64 | ///dev: hid_device* 65 | ///data: unsigned char* 66 | ///length: size_t->unsigned int 67 | ///milliseconds: int 68 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 69 | public static extern int hid_read_timeout(IntPtr device, [Out] byte[] buf_data, uint length, int milliseconds); 70 | 71 | 72 | /// Return Type: int 73 | ///device: hid_device* 74 | ///data: unsigned char* 75 | ///length: size_t->unsigned int 76 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 77 | public static extern int hid_read(IntPtr device, [Out] byte[] buf_data, uint length); 78 | 79 | 80 | /// Return Type: int 81 | ///device: hid_device* 82 | ///nonblock: int 83 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 84 | public extern static int hid_set_nonblocking(IntPtr device, int nonblock); 85 | 86 | 87 | /// Return Type: int 88 | ///device: hid_device* 89 | ///data: char* 90 | ///length: size_t->unsigned int 91 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 92 | public static extern int hid_send_feature_report(IntPtr device, [In] byte[] data, uint length); 93 | 94 | 95 | /// Return Type: int 96 | ///device: hid_device* 97 | ///data: unsigned char* 98 | ///length: size_t->unsigned int 99 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 100 | public static extern int hid_get_feature_report(IntPtr device, [Out] byte[] buf_data, uint length); 101 | 102 | 103 | /// Return Type: void 104 | ///device: hid_device* 105 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl)] 106 | public extern static void hid_close(IntPtr device); 107 | 108 | 109 | /// Return Type: int 110 | ///device: hid_device* 111 | ///string: wchar_t* 112 | ///maxlen: size_t->unsigned int 113 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] 114 | public static extern int hid_get_manufacturer_string(IntPtr device, StringBuilder buf_string, uint length); 115 | 116 | 117 | /// Return Type: int 118 | ///device: hid_device* 119 | ///string: wchar_t* 120 | ///maxlen: size_t->unsigned int 121 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] 122 | public static extern int hid_get_product_string(IntPtr device, StringBuilder buf_string, uint length); 123 | 124 | 125 | /// Return Type: int 126 | ///device: hid_device* 127 | ///string: wchar_t* 128 | ///maxlen: size_t->unsigned int 129 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] 130 | public static extern int hid_get_serial_number_string(IntPtr device, StringBuilder buf_serial, uint maxlen); 131 | 132 | 133 | /// Return Type: int 134 | ///device: hid_device* 135 | ///string_index: int 136 | ///string: wchar_t* 137 | ///maxlen: size_t->unsigned int 138 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] 139 | public static extern int hid_get_indexed_string(IntPtr device, int string_index, StringBuilder buf_string, uint maxlen); 140 | 141 | 142 | /// Return Type: wchar_t* 143 | ///device: hid_device* 144 | [DllImport(DLL_FILE_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] 145 | public static extern IntPtr hid_error(IntPtr device); 146 | 147 | 148 | 149 | #endregion 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /USBInterface/ReportEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace USBInterface 7 | { 8 | // Readonly data that you get from the device 9 | public class ReportEventArgs : EventArgs 10 | { 11 | public ReportEventArgs(byte[] data) 12 | { 13 | Data = data; 14 | } 15 | 16 | public byte[] Data { get; private set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /USBInterface/USBDevice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Runtime.InteropServices; 7 | using System.Globalization; 8 | using System.Threading; 9 | 10 | namespace USBInterface 11 | { 12 | 13 | public class USBDevice : IDisposable 14 | { 15 | 16 | public event EventHandler InputReportArrivedEvent; 17 | public event EventHandler DeviceDisconnecedEvent; 18 | 19 | public bool isOpen 20 | { 21 | get { return DeviceHandle != IntPtr.Zero; } 22 | } 23 | 24 | // If the read process grabs ownership of device 25 | // and blocks (unable to get any data from device) 26 | // for more than Timeout millisecons 27 | // it will abandon reading, pause for readIntervalInMillisecs 28 | // and try reading again. 29 | private int readTimeoutInMillisecs = 1; 30 | public int ReadTimeoutInMillisecs 31 | { 32 | get { lock (syncLock) { return readTimeoutInMillisecs; } } 33 | set { lock(syncLock) { readTimeoutInMillisecs = value; } } 34 | } 35 | 36 | // Interval of time between two reads, 37 | // during this time the device is free and 38 | // we can write to it. 39 | private int readIntervalInMillisecs = 4; 40 | public int ReadIntervalInMillisecs 41 | { 42 | get { lock (syncLock) { return readIntervalInMillisecs; } } 43 | set { lock(syncLock) { readIntervalInMillisecs = value; } } 44 | } 45 | 46 | // for async reading 47 | private object syncLock = new object(); 48 | private Thread readThread; 49 | private volatile bool asyncReadOn = false; 50 | 51 | // Flag: Has Dispose already been called? 52 | // Marked as volatile because Dispose() can be called from another thread. 53 | private volatile bool disposed = false; 54 | 55 | private IntPtr DeviceHandle = IntPtr.Zero; 56 | 57 | // this will be the return buffer for strings, 58 | // make it big, becasue by the HID spec (can not find page) 59 | // we are allowed to request more bytes than the device can return. 60 | private StringBuilder pOutBuf = new StringBuilder(1024); 61 | 62 | // This is very convinient to use for the 90% of devices that 63 | // dont use ReportIDs and so have only one input report 64 | private int DefaultInputReportLength = -1; 65 | 66 | // This only affects the read function. 67 | // receiving / sending a feature report, 68 | // and writing to device always requiers you to prefix the 69 | // data with a Report ID (use 0x00 if device does not use Report IDs) 70 | // however when reading if the device does NOT use Report IDs then 71 | // the prefix byte is NOT inserted. On the other hand if the device uses 72 | // Report IDs then when reading we must read +1 byte and byte 0 73 | // of returned data array will be the Report ID. 74 | private bool hasReportIds = false; 75 | 76 | // HIDAPI does not provide any way to get or parse the HID Report Descriptor, 77 | // This means you must know in advance what it the report size for your device. 78 | // For this reason, reportLen is a necessary parameter to the constructor. 79 | // 80 | // Serial Number is optional, pass null (do NOT pass an empty string) if it is unknown. 81 | // 82 | public USBDevice(ushort VendorID 83 | , ushort ProductID 84 | , string serial_number 85 | , bool HasReportIDs = true 86 | , int defaultInputReportLen = -1) 87 | { 88 | DeviceHandle = HidApi.hid_open(VendorID, ProductID, serial_number); 89 | AssertValidDev(); 90 | DefaultInputReportLength = defaultInputReportLen; 91 | hasReportIds = HasReportIDs; 92 | } 93 | 94 | private void AssertValidDev() 95 | { 96 | if (DeviceHandle == IntPtr.Zero) throw new Exception("No device opened"); 97 | } 98 | 99 | public void GetFeatureReport(byte[] buffer, int length = -1) 100 | { 101 | AssertValidDev(); 102 | if (length < 0) 103 | { 104 | length = buffer.Length; 105 | } 106 | if (HidApi.hid_get_feature_report(DeviceHandle, buffer, (uint)length) < 0) 107 | { 108 | throw new Exception("failed to get feature report"); 109 | } 110 | } 111 | 112 | public void SendFeatureReport(byte[] buffer, int length = -1) 113 | { 114 | AssertValidDev(); 115 | if (length < 0) 116 | { 117 | length = buffer.Length; 118 | } 119 | if (HidApi.hid_send_feature_report(DeviceHandle, buffer, (uint)length) < 0) 120 | { 121 | throw new Exception("failed to send feature report"); 122 | } 123 | } 124 | 125 | // either everything is good, or throw exception 126 | // Meaning InputReport 127 | // This function is slightly different, as we must return the number of bytes read. 128 | private int ReadRaw(byte[] buffer, int length = -1) 129 | { 130 | AssertValidDev(); 131 | if (length < 0) 132 | { 133 | length = buffer.Length; 134 | } 135 | int bytes_read = HidApi.hid_read_timeout(DeviceHandle, buffer, (uint)length, readTimeoutInMillisecs); 136 | if (bytes_read < 0) 137 | { 138 | throw new Exception("Failed to Read."); 139 | } 140 | return bytes_read; 141 | } 142 | 143 | // Meaning OutputReport 144 | private void WriteRaw(byte[] buffer, int length = -1) 145 | { 146 | AssertValidDev(); 147 | if (length < 0) 148 | { 149 | length = buffer.Length; 150 | } 151 | if (HidApi.hid_write(DeviceHandle, buffer, (uint)length) < 0) 152 | { 153 | throw new Exception("Failed to write."); 154 | } 155 | } 156 | 157 | public string GetErrorString() 158 | { 159 | AssertValidDev(); 160 | IntPtr ret = HidApi.hid_error(DeviceHandle); 161 | // I can not find the info in the docs, but guess this frees 162 | // the ret pointer after we created a managed string object 163 | // else this would be a memory leak 164 | return Marshal.PtrToStringAuto(ret); 165 | } 166 | 167 | // All the string functions are in a little bit of trouble becasue 168 | // wchar_t is 2 bytes on windows and 4 bytes on linux. 169 | // So we should just alloc a hell load of space for the return buffer. 170 | // 171 | // We must divide Capacity / 4 because this takes the buffer length in multiples of 172 | // wchar_t whoose length is 4 on Linux and 2 on Windows. So we allocate a big 173 | // buffer beforehand and just divide the capacity by 4. 174 | public string GetIndexedString(int index) 175 | { 176 | lock(syncLock) 177 | { 178 | AssertValidDev(); 179 | if (HidApi.hid_get_indexed_string(DeviceHandle, index, pOutBuf, (uint)pOutBuf.Capacity / 4) < 0) 180 | { 181 | throw new Exception("failed to get indexed string"); 182 | } 183 | return pOutBuf.ToString(); 184 | } 185 | } 186 | 187 | public string GetManufacturerString() 188 | { 189 | lock (syncLock) 190 | { 191 | AssertValidDev(); 192 | pOutBuf.Clear(); 193 | if (HidApi.hid_get_manufacturer_string(DeviceHandle, pOutBuf, (uint)pOutBuf.Capacity / 4) < 0) 194 | { 195 | throw new Exception("failed to get manufacturer string"); 196 | } 197 | return pOutBuf.ToString(); 198 | } 199 | } 200 | 201 | public string GetProductString() 202 | { 203 | lock (syncLock) 204 | { 205 | AssertValidDev(); 206 | pOutBuf.Clear(); 207 | if (HidApi.hid_get_product_string(DeviceHandle, pOutBuf, (uint)pOutBuf.Capacity / 4) < 0) 208 | { 209 | throw new Exception("failed to get product string"); 210 | } 211 | return pOutBuf.ToString(); 212 | } 213 | } 214 | 215 | public string GetSerialNumberString() 216 | { 217 | lock (syncLock) 218 | { 219 | AssertValidDev(); 220 | pOutBuf.Clear(); 221 | if (HidApi.hid_get_serial_number_string(DeviceHandle, pOutBuf, (uint)pOutBuf.Capacity / 4) < 0) 222 | { 223 | throw new Exception("failed to get serial number string"); 224 | } 225 | return pOutBuf.ToString(); 226 | } 227 | } 228 | 229 | public string Description() 230 | { 231 | AssertValidDev(); 232 | return string.Format("Manufacturer: {0}\nProduct: {1}\nSerial number:{2}\n" 233 | , GetManufacturerString(), GetProductString(), GetSerialNumberString()); 234 | } 235 | 236 | public void Write(byte[] user_data) 237 | { 238 | // so we don't read and write at the same time 239 | lock (syncLock) 240 | { 241 | byte[] output_report = new byte[user_data.Length]; 242 | Array.Copy(user_data, output_report, output_report.Length); 243 | WriteRaw(output_report); 244 | } 245 | } 246 | 247 | // Returnes a bytes array. 248 | // If an error occured while reading an exception will be 249 | // thrown by the underlying ReadRaw method 250 | // 251 | // Note for reportLen: This is the real actual size of the 252 | // actual HID report according to his descriptor, 253 | // so Report Size * Report Count depending on each of the 254 | // Output, Input, Feature reports. 255 | public byte[] Read(int reportLen = -1) 256 | { 257 | lock(syncLock) 258 | { 259 | int length = reportLen; 260 | if (length < 0) 261 | { 262 | // when we have Report IDs and the user did not specify the reportLen explicitly 263 | // then add an extra byte to account for the Report ID 264 | length = hasReportIds ? DefaultInputReportLength + 1 : DefaultInputReportLength; 265 | } 266 | byte[] input_report = new byte[length]; 267 | int read_bytes = ReadRaw(input_report); 268 | byte[] ret = new byte[read_bytes]; 269 | Array.Copy(input_report, 0, ret, 0, read_bytes); 270 | return ret; 271 | } 272 | } 273 | 274 | public void StartAsyncRead() 275 | { 276 | // Build the thread to listen for reads 277 | if (asyncReadOn) 278 | { 279 | // dont run more than one read 280 | return; 281 | } 282 | asyncReadOn = true; 283 | readThread = new Thread(ReadLoop); 284 | readThread.Name = "HidApiReadAsyncThread"; 285 | readThread.Start(); 286 | } 287 | 288 | public void StopAsyncRead() 289 | { 290 | asyncReadOn = false; 291 | } 292 | 293 | private void ReadLoop() 294 | { 295 | var culture = CultureInfo.InvariantCulture; 296 | Thread.CurrentThread.CurrentCulture = culture; 297 | Thread.CurrentThread.CurrentUICulture = culture; 298 | 299 | // The read has a timeout parameter, so every X milliseconds 300 | // we check if the user wants us to continue reading. 301 | while (asyncReadOn) 302 | { 303 | try 304 | { 305 | byte[] res = Read(); 306 | // when read >0 bytes, tell others about data 307 | if (res.Length > 0 && this.InputReportArrivedEvent != null) 308 | { 309 | InputReportArrivedEvent(this, new ReportEventArgs(res)); 310 | } 311 | } 312 | catch (Exception) 313 | { 314 | // when read <0 bytes, means an error has occurred 315 | // stop device, break from loop and stop this thread 316 | if (this.DeviceDisconnecedEvent != null) 317 | { 318 | DeviceDisconnecedEvent(this, EventArgs.Empty); 319 | } 320 | // call the dispose method in separate thread, 321 | // otherwise this thread would never get to die 322 | new Thread(Dispose).Start(); 323 | break; 324 | } 325 | // when read 0 bytes, sleep and read again 326 | // We must sleep for some time to allow others 327 | // to write to the device. 328 | Thread.Sleep(readIntervalInMillisecs); 329 | } 330 | } 331 | 332 | // Public implementation of Dispose pattern callable by consumers. 333 | public void Dispose() 334 | { 335 | Dispose(true); 336 | GC.SuppressFinalize(this); 337 | } 338 | 339 | // Protected implementation of Dispose pattern. 340 | protected virtual void Dispose(bool disposing) 341 | { 342 | if (disposed) 343 | { 344 | return; 345 | } 346 | if (disposing) 347 | { 348 | // Free any other managed objects here. 349 | if (asyncReadOn) 350 | { 351 | asyncReadOn = false; 352 | readThread.Join(readTimeoutInMillisecs); 353 | if (readThread.IsAlive) 354 | { 355 | readThread.Abort(); 356 | } 357 | } 358 | } 359 | // Free any UN-managed objects here. 360 | // so we are not reading or writing as the device gets closed 361 | lock (syncLock) 362 | { 363 | if (isOpen) 364 | { 365 | HidApi.hid_close(DeviceHandle); 366 | DeviceHandle = IntPtr.Zero; 367 | } 368 | } 369 | HidApi.hid_exit(); 370 | // mark object as having been disposed 371 | disposed = true; 372 | } 373 | 374 | ~USBDevice() 375 | { 376 | Dispose(false); 377 | } 378 | 379 | private string EncodeBuffer(byte[] buffer) 380 | { 381 | // the buffer contains trailing '\0' char to mark its end. 382 | return Encoding.Unicode.GetString(buffer).Trim('\0'); 383 | } 384 | 385 | } 386 | } 387 | 388 | 389 | -------------------------------------------------------------------------------- /USBInterface/USBInterface.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | --------------------------------------------------------------------------------