201702 月发表在 分享

12通过程序控制 Raspberry Pi 的 USB 接口供电

Raspberry Pi (树莓派)的 USB 接口供电是会在系统停止的时候自动断开的,于是我意识到了其 USB 供电电路一定是受 CPU 控制的,可能就是通过某个 GPIO 控制的。 通过在网上的一番查找,发现和我想象的还是有一些出入的:

  • USB 接口的供电并不是通过 GPIO 直接控制的,而是通过 USB 集线器 LAN9514 控制的。CPU 可以通过设置集线器的 PORT_POWER 选项完成 USB 供电电路的开关。
  • 控制 USB 供电的电路出现在 Raspberry Pi Model B+ 之后的版本。原版的 Raspberry Pi Model B 的 USB 供电电路直接与 5V 相连,不受控制。

初步尝试

通过 ssh 登录 Pi,下载并编译 https://github.com/codazoda/hub-ctrl.c(这个程序依赖于 libusb,编译时请带上参数 -lusb)。在 USB 接口上接上一个如下图所示的 USB LED 灯。由于默认情况下 USB 供电是打开的,所以灯是亮的。使用如下命令可以关闭灯的供电:

sudo ./hub-ctrl -P 2 -p 0

-p 参数决定了是否启用供电,将其后的 0 改为 1,灯就再次亮了:

sudo ./hub-ctrl -P 2 -p 1

-P 参数用于指定要控制的端口,1 为集成网卡,2 为 4 个 USB 口。网卡的供电可以独立控制,所以开关 USB 口的供电并不会导致 ssh 断开。

usblight.jpg

低延迟遥控灯光

因为 USB 小灯对开关的响应非常快,所以没有理由不好好玩一玩它~ 一个典型的玩法就是用手机遥控小灯,跟着音乐的节奏开关小灯,加强氛围。为了跟上音乐节奏,极低的延迟非常重要。

手机端我选择利用 HTML5 实现,手机端的程序非常小,而且用不到推送通知,所以 iOS 的 HTML5 App 就完全可以满足要求了。为了实现低延迟,我采取了如下措施:

  • 使用 WebSocket 和服务器通信。WebSocket 可以实现相当低的延迟。虽然写成 App,用 UDP socket 可以实现更低的延迟,但是写 App 毕竟比写 HTML 麻烦太多了啊→_→
  • 修改 hub-ctrl 的代码,使其可以通过 stdin 接收控制指令。 这么做可以避免在每次开关 USB 小灯的时候,在创建进程和初始化 libusb 上花费时间。

下面贴出代码

修改版 hub-ctrl:hub-power.c

#include <errno.h>
#include <usb.h>
#include <stdio.h>
#include <string.h>

#define USB_RT_HUB                      (USB_TYPE_CLASS | USB_RECIP_DEVICE)
#define USB_RT_PORT                     (USB_TYPE_CLASS | USB_RECIP_OTHER)
#define USB_PORT_FEAT_POWER             8
#define USB_PORT_FEAT_INDICATOR         22
#define USB_DIR_IN                      0x80            /* to host */

static void usage (const char *progname)
{
  fprintf (stderr,
           "Usage: %s [{-h HUBNUM | -b BUSNUM -d DEVNUM}] \\\n"
           "          [-P PORT]\n", progname);
}

static void exit_with_usage (const char *progname)
{
  usage (progname);
  exit (1);
}

#define HUB_CHAR_LPSM           0x0003
#define HUB_CHAR_PORTIND        0x0080

struct usb_hub_descriptor {
  unsigned char bDescLength;
  unsigned char bDescriptorType;
  unsigned char bNbrPorts;
  unsigned char wHubCharacteristics[2];
  unsigned char bPwrOn2PwrGood;
  unsigned char bHubContrCurrent;
  unsigned char data[0];
};

#define CTRL_TIMEOUT 1000
#define USB_STATUS_SIZE 4

#define MAX_HUBS 128
struct hub_info {
  int busnum, devnum;
  struct usb_device *dev;
  int nport;
  int indicator_support;
};

static struct hub_info hubs[MAX_HUBS];
static int number_of_hubs_with_feature;

static int usb_find_hubs (int listing, int verbose, int busnum, int devnum, int hub)
{
  struct usb_bus *busses;
  struct usb_bus *bus;

  number_of_hubs_with_feature = 0;
  busses = usb_get_busses();
  if (busses == NULL)
    {
      perror ("failed to access USB");
      return -1;
    }

  for (bus = busses; bus; bus = bus->next)
    {
      struct usb_device *dev;

      for (dev = bus->devices; dev; dev = dev->next)
        {
          usb_dev_handle *uh;
          int print = 0;

          if (dev->descriptor.bDeviceClass != USB_CLASS_HUB)
            continue;

          if (listing
              || (verbose
                  && ((atoi (bus->dirname) == busnum && dev->devnum == devnum)
                      || hub == number_of_hubs_with_feature)))
            print = 1;

          uh = usb_open (dev);

          if (uh != NULL)
            {
              char buf[1024];
              int len;
              int nport;
              struct usb_hub_descriptor *uhd = (struct usb_hub_descriptor *)buf;
              if ((len = usb_control_msg (uh, USB_DIR_IN | USB_RT_HUB,
                                          USB_REQ_GET_DESCRIPTOR,
                                          USB_DT_HUB << 8, 0,
                                          buf, sizeof (buf), CTRL_TIMEOUT))
                  > sizeof (struct usb_hub_descriptor))
                {
                  if (!(uhd->wHubCharacteristics[0] & HUB_CHAR_PORTIND)
                      && (uhd->wHubCharacteristics[0] & HUB_CHAR_LPSM) >= 2)
                    continue;

                  if (print)
                    printf ("Hub #%d at %s:%03d\n",
                            number_of_hubs_with_feature,
                            bus->dirname, dev->devnum);

                  switch ((uhd->wHubCharacteristics[0] & HUB_CHAR_LPSM))
                    {
                    case 0:
                      if (print)
                        fprintf (stderr, " INFO: ganged switching.\n");
                      break;
                    case 1:
                      if (print)
                        fprintf (stderr, " INFO: individual power switching.\n");
                      break;
                    case 2:
                    case 3:
                      if (print)
                        fprintf (stderr, " WARN: No power switching.\n");
                      break;
                    }

                  if (print
                      && !(uhd->wHubCharacteristics[0] & HUB_CHAR_PORTIND))
                    fprintf (stderr, " WARN: Port indicators are NOT supported.\n");
                }
              else
                {
                  perror ("Can't get hub descriptor");
                  usb_close (uh);
                  continue;
                }

              nport = buf[2];
              hubs[number_of_hubs_with_feature].busnum = atoi (bus->dirname);
              hubs[number_of_hubs_with_feature].devnum = dev->devnum;
              hubs[number_of_hubs_with_feature].dev = dev;
              hubs[number_of_hubs_with_feature].indicator_support =
                (uhd->wHubCharacteristics[0] & HUB_CHAR_PORTIND)? 1 : 0;
              hubs[number_of_hubs_with_feature].nport = nport;

              number_of_hubs_with_feature++;

              usb_close (uh);
            }
        }
    }

  return number_of_hubs_with_feature;
}

int get_hub (int busnum, int devnum)
{
  int i;

  for (i = 0; i < number_of_hubs_with_feature; i++)
    if (hubs[i].busnum == busnum && hubs[i].devnum == devnum)
      return i;

  return -1;
}

int main (int argc, const char *argv[])
{
  int busnum = 0, devnum = 0;
  int port = 1;
  int hub = -1;
  char ch;
  usb_dev_handle *uh = NULL;
  int i;

  for (i = 1; i < argc; i++)
    if (argv[i][0] == '-')
      switch (argv[i][3])
        {
        case 'h':
          if (++i >= argc || busnum > 0 || devnum > 0)
            exit_with_usage (argv[0]);
          hub = atoi (argv[i]);
          break;

        case 'b':
          if (++i >= argc || hub >= 0)
            exit_with_usage (argv[0]);
          busnum = atoi (argv[i]);
          break;

        case 'd':
          if (++i >= argc || hub >= 0)
            exit_with_usage (argv[0]);
          devnum = atoi (argv[i]);
          break;

        case 'P':
          if (++i >= argc)
            exit_with_usage (argv[0]);
          port = atoi (argv[i]);
          break;

        default:
          exit_with_usage (argv[0]);
        }
    else
      exit_with_usage (argv[0]);

  if ((busnum > 0 && devnum <= 0) || (busnum <= 0 && devnum > 0))
    /* BUS is specified, but DEV is'nt, or ... */
    exit_with_usage (argv[0]);

  /* Default is the hub #0 */
  if (hub < 0 && busnum == 0) hub = 0;

  usb_init ();
  usb_find_busses ();
  usb_find_devices ();

  if (usb_find_hubs (0, 0, busnum, devnum, hub) <= 0)
    {
      fprintf (stderr, "No hubs found.\n");
      return 1;
    }

  if (hub < 0) hub = get_hub (busnum, devnum);

  if (hub >= 0 && hub < number_of_hubs_with_feature)
    uh = usb_open (hubs[hub].dev);

  if (uh == NULL)
    {
      fprintf (stderr, "Device not found.\n");
      return 1;
    }

  while((ch = getchar()) > 0) {
    switch(ch) {
    case '1':
      usb_control_msg (uh, USB_RT_PORT, USB_REQ_SET_FEATURE,
                       USB_PORT_FEAT_POWER, port,
                       NULL, 0, CTRL_TIMEOUT);
      break;
    case '0':
      usb_control_msg (uh, USB_RT_PORT, USB_REQ_CLEAR_FEATURE,
                       USB_PORT_FEAT_POWER, port,
                       NULL, 0, CTRL_TIMEOUT);
      break;
    }
  }

  usb_close (uh);
  return 0;
}

WebSocket 服务端

var WebSocketServer = require('ws').Server;
var spawn = require('child_process').spawn;
var wss = new WebSocketServer({ port: 2332 });
var hpc = spawn('hub-power', ['-P', '2'], {stdio: 'pipe'});

wss.on('connection', function(ws) {
    ws.on('message', function(msg) {
        if(msg == 'light') {
            hpc.stdin.write('1\n');
        } else if(msg == 'off') {
            hpc.stdin.write('0\n');
        }
    });
});

HTML5 客户端

客户端已经指定好了关于 iOS Web App 相关的参数。可以直接通过 Safari 的添加到主屏幕转化为 App。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
    <meta name="format-detection" content="telephone=no" />
    <meta name="apple-mobile-web-app-title" content="灯光师" />
    <title>灯光师</title>
    <style>
body { background-color: black; }
.button {
    margin: 30px 10px;
    border: solid 1px white;
    border-radius: 30px;
    text-align: center;
    color: white;
    user-select: none;
    -webkit-user-select: none;
    cursor: default;
}
#lightBtn {
    height: 300px;
    font-size: 60px;
    line-height: 100px;
}
#blinkBtn {
    line-height: 100px;
    font-size: 30px;
};
    </style>
</head>
<body>
    <div class="button" id="lightBtn"></div>
    <div class="button" id="blinkBtn">FLASH</div>
    <script>
var button = document.getElementById('lightBtn');
var buttonB = document.getElementById('blinkBtn');
var ws = new WebSocket('ws://' + document.location.hostname + ':2332');
ws.onopen = function() {
    var bInterval, blinkState;
    ws.send('off');
    button.innerText = 'LIGHT';
    button.ontouchstart = function() {
        ws.send('light');
        document.body.style.backgroundColor = '#eee';
        button.style.borderColor = "black";
    };
    button.ontouchend = function() {
        ws.send('off');
        document.body.style.backgroundColor = 'black';
        button.style.borderColor = "white";
    };
    buttonB.ontouchstart = function() {
        if(bInterval) return;
        blinkState = 0;
        button.ontouchstart();
        bInterval = setInterval(function() {
            if(blinkState == 0) {
                button.ontouchstart();
            } else if(blinkState == 1) {
                button.ontouchend();
            }
            if(++blinkState > 3) blinkState = 0;
        }, 30);
        buttonB.style.backgroundColor = '#eee';
    }
    buttonB.ontouchend = function() {
        clearInterval(bInterval);
        bInterval = undefined;
        button.ontouchend();
        buttonB.style.backgroundColor = '';
    }
    ws.onclose = function() {
        button.ontouchstart = undefined;
        button.ontouchend = undefined;
        button.innerText = '';
    };
};
document.addEventListener("touchmove",function(e){
    e.preventDefault();
    e.stopPropagation();
});
    </script>
</body>
</html>

用户界面如下图所示,按住 LIGHT 区域可以让小灯发光,按住 FLASH 区域可以让小灯闪烁。

UI.png

zyxwvu
UNDER CONSTRUCTION