最近在研究一个课题,如何能利用键盘的led灯通道进行有效通信,那么首先要做的就是尝试能否在不影响键盘的情况下控制LED灯(num lock ,caps lock ,scroll lock)的使用。
首先,如果并不是HID USB键盘,下面的C代码就可以解决:
http://www.rohitab.com/discuss/topic/32092-toggle-led-lights/?p=10051697#entry10051697
同样的C#版的在此:
http://www.aboutmycode.com/miscellaneous/faking-num-lock-caps-lock-and-scroll-lock-leds/
如果是USB键盘上面的代码是不奏效的,其中一个重要原因就是使用Windows API的Createfile是打不开USB键盘设备,无法得到他的句柄(通过GetLastError()得到2查看错误编码解释就知道了),这是因为Mouse和Keyboard这类HID类设备是被系统独占的。但是现在很多人都要使用USB设备的键盘,那怎么办呢?
这里介绍一款Autohotkey的软件,使用简单的脚本编写就能实现替代手工键盘操作的方式,他可以向游戏出大招一样制定一系列的操作,包括开机后你想干什么干什么用这个软件编写些脚本很容易实现,用这款脚本实现的LED灯控制在下面这篇文章中也得到了实现:
http://www.autohotkey.com/board/topic/9587-keyboard-led-control-capslocknumlockscrolllock-lights/
OK,到此结束……
/*----------------------------------------------------------------------------------------------------------------------------*/
显然,你不想为一个功能装上一个软件吧!所以我们就对C代码进行一个修改,让他能够完成这项工作。
这里就不得不用到一个内核级的WindowsAPI调用NtCreatefile()神器。
首先此函数在 Winternl.h 下有申明,里面记载着内核调用的各种原型,但它并没有导入到标准的库中,要使用他只能采用载入运行时动态链接库的方式,简称动态载入库。
在代码中写入:
HINSTANCE hinstLib; hinstLib=LoadLibrary(_T("NtDll.dll"));
在函数外定义好NtCreatefile的原型:(参数中的WINAPI漏了会出现外部命令解析错误)
typedef NTSTATUS (WINAPI *ntcf)( _Out_ PHANDLE FileHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_opt_ PLARGE_INTEGER AllocationSize,
_In_ ULONG FileAttributes,
_In_ ULONG ShareAccess,
_In_ ULONG CreateDisposition,
_In_ ULONG CreateOptions,
_In_ PVOID EaBuffer,
_In_ ULONG EaLength
);
重点是设置好设备注册时的路径:(这个在注册表里可以查到的)
wfilename->Buffer = _T("\Device\KeyboardClass2");
这里我的是KeyboardClass2,因电脑而已,0、1、2、3都尝试一下吧。(注:_T是获取Unicode的编码格式)
完事后要检查loadlibrary的成功性,获取函数入口地址并且执行:
if (hinstLib != NULL) { ProcAdd = (ntcf) GetProcAddress(hinstLib, "NtCreateFile"); // If the function address is valid, call the function. if (NULL != ProcAdd) { fRunTimeLinkSuccess = TRUE; (ProcAdd) ((PHANDLE)&hKeybd,(ACCESS_MASK)(0+0x00000100+0x00000080+0x00100000),objattrib,io,0,0,1,1,(ULONG)(0x00000040+0x00000020),0,0); } // Free the DLL module. fFreeResult = FreeLibrary(hinstLib); }
这里的参数很复杂,也让我头疼了半天,主要是仔细看MSDN官方文档,尤其是其中的objattrib设置是关键:
InitializeObjectAttributes(objattrib,wfilename,OBJ_CASE_INSENSITIVE,NULL,NULL);
这是一个专门初始化这个数据结构的函数,一般情况下第三个参数就是使用OBJ_CASE_INSENSITIVE,别的一加进去就不行,即便你用的或,由于或实际上是“+”,也就是功能相加,如果其中的参数是带限制性的,出来的问题就是受限制更多。如果函数执行时出现IO方面的错误,通过上面的io参数查看对应码就能知道了。
若一切顺利下面的检查就能通过了:
// If unable to call the DLL function, use an alternative. if (! fRunTimeLinkSuccess) { printf("Message printed from executable "); return ; }
当然,hKeybd句柄的返回值一定不能是NULL,必须保证有所返回才说明函数执行成功。
成功后就可以发参数操控了,这里先解释一下操控原理:
每当用户在键盘上按下一个按键的时候就会在系统中写入一个键盘输出报告,这个报告会通知所有键盘设备作出相应的改变,例如你在笔记本电脑把scroll lock灯点亮的,别的键盘的灯也会亮,实际上他只是在键盘输入报告中写入了一个字节:
后三位就是灯状态的记录,1表示亮,0表示不亮,所有键盘都会根据这个状态改变,所有我们要做的就是更改这个键盘报告这个字节中的后三位即可。
根据这个我们可以写三个宏定义,表示灯的情况:
#define KBD_CAPS 4 #define KBD_NUM 2 #define KBD_SCROLL 1
更改输出报告的状态,使用DeviceIoControl()神器,原型如下:
BOOL WINAPI DeviceIoControl(
_In_ HANDLE hDevice,
_In_ DWORD dwIoControlCode,
_In_opt_ LPVOID lpInBuffer,
_In_ DWORD nInBufferSize,
_Out_opt_ LPVOID lpOutBuffer,
_In_ DWORD nOutBufferSize,
_Out_opt_ LPDWORD lpBytesReturned,
_Inout_opt_ LPOVERLAPPED lpOverlapped
);
哪些参数是输入哪些是输出一目了然。
首先通过buffer获取灯状态:
DeviceIoControl(hKeybd, IOCTL_KEYBOARD_QUERY_INDICATORS, 0, 0, &buffer, sizeof(buffer), &retlen, 0);//buffer拿不到灯状态信息.
接着给buffer赋值:
buffer.wFlags ^= wFlags ;
这个异或操作实际上是为了关与开,如果原来是开的那就关了他,如果关着的就打开它:
DeviceIoControl(hKeybd, IOCTL_KEYBOARD_SET_INDICATORS, &buffer, sizeof(buffer), 0, 0, &retlen, 0);
其中的宏需要有自定义:
// most of these are taken from ntkbdddk.h or w/e it's called #define IOCTL_KEYBOARD_SET_INDICATORS CTL_CODE(FILE_DEVICE_KEYBOARD, 0x0002, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_KEYBOARD_QUERY_TYPEMATIC CTL_CODE(FILE_DEVICE_KEYBOARD, 0x0008, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_KEYBOARD_QUERY_INDICATORS CTL_CODE(FILE_DEVICE_KEYBOARD, 0x0010, METHOD_BUFFERED, FILE_ANY_ACCESS)
好了,做个测试,给buffer传对应参数,这个时候对应灯就成功点亮了。
这个技能可以做的事情很多,尤其是接近被废弃的scroll lock的灯,可以将他变成邮件通知,或者是各种需求的通知,这就根据需要而定了,如果要改成C#版本也非常容易,毕竟都是调用dll的api实现的,或许还能做成让他随音乐跳到,变成disco模式,随便玩吧。