内容


ThinkPad 健身操:通过翻转和摇动笔记本电脑控制应用程序

向使用 HDAPS 的笔记本电脑添加 Linux 和一点 Perl,即可为您的应用程序创建一个类似 Wii 的强大控制程序!

Comments

创新的触觉界面系统实现了一种全新的应用程序交互方式。使用廉价的电子游戏外围设备、嵌入式加速计和从笔记本电脑到手持电话等各种 PC 设备,可以实现控制应用程序的新方式。应用程序通常在界面控制方面比较滞后,由于 API 十分有限,应用程序的功能被局限于键盘和鼠标事件。

本文将演示的技术和代码可以使您将传统桌面计算环境中的应用程序与支持加速计的设备连接起来。

要求

硬件

2003 年制造的 IBM® ThinkPad 和后期推出的硬盘活动保护系统(Hard Disk Active Protection System,HDAPS)。如果您不确定自己的硬件配置,请查看 Lenovo Web 站点中的详细技术细节。

软件

您需要一个内核支持 HDAPS 的 Linux® 发行版。HDAPS 驱动程序必须包含在内核中以支持加速计访问。较新的内核版本,包括 Red Hat、Debian、Gentoo 和 Ubuntu 都包含 HDAPS 驱动程序。有关如何开始使用 HDAPS 内核驱动程序的更多信息,请参考 参考资料 小节中有关 HDAPS 的文章。

您需要使用来自 CPAN 的 Time::HiRes 模块,以便在记录数据时提供亚秒级的计时控制。此外,还需要使用 X11::GUITest 模块将合成的 X 事件发送到应用程序(参见 参考资料)。

注意,本文介绍的技术和算法应该适用于任何具有板载加速计的笔记本电脑。对于运行除 Linux 以外的其他操作系统的 MacBook 或 ThinkPad ,应用本文给出的代码也不会太困难。手持设备和其他支持加速计的系统也可以用于实现翻转、倾斜和命令之间的转换。如果在其他操作系统中运行,那么可以使用多种方法将合成的键盘和鼠标事件发送给应用程序。如果使用的是 Microsoft® Windows®,则考虑使用命令的 SendKeys 类实现应用程序访问。

合成的 X 事件使用 xorg 组件

对特定于应用程序的特性进行控制,这通常是通过 API 层的功能钩子实现的。然而,很多应用程序并不具备完整的 API 集,因此需要借助鼠标和键盘输入才能全面地操作应用程序。其他的应用程序没有提供 API,而只通过传统的人机界面设备提供交互。结合本文演示的组件和算法,无需 D-Bus 或其他 API 就可以实现应用程序控制。

要实现对 D-Bus 和没有提供 API 的应用程序的完全控制,向应用程序发送合成的 X 事件是一种重要方法。

X11::GUITest

您需要花费大量时间将 XTest 库函数调用集成到您的应用程序中。您需要指定键释放速率并在识别窗口中查找所有细微区别,同时跟踪键盘和鼠标状态。或者,可以使用 X11::GUITest 为您完成这些任务。X11::GUITest 模块为 XTest 库提供了一个健壮的包装器,可以显著提高事件处理效率。

对于很多可以响应用户键盘和鼠标事件的应用程序,在使用合成事件时将会以无法预期的方式执行。您可能需要调整键盘重复率和延迟,或寻求不同于标准输入机制的新方法。例如,Google Earth 并不始终以有效的方式响应按键事件,无论重复率和速度如何。本文使用了一种可以代替等价合成输入的方法:按住导航罗盘的中心控制杆。在尝试创建合成 X 事件以执行与实际替代方法相同的功能时,您可能会在其他程序中发现类似的误差或限制。在使用通过编程方式触发的键盘和鼠标事件连接应用程序时,您需要进行实验以找出可提供最佳可用性体验的输入方法。

将 HDAPS 传感器数据转换为应用程序控制

我们已经启用了传感器并设置了控制框架,因此接下来将在一个 Perl 程序内进行集成,将移动转换为操作。清单 1 展示了 hdapsGoogleEarth.pl 脚本的头部和变量声明。

清单 1. hdapsGoogleEarth.pl 全局变量和包含内容
#!/usr/local/bin/perl -w
# hdapsGoogleEarth.pl - control google earth by the pitch and roll of a thinkpad
use strict;
use Time::HiRes  qw( usleep );
use X11::GUITest qw( :ALL );

my $threshMove = 3;    # lower limit indicating motion
my $shakMin = 20;      # amount of a shake required to indicate change in mode
my $timeThreshold = 1; # number of seconds between mode changes
my $maxMove = 25;      # maximum movement of mouse from center point
my @avgX = ();         # array of 10 recent hdaps X values for shake detection
my $lastTime = time;   # record time of last mode change
my $slptim = 25000;    # microseconds to pause between data reads
my $maxTip = 100;      # turn thinkpad on its' side to exit the program
my $restX = 0;         # `resting' positiong of X axis accelerometer
my $restY = 0;         # `resting' positiong of Y axis accelerometer
my @windows;           # array of window id's associated with google earth
my $hdapsFN = "/sys/devices/platform/hdaps/position"; # hdaps sensor

包含了必需的模块后,将设置全局变量来处理移动并保持输入模式。本文演示了以升降或移动模式控制 Google Earth。通过摇动 ThinkPad,能够在多个模式之间快速切换,从而实现更直观的导航。清单 2 可以识别并将焦点置于 Google Earth 窗口。

清单 2. 识别并将焦点置于 Google Earth 窗口
@windows = FindWindowLike("Google Earth");
die "No google earth windows found" unless( scalar(@windows) > 0 );
my ( $geX, $geY, $geW, $geH, undef, undef ) = GetWindowPos( $windows[0] );

# rose center is left 110 from geX+geW, down 135 from geY
# zoom center is left 22 from geX+geW, down 135 from geY
my $cntrCompX = ($geX + $geW) - 110;
my $cntrCompY = $geY + 135;

my $centerZoomX = ($geX + $geW) - 22;
my $centerZoomY = $geY + 135;

my $maxZoomTop    = $centerZoomY - $maxMove;
my $maxZoomBottom = $centerZoomY + $maxMove;

my $maxLeft   = $cntrCompX - $maxMove;
my $maxRight  = $cntrCompX + $maxMove;
my $maxTop    = $cntrCompY - $maxMove;
my $maxBottom = $cntrCompY + $maxMove;

my $mouseX = $cntrCompX;
my $mouseY = $cntrCompY;

my $controlMode = "movement";

MoveMouseAbs( $cntrCompX, $cntrCompY );
PressMouseButton M_LEFT;

($restX, $restY ) = readPosition();

第一步是使用 X11::GUITest 模块,通过 FindWindowLike 函数定位 Google Earth 窗口。有时可以识别多个 Google Earth 窗口,但是第一个窗口 ID 通常为主窗口。注意,如果第一个窗口 ID 不是 Google Earth 主窗口,程序将不能正确处理输入。如果您的机器没有将 Google Earth 主窗口指定为第一个 Google Earth 窗口 ID,则会出现问题。如果发生这种情况,在 @windows 数组中的不同位置测试窗口 ID,直至找到 Google Earth 主窗口的正确 ID。

确定了 Google Earth 输入窗口的正确坐标并将鼠标移至适当位置之后,可通过 readPosition 确定加速计的基准。清单 3 展示了 readPosition 子例程。

清单 3. readPosition 子例程
sub readPosition {

  my ($posX, $posY) = "";

  open(FH," $hdapsFN") or die "can't open hdaps file";

    while(my $hdapsLine = <FH>)
    {
      $hdapsLine =~ s/(\(|\))//g; # remove parens
      ($posX, $posY) = split ",", $hdapsLine;
    }# while hdaps line read 

  close(FH);

  return( $posX, $posY );

}#readPosition

从 HDAPS 传感器读取数据非常简单,由于其不可查找特性,每次读取数据时都需要打开和关闭文件。(有关使用特定 HDAPS 传感器的更多信息,请查阅 参考资料)。完成了基本变量声明和子例程后,程序将继续进行主程序循环,如下所示。

清单 4. 主程序循环,第 1 部分
while(1)
{   
  # get accelerometer values
  my ($currX, $currY) = readPosition();
  $currX -= $restX;  # adjust for rest data state
  $currY -= $restY;
    
  # exit loop if ThinkPad tipped past 'exit' range
  last if( abs($currX) > $maxTip || abs($currY) > $maxTip );

  my $testX = abs($currX);  # keep original values for trending
  my $testY = abs($currY);
  
  if( $controlMode eq "movement" ) 
  {
    #  stay at the maximum distance to move even if severely tipped
    if( $testX > $maxMove ){ $testX = $maxMove }
    if( $testY > $maxMove ){ $testY = $maxMove }
      
    $mouseX = ($cntrCompX - $testX) if( $currX < -$threshMove );
    $mouseX = ($cntrCompX + $testX) if( $currX > $threshMove );
    $mouseY = ($cntrCompY - $testY) if( $currY < -$threshMove );
    $mouseY = ($cntrCompY + $testY) if( $currY > $threshMove );
    
    if( $mouseX < $maxLeft )  {  $mouseX = $maxLeft   }
    if( $mouseX > $maxRight ) {  $mouseX = $maxRight  }
    if( $mouseY < $maxTop )   {  $mouseY = $maxTop    }
    if( $mouseY > $maxBottom ){  $mouseY = $maxBottom }

    # reset the mouse to the center if the current reading is not past
    # the threshold
    $mouseY = $cntrCompY if( $currY < $threshMove && $currY > -$threshMove );
    $mouseX = $cntrCompX if( $currX < $threshMove && $currX > -$threshMove );

每执行一次主程序循环都将从加速计传感器中读取新的值。将从之前创建的引用状态中减去这些值,以便将类似 ‘-379,-380’ 的值转换为更为有用的 ‘0,0’(表示 ThinkPad 保持静止)。接下来,程序将检查 ThinkPad 是否从静止状态变为动态翻转,从而表示一种退出条件。这是必须的,因为全部的键盘和鼠标控制已经由应用程序接管,因此,需要使用一种物理定向(physical-orientation)方法告诉应用程序执行退出。

如果应用程序处于移动处理模式,接下来的两行将移动边界的上限设置为 $maxMove。接下来的四行代码根据翻转 ThinkPad 而超过移动阈值($threshMove)的程度将鼠标移至相应的 X 和 Y 维上。人们很少能够始终将 ThinkPads 保持在理想水平,对于每个维度使用一个倾斜阈值有助于简化基状态检测。

接下来的四行代码将鼠标移动限制为从每个维度中心移动的最大距离。最后,如果没有达到上面提到的移动阈值,则将鼠标位置重置到中心。如果重设中心位置,那么在倾斜超过阈值后将产生一个跳跃的移动,但是在多数应用程序中,缓慢的移动或未知的初始状态比较可取。

清单 5 显示了一个相对简单的升降控制模式。

清单 5. 主程序循环,第 2 部分
  }else
  {

    # zoom control - move up and down only
    $mouseY = ( $centerZoomY - $testY ) if( $currY < -$threshMove );
    $mouseY = ( $centerZoomY + $testY ) if( $currY > $threshMove  );
    if( $mouseY < $maxZoomTop ){  $mouseY = $maxZoomTop }
    if( $mouseY > $maxZoomBottom ){  $mouseY = $maxZoomBottom }

    $mouseX = $centerZoomX;
    $mouseY = $centerZoomY if( $currY < $threshMove && $currY > -$threshMove )

  }#if in zoom mode

  MoveMouseAbs( $mouseX, $mouseY);  # move the mouse after coordinates computed

类似上文的移动模式,升降控制模式将根据翻转 ThinkPad 而超过移动阈值的程度在 Y 维度移动鼠标。计算得到的移动将被限制在从 zoom 滑块的中心位置移动的最大距离。同样,如果没有达到移动阈值,则将鼠标位置重设为中心。

计算鼠标位置之后,对于升降或移动模式,都将移动鼠标来控制应用程序。

通过捕捉动作切换模式

通过使用上面的代码,您可以通过移动 ThinkPad 来控制 Google Earth 的移动或升降模式。要在模式间进行切换,需要对主程序循环的每次循环执行额外的检查。清单 6 和 7 展示了主程序逻辑的其余部分以及 shakeCheck 子例程,它将捕捉动作并切换模式。

清单 6. 主程序循环,第 3 部分
  # mode switch by shaking, only if at least timeThreshold seconds have passed
  # since last mode switch
  if( shakeCheck( $currX ) == 1 && abs($lastTime - time) > $timeThreshold)
  {
    $lastTime = time;
    if( $controlMode eq "movement" )
    {
      $controlMode = "zoomRotate";
      $mouseX = $centerZoomX;
      $mouseY = $centerZoomY;
    }else
    {
      $controlMode = "movement";
      $mouseX = $cntrCompX;
      $mouseY = $cntrCompY;
    }#if movement or zoomRotate mode

    ReleaseMouseButton(M_LEFT) if( IsMouseButtonPressed(M_LEFT) );
    MoveMouseAbs( $mouseX, $mouseY);
    PressMouseButton( M_LEFT );

    print "$controlMode mode\n";

  }# if a large enough shake in X dimension detected

  usleep($slptim);

}#while

ReleaseMouseButton M_LEFT;   # let go of button on exit

在移动和升降模式之间切换时,将交替生成一个由 shakeCheck 子例程返回的 true 值,以及一个可接受的模式切换之间的时间间隔。可以将 $timeThreshold 配置为要求模式切换之间具有一定的时间。您可能希望将这个参数设得更大一些,同样,对于 shakeCheck 子例程中显示的 shakMin 变量也一样,如全局定义部分和清单 7 所示。

清单 7. shakeCheck 子例程
sub shakeCheck
{
  my $xVal  = $_[0];
  push @avgX, $xVal;
  # build an array of 10 samples before attempting processing
  return(0) if( @avgX < 10 );

  my $currAvg = 0;
  for my $eachX ( @avgX ){ $currAvg += $eachX };
  $currAvg = $currAvg /10;
  shift @avgX;

  # if current value is a big enough deviation from average value
  return(1) if( abs($currAvg - $xVal) > $shakMin );
  return(0);

}#shakeCheck

子例程 shakeCheck 只对 X 维构建了过去 10 次读取 HDAPS 传感器的平均值。将 X 维的当前值与平均值比较,如果它们至少偏离了 $shakMin,则设置一个摇动条件并由子例程返回 true。可以很容易地检测到一次快速摇动,这是因为,尽管移动幅度相对较小,但是加速度比较大,足以捕捉到有用的动作。

使用 hdapsGoogleEarth.pl 和 hdapsGoogleStreeView.pl

要运行上面构建的 hdapsGoogleEarth.pl 程序,请启动 Google Earth 应用程序。当显示默认的地球仪时,使用 perl hdapsGoogleEarth.pl 命令启动合成 X 事件转换程序。您应当看到 Google Earth 窗口将获得焦点,同时在右上角鼠标将获取移动控制杆。拿起 ThinkPad 并开始进行导航,并注意在靠近垂直方向上向任意维度倾斜您的 ThinkPad 以便退出程序。

查看 参考资料 小节中的代码归档文件,其中包含另一个 Perl 脚本,它通过将 ThinkPad 的倾斜和翻转转换为按键事件来控制 Google 街道视图。要使用 hdapsGoogleStreetView.pl 脚本,请确保您使用的是 Firefox Web 浏览器,并在其中打开了 Google Maps 并处于街道视图(street-view)模式。使用 perl hdapsGoogleStreetView.pl 启动应用程序,您将能够通过倾斜和翻转 ThinkPad 导航城市街道。查看源代码了解 hdapsGoogleStreetView.pl 应用程序如何使用按键代替鼠标移动来控制应用程序。

结束语

对于诸如地图绘制软件这样需要持续输入的应用程序,非常适合应用直观的导航系统。使用本文演示的代码,您将可以通过合成的键盘和鼠标事件将这种新一代导航系统应用到任何应用程序。

通过 Bluetooth 将您的支持加速计的手机连接到 PC,并通过翻转手机控制应用程序。将一个惯性陀螺仪连接到 ThinkPad,并通过转动、倾斜和翻转动作扫描地图。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=301674
ArticleTitle=ThinkPad 健身操:通过翻转和摇动笔记本电脑控制应用程序
publish-date=04172008