蓝牙键盘模块的实验

之前介绍过两种Arduino 模拟键盘的方法,一种是普通的Uno加上电阻之类的元件;一种是使用自带 USB 功能的 Arduino ,比如 Leonardo ,内部集成了USB Slave控制器。 这里再介绍蓝牙方案。

我们最常见的就是蓝牙透传模块,用蓝牙搜索安装之后能在系统中模拟出来一个串口,上位机直接按照串口方式即可进行通讯。这次介绍一款蓝牙键盘模块(实际上是键盘鼠标模块)。

外观和普通蓝牙透传模块一样(蓝牙芯片真正有用的都是内部Firmware)

TB1E9gOIVXXXXbOXpXXXXXXXXXX_!!0-item_pic

用法非常类似,在蓝牙中搜索连接之后系统中会出现键盘设备。

btkeyboard

然后数据是从串口送到蓝牙设备中的。根据说明我用 Arduino 编写了一个简单的测试程序,每隔5秒发送 “1” 字符。

输入 1:
按下数据 1 数据包为: 0C 00 A1 01 00 00 1E 00 00 00 00 00
按键弹起: 0C 00 A1 01 00 00 00 00 00 00 00 00

程序如下:

char KeyPress[]={0x0C,0x00,0xA1,0x01,0x00,0x00,0x1E,0x00,0x00,0x00,0x00,0x00};
char KeyRelease[]={0x0C,0x00,0xA1,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
void setup() {
  // put your setup code here, to run once:
    Serial1.begin(9600);      //设置串口波特率
}

void loop() {
   for (byte i=0;i<sizeof(KeyPress);i++)
    {
     Serial1.write(KeyPress[i]);
    } 
   for (byte i=0;i<sizeof(KeyRelease);i++)
    {
     Serial1.write(KeyRelease[i]);
    }
  delay(5000);  
}

 

测试结果,每隔5秒我的电脑上就可以收到一个 1 的输入。

更多的好玩还在研究中。有模拟键盘需要的朋友不妨考虑这样的蓝牙模块,顺便说一下,这种模块在35元左右,比普通透传模块贵多了(通常20左右)。当然,你可以看看国外类似的产品,Adafruit出品的“EZ-Key BT HID Keyboard Controller纸模块”价格在180元,感觉就不那么贵了……

TB1y1XyJXXXXXbnXXXXXXXXXXXX_!!0-item_pic

==================================================================
2016年3月13日 更新
我一直以为在文章中放出来过购买链接,昨天有朋友问检查了一下才发现我忘记了。这个模块的购买链接是 https://item.taobao.com/item.htm?spm=a1z09.2.0.0.V7mzul&id=521222818182&_u=dkf8s9fbec
卖家名字是 “重庆翔码电子工厂店”。买的时候你不妨问一下卖家,说你要做蓝牙HID设备,这个和普通常用的蓝牙串口透传模块之类的是不同的。
再放一下这个模块的说明书:BTHID

特别注意:之前我一直以为蓝牙键盘模块和蓝牙扫描枪模块是同一个东西,结果有朋友买成了“蓝牙扫描枪模块”,结果无法使用。他们之间的差别在于,键盘模块可以发送 shift/alt/ctrl 等等。蓝牙扫描枪模块只能发送可见的 ascii。
所以一定要询问清楚(估计是当时我和卖家说了,然后虽然拍下的是蓝牙扫描枪模块,但是给我的是蓝牙键盘模块)。

攀藤 G3

之前一直在使用攀藤 G1 ,前面提到 G1 都坏掉了郁闷无比。然后入手了一个 G3 。之所以还选择攀藤的产品最主要是考虑尽可能的复用之前的代码……

下面图是 G1 和 G3 外观,可以看出 G3 要小一点:

g1g3

数据上和G1稍微有些差别,主要是送出来的数据短了一些。

下面是串口实际获得的数据

G3

具体解读(来自说明书)

g3result

最后修改一下之前给 G1 写的 APP 即可

G3App

完整的代码
G3Ver1source

编译后的程序
Project2

Processing 做一个输入的文本框

根据网上搜到的结果,在【参考1】下载了 controlP5 的库,然后使用下面的程序

import controlP5.*;

ControlP5 cp5;

String[] textfieldNames = {"tf1", "tf2", "tf3", "tf4", "tf5"};

void setup() {
  size(700,400);

  PFont font = createFont("arial",20);

  cp5 = new ControlP5(this);

  int y = 20;
  int spacing = 60;
  for(String name: textfieldNames){
    cp5.addTextfield(name)
       .setPosition(20,y)
       .setSize(100,40)
       .setFont(font)
       .setFocus(true)
       .setColor(color(255,0,0))
       ;
     y += spacing;
  }

  textFont(font);
}

void draw() {
  background(0);
}

void controlEvent(ControlEvent theEvent) {
  if(theEvent.isAssignableFrom(Textfield.class)) {
    println("controlEvent: accessing a string from controller '"
            +theEvent.getName()+"': "
            +theEvent.getStringValue()
            );
  }
}

 

运行结果

prctext
参考:

1. http://www.sojamo.de/libraries/controlP5/

Step to UEFI (60) —– GUID和UUID

了解了一下 UUID 和 GUID , 资料上来看,这两个是同一个东西。GUID 是 MS 定义的 UUID 的一种实现方法. 两者在写法上稍微有些差别【参考1】【参考2】:

GUID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)

UUID:xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx (8-4-4-16)

在 UEFI 中,我们经常看到类似下面的定义

“cf8e16a5-c1e8-4e25-b712-4f54a96702c8”

#define EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID \
{ \
0x964e5b22, 0x6459, 0x11d2, {0x8e, 0x39, 0x0, 0xa0, 0xc9, 0x69, 0x72, 0x3b } \
}

UUID PadFileGuid;

对于 GUID ,可以在 \MdePkg\Include\Base.h 看到下面的定义

///
/// 128 bit buffer containing a unique identifier value.
/// Unless otherwise specified, aligned on a 64 bit boundary.
///
typedef struct {
  UINT32  Data1;
  UINT16  Data2;
  UINT16  Data3;
  UINT8   Data4[8];
} GUID;

 

同时,在 \MdePkg\Include\Uefi\UefiBaseType.h 有下面的定义

///
/// 128-bit buffer containing a unique identifier value.
///
typedef GUID                      EFI_GUID;

 

在 \BaseTools\Source\C\Common\CommonLib.c 有一个打印输出的例子可以直接拿出来用


EFI_STATUS
PrintGuid (
  IN EFI_GUID *Guid
  )
/*++

Routine Description:

  This function prints a GUID to STDOUT.

Arguments:

  Guid    Pointer to a GUID to print.

Returns:

  EFI_SUCCESS             The GUID was printed.
  EFI_INVALID_PARAMETER   The input was NULL.

--*/
{
  if (Guid == NULL) {
    Error (NULL, 0, 2000, "Invalid parameter", "PrintGuidToBuffer() called with a NULL value");
    return EFI_INVALID_PARAMETER;
  }

  printf (
    "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x\n",
    (unsigned) Guid->Data1,
    Guid->Data2,
    Guid->Data3,
    Guid->Data4[0],
    Guid->Data4[1],
    Guid->Data4[2],
    Guid->Data4[3],
    Guid->Data4[4],
    Guid->Data4[5],
    Guid->Data4[6],
    Guid->Data4[7]
    );
  return EFI_SUCCESS;
}

 

最后,写一个输出的例子

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

#include  <stdio.h>
#include  <stdlib.h>
#include  <wchar.h>

#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>


extern EFI_BOOT_SERVICES         *gBS;
extern EFI_SYSTEM_TABLE			 *gST;
extern EFI_RUNTIME_SERVICES 	 *gRT;

#define GUID1 \
  { \
    0x964e5b22, 0x6459, 0x11d2, {0x8e, 0x39, 0x0, 0xa0, 0xc9, 0x69, 0x72, 0x3b } \
  }

#define GUID2 \
  { \
    0x33221100, 0x5544, 0x7766, {0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF } \
  }
  
EFI_STATUS
PrintGuid (
  IN EFI_GUID *Guid
  )
  
/*++

Routine Description:

  This function prints a GUID to STDOUT.

Arguments:

  Guid    Pointer to a GUID to print.

Returns:

  EFI_SUCCESS             The GUID was printed.
  EFI_INVALID_PARAMETER   The input was NULL.

--*/
{
  if (Guid == NULL) {
	printf("Parameter error!\n");
    return EFI_INVALID_PARAMETER;
  }

  printf (
    "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x\n",
    (unsigned) Guid->Data1,
    Guid->Data2,
    Guid->Data3,
    Guid->Data4[0],
    Guid->Data4[1],
    Guid->Data4[2],
    Guid->Data4[3],
    Guid->Data4[4],
    Guid->Data4[5],
    Guid->Data4[6],
    Guid->Data4[7]
    );
  return EFI_SUCCESS;
}

int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{
  EFI_GUID T1=GUID1;
  EFI_GUID T2=GUID2;
  
  PrintGuid(&T1);
  PrintGuid(&T2);
  
  return EFI_SUCCESS;
}

 

运行结果

guid

完整代码下载

GUIDTest

参考:
1.http://baike.baidu.com/view/1052579.htm UUID
2.http://blog.csdn.net/forlong401/article/details/7580147 UUID 和 GUID 的区别
3.https://en.wikipedia.org/wiki/Globally_unique_identifier Globally unique identifier
4.https://en.wikipedia.org/wiki/Universally_unique_identifier Universally unique identifier

拆了攀登 G1 PM 2.5 传感器

上次买了攀藤 G1 PM2.5 传感器了,这次有需要又拿了出来使用【参考1】。很不幸,经常不工作,探究原因似乎与接线有关系,于是我又重新做了一个线(上次和卖家多要了一根线),用上之后现象稍微有些改善,但是移动之类的还会导致失灵,具体现象就是风扇不转,碰碰插头之类的就好了。一怒之下,拆开研究:

首先,外面是一层壳子,这个可以直接撬开(蓝色的是外面的塑料膜,我基本上没有用,所以这层薄膜还在的)

1

然后,需要拧掉周围四个固定外壳的螺丝,此外还要拧下固定风扇的三个螺丝才能继续拆解。螺丝居然生锈了,我很惊奇。基本上没怎么用,也都是一直放在家里不知道为什么会这样,观察另外一只(好吧,其实我是买个2个的),同样也有生锈的情况

2

最后,用螺丝刀之类的撬开即可

3

主板上有一颗CY8C 的单片机。更惊奇的是:主板上固定的螺丝竟然也有锈蚀!

4

背面是关键,可以看出有一个激光头样子的东西,这应该是这个传感器的关键部件,配合风扇和壳子内部形成的风道,可以计量单位时间颗粒物的数量。

5

研究发现插头不紧的原因是:他选用的那种连接件不是自锁样式的。比如:在拆开上面状态下,无法插入接线然后保持卡住的状态。连接件只是让金属接触上,然后卡住是外壳的功劳。这样,如果外壳出现松动或者接头磨损之类的,就根本无法卡住,就会出现我遇到的问题。

前几个月,我在实验点亮各种 MIPI 屏幕,然后发现接口卡具没多久就坏掉了,我去和硬件工程师说,你们做的时候不能选点好的连接器么,他回答是:这个东西装上基本上不会再拆了,像你这样拆来拆去的属于特殊情况,从成本角度考虑我们不会有选择结实一点的需求,估计这个传感器也是本着这样的思路设计的。

最后的结论:东西不错,但是连接设计太差,如果你要像我这样做 DIY 有经常插拔的需求,不建议选择这款。如果你是做毕业设计之类的展示性工作,最好再多买一个备份。

后面的话我想找个 HW 帮着焊接出来一根永久固定的线吧。

参考:

1.http://www.lab-z.com/g1pm25/ 攀藤 G1 PM2.5传感器

Arduino 控制USB设备(6)解析USB鼠标的例子(上)

前面介绍了USB键盘的使用,这里介绍一下USB鼠标的调用。根据【参考1】的文章进行实验,这次我们的目标是:获得鼠标移动按键信息,串口输出之。

首先运行一下之前的取得描述符的工具抓取一下描述符:

Descriptor of HP Mouse
Start
Device addressed… Requesting device descriptor.
Device descriptor:

Descriptor Length: 12
USB version: 2.0
Class: 00 Use class information in the Interface Descriptor
Subclass: 00
Protocol: 00
Max.packet size: 08
Vendor ID: 046D
Product ID: C077
Revision ID: 6700
Mfg.string index: 01 Length: 18 Contents: Logitech
Prod.string index: 02 Length: 36 Contents: USB Optical Mouse
Serial number index: 00
Number of conf.: 01

Configuration number 0
Total configuration length: 34 bytes

Configuration descriptor:
Total length: 0022
Number of interfaces: 01
Configuration value: 01
Configuration string: 00
Attributes: A0 Remote Wakeup
Max.power: 31 98ma

Interface descriptor:
Interface number: 00
Alternate setting: 00
Endpoints: 01
Class: 03 HID (Human Interface Device)
Subclass: 01
Protocol: 02
Interface string: 00

HID descriptor:
Descriptor length: 09 9 bytes
HID version: 1.11
Country Code: 0 Not Supported
Class Descriptors: 1
Class Descriptor Type: 22 Report
Class Descriptor Length:67 bytes

HID report descriptor:

Length: 1 Type: Global Tag: Usage Page Generic Desktop Controls Data: 01
Length: 1 Type: Local Tag: Usage Data: 02
Length: 1 Type: Main Tag: Collection Application (mouse, keyboard) Data: 01
Length: 1 Type: Local Tag: Usage Data: 01
Length: 1 Type: Main Tag: Collection Physical (group of axes) Data: 00
Length: 1 Type: Global Tag: Usage Page Button Data: 09
Length: 1 Type: Local Tag: Usage Minimum Data: 01
Length: 1 Type: Local Tag: Usage Maximum Data: 08
Length: 1 Type: Global Tag: Logical Minimum Data: 00
Length: 1 Type: Global Tag: Logical Maximum Data: 01
Length: 1 Type: Global Tag: Report Size Data: 01
Length: 1 Type: Global Tag: Report Count Data: 08
Length: 1 Type: Main Tag: Input Data,Variable,Absolute………
Length: 1 Type: Global Tag: Usage Page Generic Desktop Controls Data: 01
Length: 2 Type: Global Tag: Logical Minimum Data: 01 Data: F8
Length: 2 Type: Global Tag: Logical Maximum Data: FF Data: 07
Length: 1 Type: Global Tag: Report Size Data: 0C
Length: 1 Type: Global Tag: Report Count Data: 02
Length: 1 Type: Local Tag: Usage Data: 30
Length: 1 Type: Local Tag: Usage Data: 31
Length: 1 Type: Main Tag: Input Data,Variable,Relative………
Length: 1 Type: Global Tag: Logical Minimum Data: 81
Length: 1 Type: Global Tag: Logical Maximum Data: 7F
Length: 1 Type: Global Tag: Report Size Data: 08
Length: 1 Type: Global Tag: Report Count Data: 01
Length: 1 Type: Local Tag: Usage Data: 38
Length: 1 Type: Main Tag: Input Data,Variable,Relative………
Length: 1 Type: Global Tag: Usage Page Consumer Data: 0C
Length: 2 Type: Local Tag: Usage Data: 38 Data: 02
Length: 1 Type: Global Tag: Report Count Data: 01
Length: 1 Type: Main Tag: Input Data,Variable,Relative………
Length: 0 Type: Main Tag: End Collection
Length: 0 Type: Main Tag: End Collection

Endpoint descriptor:
Endpoint address: 01 Direction: IN
Attributes: 03 Transfer type: Interrupt
Max.packet size: 0006
Polling interval: 0A 10 ms

根据上面的结果,计算长度应该是1×8+Cx2+8×1=40 bits= 5 bytes ,修改【参考1】的代码如下:

#include "Max3421e.h"
#include "Usb.h"

 
#define DEVADDR 1
#define CONFVALUE 1
 
void setup();
void loop();
 
MAX3421E Max;
USB Usb;
 
void setup()
{
    Serial.begin( 115200 );
    Serial.println("Start");
    Max.powerOn();
    delay( 200 );
}
 
void loop()
{
 byte rcode;
    Max.Task();
    Usb.Task();
    if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING ) {
        mouse0_init();
    }//if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING...
    if( Usb.getUsbTaskState() == USB_STATE_RUNNING ) {  //poll the keyboard
        rcode = mouse0_poll();
        if( rcode ) {
          Serial.print("Mouse Poll Error: ");
          Serial.println( rcode, HEX );
        }//if( rcode...
    }//if( Usb.getUsbTaskState() == USB_STATE_RUNNING...
}
/* Initialize mouse */
void mouse0_init( void )
{
 byte rcode = 0;  //return code
  /**/
  Usb.setDevTableEntry( 1, Usb.getDevTableEntry( 0,0 ) );              //copy device 0 endpoint information to device 1
  /* Configure device */
  rcode = Usb.setConf( DEVADDR, 0, CONFVALUE );
  if( rcode ) {
    Serial.print("Error configuring mouse. Return code : ");
    Serial.println( rcode, HEX );
    while(1);  //stop
  }//if( rcode...

  Usb.setUsbTaskState( USB_STATE_RUNNING );
  return;
}
/* Poll mouse using Get Report and print result */
byte mouse0_poll( void )
{
  byte rcode,i;
  char buf[ 4 ] = { 0 };      //mouse buffer
  static char old_buf[ 4 ] = { 0 };  //last poll
    /* poll mouse */
    rcode = Usb.getReport( DEVADDR, 0, 4, 0, 1, 0, buf );
    if( rcode ) {  //error
      return( rcode );
    }
    for( i = 0; i < 4; i++) {  //check for new information
      if( buf[ i ] != old_buf[ i ] ) { //new info in buffer
        break;
      }
    }
    if( i == 4 ) {
      return( 0 );  //all bytes are the same
    }
    /* print buffer */
    if( buf[ 0 ] & 0x01 ) {
      Serial.print("Button1 pressed ");
    }
    if( buf[ 0 ] & 0x02 ) {
      Serial.print("Button2 pressed ");
    }
    if( buf[ 0 ] & 0x04 ) {
      Serial.print("Button3 pressed ");
    }
    Serial.println("");
    Serial.print("X-axis: ");
    Serial.println( buf[ 1 ], DEC);
    Serial.print("Y-axis: ");
    Serial.println( buf[ 2 ], DEC);
    Serial.print("Wheel: ");
    Serial.println( buf[ 3 ], DEC);
    
    for( i = 0; i < 4; i++ ) {
      old_buf[ i ] = buf[ i ];  //copy buffer
    }
    Serial.println("");
    return( rcode );
}

 

运行结果

usb61

完整代码下载

mouse0

看起来按键和X Y都已经正确,但是 wheel 并不正常。再仔细研究代码,取出来的信息长度和描述符中的不一致。就是说,程序虽然能够运行也能够取到变化的值,但是解读有误。

再进一步研读 Descriptor
Length: 1 Type: Global Tag: Usage Page Generic Desktop Controls Data: 01
Length: 2 Type: Global Tag: Logical Minimum Data: 01 Data: F8 //最小 F801 = 1111 1000 0000 0001 = -2047
Length: 2 Type: Global Tag: Logical Maximum Data: FF Data: 07//最大 07FF = 0000 0111 1111 1111 = 2047
Length: 1 Type: Global Tag: Report Size Data: 0C //这里指出了上面数值的大小是 12位
Length: 1 Type: Global Tag: Report Count Data: 02
Length: 1 Type: Local Tag: Usage Data: 30 // X
Length: 1 Type: Local Tag: Usage Data: 31 // Y

应该是意味着2个12bits长度的数值,对应X Y 分配如下,其中 X[B] 和Y[B]应该是最高的符号位

usb62

根据这个改写程序,按照上面的表格取值再拼接起来,特别需要注意的是,我发现Arduino在不同尺寸的数据转换上似乎有一些bug,我只能把值放在 unsigned long int中然后才能进行拼接和输出的动作。

    Serial.print("X-axis: ");
    x=((libuf[2] & 0xF)<<8)+(libuf[1] & 0xFF );
    if (x & 0x800) {
      Serial.print("-");
      x = ((~x) & 0x7FF) +1;
    }
   
    Serial.println(x, HEX);
    
    Serial.print("Y-axis: ");    
    y=(((libuf[2]>>4) & 0xF) +((libuf[3] & 0xFF )<<4));
    if (y & 0x800) {
      Serial.print("-");
      y = ((~y) & 0x7FF) +1;
    }    
Serial.println(y, HEX);

 

最终,程序可以正常执行。

工作正常的程序如下:

#include "Max3421e.h"
#include "Usb.h"

 
#define DEVADDR 1
#define CONFVALUE 1
 
void setup();
void loop();
 
MAX3421E Max;
USB Usb;
 
void setup()
{
    Serial.begin( 115200 );
    Serial.println("Start");
    Max.powerOn();
    delay( 200 );
}
 
void loop()
{
 byte rcode;
    Max.Task();
    Usb.Task();
    if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING ) {
        mouse0_init();
    }//if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING...
    if( Usb.getUsbTaskState() == USB_STATE_RUNNING ) {  //poll the keyboard
        rcode = mouse0_poll();
        if( rcode ) {
          Serial.print("Mouse Poll Error: ");
          Serial.println( rcode, HEX );
        }//if( rcode...
    }//if( Usb.getUsbTaskState() == USB_STATE_RUNNING...
}
/* Initialize mouse */
void mouse0_init( void )
{
 byte rcode = 0;  //return code
  /**/
  Usb.setDevTableEntry( 1, Usb.getDevTableEntry( 0,0 ) );              //copy device 0 endpoint information to device 1
  /* Configure device */
  rcode = Usb.setConf( DEVADDR, 0, CONFVALUE );
  if( rcode ) {
    Serial.print("Error configuring mouse. Return code : ");
    Serial.println( rcode, HEX );
    while(1);  //stop
  }//if( rcode...

  Usb.setUsbTaskState( USB_STATE_RUNNING );
  return;
}
/* Poll mouse using Get Report and print result */
byte mouse0_poll( void )
{
  byte rcode,i;
  char buf[ 5 ] = { 0 };      //mouse buffer
  static char old_buf[ sizeof(buf) ] = { 0 };  //last poll
  unsigned long int libuf[ sizeof(buf) ];
  unsigned long int x;
  unsigned long int y;
  
    /* poll mouse */
    rcode = Usb.getReport( DEVADDR, 0, sizeof(buf), 0, 1, 0, buf );
    if( rcode ) {  //error
      return( rcode );
    }
    for( i = 0; i < sizeof(buf); i++) {  //check for new information
      if( buf[ i ] != old_buf[ i ] ) { //new info in buffer
        break;
      }
    }
    if( i == sizeof(buf)) {
      return( 0 );  //all bytes are the same
    }
    /* print buffer */
    if( buf[ 0 ] & 0x01 ) {
      Serial.print("Button1 pressed ");
    }
    if( buf[ 0 ] & 0x02 ) {
      Serial.print("Button2 pressed ");
    }
    if( buf[ 0 ] & 0x04 ) {
      Serial.print("Button3 pressed ");
    }
    
    for( i = 0; i < sizeof(buf); i++ ) {
      old_buf[ i ] = buf[ i ];  //copy buffer
      libuf[ i] = buf[i];
    }

    //Serial.print(libuf[0],HEX); Serial.print("  "); 
    //Serial.print(libuf[1],HEX); Serial.print("  "); 
    //Serial.print(libuf[2],HEX); Serial.print("  "); 
    //Serial.print(libuf[3],HEX); Serial.print("  ");     
    //Serial.println("");
    
    Serial.print("X-axis: ");
    x=((libuf[2] & 0xF)<<8)+(libuf[1] & 0xFF );
    if (x & 0x800) {
      Serial.print("-");
      x = ((~x) & 0x7FF) +1;
    }
   
    Serial.println(x, HEX);
    
    Serial.print("Y-axis: ");    
    y=(((libuf[2]>>4) & 0xF) +((libuf[3] & 0xFF )<<4));
    if (y & 0x800) {
      Serial.print("-");
      y = ((~y) & 0x7FF) +1;
    }    
    Serial.println(y, HEX);

    Serial.print("Wheel: ");    
    Serial.println(buf [4] & 0xFF, HEX);    
    return( rcode );
}

 

完整代码下载:

mousea

补遗:在运行中,我还发现了一个奇怪的问题:当使用 Get_Report取得数据时,鼠标向一个方向移动,会出现最大值。意思是:比如,向右一直移动鼠标,输出达到 7FF 之后,继续右移,输出值会维持在 7FF 而不会变化。没有找到直接描述这个问题的资料,不过有一些类似的介绍【参考2】,上面提到USB鼠标在送出数据的时候,会有两种方式,一种是Relative ,一种是Absolute。差别在于,比如前者输出 (1,1),意思是告诉系统鼠标移动了(1,1);而后者如果输出 (1,1)则是告诉系统,鼠标移动到(1,1)。看起来,我这个鼠标描述符中提到的是Relative,但是实际输出是 Absolute。

Length: 1 Type: Global Tag: Report Size Data: 0C
Length: 1 Type: Global Tag: Report Count Data: 02
Length: 1 Type: Local Tag: Usage Data: 30
Length: 1 Type: Local Tag: Usage Data: 31
Length: 1 Type: Main Tag: Input Data,Variable,Relative………

于此可以形成对照的是 Wheel,每次滚动之后,很快会再次输出一个 0 的位置,下次再给出新的滚动值。
至于Windows 下,根据资料,系统会自动根据鼠标信息计算出一个大概范围方便使用(比如,屏幕是 1024×768,鼠标是Relative -100到+100的话,如果不做优化,从左上到右下需要做几次才行)。而我用的这个鼠标,声明自己是Relative,但是实际上发出Absolute,只是因为自己的范围很大(-7FF到+7FF),所以也不会有什么感觉。

上述只是猜测,是目前已知的最合理的解释而已。使用逻辑分析仪分析USB鼠标信息得到的结果相同。

逻辑分析仪抓取的结果,Windows使用 Interrupt 方式来和鼠标通讯

usb64

下面展示一下USB逻辑分析仪抓到的启动时的一些信息,首先是 Overview

image016

image005

image006
不清楚为什么VendorID 居然是罗技的,一种可能是因为这个是假的HP鼠标,乱写一个ID;另外的可能是HP 找罗技代工的。

image007

image008

可以看到,MaxPower确实是一个很古怪的数值:98ma

image009

image010

image011

image012

接下来是最重要的HID 描述符
image013

image014

image015

参考:
1.https://www.circuitsathome.com/communicating-arduino-with-hid-devices-part-1
2.http://www.microchip.com/forums/m482197.aspx MOUSE HID / Resolution problem

Processing导入模型

最近玩玩 Processing , 尝试导入一个 DXF 的模型,OBJ 格式的. 使用 Processing 加载之。具体做法是这样的:

1. 下载 OBJLoader 库 【参考1】
2. 把这个库放在 Processing -> File -> Preferences -> Sketchbook location 指定的位置中,例如:

prc1

特别注意,默认目录中可能有空格或者中文,那么请你指定一个不含有中文或者空格的目录,像我一样,放在 D:\ 下面最好。就是下面这个样子

prc2

prc3

这个库可以直接去【参考1】给出的网站上下载,也可以用 Tools -> Add Tool 调出 Tool Manager,他会自动下载列表,你选择 ObjLoader 进行安装(可能是 GFW 的缘故,这个功能不稳定)

toolm

3.安装好之后,可以编译 ObjLoader 中 Examples 下面的例子,确保安装正确 (如果出现错误,并且错误不是 ObjLoader 导致的,那么请换另外的例子,有些例子用到了第三方的库)

4.最后编写自己的程序。从 Sketch -> Add Files 中导入 OBJ 模型,再输入下面的代码

import saito.objloader.*;

OBJModel model;

void setup() {
  size(600,600,P3D);
  model= new OBJModel(this);
  model.load("teapot.obj");
  model.setDrawMode(POLYGON);
  noStroke();
}  

void draw() {
  background(0);
  lights();
  pushMatrix();
  translate(width/2,height,-width);
  rotateY(map(mouseX,0,width,-PI,PI));
  rotateX(PI/4);
  scale(6.0);
  model.draw();
  popMatrix();
}  

 

最终运行结果如下

teapot

完整的代码下载

sketch_150823b

参考:

1.https://processing.org/reference/libraries/ 上提到的 OBJLoader by Tatsuya Saito and Matt Ditton .OBJ 3D model file loader

Arduino 打造一个小夜灯

玩了很久的 arduino 老婆一直抱怨没有做过什么实用的东西,这次就做个方便晚上下床的小夜灯。
从技术角度讲非常简单:一个红外遮挡开关(本来是打算用圆柱形的那种,结果买来不好用【参考1】),当收到被阻挡的信号之后,自动从暗到亮,间隔特定时间之后再从亮到暗的灭掉。灯珠选择的是 WS2811。这种是全色灯,理论上有 2^24种颜色(其实肉眼根本分别不出这么多种类,另外,你再看到什么灯吹嘘自己一千六百万种颜色,就知道是和“奥氏体304不锈钢”一样,听上去牛逼的名字而已)。Arduino搭配专门的库函数,控制这个灯还是很方便的。顺便介绍一下原理:这些灯都是WS2811芯片串联起来的,每个灯珠里面都封装着一颗LED和芯片。对外的线有2组,每组都是三根线:VCC/GND/DATA。VCC是5V,所以用起来比较方便的,直接Arduino供电就可以(前提是不要超过Arduino提供电流的上限200ma,如果你不确定最大电流最好像我一样单独供电)。信号方面,每个灯颜色是8位的R ,8位的G,8位的B,串行给出。当第一个WS2811收到信号之后,他会取下自己的RGB颜色,把剩下的信号继续传下去。这样,一根信号线即可给出全部灯珠的颜色信号。

image002

下面就动手开始做了。买来的灯是连接好的一整条,我把它拆成2个一组了。中间使用“SM2.54 接插件 2.54MM 公母壳+公母簧片 对插SM-3P”进行连接。使用这样的插接线的好处是:容易扩充和调整。缺点是:需要手工压头,感觉上我做的连接可靠性不是很高。后来每个插脚除了压合,我还用电烙铁简单焊接了一下。

image003
图片来自佐田旗舰店【参考2】

电路上比较简单,我选择 Arduino Pro Micro,编程方便,体积适中。下面是连接的示意图,特别注意WS2811灯条是有方向的。一边是输入,一边是输出。

image005

代码如下,其中的颜色可以根据具体情况进行调整,这里只是简单演示:

#include "FastLED.h"

//红外传感器(开关用)
#define  IRNear A2
//灯带用的数据Pin
#define DATA_PIN 10

//灯带上灯的数量
#define NUM_LEDS 6

//灯带颜色
CRGB leds[NUM_LEDS];


void setup() {
  //初始化灯带
  FastLED.addLeds<WS2811, DATA_PIN, RGB>(leds, NUM_LEDS);

  //初始化接近开关
  pinMode(IRNear, INPUT_PULLUP);
}

void loop() {

  Serial.println("Led start working");

  //灯带关闭状态
  for (int i = 0; i < NUM_LEDS; i++ )
  {
    leds[i] = CRGB::Black;
    FastLED.show();
  }
  //等待稳定
  delay(1000);
  
  if (0==digitalRead(IRNear)) {
     delay(200);
     if (0==digitalRead(IRNear)) {
        for (int i = 0; i < NUM_LEDS; i++ )
           {
             leds[i] = CRGB(255,83,0);
           }
        for (int j=0; j<255; j++)
           {
            FastLED.show(j);
            delay(100);
           }   
        delay(15000);    
        for (int i = 0; i < NUM_LEDS; i++ )
           {
             leds[i] = CRGB::Red;
           }        
        for (int j=255; j>10; j--)
           {
            FastLED.show(j);
            delay(100);
           } 
        for (int i = 0; i < NUM_LEDS; i++ )
           {
             leds[i] = CRGB::Black;
             FastLED.show();
           }    
        
     }//if (0==digitalRead(IRNear)) {
  }//if (0==digitalRead(IRNear)) {
}

 

组装好的灯测试是下面这样
image007

采用磁铁固定,用粘性低的胶带包裹然后固定在线上。
image009

最上面是遮挡开关,最下面是多出来的一根线。如果等不够,随时可以再添加上一些。
为了美观和可靠,后面又用纸盒把遮挡开关包裹了一下,同样在这个开关后面也有个用于固定的磁铁。

image011

床是铁的,直接粘在外面实验一下。

image013

最后安装之后就是这个样子,其实最好是让每个灯垂直照射地面,不过晚上实验表明这个作为照明已经足够了。

image015

当一只脚落在地面上,遮挡开关会触发,然后灯会缓慢亮起,到达最亮之后维持15秒,切换为红色,随后再逐渐暗下。
老婆实验之后,提出了一个问题:如果我把一直腿垂在床边,那他岂不是要一直工作?这不好吧?
我的回复是谢霆锋的 《1999谢谢你的爱》。

参考:
1. 再次提醒,买回来的东西一定要及时测试,否则卖家不认账
2. 作者已经哭死,从他们家买了三样东西,历时一周才拿到手上(熊猫慢递)。反复盘点发现他们居然少发了电线。联系卖家一直没动静。然后选择了天猫的退款。结果点的是“退货退款”……两天后,客服告诉我,要选“只退款”。等我取消上一次的请求之后惊奇的发现竟然没有办法再次申请。联系淘宝客服,对方听完我的陈述之后,告诉我去找天猫,俺们不是一家。我又拨打了天猫的热线,客服小伙听过讲述之后,很快发给我一个无法打开的链接(鬼知道为什么搞错了);没办法,又打了一次,给了一个可以打开的链接,目前正在处理中ing.最后结果是退款了,但是前后差不多用了2周。可以说,如果以后有机会在淘宝店买东西也不要在天猫店买东西,服务差别很大。

随便说两句

网站趴窝了几天,具体原因是2015第13号台风“苏迪罗”间接造成的…….

解释一下就是台风来了,有人在果壳发帖子【参考1】,然后我跟帖,外链贴了一下很早之前做的台风的GIF动画,大小是 20MB 。结果一个晚上,台风还没有来,我的网站就趴窝了。

检查日志发现因为这个帖子访问量很大,每个浏览帖子的人都要从网站走20MB,很快就用光了一个月的配额。

后来联系供应商,他也很吃惊,那么多这个月没几天就用光了。后来他给加了一些,又开启了。

上一次出现这样的情况是因为我在网站上放了2个MP3,结果被一个淘宝店铺引用到,只是那次是在月末,我删除了 MP 3第二个月就没事了。没想到这次这么严重。

参考:

1.http://www.guokr.com/post/694425/?page=3#6650529 【直播】2015年最强台风苏迪罗要来了!8月8日凌晨已登陆台湾

Arduino 控制USB设备(5)解析USB键盘的例子

下面是一个获得 USB 键盘数据的例子【参考1】。原理上说,是将键盘切换为 Boot Protocol 这样就避免了需要具体解析HID的麻烦。

/* MAX3421E USB Host controller LCD/keyboard demonstration */
//#include <Spi.h>
#include "Max3421e.h"
#include "Usb.h"

/* keyboard data taken from configuration descriptor */
#define KBD_ADDR        1
#define KBD_EP          1
#define KBD_IF          0
#define EP_MAXPKTSIZE   8
#define EP_POLL         0x0a
/**/
//******************************************************************************
//  macros to identify special charaters(other than Digits and Alphabets)
//******************************************************************************
#define BANG        (0x1E)
#define AT          (0x1F)
#define POUND       (0x20)
#define DOLLAR      (0x21)
#define PERCENT     (0x22)
#define CAP         (0x23)
#define AND         (0x24)
#define STAR        (0x25)
#define OPENBKT     (0x26)
#define CLOSEBKT    (0x27)

#define RETURN      (0x28)
#define ESCAPE      (0x29)
#define BACKSPACE   (0x2A)
#define TAB         (0x2B)
#define SPACE       (0x2C)
#define HYPHEN      (0x2D)
#define EQUAL       (0x2E)
#define SQBKTOPEN   (0x2F)
#define SQBKTCLOSE  (0x30)
#define BACKSLASH   (0x31)
#define SEMICOLON   (0x33)
#define INVCOMMA    (0x34)
#define TILDE       (0x35)
#define COMMA       (0x36)
#define PERIOD      (0x37)
#define FRONTSLASH  (0x38)
#define DELETE      (0x4c)
/**/
/* Modifier masks. One for both modifiers */
#define SHIFT       0x22
#define CTRL        0x11
#define ALT         0x44
#define GUI         0x88
/**/
/* "Sticky keys */
#define CAPSLOCK    (0x39)
#define NUMLOCK     (0x53)
#define SCROLLLOCK  (0x47)
/* Sticky keys output report bitmasks */
#define bmNUMLOCK       0x01
#define bmCAPSLOCK      0x02
#define bmSCROLLLOCK    0x04
/**/
EP_RECORD ep_record[ 2 ];  //endpoint record structure for the keyboard

char buf[ 8 ] = { 0 };      //keyboard buffer
char old_buf[ 8 ] = { 0 };  //last poll
/* Sticky key state */
bool numLock = false;
bool capsLock = false;
bool scrollLock = false;
bool line = false;

void setup();
void loop();

MAX3421E Max;
USB Usb;

void setup() {
  Serial.begin( 9600 );
  Serial.println("Start");
  Max.powerOn();
  delay( 200 );
}

void loop() {
    Max.Task();
    Usb.Task();
    if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING ) { 
 //wait for addressing state
        kbd_init();
        Usb.setUsbTaskState( USB_STATE_RUNNING );
    }
    if( Usb.getUsbTaskState() == USB_STATE_RUNNING ) {  
//poll the keyboard  
        kbd_poll();
    }
}
/* Initialize keyboard */
void kbd_init( void )
{
 byte rcode = 0;  //return code
/**/
    /* Initialize data structures */
    ep_record[ 0 ] = *( Usb.getDevTableEntry( 0,0 ));  
                           //copy endpoint 0 parameters
    ep_record[ 1 ].MaxPktSize = EP_MAXPKTSIZE;
    ep_record[ 1 ].Interval  = EP_POLL;
    ep_record[ 1 ].sndToggle = bmSNDTOG0;
    ep_record[ 1 ].rcvToggle = bmRCVTOG0;
    Usb.setDevTableEntry( 1, ep_record );              
                           //plug kbd.endpoint parameters to devtable
    /* Configure device */
    rcode = Usb.setConf( KBD_ADDR, 0, 1 );                    
    if( rcode ) {
        Serial.print("Error attempting to configure keyboard. Return code :");
        Serial.println( rcode, HEX );
        while(1);  //stop
    }
    /* Set boot protocol */
    rcode = Usb.setProto( KBD_ADDR, 0, 0, 0 );
    if( rcode ) {
        Serial.print("Error attempting to configure boot protocol. Return code :");
        Serial.println( rcode, HEX );
        while( 1 );  //stop
    }
    delay(2000);
    Serial.println("Keyboard initialized");
}

/* Poll keyboard and print result */
/* buffer starts at position 2, 0 is modifier key state and 1 is irrelevant */
void kbd_poll( void )
{
 char i;
 static char leds = 0;
 byte rcode = 0;     //return code
    /* poll keyboard */
    rcode = Usb.inTransfer( KBD_ADDR, KBD_EP, 8, buf );
    if( rcode != 0 ) {
        return;
    }//if ( rcode..
    for( i = 2; i < 8; i++ ) {
     if( buf[ i ] == 0 ) {  //end of non-empty space
        break;
     }
      if( buf_compare( buf[ i ] ) == false ) {   //if new key
        switch( buf[ i ] ) {
          case CAPSLOCK:
            capsLock =! capsLock;
            leds = ( capsLock ) ? leds |= bmCAPSLOCK : leds &= ~bmCAPSLOCK;    
                      // set or clear bit 1 of LED report byte
            break;
          case NUMLOCK:
            numLock =! numLock;
            leds = ( numLock ) ? leds |= bmNUMLOCK : leds &= ~bmNUMLOCK;       
                                  // set or clear bit 0 of LED report byte
            break;
          case SCROLLLOCK:
            scrollLock =! scrollLock;
            leds = ( scrollLock ) ? leds |= bmSCROLLLOCK : leds &= ~bmSCROLLLOCK; 
                                  // set or clear bit 2 of LED report byte
            break;
          case DELETE:
            line = false;
            break;
          case RETURN:
            line =! line;
            break;  
          default:
            //Serial.print(HIDtoA( buf[ i ], buf[ 0 ] ));
            break;
        }//switch( buf[ i ...
        Serial.print(buf[ i ],HEX);
        Serial.print(' ');        
        Serial.println(buf[ 0 ],HEX);        
        rcode = Usb.setReport( KBD_ADDR, 0, 1, KBD_IF, 0x02, 0, &leds );
        if( rcode ) {
          Serial.print("Set report error: ");
          Serial.println( rcode, HEX );
        }//if( rcode ...
     }//if( buf_compare( buf[ i ] ) == false ...
    }//for( i = 2...
    for( i = 2; i < 8; i++ ) {                    //copy new buffer to old
      old_buf[ i ] = buf[ i ];
    }
}
/* compare byte against bytes in old buffer */
bool buf_compare( byte data )
{
 char i;
 for( i = 2; i < 8; i++ ) {
   if( old_buf[ i ] == data ) {
     return( true );
   }
 }
 return( false );
}

/* HID to ASCII converter. Takes HID keyboard scancode, returns ASCII code */
byte HIDtoA( byte HIDbyte, byte mod )
{
  /* upper row of the keyboard, numbers and special symbols */
  if( HIDbyte >= 0x1e && HIDbyte <= 0x27 ) {
    if(( mod & SHIFT ) || numLock ) {    //shift key pressed
      switch( HIDbyte ) {
        case BANG:  return( 0x21 );
        case AT:    return( 0x40 );
        case POUND: return( 0x23 );
        case DOLLAR: return( 0x24 );
        case PERCENT: return( 0x25 );
        case CAP: return( 0x5e );
        case AND: return( 0x26 );
        case STAR: return( 0x2a );
        case OPENBKT: return( 0x28 );
        case CLOSEBKT: return( 0x29 );
      }//switch( HIDbyte...
    }
    else {                  //numbers
      if( HIDbyte == 0x27 ) {  //zero
        return( 0x30 );
      }
      else {
        return( HIDbyte + 0x13 );
      }
    }//numbers
  }//if( HIDbyte >= 0x1e && HIDbyte <= 0x27
  /**/
  /* number pad. Arrows are not supported */
  if(( HIDbyte >= 0x59 && HIDbyte <= 0x61 ) && 
                   ( numLock == true )) {  // numbers 1-9
    return( HIDbyte - 0x28 );
  }
  if(( HIDbyte == 0x62 ) && ( numLock == true )) {  //zero
    return( 0x30 );
  }
  /* Letters a-z */
  if( HIDbyte >= 0x04 && HIDbyte <= 0x1d ) {
    if((( capsLock == true ) && ( mod & SHIFT ) == 0 ) 
          || (( capsLock == false ) && ( mod & SHIFT ))) {  
                     //upper case
      return( HIDbyte + 0x3d );
    }
    else {  //lower case
      return( HIDbyte + 0x5d );
    }
  }//if( HIDbyte >= 0x04 && HIDbyte <= 0x1d...
  /* Other special symbols */
  if( HIDbyte >= 0x2c && HIDbyte <= 0x38 ) {
    switch( HIDbyte ) {
      case SPACE: return( 0x20 ); 
      case HYPHEN:
        if(( mod & SHIFT ) == false ) {
         return( 0x2d );
        }
        else {
          return( 0x5f );
        }
      case EQUAL:
       if(( mod & SHIFT ) == false ) {
        return( 0x3d );
       }
       else {
        return( 0x2b );
       }
       case SQBKTOPEN:
         if(( mod & SHIFT ) == false ) {
          return( 0x5b );
         }
         else {
          return( 0x7b );
         }
       case SQBKTCLOSE:
         if(( mod & SHIFT ) == false ) {
          return( 0x5d );
         }
         else {
          return( 0x7d );
         } 
       case BACKSLASH:
         if(( mod & SHIFT ) == false ) {
           return( 0x5c );
         }
         else {
           return( 0x7c );
         }
       case SEMICOLON:
         if(( mod & SHIFT ) == false ) {
           return( 0x3b );
         }
         else {
           return( 0x3a );
         }
      case INVCOMMA:
        if(( mod & SHIFT ) == false ) {
          return( 0x27 );
        }
        else {
          return( 0x22 );
        }
      case TILDE:  
        if(( mod & SHIFT ) == false ) {
          return( 0x60 );
        }
        else {
          return( 0x7e );
        }
      case COMMA:   
        if(( mod & SHIFT ) == false ) {
          return( 0x2c );
        }
        else {
          return( 0x3c );
        }
      case PERIOD:
        if(( mod & SHIFT ) == false ) {
          return( 0x2e );
        }
        else {
          return( 0x3e );
        }
      case FRONTSLASH:
        if(( mod & SHIFT ) == false ) {
          return( 0x2f );
        }
        else {
          return( 0x3f );
        }
      default:
        break;
    }//switch( HIDbyte..
  }//if( HIDbye >= 0x2d && HIDbyte <= 0x38..
  return( 0 );
}

实验依然使用上一次的USB小键盘。上面的按键分布如下:

usbskb1

关于键值的介绍可以在【参考1】找到

NumLock OFF的情况下,各输出键值:

usbskb2
*输出三次62,相当于输出3个0

NumLock ON的情况下,各输出键值:
usbskb3
*输出三次62外加一个53

运行结果

kbr

本文完整代码下载 LCDkbd

参考:
1. https://github.com/felis/USB_Host_Shield/tree/master/examples/descriptor_parser USB_Host_Shield/examples/LCDkbd/ 本文原始代码
2. http://www.quadibloc.com/comp/scan.htm