Personal C Sharp                                                         By
Home How To Start Examples-Desktop Examples-Web PC# Methods Reference-Desktop Reference-Web

======================================= SPEEDING-UP Personal C SHARP, Accessing Operating system functions and communicating with hardware devices ======================================= for PC# version 4.25 and later As you know, Personal C Sharp has been made for programmers of different levels. ** For a person who is interested in writing programs to help him in managing personal matters like his personal finance or to help him with any type of research he is doing, this chapter may be beyond his need. ** For a well educated person in any field who likes to go further with his programming abilities and/or to learn how to make small computer controled electronic projects, this chapter is necessary. The introduction at the beginning of this chapter should be helpful to you. ** For a software developer who uses Personal C Sharp to experiment with new software ideas, to test new projects or to do anything else, you may skip the introduction and read the rest of this chapter. ============================================================================================= ============ INTRODUCTION ============ What is inside my computer? =========================== The heart of the computer is the Central Processing Unit (CPU) The CPU has been shrinking in size over the years and getting faster at the same time. It used to occupy a whole room. Now, it fits into one chip (A chip is called also Integrated Circuit or IC) Like any chip, the CPU has many pins (or terminals) Some pins are used to supply necessary power or signals to the CPU and some are to receive the CPU output signals. Here are the most important input and output CPU pins: (1) POWER SUPPLY AND GROUND PINS: This is where we supply power to the CPU. (2) CLOCK PIN: This is a pulsating signal which we supply to the CPU. The CPU uses this signal to time its operations. CPU speeds are normally defined by the frequency of this clock signal. (3) DATA PINS: This is where we tell the CPU what to do. As you know, the original CPU's made by Intel have been called 8-bit CPU's. Later they made 16-bit, 32-bit and lately 64-bit CPU's. This number is the number of data bits which the CPU can receive and respond to. Each bit comes to the CPU through one data pin. If you are familiar with binary, the range of instructions which we can send to an 8-bit CPU is (00000000 : 11111111) in binary which translates to (00 : FF) hex and (0 : 255) decimal; their total number can be 256 instructions or 2^8 (We mean 2 to the power 8) The range of instructions which we can send to a 32-bit CPU is (00000000 : FFFFFFFF) hex, which means that we can send upto (2^32) instructios. (4) ADDRESS PINS: The data on these pins are outputed by the CPU. An old 8-bit CPU used to have 16 address pins. This means that they used to be able to address (2^16) locations ranging (0 : 65535) decimal. The CPU addresses are mapped to various devices. For example, each memory cell in available RAM is mapped to a specific address. This means that whenever the CPU reaches this address, data stored at that cell will be connected to the CPU's data pins. How does the CPU operate? ------------------------- The CPU starts by applying low signals (meaning logic zero) to all address pins. This marks address zero. It follows that by looking at the data bits applied to its data pins. If it finds that they are all zeros, which means "Do nothing", it waits for proper clock timing then increments the address and looks at its data pins again. If it finds instructions to do, it performs what it should do before incrementing the address again. It keeps repeating this operation until it reaches the highest possible address then returns to address zero and starts again. The type of instructions which the CPU can do are many. Some of them are: (1) Applying arithmatic or bitwise operations to a number which is stored temporarely into one of its registers or at a specific memory location. (2) Reading the data at one memory location or writing data there. (4) Moving a block of data from one location in the memory to another. (5) Sending instruction to and receiving data from any device which is connected to the host. (6) Making a forward or backward jump to a new address. Computer programming levels: ============================ (1) Applying machine codes directly to the CPU: This is the lowest level. All we need to do is to get the CPU instruction set which tells us all codes for the operations the CPU can perform and use them to do any programming job we need. This is the fastest way except that it requires lengthy programs which are hard to manage especially if the project was shared among a number of developers. (2) Using an assembly language This improves program management to a degree, but still not preferred for most bussiness applications. (3) Using "C" and "C++" languages: Although programs developed by these languages run slower than the ones described above, Windows operating system and most commercial developments use them. (4) Using the .NET, Java and similar languages: These languages manage your code. They guarantee that no data can ever expand to other data locations. They also do garbage collection which constantly eliminates items which are no longer needed by your program while it's running. Although these operations add great reliability to your programming, they slow down your programs significantly. For this reason, Microsoft has allowed you to write unmanaged code within your programs to speed them up whenever necessary and also to allow you to call operating system functions. (5) Using Personal C Sharp: Since PC# is written in C# language, it could be looked at as slower, but due to the techniques we use, we believe that in most cases a program written in PC# may run faster than a program written in standard C# which does the same job. We'll also show you here how to write unmanaged code in PC# to increase execution speed. Additionally, We'll teach you how to access operating system functions directly within a PC# program. If you like to do some operations at extra-high speed, we'll show you in the next chapter how to add to your Personal C Sharp program "C" or "Assembly language" subroutines which you can call at any time supplying them with parameters and receiving their return values as you do with any local method. The numbering systems: ====================== We're going to be dealing with 3 numbering systems: (1) The Decimal System: ----------------------- This system has been used for thousands of years. It's based on the number 10 which is the number of our hand fingers. See how we write from zero to 100 using the decimal system: 000 001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 ................................................ 090 091 092 093 094 095 096 097 098 099 100 The idea is that we only have 10 digits (0123456789), We write them in order with zeros at their left side. After we finish them all, we increment the next digit to the left, and write them again and so on. (2) The HEX System: ------------------- Since the system used inside the computer is binary whose base is (2), Expressing numbers in decimal (base 10) makes numbers hard to make sense of, we need to use a multiple of (2) base. Base 16 is the commonly used. The HEX digits are (0123456789ABCDEF) Here is how we write them: 000 001 002 003 004 005 006 007 008 009 00A 00B 00C 00D 00E 00F 010 011 012 013 014 015 016 017 018 019 01A 01B 01C 01D 01E 01F ................................................ 0F0 0F1 0F2 0F3 0F4 0F5 0F6 0F7 0F8 0F9 0FA 0FB 0FC 0FD 0FE 0FF 100 Notice that the last number (100) is a HEX number whose value is (16 X 16)=256 decimal. (3) The Binary System: ---------------------- This is the system computers use internally since it achieves maximum precision. It's a base two system. Its digits are zero and one only. Here is how the numbers (0:15) are expressed in hex and binary: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0000, 0001, 0010, 0011, 0100, 0101, 0110, 0111, 1000, 1001, 1010, 1011, 1100, 1101, 1110, 1111 Converting hex to binary and vice versa: ---------------------------------------- The hex and binary numbers convert to each others easily. Here is an example: The two hex digits 1 and 2 in binary are 0001 and 0010 respectively. This tells us that the hex number (12) in binary is (0001 0010) and the hex number (21) in binary is (0010 0001) This is why we find the hex numbers to be easier to use than the decimal ones when we work with computers. Converting hex to decimal and vice versa: ----------------------------------------- Method om() at mode "th" converts an integer number to hex and mode "fh" does the opposite. Both numbers must be in string form and both should be assigned to (os) Here is an example: os=""+255;om("th");tm(); // Displays "FF" os= "FF"; om("fh");tm(); // Displays "255" Using Pointers: =============== Pointers are variables which store integer numbers that tells us where objects are stored in memory. C# uses pointers internally (We call them references) A standard C# program should not get involved with where references are or what they store. Although this insures reliability and guards against errors, it reduces execution speed. (C/C++) programmers manage pointers by themselves. This is why We call C# code a "managed" code while calling (C/C++) code an "unmanaged" one. Despite that, we are going to learn later in this chapter, how to use in special situations a type of pointers in C# which (as far as we're concerned) is independant from C# references. How can using pointers speed-up program execution? -------------------------------------------------- Let us assume that you have a number which is assigned to (o) and you like to know if it was positive or negative. Most likely you will compare its value with (0) in a statement like this: if (o<0) {// Consider it a -ve number} else {// Consider it positive or zero} A number of type int occupies 4 bytes. Bit # 7 of the last one indicates the sign. When you compare the value of the number with zero, The entire 4 bytes must be read and the type int number's value must be computed before the comparison is made. This is a lengthy process. If you can access the pointer to the address where (o) is stored in memory, all you need to do, is to use simple bitwise arithmatic to know if the bit which stores the sign is (0) or (1) This maps directly to CPU instructions, so it runs very fast. Why is working on pointers risky? --------------------------------- When you manage pointers by yourself, you must make sure to allocate enough memory to store your variables' values even when they reach their maximum capacities. This could be hard to predict with strings, arrays and most objects. An error can cause stored values to expand to each others space causing a big mess. You should also be constantly erasing areas of memory which are occupied by objects which you no longer need. Failure to do that could propably cause a crash. ============================================================================================= =========================== Dealing with unmanaged code =========================== The new class (pcsu): --------------------- This class extends class (pcs) and your class should extend (pcsu) This means that your class can call any method in class (pcs) as usual and additionally, it can call any method in this new class. Class (pcsu) is compiled as an unsafe class since it contains unsafe methods which use pointers in their operations. You may need to do the same to your class if it contains unsafe code. This is done by simply adding the commented word "// unsafe" at your class declaration line. You will see how this is done in the comming examples. The new method hm(): -------------------- This method simplifies your access to operating system's objects which reside into unmanaged memory using "handles" and the communication with hardware devices which are connected to the host computer. We know that most of the people who read this chapter are interested in writing C# code, so we'll always give a brief explanation on how each mode is written or present a link to where you can find MDSN documentation for each subject. The method allows you to do your jobs much faster and more importantly without plenty of errors. New addition to the GUV's: -------------------------- (1) The byte variables (iy,jy,ky) have been added since we are going to do more work on bytes. (2) The IntPtr variables (ip,jp,kp,op) have been added. IntPtr is a struct which works as a pointer to unmanaged memory. its size is 32 bits on 32-bit systems and 64 bits on 64-bit ones. (3) We have added some of the missing unsigned integer types which the .NET supports to the o-based variable group. The new addition is: ou : Type uint which is a 4-byte unsigned integer. olu: Type ulong which is an 8-byte unsigned integer. (4) The byte array Y[] is used to get/put a number of bytes from/into unmanaged memory buffer. REMARKS: -------- (1) The new additions are available to your class if it extends class (pcsu) only. If it extends class (pcs), these additions will not be available. (2) As you know, PC# resets the (i,j,k) based GUV's after the execution of any method call. You may be wondering how an IntPtr variable like (ip) is reset. Here is how: ip=IntPtr.Zero; C# managed Data types: ---------------------- All the types and their capacities can be found here: Most of these types can be declared in C# under other names, for example: Int16:short Int32:int Int64:long UInt16:ushort UInt32:uint UInt64:ulong Double:double Char:char String:string Byte:byte Unmanaged Data types: --------------------- I2:short I4:int I8:long U2:ushort DWORD,U4:uint QWORD,U8:ulong R4:float F8:double LPStr :Null terminated 1 byte/char string. LPWStr:Null terminated 2 bytes/char string. How to match unmanaged data types with managed ones: ---------------------------------------------------- Normally, we like to call Windows API functions not the other way around. So the C# data types which we send as parameters to an API function, need to be marshaled to match the unmanaged types which the function requires. The return value must also be marshaled to match a .NET type. The Marshal class methods are all documented here: USING POINTERS TO OPERATE ON OBJECTS IN C#: =========================================== There are two means to do so. One operates on objects while they are inside the "stack" or the "heap" areas of the memory and the other one copies the objects to an unmanaged area outside those two areas and operates on them there. The first one allows you to speed-up sections of your program and the second one allows you to interact with the outside world so you can call Windows' API's to do system operations, to communicate with hardware devices or to share data with other processes. Operating on objects while they are inside the stack or the heap: ----------------------------------------------------------------- As you know, these areas are where the "Garbage collector" is active. It keeps eliminating objects which are no longer necessary and relocating all active objects to fill the gaps created. So how can pointers be useful if the objects they point to don't stay at one location? The answer is that you are allowed to freeze the location of one object until you finish operating on it using the "fixed" statement. This statement tells the garbage collector not to alter the location of a specific object until the operation terminates. The pointers used in this type are the old C type pointers whose functions can be explained with this short code block: x=5; // Assign (5) to (x) y=&x; // y is a pointer to (x) meaning that it contains the address where (x) is stored in memory z=*y; // (z) is the value stored at the address which is assigned to (y), so it should be (=5) This code showed you what the (*) and (&) operators mean, but it will not work as is in a C# program for the following reasons: (1) y is an ordinary integer. You can't use any integer variable as a pointer. You must define it as a pointer to an (int) with (int* y=&x) and this does not qualify it to be a pointer to data of any other type. So types (int*, char*, byte* and double*) are not the same. (2) (int* y=&x) is also illegal. This is because the address of (x) keeps changing every moment, so such assignment is not allowed. You must keep it in a fixed place until operation is complete. So, your code should be changed to: fixed(int* y=&x) {z=*y;} (3) Any method or block of code which contains this kind of pointers must be marked "unsafe". So your code block needs to be changed to: unsafe { // Mark this block unsafe. x=5; // Assign (5) to (x) fixed(int* y=&x) { // Fix address of (x) while assigning it to (y) and doing: z=*y; // Assign the value pointed to by (y) to (z) } // Release address of (x) os=""+z;tm(); // Display (z) It should display "5". } // End of unsafe block If you mark the method which contains this block usafe, you'll not need to mark the block unsafe. The method declaration would look like this "unsafe void MyMethod() {" (4) The only thing left now is compiling. Classes which contain unsafe code are treated differently by the compiler. You need to add the commented word "unsafe" anywhere at your class declaration statement line so that PC# can tell the compiler that this class contains unsafe code. So your class decleration should look like this "public class a:pcsu { // unsafe". REMARK: ======= In the fixed statement, (y) which is the fixed address of (x) must be a new variable defined in the fixed statement header and used within fixed statement boundaries only without modification. You must assign it or assign "the value it points to", to other variables then you can apply any necessary modification to the new variables. ============================================================================================ Example 1: Use pointers to copy an int array and a string to byte arrays. Then display the bytes generated in each case in hex. ============================================================================================ //assembly pcsu.exe; public class a:pcsu { // unsafe // The commented word "unsafe" is required public override void init() { tia=toa="t"; base.init(); } //------------------------------------- CopyArray() ------------------------------------- // This method must be declared unsafe since pointers are used inside it. unsafe void CopyArray() { // Copy int array J[] to byte array JY[] // If more than one object must be fixed, nest (fixed) statements fixed (int* pSrc=J) fixed (byte* pDst=JY) { // Assign addresses to pSrc,pDst and freez them byte* ps=(byte*)pSrc;byte* pd=pDst; // ps,pd point to 1st byte of each array for (int n=0;n<20;n++) { // Scan all bytes of J[];Copy each byte pointed *((byte*)pd) = *((byte*)ps); // to by ps to the byte pointed to by ds pd ++;ps ++; // Increment the 2 addresses } } } //------------------------------------- CopyString() ------------------------------------- unsafe void CopyString() { // Copy string (os) to byte array JY[] char[] C=os.ToCharArray(); // Covert string to char array fixed (char* pSrc=C) fixed (byte* pDst=JY) { byte* ps = (byte*)pSrc;byte* pd = pDst; // Do the same as with the int array for (int n=0;n<8;n++) { *((byte*)pd) = *((byte*)ps); pd ++;ps ++; } } } public override void run() { cls="r0"; os=" Using pointers to operate on objects without relocation"; tm();os="";tm(); //------ Convert int array into bytes and display them ------ cls="b0";os="Converting type (int) array into (byte) array:";tm();cls="S9"; J=new int[5];JY=new byte[20]; // Redimension arrays to take 5 int's (20 bytes) J[0]=5;J[1]=-5;J[2]=256;J[3]=-256;J[4]=123456789; // Assign values to the source array J[] for(int i=0;i<20;i++) JY[i]=0; // Zero all rows of destination array CopyArray(); // Call method to copy J[] to JD[] xs=""; // Initialize display string for(int i=0;i<20;i++) { // Scan destination byte array JY[] if (i%4==0 && i!=0) xs+=" , "; // add "," before each new int (4 bytes) to (xs) os=""+JY[i];om("th"); // Convert each byte to hex string os="00"+os;os=os.Substring(os.Length-2); // Make sure we have 2 hex digits/byte xs+=os+" "; // Seperate each 2 bytes with a space. } os="Content of Source array: 5 , -5 , 256 , -256 , 123456789";tm();//os="";tm(); os="Content of Destination array:";tm(); os=xs;tm(); // Display (xs) when finished. //------ Convert a string into bytes and display them ------ os="";tm();cls="b0";os="Converting a string into (byte) array:";tm();cls="S9"; JY=new byte[8]; // Redimension arrays to take 4 char's (8 bytes) for(int i=0;i<8;i++) JY[i]=0; // Zero all rows of destination array os="John"; CopyString (); // Call method to copy (os) char's to JD[] xs=""; // Do the same to display the bytes. for(int i=0;i<8;i++) { if (i%2==0 && i!=0) xs+=" , "; os=""+JY[i];om("th");os="00"+os;os=os.Substring(os.Length-2); xs+=os+" "; } os="Content of Source array: John";tm(); os="Content of Destination array:";tm(); os=xs;tm(); } } ================================================================================================ Notice how each 4 bytes which make an integer number are orderd in memory. They start with the least significant byte and end with the most significant one. This is not the same as we do when we write numbers. We always like to see the most significant digits at the left when we write a number. Look at this table: Numbers in Decimal: 5 , -5 256 , -256 , 123456789 Bytes in memory : 05 00 00 00 , FB FF FF FF , 00 01 00 00 , 00 FF FF FF , 15 CD 5B 07 How we write them : 00 00 00 05 , FF FF FF FB , 00 00 01 00 , FF FF FF 00 , 07 5B CD 15 ================================================================================================

STRUCTS: ======== Structs are the ancestors of classes. They are classes which contain fields only. No methods or delegates. They are used extensively in (C/C++) programs which the Windows operating system and device drivers are written into. You can also define and use a struct in C#. What we need to do now is to make C# structs available to unmanaged code and to make (C/C++) structs available to C#'s managed code. Handling C# structs: -------------------- A C# struct requires 3 items: (1) The struct definition. (2) The struct instance (If you like it to be a classwide variable) (3) The method which will use the struct. For better organization, don't include all the structs' code into method run() Make a seperate method for each struct or group of structs and call this method from run() Next example will show you how to make a C# struct and to copy its data into unmanaged memory and how to access the struct at both locations. Windows API structs and functions: ---------------------------------- This type requires 4 items: (1) The struct definition modified to be within C# specs for variable types. (2) The signature of the API functions which operate on the structs. (3) The struct instance (If you like it to be a classwide variable) (4) The method which will use the struct. ** IMPORTANT REMARK: ==================== Most of the structs and method signatures and instances which are used for developing software to handle communication with USB devices are declared in class (pcsu) which your class extends. Declaring them again in your class is unnecessary. They are all public and you can use them without declaration. a list of them will follow next example. About the next example: ----------------------- The process in the next example is a real one which is part of multiple processes used internally to obtain the path name and to communicate with a HID device. The full process is as follows: (1) Obtaining a pointer to the "device information set" using hm("dil") The pointer comes assigned to (op) (2) Obtaining a pointer to a particular Device Interface Data struct inside the information set using method hm("di") The pointer comes assigned to (dip) (3) Obtaining the device path name assigned to (os) using method hm("dp") In the next example, We're going to demonstrate how step (2) is performed. Microsoft supplies us with methods and structs written in (C/C++) to do the process. We like to see how to use them into a C# program. The function is included in the system DLL class "setupapi.dll" and its signature is: BOOL SetupDiEnumDeviceInterfaces( __in HDEVINFO DeviceInfoSet, __in_opt PSP_DEVINFO_DATA DeviceInfoData, __in const GUID *InterfaceClassGuid, __in DWORD MemberIndex, __out PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData ); and you can find documentation about it here: As you can see, these functions are slightly different from C# methods. The main return value is boolean. It indicates whether the method has ended successfully or with an error. True means success. The method returns another value which is labled (__out), it's an instance of the SP_DEVICE_INTERFACE_DATA struct. The item labeled (__in_opt) is optional. In a C# program, you must declare this method's signature as you do with any top class variable before you can use it inside a C# method. Here is the declaration: [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern Boolean SetupDiEnumDeviceInterfaces ( IntPtr DeviceInfoSet, IntPtr DeviceInfoData, ref System.Guid InterfaceClassGuid, Int32 MemberIndex, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData ); The attributes declaration at the top tells the compiler which system DLL contains the method. It can also tell it how to handle CharSet's for strings and to bring us back last error code. if the function ended with error. Whenever such error occurs in method hm(), you will get: erb=true; eri=The returned numeric error code; ers=The error message. Struct SP_DEVICE_INTERFACE_DATA in (C/C++) is defined as follows: typedef struct _SP_DEVICE_INTERFACE_DATA { DWORD cbSize; GUID InterfaceClassGuid; DWORD Flags; ULONG_PTR Reserved; } SP_DEVICE_INTERFACE_DATA, *PSP_DEVICE_INTERFACE_DATA; and you can find documentation about it here: Notice that the first item in the struct is its own size in bytes. This is useful in memory allocation computation. Here is how it's declared into a C# program: public struct SP_DEVICE_INTERFACE_DATA { public Int32 cbSize; public Guid InterfaceClassGuid; public Int32 Flags; public IntPtr Reserved; } and here is an instance of the struct: public SP_DEVICE_INTERFACE_DATA dip= new SP_DEVICE_INTERFACE_DATA(); REMARK: Struct SP_DEVICE_INTERFACE_DATA, method SetupDiEnumDeviceInterfaces's signature and the object (dip) have all been declared public in class (pcsu), so don't declare them again as stated earlier. ============================================================================================= Example 2: Write a program to do the following: (1) Send a single value which is stored into a C# variable to unmanaged memory and get it back. (2) Send C# type arrays to unmanaged memory and show how to access them there. (3) Send C# struct to unmanaged memory and show how to access it there. (4) Show how to use Windows API structs and functions to obtain the path name of a USB HID device assuming that you already have a pointer to the Interface associated with the path name. ============================================================================================= //assembly pcsu.exe public class a:pcsu { public override void init() { tia=toa="t"; // Select "text screen" display base.init(); // Initialize PC# classes } public override void run() { cls="r0";os=" DEALING WITH UNMANAGED MEMORY"; tm();os="";tm(); cls="b0";os="Sending single C# variables to unmanaged memory and getting them back:";tm(); cls="S9";svalues();os="";tm(); cls="b0";os="Sending C# arrays to unmanaged memory and using them there:";tm(); cls="S9";arrays();os="";tm(); cls="b0";os="Using C# structs both locally and after sending them to unmanaged memory:";tm(); cls="S9";CsStruct();os="";tm(); cls="b0";os="Using Windows API structs and functions in C#:";tm(); cls="S9";ApiStruct(); } //------------------------------------ Single Values -------------------------------------- void svalues() { od=1.75;hm("fd"); // Copy this double value in (od) to unmanaged // memory pointed to by (op) os="Address at (op)= "+op.ToString();tm(); // Display address at (op) od=0;hm("td"); // Zero (od) then ovewrite it with the value os="Value received back= "+od;tm(); // stored at (op) and display its value. } //---------------------------------------- Arrays ----------------------------------------- void arrays() { OY=new byte[] {1,2,3,4,5,6,7,8,9,0}; // Load OY[] with 10 numbers. hm("fY"); // Copy it to unmanaged memory at (op) Y[0]=9;i=3;o=1;oi=10;hm("fa"); // Modify 4th byte in mem by making it (9) i=2;o=5;hm("ta"); // Get the 5 bytes starting by 3rd one from mem. os="";for(int n=0;n<5;n++) os+=""+Y[n]+" "; // and display them. os="Returned bytes (2:6) from memory: "+os;tm(); } //-------------------------------- Using C# Struct Employee -------------------------------- public struct Employee { public string Name; // String of variable length public byte DepNo; // Department no. fits into one byte public int EmpNo; // Employee no. fits into 4 bytes. } public Employee em0=new Employee(); void CsStruct() { //------ Creating two struct instances ------ em0=new Employee();em0.Name="John";em0.DepNo=10;em0.EmpNo=1234; Employee empty=new Employee();empty.Name="";empty.DepNo=0;empty.EmpNo=0; //------ Using it with managed C# code ------ os="I'm " + em0.Name.Trim() + ". My employee number is "+em0.EmpNo; os+=". My department number is "+em0.DepNo+"."; tm(); //------ Putting the struct into unmanaged memory and use it there ------ // (1) Copy Struct Employee to unmanaged memory buffer pointed to by (op) i=Marshal.SizeOf(em0);hm("ma"); // Allocate memory Marshal.StructureToPtr(em0,op,false); // Send the struct instance there // (2) Empty (em0) then overwrite it with the stored struct in memory and inspect result em0=empty; // Empty (em0) em0=(Employee)Marshal.PtrToStructure(op, typeof(Employee)); os="name: "+em0.Name;tm(); // Extract name & display it to inspect it. // (3) Modify (em0) by changing the name then putting it back into unmanaged memory em0.Name="Shawn"; // Modify name Marshal.StructureToPtr(em0,op,false); // Send to unmanaged memory again // (4) Repeat what you have done in (2) em0=empty; // Empty (em0) em0=(Employee)Marshal.PtrToStructure(op, typeof(Employee)); os="name: "+em0.Name;tm(); // Extract name & display it to inspect it. } //---------------------- Using WinAPI Struct SP_DEVICE_INTERFACE_DATA ---------------------- void ApiStruct() {// IN:j=Offset,k=# of bytes from (op) OUT: oy // Aftr calling this method, (op) becomes a pointer to the device info list and (gup) // contains the correct GUID for a HID device. ks="h";hm("dil"); // Get DevInfoList pointer in (op) if(erb) {ers="Error in obtaining dev info list;return.";} //------ This is the part which we will do without method hm() ------ dip.cbSize = Marshal.SizeOf(dip); // Set Size of SP_DEVICE_INTERFACE_DATA instance. i=0; // Get 1st interface in the list bool NoError=SetupDiEnumDeviceInterfaces (op,IntPtr.Zero,ref gup,i,ref dip); if (! NoError) { // If error encountered: hm("uer"); // Get the unmanaged error code and message. os="Error code: "+eri+" Error message: "+ers;tm(); return; // Display the code and message then exit. } hm("dp"); // Use (dip) to get the device path name. if(erb) {ers="Error in obtaining dev path from dev details.";return;} os="Device path: "+os;tm(); // Display path name } } =============================================================================================

============================================================================================= The following classes, structs and method signatures are available into class (pcsu) which your class extends. They are all declared public, so you can use them into your class. You have no reason to declare them again. Structs and classes which have been declared public in class (pcsu): -------------------------------------------------------------------- struct HIDD_ATTRIBUTES struct HIDP_CAPS struct SP_DEVICE_INTERFACE_DATA class DEV_BROADCAST_HDR class DEV_BROADCAST_DEVICEINTERFACE_1 class DEV_BROADCAST_DEVICEINTERFACE struct USB_INTERFACE_DESCRIPTOR enum USBD_PIPE_TYPE:int struct WINUSB_PIPE_INFORMATION enum POLICY_TYPE : int struct WINUSB_SETUP_PACKET Method signatures which have been declared public in class (pcsu): --------------------------------------------------------------- kernel32.dll:CreateFile() kernel32.dll:ReadFile() kernel32.dll:WriteFile() kernel32.dll:FlushFileBuffers() kernel32.dll:CreateEvent() kernel32.dll:CreateEvent() kernel32.dll:CloseHandle() hid.dll:HidD_GetAttributes() hid.dll:HidD_GetPreparsedData() hid.dll:HidP_GetCaps() hid.dll:HidD_FreePreparsedData() hid.dll:HidD_SetOutputReport() hid.dll:HidD_SetFeature() hid.dll:HidD_SetNumInputBuffers() hid.dll:HidD_GetHidGuid() setupapi.dll:SetupDiGetClassDevs() setupapi.dll:SetupDiEnumDeviceInterfaces() setupapi.dll:SetupDiGetDeviceInterfaceDetail() setupapi.dll:SetupDiDestroyDeviceInfoList() user32.dll:RegisterDeviceNotification() winusb.dll:WinUsb_Initialize() winusb.dll:WinUsb_QueryInterfaceSettings() winusb.dll:WinUsb_QueryPipe() winusb.dll:WinUsb_SetPipePolicy() winusb.dll:WinUsb_ReadPipe() winusb.dll:WinUsb_WritePipe() winusb.dll:WinUsb_ControlTransfer() winusb.dll:WinUsb_Free() Need to call more API's from PC#? ================================= This website can help with finding C# signatures of general win32 functions: Allocating Memory for objects: ============================== Whenever you copy an object to an area beyond the heap, you need to allocate memory for the object first with: pobject=Marshal.AllocHGlobal(i); where (pobject) is the pointer you have assigned to your object and (i) is the amount of memory in bytes which you like to allocate. It should be at least equal to the object size. After you have finished your job you must free the memory allocated with: Marshal.FreeHGlobal(pobject); If your pointer was (op), you can do those two jobs faster with hm("ma") and hm("mf") ============================================================================================= ================================================= Performing I/O operations with a general resource ================================================= We have a chapter on filing which includes how to read from and write into a sequential access file. Method fm() has the modes to perform such tasks, but it can only handle files. Method hm() has the more general modes which allow you to read from and write into files, USB devices, disk volumes, pipes, and many other resources. Pointers are used in this mode to interface with unmanaged code in a similar manner to what you have done in Example 2. (1) Opening Communication with the resource (Mode "ro"): -------------------------------------------------------- Whenever you request "opening com with a resource", you receive a SafeFileHandle which you use to communicate with the resource. The present object of a SafeFileHandle is (dhp) The mode for opening (or obtaining a handle for) the resource is "ro". It requires the following parameters: fls: Resource path name. js: Resource access code It could be: js="r": Read only(default) js="w": Write only. js="rw": Read/Write. ks: Share access which you grant other processes which will try to open same resource after you. Could be: ks="": No access(default) ks="r": Read only. ks="w": Write only. ks="rw": Read /Write. ip: Security attributes. If you want no security restrictions to apply to your resource's access, supply ip=IntPtr.Zero. (2) Receiving data from the resource (Mode "rr"): ------------------------------------------------- Before you make the read call, you need to dimension OY[] with a size which can at least take the number of bytes which you will request reading. Here are the input and output of this mode: IN :OY[]: Byte array to receive data. i : (Optional) Index in OY[] where received data will start at. o : Number of bytes you like to receive. Must be <= Length of OY[] OUT:OY[]: Byte array loaded with requested data. oi : Number of bytes received. Notice that you can receive your data in more than one trip if you assign a small value to (o) and assign to (i) in each trip an index which follows the data already received in OY[]. (3) Sending data to the resource (Mode "rw"): --------------------------------------------- Before you make the write call, you need to load the data to be written into OY[] IN :OY[]: Data to be sent to resource. i : (Optional) Index in OY[] where the section you like to write starts. o : Number of bytes to be written. OUT:oi : Number of bytes written into resource. Notice that in most cases, you dimension OY[] and load it with exactly the data size you like to write. So you keep (i=0) and assign length of OY[] to (o) (4) Closing the communication with the resource (Mode "rc"): ------------------------------------------------------------ It closes the handle to the resource. It's only parameter is (dhp) Asynchronous I/O: ----------------- When we communicate with a hardware device, we can't guarantee how long it's going to take to respond if it will respond at all. This is why the .NET has methods to do asynchronous I/O with timout. We have a simpler and more general way to do the same. All we need to do, is to use an async method which does the operation and sends a message to the main thread when done. We can then use timeout when we attempt to receive the message. Your program can cause the thread to abort whenever it finds that it will never finish the process. You may review the chapter of "Handling Threads" to see how this is done. We'll also show you how to do a multiple read and write operations with a resource, both synchronously and asynchronously in the next example. We are going to use a file for the demonstration, but as you know, the resource does not have to be a file. REMARKS: ======== (1) Those filing modes access internally the kernel32 functions CreateFile(), ReadFile(), WriteFile() and CloseHandle() whose signatures are declared public in class (pcsu) (2) In the next chapter's "lesson 8", we'll show you how to call those same kernel32 functions using Assembly Language. ============================================================================================== Example 3: Show What happens when you start with an empty file, then do several operations of reading followed with writing. Do this demonstration both synchronously and asynchronously. ============================================================================================== //assembly pcsu.exe public class a : pcsu { public override void init() { tia=toa="t"; o=2;dm("ht"); // Allow the creation of upto 2 threads. bli=0; // Main thread will start at block 0. base.init(); } public override void run() { int t;Thread thp=Thread.CurrentThread;t=Int32.Parse(thp.Name); // As expected for threads if (blpg[t]==0) { // Startup block of main thread //------------------------------ Synchronous Communication ------------------------------- cls="r0"; os=" Communicating with a resource using synchronous and asynchronous operations"; tm();os="";tm(); // Display titles cls="b0";os="Multiple Read and Write using Synchronous Operations:";tm();cls="S9"; fls="test.txt";ks="f";fm("M"); // (Re)Create file for(int n=0;n<6;n++) { // Do 6 operations of read followed with write js="rw";ks="rw";ip=IntPtr.Zero;hm("ro"); //open file OY=new byte[100];i=0;o=100;hm("rr");oc='a';om("fY");om("c");//Read data if(n>0) tm(); //display data read os="Sync ";om("tY");i=0;o=os.Length;hm("rw"); //write "Sync "; hm("rc"); //Close file } os="";tm(); // Skip one line //---------------------------- Asynchoronous Communication ----------------------------- cls="b0";os="Multiple Read and Write using Asynchronous Operations:";tm();cls="S9"; fls="test.txt";ks="f";fm("M"); // Recreate file for(int n=0;n<6;n++) { // do the following read/write operations 6 times: o=1;bli=1;xm("tc"); // Create a thread to start at block 1. //--- Receiving message with timeout --- dnb=false; // Initialize the "message found" flag ib=true;um("t"); // Start timing while (!dnb) { // Continue looping until message found or timeout um("t"); // Get the amount of time passed if (o>1000) { // If found to exceed timeout limit, os="T/O";break; // Exit loop with os="T/O". } xm("tmr");Thread.Sleep(10); // Attempt receiving the message, pause 10 ms. } // Repeat. If msg received, exit. // If Thread has sent its "Done" msg, we know that (xs) now contains all data read. if(os!="T/O" && n>0) {os=xs;tm();} // Display (xs), skip 1st trip (brings no data) } } //----- Created thread's actions ----- else if (blpg[t]==1) { // Block 1 is for the created thread. pcsu p=new pcsu(); // Create an instance of class (pcsu) p.fls=fls; // Get file name p.js="rw";p.ks="rw";p.ip=IntPtr.Zero;"ro"); //open file p.OY=new byte[100];p.i=0;p.o=100;"rr");p.oc='a';"fY");"c");//read data xs=p.os; // Convert data to string,assign to xs of thread 0 p.os="Async ";"tY");p.i=0;p.o=p.os.Length;"rw");"rc"); //Write "Asynch " into file and Close it osg[t]="Done.";og[t]=0; // Send the msg "Done" to thread 0 dnbg[t]=false;while (!dnbg[t]) {mm(t,"xtms");Thread.Sleep(10);}// Send msg } } } ============================================================================================== REMARKS: ======== (1) The reason the number of words written into the file keeps growing with each "Read then Write" operation is that after the reading is done, the file pointer stays at the end of the data read, so any writing done at this point is added to the existing data in file. (2) We did not care to check for errors in this example to keep it simple. In a real project you should check for errors after each call to method hm() and do what is necessary. Whenever method hm()'s call to an operating system function ends with an error, it sets the error flag (erb) then it analyses the error and returns to you the numeric error code and the error message's text assigned to (eri,ers) respectively. You can display them with: if(erb) {os="Error code: "+eri+". Error message: "+ers+".";tm();} ==============================================================================================

============================== Handling Unmanaged Code errors ============================== Whenever an error is generated while using a Windows API function, we receive a numeric error code which could be translated into a text message. Many functions return a boolean value which indicates "success". If this return value was false, it means that there is an error. Whenever this happen, you can immediately call hm("uer") to find for you the Unmanaged code error and returns: eri=Numeric error code. ers=Error message's text. If the function does not return that boolean value, you may call hm("uer") anyway immediately after your function call. If no error was detected, you'll receive ers="". If you know the numeric error code and like to get the message text, assign the code to (eri) and call hm("uem") ============================================================================================== =================================== COMMUNICATING WITH HARDWARE DEVICES =================================== Which hardware devices are we interested in? -------------------------------------------- Nowadays, most computer hardware devices connect to their host computers through "USB" ports. So, we're going to receive only the events generated when a change happens to a USB device. If you are a design engineer and like to know more about USB devices' hardware and software, a good book to read would be "USB COMPLETE by Jan Axelson". Here, we'll learn the simplified Personal C Sharp way to write a program to detect USB device events like their connection and disconnection to/from the host computer, to search for a device with its vendor and product ID's and to send/receive data reports to/from them. We'll describe briefly how the PC# software is written, but will leave the details to the "USB COMPLETE" book and Microsoft documentations. What are the USB device class and device interface classes? ----------------------------------------------------------- This has nothing to do with C# classes or interfaces. The USB specs classifies devices by the jobs they do. So each device belongs to a class of devices. Composite devices which do multiple jobs belong to multiple interface classes. The operating system knows the class and all the interface classes which a device belongs to by reading its descriptor. What is the device's descriptor? -------------------------------- The descriptor is a hard coded array of bytes which is stored into a ROM chip inside the device. The descriptor contains data which help us in communicating with the device. Here are some of the data: (1) Vendor, Product and Release numbers. (2) Device class which can be one of the categories "Communication device", "Hub", "Wireless controller", Human interface device (HID), "Mass Storage","Printer","Audio", "Video",..etc. Normally, the categry is found at each interface the descriptor specifies not at the class description. (3) Number of interface classes, their class category and the specs of the endpoints where they send/receive packets of data. The specs include each endpoint address, transfer type, maximum size of packets and maximum number of packets which can be sent or received. An end point is a data buffer. The endpoint address is stored into one byte (8 bits) The 7th bit stores the direction (0=Out, 1=In) The address is stored into 1st 4 bits (0:3) So it can be in the range (0:15) Address (0) is the default endpoint address. It's not specified since it's always available. This is why the device is always ready to receive data after a reset. The transfer type can be bulk,interrupt,control and isocronous. We'll explain that later. REMARKS: ======== (1) When we talk about host to device communication, we are not talking about two equal parties. The host always initiate the conversation no matter in which direction the data flows. If the driver at the host wants to send data to the device interface to which it's assigned, it sends the data to its "Out" end point. If it wants to receive data it contacts its "In" endpoint. Notice that the words "In" and "Out" are relative to the host although the end points are actually at the device. The stream of data is called "Downstream" when data flows from host to device, and "Upstream" when it flows from device to host. (2) In general, the USB specs allow devices to have more than one configuration, each could have more than one interface. Let us assume that you have developed a device which can be configured to be either a TV or a computer monitor. The TV configuration specifies two interfaces, one for the "Tuner" and one for the "Display", but the monitor needs no more than one interface. So, there will be 3 interfaces in this device each one will be assigned one driver. You can still write one single application which controls the entire device. Start-up of a USB device: ------------------------- When the operating system restarts or when a USB device is first attached to the host, the device is enumerated after a short communication with it using "Control" transfer. The endpoint used for the communication is the default one at address zero. The host receives the device's descriptor, checks all listed interface classes and assigns a driver to handle each. It also selects the wanted configuration then sends to the device a unique address to identify it with. The Human Interface Devices class: ---------------------------------- This is the most popular. It's very easy to use and normally requires no driver since most operating systems have a default HID driver built into them. Only Control and Interrupt transfer types can be used. Data travels both ways in "reports". Each report is made of several packets. Detecting and Communicating with HID devices is the project which we are going to start with. Window's device Setup and Driver classes: ----------------------------------------- Now we are talking about operating system classes. If you look at the Device Manager, you see groups of devices like "Disk Drives","Keyboards", "Human Interface Devices (HID)",..etc. Each of these groups which are installed by Windows in the same manner and require similar configuration, share the same "Device Setup class". The Device Setup class defines functions which do common setup jobs like "Enabling", "Disabling" and "Updating driver" of a device. Notice that those are the same functions which you can use Device Manager to do manually. One device may belong to more than one group (meaning more than one setup class) A USB keyboard for example may belong to the HID and Keyboard setup classes. Each driver class contains functions which are used for the communication between the application and the device interface which it has been assigned to. Such functions are like reading data from the device or writing data into it. Vendors can make their own drivers, but most vendors like to use the default Windows' drivers written by Microsoft for their device type. We are interested in two Window's drivers, the HID driver and the WinUSB driver. The GUID Identifier: -------------------- The GUID is a unique 128 bit code which Windows use for security purposes. Each class of USB devices is assigned a GUID. Here is a list of them: DEVICE GUID ================== ==================================== USB Raw Device A5DCBF10-6530-11D2-901F-00C04FB951ED USB Disk Device 53f56307-b6bf-11d0-94f2-00a0c91efb8b Network Card ad498944-762f-11d0-8dcb-00c04fc3358c Human Interface Device 4d1e55b2-f16f-11cf-88cb-001111000030 Palm 784126bf-4190-11d4-b5c2-00c04f687a67 USB_HUB F18A0E88-C30C-11D0-8815-00A0C906BED8 USB_HOST_CONTROLLER 3ABF6F2D-71C4-462A-8A92-1E6861E6AF27 You can obtain a list of all device classes' GUID's on your computer by calling hm("dcl") The device path name: ===================== A USB device can be communicated with like any file if we know its path name. A device path name does not look like a file path name. Here is a sample of a device path name: \\?\USB#VID_0781&PID_5530&MI_006C&COL_01#20043512910EFE3187D6#{a5dcbf10-6530-11d2-901f-00c04fb951ed} Here are descriptions of some sections of this Device path name: (1) VID_0781: A number in hex which represents the Vendor ID. (2) PID_5530: A number in hex which represents the Product ID. (3) MI_006C : Composite USB devices which do multiple functions require this Interface number. Each function is accessed with a seperate path name. VID & PID of all path names should be the same, but each one must have a unique Manufacturer Interface number. (4) COL_01 : Composite USB devices may also have multiple COLLECTION numbers for each interface. (5) GUID : Interface class GUID. This the number between {} at the end of the path name. See table above for a list of device types and the GUID numbers which identify them. How to obtain information about all USB devices on your computer? ----------------------------------------------------------------- There are 3 sources which can supply us with information: (1) Device Manager: Can be reached by clicking on [Control Panel][System][Device Manager] Reach your device entry, Right click it, then select "Properties". This will give you few information, the most important one is driver file's pathname. By default, Device manager shows only the devices which are currently connected to the computer, but you can set it to show all devices. (2) System Registry: The registry has been discussed in the chapter of "Accessing External Objects And Performing System Operations". Two examples in that chapter showed you how Method sm() can easily and safely be used to save data into the registry under the two root keys HKEY_CURRENT_USER and HKEY_USERS and to read the data back. USB devices path name data are stored under the two subkeys: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\HID You can easily extract and combine the two lists programmatically, however PC# has done this job for you. You can obtain the combined registry list of USB devices on your system by calling hm("drl") The list comes assigned to OS[]. The list gives you the path name of each device without the GUID. So, for the sample path name above, you'll get: VID_0781&PID_5530&MI_006&COL_01#20043512910EFE3187D6 If you can identify the device you are interested in, access the Registry manually, reach the subkey under "USB" or "HID" key whose name is "VID_0781&PID_5530&MI_006" Please know that this is just a hypothetical device name for the pupose of education only, click it, you should see one or more items, one of them is "20043512910EFE3187D6" and you should see keys and values on the right side which can complete the path name. (3) Programmatically we can get a list of all USB-HID devices. You have the choice to either make the list for devices which are currently connected to the computer or to all. You can easily get the system registry list and the "USB COMPLETE" book can help you with the HID list. You can also use method hm() to get the two lists by calling hm("drl") and hm("dpl") ============================================================================================= EXAMPLE 4: Find and display the following lists: (1) All devices listed in the registry under the two keys "USB" and "HID". (2) A GUID list of all device classes listed in the registry. (3) Path names of HID devices which are currently connected to your computer. (4) Path names of all HID devices. ============================================================================================= //assembly pcsu.exe public class a:pcsu { public override void init() { tia=toa="t"; // Select "text screen" display base.init(); // Initialize PC# classes } public override void run() { cls="r0";os="USB DEVICE INFORMATION:";tm();cls="S9"; hm("drl");om("fa");tm(); // Get all devices from the registry in OS[], Covert // OS[] to (os) then display it. cls="r0";os="GUID's OF DEVICE CLASSES:";tm();cls="S9"; hm("dcl");om("fa");tm(); // Get classes list of all devices from the registry // Conver OS[] to (os) then display it. cls="r0";os="DEVICE PATH OF CURRENTLY CONNECTED HID DEVICES:";tm();cls="S9"; hm("dpl");om("fa");tm(); // Get a list of complete device pathes of HID devices // which are currently connected. cls="r0";os="DEVICE PATH OF ALL HID DEVICES:";tm();cls="S9"; ib=true;hm("dpl");om("fa");tm(); // Do the same for all classes. } } ============================================================================================== Method hm("dpl") calls several PC# methods internally to get the list. If you like to get the list in steps, here is what to do: (1) Call method hm("dil") To obtain a pointer to the "device information list" You need to supply the method with the GUID object of the class which your USB device belongs to. This is done by assigning a code to (ks) as follows: ks="" means Use the GUID which has already been assigned to the present GUID object (gup) ks="js" means Use the GUID which is assigned in its string form to (js) ks="r" means use the raw USB GUID (See table above) ks="h" means use the HID class GUID. ks="b" means use the HUB class GUID ks="c" means use the host controller class GUID Whatever your selection is, the requested GUID will be assigned to (gup) and will be used in the coming operations. The pointer to the information list is returned assigned to (op) which is a public pointer of type "IntPtr". You can use it as you like, but make sure it stays unchanged since it is used in next operations. When you're finished with it, call hm("mf") to destroy the list. The information list is an array of structures for the device class specified by the GUID. The pointer is obtained internally using function Setupapi() The list will be for devices which are currently connected only or it could be for all devices if you supply (ib=true) (2) Call method hm("di") to get a pointer to the Device Interface Data struct of the interface you select and assigns it to the present object of this type (dip) which is also public. This method requires (gup) and (op) which have been returned by method hm("dil") in addition to the index of your wanted interface inside the device information list assigned to (i) Functions Setupapi() and SetupDiEnumDeviceInterfaces() are used internally to obtain (dip) (3) Call method hm("dp") to get the device path name which we use to communicate with the device. It requires (gup),(op) and (dip) which have previously been assigned. The path name is returned assigned to (os) Function SetupDiGetDeviceInterfaceDetail() is used internally by this method. (4) After getting your device path name you must call hm("mf") to destroy the device information list object to free its used resources and also because we like to free (op) so we can use it for another purpose. =============================================================================================

Searching for HID device path name: ----------------------------------- After reading the steps above, you should know that searching for a device path name is easy if you know its Vendor ID (VID), its Product ID (PID), its Manufacturer Interface (MI) and its Collection number (COL) The steps should be similar to the steps above, except that after executing step (1) to get the list, you start a loop which scans all available interfaces in the list, call hm("di") and hm("dp") as in steps (2) and (3) to obtain the path name of each, extract VID,PID,MI and COL and see if they match the ID's you have. You should exit the loop when a match has been found and do step (4) Method hm("dvp") can help in extracting the wanted data from the device path name and method hm("ds") can do the entire search job for you. Here are their descriptions: hm("dvp"): Get Vendor parameters. IN : Device path name OUT: Array O[] containing the following data: O[0]=VID, O[1]=PID, O[2]=MI, O[3]=COL hm("ds") : Find a device Path Name or names. If you like to receive one path name only, you need to assign values to all parameters as in the next paragraph. If you keep some parameters unassigned, you'll receive a list of matching path names. For example, if you keep (k=0), path names for all product ID numbers available will be returned. Same logic applies to (jy) and (ky) IN : j=VID, k=PID, jy=(byte)MI, ky=(byte)COL, ks=GUID code (See mode "dil") OUT: OS[]= Matching path name(s), oi=Number of matching path names o=Interface index number of first match in the list. Handling events generated by USB devices: ========================================= We like to be able to detect the Attachment and Removal of USB devices in addition to other events they generate. Fortunately, we are set for this task since class (Form) is the parent of class (pcs) and your class is a child of that class. This qualifies your class to be a recepient of all System Broadcast Messages. SYSTEM BROADCAST MESSAGES: -------------------------- Broadcasted messages are many. They notify both the operating system and our applications whenever an event takes place. The source of the the event can be a control, a device or something else. The (Form) class which class (pcs) extends, receives the control event messages. It forwards them to delegates inside class (pcs) Class (pcs) gets the keyname of the control which has generated an event, assigns it to (cs) and calls method update() where your program handles the event. It will be nice if we extend the ability of your class by making it receive "device change" messages which come when some event takes place at a hardware device. Which type of data do we like to get? ------------------------------------ We can receive one or more of the following structures: CODE STRUCTURE SENT ==== =============== 0 OEM 2 VOLUME 3 PORT 5 DEVICEINTERFACE 6 HANDLE The DEVICEINTERFACE structure (Code 5) is the most important data type. The popular USB devices which are used as "Removable storage devices" send two types of data in two different messages when they are attached to the system. The first is of code (5) and the second is of code (2) Which kind of events do we like to handle? ------------------------------------------ The most interesting two events are "DEVICE ARRIVAL" (code 0x8000) and "DEVICE REMOVE COMPLETE" (code 0x8004) which come when the device is attached and when the device is removed. Here are all the events which we receive and their hex identification codes: CODE EVENT ==== ========================== 0x0019 CONFIG CHANGE CANCELED 0x0018 CONFIG CHANGED 0x8006 CUSTOM EVENT 0x8000 DEVICE ARRIVAL 0x8001 DEVICE QUERY REMOVE 0x8002 DEVICE QUERY REMOVE FAILED 0x8004 DEVICE REMOVE COMPLETE 0x8003 DEVICE REMOVE PENDING 0x8005 DEVICE TYPE SPECIFIC 0x0007 DEV NODES CHANGED 0x0017 QUERY CHANGE CONFIG 0xFFFF USER DEFINED Registering your class to receive "Device Change" events: --------------------------------------------------------- It should be done into method setup() where all Form related setups are made. You need to call method hm("dr") with the following assignments: ks=Class GUID selection code. See mode "dil" above for possible values to assign to (ks) k= Code number of the Struct which we are interested in receiving. It can be: k=0:OEM, k=2:Volume, k=3:port, k=5:Interface, k=6:Handle You will most likely choose (k=5) and (ks="h") to receive the "interface" structure of a HID device whenever it generates an event, like when it is just plugged in or plugged out. However you can make whatever setup you like. Internally, method hm("dr") will instantiate the struct DEV_BROADCAST_DEVICEINTERFACE and sets the instance with your parameters. Then it will use method RegisterDeviceNotification() to register the running instance of class (pcsu) to receive broadcast whenever your selected event takes place. What happens when the event notification is received: ----------------------------------------------------- Class (pcsu) will call method update() after making the following assignments: cs="dvn": This is to let you know that this is a device notification event. oy=Event type code which is the number of the structure received (See table above) o = The event code as a 4-digit hex code (See table above for codes & meanings) os:If the event was found to be "Arrival or departure" (o=0x8000 or o=0x8004), the device path of the device which has generated the event will be assigned to (os) Receiving and handling the device change event: ----------------------------------------------- As usual, your code block in method update() should start with [if (cs=="dvn")] and use the received device path to do the necessary communication there or to make a jump to a new block in method run() where the communication will be done in the right place. ============================================================================================ The usb device which was used to test this code with, is an old usb keyboard which uses the default Windows HID driver. EXAMPLE 5: Show how to search for a device whose vendor has multiple products on your computer and display all matching device path names. Show how to create events and handle them by removing a usb device then connecting it back. ============================================================================================= //assembly pcsu.exe public class a:pcsu { public override void init() { tia=toa="t"; // Use TextScreen for text base.init(); } public override void setup() { // Register this Form to receive device change events k=5;ks="r";hm("dr"); // Struct wanted="Dev interface". Dev Type: "raw USB" } public override void update() { if ("dvn".Equals(cs)) { // If Device change notification received: string o1s=os; // Save device path name temporarely os="USB DEVICE CHANGE EVENT RECEIVED:";tm(); // Display title os=""+o;om("th"); // Covert event code to hex os="Type: "+oy+" Event: 0x"+os;tm(); // Display Event type and event code. os="Path: "+o1s;tm(); // Display path name if (o==0x8000) {os="EVENT: Arrived";tm();} // If event was "Arrival", display else if (o==0x8004) {os="EVENT: Removed";tm();} // If event was "Removal", display } } public override void run() { //------ Search for a device ------ cls="r0";os="Search results:";tm(); // Display title in red. j=0x049F;k=0x000E;jy=0;ky=0;hm("ds"); // Search for device(s) with VID,PID // specified and any MI,COL cls="S9";if(oi<1) os="No match found"; // If match count=0, display message. else om("fa"); // Else, Convert OS[] into (os) tm(); // Display (os) //------ Generate Device Removal Event and handle it ------ cls="r0";os="Disconnect device if was connected or connect it if was disconnected.";tm(); cls="p0"; } } =============================================================================================== REMARK: ======= If you eliminate methods setup() and update() from the example above, you will discover that it continues receiving device change events and displaying them. How is this possible? It's because we have made any class which extends class (pcsu) able to receive these events by default. The default registeration parameters are (k=5;ks="r";) If you don't like your class to receive those events, include method setup() into your class in order to hide the default one. Similarly, if you like to handle your events in a different manner than just displaying their data, include method update() into your class and write whatever you should into it or just keep it empty. How to communicate with USB Devices: ==================================== You can access them as you do with any file using the following modes: ro: Open a link (obtain handle) to the device. rr: Receive data report from the device. rw: Send a data report to the device. rc: Close link (release handle) to the device. Example 3 has shown how to use these methods synchronously and asynchronously. Can I communicate with my USB keyboard, mouse and flash drive? -------------------------------------------------------------- Oops! here is a problem. You know that when you open a link to a resource, you can deny anyone else the privilege to connect to the same resource after you. A Windows' process has connected to each one of these devices immediately after installation and denied everyone else the privilege to communicate with them. This is understandable, since hackers could love to send a software to you which reads your keyboard, learns your passwords and sends them to their place. There maybe a way to make windows stop using a device temporarely, so you can test your software on it. If it was possible, it would probably be done by changing key values into the system registry. For now, if you call hm("ro") with (fls) assigned the path name of a device which uses the standard HID driver, you'll get the error message: Error code: 5 Error message: UCError: Access is denied What we can communicate with, is a device which has been installed and assigned a default windows driver (preferably the HID driver), and at the same time, windows has no need for the device in its operations, so it was kept free. We are going to take care of this simple problem later, but let us complete the subjects of communicating with devices which use the HID driver and the devices which use the WinUSB driver before all. Getting the capabilities list of a device with HID interface (mode "dcp"): -------------------------------------------------------------------------- During the device installation, the operating system has learned all its capabilities and specs and set them in memory. We like to be able to access this information for many reasons. One reason is to know how large (OY) should be before we call hm("rr") and hm("rw") to do a read and write operation with the device. We need to obtain a pointer to the data buffer, then obtain a pointer to the "Capabilities" struct within the buffer. Since (op) which was used to point to the "device information set" is now free, we'll make it point to the data buffer, and we'll make a public instance of the "Capabilities" struct named (cap) which will contain all the capability information which are: Usage UsagePage InputReportByteLength OutputReportByteLength FeatureReportByteLength NumberLinkCollectionNodes NumberInputButtonCaps NumberInputValueCaps NumberInputDataIndices NumberOutputButtonCaps NumberOutputValueCaps; NumberOutputDataIndices NumberFeatureButtonCaps NumberFeatureValueCaps NumberFeatureDataIndices Example: ======== ------ This is how to do a read operation with a device: ------ fls=@"\\?\HID#VID_049F&PID_000E&MI_00#7&1992e121&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"; i=0;o=cap.InputReportByteLength; // Assign path name to (fls) and "In" report length to (o) OY=new byte[o]; // Dimension OY[] to same report length hm("rr"); // Read data from device. if(erb) {os=ers;tm();} // If error encountered, display error message. ------ And this is how to do a write operation with the device: ------ fls=@"\\?\HID#VID_049F&PID_000E&MI_00#7&1992e121&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"; i=0;o=cap.OutputReportByteLength; // Assign path name to (fls) and "Out" report length to (o) OY=new byte[o]; // Dimension OY[] to same report length. // OY[0]=... // Load OY[] with data to be sent. hm("rw"); // Write data into device. if(erb) {os=ers;tm();} // If error encountered, display error message. ============================================================================================== Using the WinUSB driver: ======================== This driver allows bulk transfer in addition to Control and interrupt. The devices which this driver can be assigned to must be capable of handling all three transfer types. To communicate with devices with this driver, you need to get a WinUSB handle. This handle is of type "IntPtr" and its present object is (whp) You get the handle by calling hm("whp") with the path name as in this example: fls=@"\\?\USB#VID_0781&PID_5530#20043512910EFE3187D6#{a5dcbf10-6530-11d2-901f-00c04fb951ed}"; js="rw";ks="";ip=IntPtr.Zero;hm("whp");