2013年01月18日

親指サイズの USB 赤外線リモコンが面白い

はてなブックマークに登録

2013.01.29 追記    Mac OS X 用の試作コードを掲載しました

昨年末、調べごとをしていた時にちょっと気になる商品が目に留まりました。 株式会社ビット・トレード・ワン 様の「USB 接続 赤外線リモコン KIT」という製品です。

特徴をざっくりまとめてみるとこんな感じです。

  • [パソコンから家庭用機器をリモコン操作]、[リモコンでパソコンを操作] の2つの機能を持つ
  • 赤外線送信用のライブラリやツール・ファームウェアのソースコードが公開されている
  • 家電協/ NEC/ SONY の各リモコンコードフォーマットに対応
  • 某清涼菓子のケースにぴったり収まるサイズ
  • キットは 1,680 円、組立ずみ製品でも 2,480 円と低価格
  • PC 側対応 OS はWindows 7, Vista, XP
PC から制御可能な学習型赤外線リモコンといえば 2006 年の発売以来ロングセラーを続ける PC-OP-RS1 がとても有名ですが、このキットは昨年(2012年)発売された製品とのことで、ソフトウェア要素の多くがオープンであることに魅力を感じました。一方で 公式フォーラムを覗いてみるとエアコンまわりをはじめいろいろ制約もあるようで、それをどう考えるかは価格とのバランスの解釈次第でしょう。そういった話題も含めて好奇心をそそられ、年明け早々に入手しました。画像は「フリスク」と並べてみた様子です。

PC とは A−miniB タイプの USB ケーブルで接続。Windows 上でヒューマンインターフェイスデバイスとして認識され標準の HID ドライバが使用されます。

とりあえず使ってみると・・

さっそく専用の 送信用設定ツール に手元のいくつかの機器のリモコンコードを学習させ本キットから信号を出力し、それを元の機器が認識できるか否かを試してみると下の図の結果となりました。

シャープ製のアナログ TV は 1990年代のかなり旧いもので、セイテック製の照明器具用リモートコンセントは 10 年ほど前に近所のホームセンターで購入したものです。これらは NG でしたが、パナソニック製の DVD レコーダは問題なく扱うことができました。ちなみに前出の PC-OP-RS1 ではいずれも学習・再生に問題はなく、本キットで処理可能な送受信データの最大長が 6 バイトであることにも関係があるのかも知れません(PC-OP-RS1 のデータは 240 バイト)。メーカーへ尋ねたところ、赤外線リモコンの仕様は完全には規格化されておらずそれらのデータは本キットで正しく認識できていない可能性がある、とのことでした。

※「動作確認済リモコンリスト」(PDF 資料):メーカーサイトより

他のリモコン機器もいくつか試したところ、手元では 家電協フォーマット に準じたものは正しく扱うことができました。このフォーマットはもともと独自仕様の乱立していた日本製家電製品の赤外線リモコン規格を統一するために業界団体(財団法人家電製品協会)が策定した標準規格とのことで国産の多くのリモコン機器がこれに準拠しているようです。毎日あちこちで赤外線リモコンを使っていながら今まで中のしくみを意識するきっかけのなかった自分にとってこういった話題は新鮮で興味を感じます。本キットのファームウェアのソースコードは勉強や実験に大いに役に立ちそうです。

「赤外線送信ライブラリ」について

本キットの赤外線送信機能を自作のコードから利用するためのライブラリが公開されています。

メーカー側のこうした配慮は利用者にとって大変嬉しいもので製品そのものの魅力も大きく向上しますね。シンプルで分かり易い API 構成にも好感を持ちました。

ただ、ちょっと残念だったのはモジュールが .NET ライブラリだったことです。個人的な好みではありますが、今のところ私自身は .NET も .NET アプリケーションもあまり好きではないのです (^^; もちろん、たとえば複数のプラットフォームでの稼動を目的とする選択であればまた話は別なのですが、Win32 API に強く依存する内容であれば少なくともライブラリとしてはネイティブ形式のほうが間口が広い分便宜が大きいようにも思います。

そんなわけで、送信用設定ツールのコードをもとにリモコンデータ送受信用の処理を C 言語でできるだけ短くコンソールアプリケーションとして実装してみました。「赤外線送信ライブラリ」のソースは公開されていないようですが、必要があればこの CUI アプリのコードから同等のネイティブ DLL を作ることも可能ですね。

以下に、一連の作業の過程で知った PC−キット間の送受信手順と、作成したコードを控えます。

Mac でも動かせないか?(2013.01.29 追記)

Mac は普段あまり触らないのですが、このキットが Windows 上でヒューマンインターフェイスデバイス (HID) として認識され標準のドライバで駆動するということは Mac でもそのまま使えるのではないか?と興味を持ち情報を探してみました。

HID Class Device Interface Guide : USB HID Overview - developer.apple.com

OS X provides the HID Manager (described in “The HID Manager”) to support access to any devices that conform to the USB HID specification. While this is most commonly used for communicating with input devices, a number of other devices also use HID descriptors, and can thus be accessed using the same mechanism.

HID Class Device Interface Guide : Accessing a HID Device - developer.apple.com

やはりアプリケーションレイヤのコードからアクセスできそうなのでさっそくコードを試作してみました。あわせて掲載します。

PC →← キット間のデータ送受信手順

PC と本キットの間のデータ送受信には 65 バイト固定長の BYTE 配列を使用する。
デバイスオープン後の送受信手順を以下に示す。

リモコンデータの受信

1. 本キットをリモコンデータ受信待ちモードに

       [0]   [1]   [2]   [3]   [4]           [61]  [62]  [63]  [64]
 write┌──┬──┬──┬──┬──┬      ┬──┬──┬──┬──┐
 →→ │0x00│0x510x01│0xFF│0xFF│・・・│0xFF│0xFF│0xFF│0xFF│
      └──┴──┴──┴──┴──┴     ┴──┴──┴──┴──┘
       [0]   [1]   [2]   [3]   [4]           [61]  [62]  [63]  [64]
 read ┌──┬──┬──┬──┬──┬      ┬──┬──┬──┬──┐
 ←← │0x00│0x51│                  ・・・
      └──┴↑─┴──┴──┴──┴     ┴──┴──┴──┴──┘
              OK

2. リモコンデータ受信

       [0]   [1]   [2]   [3]   [4]           [61]  [62]  [63]  [64]
 write┌──┬──┬──┬──┬──┬      ┬──┬──┬──┬──┐
 →→ │0x00│0x50│0xFF│0xFF│0xFF│・・・│0xFF│0xFF│0xFF│0xFF│
      └──┴──┴──┴──┴──┴     ┴──┴──┴──┴──┘
       [0]   [1]   [2]   [3]   [4]   [5]   [6]   [7]   [8]           [63]  [64]
 read ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬      ┬──┬──┐
 ←← │0x00│0x500x??0x??0x??0x??0x??0x??0x??│・・・
      └──┴↑─┴↑─┴──┴──┴──┴──┴──┴──┴     ┴──┴──┘
              OK    !=0x00
                   └──認識した7バイトのリモコンコード──┘

3. リモコンデータ受信待ちモード解除

       [0]   [1]   [2]   [3]   [4]           [61]  [62]  [63]  [64]
 write┌──┬──┬──┬──┬──┬      ┬──┬──┬──┬──┐
 →→ │0x00│0x510x00│0xFF│0xFF│・・・│0xFF│0xFF│0xFF│0xFF│
      └──┴──┴──┴──┴──┴     ┴──┴──┴──┴──┘
       [0]   [1]   [2]   [3]   [4]           [61]  [62]  [63]  [64]
 read ┌──┬──┬──┬──┬──┬      ┬──┬──┬──┬──┐
 ←← │0x00│0x51│                  ・・・
      └──┴↑─┴──┴──┴──┴     ┴──┴──┴──┴──┘
              OK

リモコンデータの送信

       [0]   [1]   [2]   [3]   [4]   [5]   [6]   [7]   [8]           [63]  [64]
 write┌──┬──┬──┬──┬──┬──┬──┬──┬──┬      ┬──┬──┐
 →→ │0x00│0x600x??0x??0x??0x??0x??0x??0x??│・・・│0xFF│0xFF│
      └──┴──┴──┴──┴──┴──┴──┴──┴──┴     ┴──┴──┘
                   └─── 7バイトのリモコンコード────┘

C 言語によるデータ送受信実装例

Windows 用

  • コンソールアプリケーションとしての実装
  • 引数なしで実行するとリモコンデータ受信モードで起動
    C:\temp>BtoIrRemocon.exe
    waiting...
    C10220B00000B0  ←キットが受信・認識したリモコンコード
    
  • リモコンコードを引数に指定して起動するとキットからそれを送出する
    C:\temp>BtoIrRemocon.exe C10220B00000B0 p2 C10220B00000B2
    [C10220B00000B0]
    [p2]                 ←複数のコードを指定可,pN = N 秒間のポーズ
    [C10220B00000B2]
    
  • Windows 8 Pro (32 / 64 bit), 7 Professional SP1 (32 / 64 bit), Vista Business (32 / 64 bit), XP Professional SP3 32 bit, XP Home SP3 での動作を確認
/**
 *
 * BtoIrRemocon.c
 *
 * 2013 KLab Inc.
 *
 * ビット・トレード・ワン社製「USB 接続赤外線リモコンキット」を操作する
 * Windows コンソールプログラム
 *
 * 同社製品ページ:
 * http://bit-trade-one.co.jp/BTOpicture/Products/005-RS/index.html
 *
 * This software is provided "as is" without any express and implied warranty
 * of any kind. The entire risk of the quality and performance of this software
 * with you, and you shall use this software your own sole judgment and
 * responsibility. KLab shall not undertake responsibility or liability for
 * any and all damages resulting from your use of this software.
 * KLab does not warrant this software to be free from bug or error in
 * programming and other defect or fit for a particular purpose, and KLab does
 * not warrant the completeness, accuracy and reliability and other warranty
 * of any kind with respect to result of your use of this software.
 * KLab shall not be obligated to support, update or upgrade this software.
 *
 */

 /*
  使い方:
  
  0. PC に USB 接続赤外線リモコンキット(以下"USB IR REMOCON")を接続
  
  1.リモコンデータ受信モード
    BtoIrRemocon.exe を引数なしで実行するとリモコンデータ受信モードで起動する。
    手持ちの赤外線リモコンから USB IR REMOCON の赤外線 LED に向けて赤外線
    データを送るとコンソールに 14 文字のリモコンデータコードが表示される。
    例:C10220B0008030
 
  2.リモコンデータ送信モード
    BtoIrRemocon.exe を上記 1. のコードを引数として起動すると USB IR REMOCON
    の赤外線 LED からそのリモコンデータが照射される。コードは複数指定可能、
    途中に p<秒数> の要領でポーズ指定を記述できる。
    例:BtoIrRemocon C10220B0008030 p3 C10220B0008034 p3 C10220B0008036

*/

#include <windows.h>
#include <stdio.h>
#include <setupapi.h>

#pragma comment(lib, "setupapi")

#define RECEIVE_WAIT_MODE_NONE  0
#define RECEIVE_WAIT_MODE_WAIT  1

#define DEVICE_BUFSIZE       65
#define REMOCON_DATA_LENGTH   7

typedef void (__stdcall *DEF_HidD_GetHidGuid)(LPGUID HidGuid);

// PC に接続ずみの「USB 接続赤外線リモコンキット」のデバイスパスを取得
DWORD GetDevicePath(OUT char *pszDevicePath, IN DWORD cchBuf)
{
    // USB IR REMOCON 固有の ID 文字列
    char *szIdStr1 = "vid_22ea&pid_001e";
    char *szIdStr2 = "mi_03";
    int i;
    char *pszProp;
    HDEVINFO DeviceInfoTable = NULL;
    SP_DEVICE_INTERFACE_DATA DeviceIfData;
    PSP_DEVICE_INTERFACE_DETAIL_DATA pDeviceIfDetailData;
    SP_DEVINFO_DATA DevInfoData;
    DWORD InterfaceIndex, dwSize, dwError = ERROR_SUCCESS;
    HGLOBAL hMem = NULL;
    BOOL bFound;
    GUID InterfaceClassGuid;
    HMODULE hHidDLL;
    DEF_HidD_GetHidGuid pHidD_GetHidGuid;
    size_t len;

    //GUID InterfaceClassGuid = {0x4d1e55b2, 0xf16f, 0x11cf, 0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30};
    // HIDClass の GUID を取得
    hHidDLL = LoadLibrary("hid.dll");
    pHidD_GetHidGuid = (DEF_HidD_GetHidGuid)GetProcAddress(hHidDLL, "HidD_GetHidGuid");
    pHidD_GetHidGuid(&InterfaceClassGuid);
    FreeLibrary(hHidDLL);

    // HIDClass に属するデバイス群の含まれるデバイス情報セットを取得
    DeviceInfoTable = SetupDiGetClassDevs(&InterfaceClassGuid,
                            NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
    if(!DeviceInfoTable) {
        dwError = GetLastError();
        goto DONE;
    }
    // デバイス情報セットを走査し IR REMOCON デバイスを探す
    for (InterfaceIndex = 0; InterfaceIndex < 10000000; InterfaceIndex++) {
        DeviceIfData.cbSize = sizeof(DeviceIfData);
        if(SetupDiEnumDeviceInterfaces(DeviceInfoTable, NULL,
                            &InterfaceClassGuid, InterfaceIndex, &DeviceIfData)) {
            dwError = GetLastError();
            if (dwError == ERROR_NO_MORE_ITEMS) {
                goto DONE;
            }
        }
        else {
            dwError = GetLastError();
            goto DONE;
        }
        // 現在見ているデバイスの VID, PID の含まれるハードウェア ID 文字列を取得
        DevInfoData.cbSize = sizeof(DevInfoData);
        SetupDiEnumDeviceInfo(DeviceInfoTable, InterfaceIndex, &DevInfoData);
        SetupDiGetDeviceRegistryProperty(DeviceInfoTable, &DevInfoData,
                                    SPDRP_HARDWAREID, NULL, NULL, 0, &dwSize);
        hMem = GlobalAlloc(0, dwSize);
        if (!hMem) {
            dwError = GetLastError();
            goto DONE;
        }
        SetupDiGetDeviceRegistryProperty(DeviceInfoTable, &DevInfoData,
                            SPDRP_HARDWAREID, NULL, (PBYTE)hMem, dwSize, NULL);
        pszProp = strdup((char*)hMem);
        GlobalFree(hMem);
        hMem = NULL;
        
        // 取得したハードウェア ID 文字列に USB IR REMOCON 固有の VID, PID が含まれるか
        len = strlen(pszProp);
        for (i = 0; i < (int)len; i++) {
            pszProp[i] = tolower(pszProp[i]);
        }
        bFound = FALSE;
        if (strstr(pszProp, szIdStr1) != NULL && strstr(pszProp, szIdStr2) != NULL) {
            bFound = TRUE;
        }
        free(pszProp);

        // USB IR REMOCON 発見
        if (bFound) {
            // USB IR REMOCON のデバイスパスを得る
            SetupDiGetDeviceInterfaceDetail(DeviceInfoTable, &DeviceIfData,
                                                    NULL, 0, &dwSize, NULL);
            hMem = GlobalAlloc(0, dwSize);
            if (!hMem) {
                dwError = GetLastError();
                goto DONE;
            }
            pDeviceIfDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)hMem;
            pDeviceIfDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
            if (SetupDiGetDeviceInterfaceDetail(DeviceInfoTable, &DeviceIfData,
                                        pDeviceIfDetailData, dwSize, NULL, NULL)) {
                if (cchBuf > dwSize - sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA)) {
                    // 成功
                    strcpy(pszDevicePath, pDeviceIfDetailData->DevicePath);
                    dwError = ERROR_SUCCESS;
                } else {
                    dwError = ERROR_NOT_ENOUGH_MEMORY;
                    goto DONE;
                }
                goto DONE;
            }
            else {
                dwError = GetLastError();
                goto DONE;
            }
        }
    }
DONE:
    if (hMem) {
        GlobalFree(hMem);
    }
    if (DeviceInfoTable) {
        SetupDiDestroyDeviceInfoList(DeviceInfoTable);
    }
    return dwError;
}

// リモコンデータ送信モード
int Transfer(HANDLE hDevice, int ac, char **av)
{
    int i, j, sts = -1;
    DWORD len;
    BYTE buf[DEVICE_BUFSIZE];
    char *pt, *e, s[3] = "\0\0";

    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x60; // 送信指定    

    // パラメータを順に走査して処理
    for (i = 1; i < ac; i++) {
        pt = av[i];
        fprintf(stderr, "[%s]\n", pt);
        // "pN" => N 秒間ポーズ
        if (tolower(*pt) == 'p') {
            if (pt[1] != '\0' && isdigit(pt[1])) {
                Sleep(1000 * atoi(&pt[1]));
                continue;
            } else {
                fprintf(stderr, "[%s] is skipped..\n", pt);
                continue;
            }
        }
        else if (strlen(pt) != REMOCON_DATA_LENGTH * 2) {
            continue;
        }
        // 16 進文字列をバイト配列へ
        for (j = 0; j < REMOCON_DATA_LENGTH; j++) {
            s[0] = pt[j*2];
            s[1] = pt[j*2+1];
            buf[j+2] = (BYTE)strtoul(s, &e, 16);
            if (*e != '\0') { // 変換エラー
                break;
            }
        }
        if (j != REMOCON_DATA_LENGTH) {
            fprintf(stderr, "[%s] is skipped..\n", pt);
            continue;
        }
        // 1件のリモコンデータを送信
        if (!WriteFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL)) {
            fprintf(stderr, "WriteFile: err=%u\n", GetLastError());
            goto DONE;
        }
    }
    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x40; // デバイスの送信バッファをクリア
    WriteFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL);
    memset(buf, 0x00, sizeof(buf));
    ReadFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL);
    sts = 0;
DONE:
    return sts;
}

// リモコンデータ受信モード
int Display(HANDLE hDevice)
{
    int i, sts = -1;
    DWORD len;
    BYTE buf[DEVICE_BUFSIZE];

    // デバイスの送信バッファをクリア
    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x40;
    WriteFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL);
    memset(buf, 0x00, sizeof(buf));
    ReadFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL);

    fprintf(stderr, "waiting...\n");

    // デバイスをリモコンデータ受信待ちモードに
    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x51;
    buf[2] = RECEIVE_WAIT_MODE_WAIT;
    if (!WriteFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL)) {
        fprintf(stderr, "WriteFile: err=%u\n", GetLastError());
        goto DONE;
    }
    memset(buf, 0x00, sizeof(buf));
    if (!ReadFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL)) {
        fprintf(stderr, "ReadFile: err=%u\n", GetLastError());
        goto DONE;
    }
    if (buf[1] != 0x51) {
        fprintf(stderr, "invalid response");
        goto DONE;
    }
    // リモコンデータ受信待ち
    while (TRUE) {
        memset(buf, 0xFF, sizeof(buf));
        buf[0] = 0;
        buf[1] = 0x50;
        WriteFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL);
        memset(buf, 0x00, sizeof(buf));
        ReadFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL);
        if (buf[1] == 0x50 && buf[2] != 0) {
            // 受信データありなら 16 進表示
            for (i = 0; i < 7; i++) {
                printf("%02X", buf[i+2]);
            }
            putchar('\n');
            break;
        }
        Sleep(500);
    }
    // リモコンデータ受信待ちモードを解除
    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x51;
    buf[2] = RECEIVE_WAIT_MODE_NONE;
    WriteFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL);
    memset(buf, 0x00, sizeof(buf));
    ReadFile(hDevice, buf, DEVICE_BUFSIZE, &len, NULL);
    sts = 0;

DONE:
    return sts;
}

// エントリーポイント
int main(int ac, char **av)
{
    int sts = -1;
    char szDevicePath[256];
    HANDLE hDevice = INVALID_HANDLE_VALUE;
    BOOL doDisplay = FALSE;
    DWORD dwError;

    // 引数無しなら受信モード
    if (ac < 2) {
        doDisplay = TRUE;
    }
    // USB IR REMOCON のデバイスパスを得る
    dwError = GetDevicePath(szDevicePath, sizeof(szDevicePath));
    if (dwError != ERROR_SUCCESS) {
        fprintf(stderr, "failed to get device path: err=%u", dwError);
        goto DONE;
    }
    // データ送受信用にデバイスをオープン
    hDevice = CreateFile(szDevicePath, GENERIC_WRITE|GENERIC_READ,
            FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
    if (hDevice == INVALID_HANDLE_VALUE) {
        DWORD err = GetLastError();
        fprintf(stderr, "CreateFile: err=%u\n", err);
        goto DONE;
    }
    if (doDisplay) {
        sts = Display(hDevice); // 受信モードへ
    } else {
        sts = Transfer(hDevice, ac, av); // 送信モードへ
    }

DONE:
    if (hDevice != INVALID_HANDLE_VALUE) {
        CloseHandle(hDevice);
    }
    return sts;
}

Mac OS X 用

  • コンソールアプリケーションとしての実装
  • 引数なしで実行するとリモコンデータ受信モードで起動
    $ BtoIrRemoconMac
    waiting...
    C10220B00000B0  ←キットが受信・認識したリモコンコード
    
  • リモコンコードを引数に指定して起動するとキットからそれを送出する
    $ BtoIrRemoconMac C10220B00000B0 p2 C10220B00000B2
    [C10220B00000B0]
    [p2]                 ←複数のコードを指定可,pN = N 秒間のポーズ
    [C10220B00000B2]
    
  • Mac OS X 10.6.8 での動作を確認
/**
 *
 * BtoIrRemoconMac.c
 *
 * 2013 KLab Inc.
 *
 * ビット・トレード・ワン社製「USB 接続赤外線リモコンキット」を操作する
 * Mac OS X 用 コンソールプログラム
 *
 * 同社製品ページ:
 * http://bit-trade-one.co.jp/BTOpicture/Products/005-RS/index.html
 *
 * ビルド方法:
 * gcc -Wall -g BtoIrRemoconMac.c -framework IOKit -framework CoreFoundation -o BtoIrRemoconMac
 *  - gcc 4.2.1 build 5666 (Xcode 3.2.6) でのビルドを確認
 *
 * HID Class Device Interfaces Guide - developer.apple.com
 * http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/HID/intro/intro.html
 *
 *
 * This software is provided "as is" without any express and implied warranty
 * of any kind. The entire risk of the quality and performance of this software
 * with you, and you shall use this software your own sole judgment and
 * responsibility. KLab shall not undertake responsibility or liability for
 * any and all damages resulting from your use of this software.
 * KLab does not warrant this software to be free from bug or error in
 * programming and other defect or fit for a particular purpose, and KLab does
 * not warrant the completeness, accuracy and reliability and other warranty
 * of any kind with respect to result of your use of this software.
 * KLab shall not be obligated to support, update or upgrade this software. 
 *
 */

 /*
  使い方:
 
  0. PC に USB 接続赤外線リモコンキット(以下"USB IR REMOCON")を接続
 
  1.リモコンデータ受信モード
  BtoIrRemoconMac を引数なしで実行するとリモコンデータ受信モードで起動する。
  手持ちの赤外線リモコンから USB IR REMOCON の赤外線 LED に向けて赤外線
  データを送るとコンソールに 14 文字のリモコンデータコードが表示される。
  例:C10220B0008030
 
  2.リモコンデータ送信モード
  BtoIrRemoconMac を上記 1. のコードを引数として起動すると USB IR REMOCON
  の赤外線 LED からそのリモコンデータが照射される。コードは複数指定可能、
  途中に p<秒数> の要領でポーズ指定を記述できる。
  例:BtoIrRemoconMac C10220B0008030 p3 C10220B0008034 p3 C10220B0008036

 */

#include <stdio.h>
#include <unistd.h>
#include <IOKit/hid/IOHIDManager.h>
#include <IOKit/hid/IOHIDKeys.h>
#include <CoreFoundation/CoreFoundation.h>

#define RECEIVE_WAIT_MODE_NONE  0
#define RECEIVE_WAIT_MODE_WAIT  1

#define DEVICE_BUFSIZE       65
#define REMOCON_DATA_LENGTH   7

static int g_readBytes;

// 指定されたキーの整数プロパティを取得
int getIntProperty(IOHIDDeviceRef inIOHIDDeviceRef, CFStringRef inKey) {
    int val;
       if (inIOHIDDeviceRef) {
        CFTypeRef tCFTypeRef = IOHIDDeviceGetProperty(inIOHIDDeviceRef, inKey);
        if (tCFTypeRef) {
            if (CFNumberGetTypeID() == CFGetTypeID(tCFTypeRef)) {
                if (!CFNumberGetValue( (CFNumberRef) tCFTypeRef, kCFNumberSInt32Type, &val)) {
                    val = -1;
                }
            }
        }
    }
    return val;
}

// レポートのコールバック関数
static void reportCallback(void *inContext, IOReturn inResult, void *inSender,
                           IOHIDReportType inType, uint32_t inReportID,
                           uint8_t *inReport, CFIndex InReportLength)
{
    g_readBytes = InReportLength;
}

// デバイスからの読み込み
int ReadFromeDevice(IOHIDDeviceRef dev, unsigned char *buf, size_t bufsize, CFTimeInterval timeoutSecs)
{
    IOHIDDeviceRegisterInputReportCallback(dev,
                                       &buf[1],
                                       bufsize-1,
                                       reportCallback,
                                       NULL);
    g_readBytes = -1;
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeoutSecs, false);
    //printf("ReadFromeDevice: len=%d, 0=%X 1=%X 2=%X\n", g_readBytes, buf[0], buf[1], buf[2]);
    return g_readBytes;
}

// デバイスへの書き込み
IOReturn WriteToDevice(IOHIDDeviceRef dev, unsigned char *data, size_t len)
{
    IOReturn ret = IOHIDDeviceSetReport(dev, kIOHIDReportTypeOutput, data[0], data+1, len-1);
    if (ret != kIOReturnSuccess) {
        //printf("WriteToDevice: ret=0x%08X\n", ret);
    }
    return ret;
}

// リモコンデータ送信モード
int Transfer(IOHIDDeviceRef refDevice, int ac, char **av)
{
    int i, j, sts = -1;
    unsigned char buf[DEVICE_BUFSIZE];
    char *pt, *e, s[3] = "\0\0";
    
    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x60; // 送信指定    
    
    // パラメータを順に走査して処理
    for (i = 1; i < ac; i++) {
        pt = av[i];
        fprintf(stderr, "[%s]\n", pt);
        // "pN" => N 秒間ポーズ
        if (tolower(*pt) == 'p') {
            if (pt[1] != '\0' && isdigit(pt[1])) {
                sleep(atoi(&pt[1]));
                continue;
            } else {
                fprintf(stderr, "[%s] is skipped..\n", pt);
                continue;
            }
        }
        else if (strlen(pt) != REMOCON_DATA_LENGTH * 2) {
            continue;
        }
        // 16 進文字列をバイト配列へ
        for (j = 0; j < REMOCON_DATA_LENGTH; j++) {
            s[0] = pt[j*2];
            s[1] = pt[j*2+1];
            buf[j+2] = (unsigned char)strtoul(s, &e, 16);
            if (*e != '\0') { // 変換エラー
                break;
            }
        }
        if (j != REMOCON_DATA_LENGTH) {
            fprintf(stderr, "[%s] is skipped..\n", pt);
            continue;
        }
        // 1件のリモコンデータを送信
        if (WriteToDevice(refDevice, buf, DEVICE_BUFSIZE) != kIOReturnSuccess) {
            fprintf(stderr, "WriteToDevice: err\n");
            goto DONE;
        }
    }
    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x40; // デバイスの送信バッファをクリア
    WriteToDevice(refDevice, buf, DEVICE_BUFSIZE);
    memset(buf, 0x00, sizeof(buf));
    ReadFromeDevice(refDevice, buf, DEVICE_BUFSIZE, 0.5);
    sts = 0;
DONE:
    return sts;
}

// リモコンデータ受信モード
int Display(IOHIDDeviceRef refDevice)
{
    int i, sts = -1;
    unsigned char buf[DEVICE_BUFSIZE];
    
    // デバイスの送信バッファをクリア
    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x40;
    WriteToDevice(refDevice, buf, DEVICE_BUFSIZE);
    memset(buf, 0x00, sizeof(buf));
    ReadFromeDevice(refDevice, buf, DEVICE_BUFSIZE, 0.5);
    
    fprintf(stderr, "waiting...\n");
    
    // デバイスをリモコンデータ受信待ちモードに
    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x51;
    buf[2] = RECEIVE_WAIT_MODE_WAIT;
    if (WriteToDevice(refDevice, buf, DEVICE_BUFSIZE) != kIOReturnSuccess) {
        fprintf(stderr, "WriteToDevice: err\n");
        goto DONE;
    }
    memset(buf, 0x00, sizeof(buf));
    if (ReadFromeDevice(refDevice, buf, DEVICE_BUFSIZE, 0.5) < 0) {
        fprintf(stderr, "ReadFromeDevice: err\n");
        goto DONE;
    }
    if (buf[1] != 0x51) {
        fprintf(stderr, "invalid response");
        goto DONE;
    }
    // リモコンデータ受信待ち
    while (true) {
        memset(buf, 0xFF, sizeof(buf));
        buf[0] = 0;
        buf[1] = 0x50;
        WriteToDevice(refDevice, buf, DEVICE_BUFSIZE);
        memset(buf, 0x00, sizeof(buf));
        ReadFromeDevice(refDevice, buf, DEVICE_BUFSIZE, 0.5);
        if (buf[1] == 0x50 && buf[2] != 0) {
            // 受信データありなら 16 進表示
            for (i = 0; i < 7; i++) {
                printf("%02X", buf[i+2]);
            }
            putchar('\n');
            break;
        }
    }
    // リモコンデータ受信待ちモードを解除
    memset(buf, 0xFF, sizeof(buf));
    buf[0] = 0;
    buf[1] = 0x51;
    buf[2] = RECEIVE_WAIT_MODE_NONE;
    WriteToDevice(refDevice, buf, DEVICE_BUFSIZE);
    memset(buf, 0x00, sizeof(buf));
    ReadFromeDevice(refDevice, buf, DEVICE_BUFSIZE, 0.5);
    sts = 0;
DONE:
    return sts;
}

int main(int ac, char *av[])
{
    int vid, myVID = 0x22ea; // BTO IR REMOCON のベンダ ID
    int pid, myPID = 0x001e; // BTO IR REMOCON のプロダクト ID
    int i, sts = -1;
    IOReturn ret;
    unsigned char buf[65];
    Boolean doDisplay = false;
    IOHIDManagerRef refHidMgr = NULL;
    IOHIDDeviceRef refDevice, *prefDevs = NULL;
    CFSetRef refDevSet = NULL;
    CFIndex numDevices;
    
    // 引数無しなら受信モード
    if (ac < 2) {
        doDisplay = true;
    }
    
    // HID マネージャリファレンスを生成
    refHidMgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
    // すべての HID デバイスを対象とする
    IOHIDManagerSetDeviceMatching(refHidMgr, NULL);
    IOHIDManagerScheduleWithRunLoop(refHidMgr, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
    // HID マネージャを開く
    IOHIDManagerOpen(refHidMgr, kIOHIDOptionsTypeNone);
    // マッチしたデバイス群のセットを得る
    refDevSet = IOHIDManagerCopyDevices(refHidMgr);
    numDevices = CFSetGetCount(refDevSet);
    prefDevs = malloc(numDevices * sizeof(IOHIDDeviceRef));
    // セットから値を取得
    CFSetGetValues(refDevSet, (const void **)prefDevs);
    
    // HID デバイス群を走査して BTO IR REMOCON を探す
    for (i = 0; i < numDevices; i++) {
        refDevice = prefDevs[i];
        // VID, PID をチェック
        vid = getIntProperty(refDevice, CFSTR(kIOHIDVendorIDKey)); 
        pid = getIntProperty(refDevice, CFSTR(kIOHIDProductIDKey));
        if (vid != myVID || pid != myPID) {
            refDevice = NULL;
            continue;
        }
        // デバイスのオープン
        ret = IOHIDDeviceOpen(refDevice, kIOHIDOptionsTypeNone);    
        if (ret != kIOReturnSuccess) {
            refDevice = NULL;
            continue;
        }
        // 試し打ち
        memset(buf, 0xFF, sizeof(buf));
        buf[0] = 0x00;
        buf[1] = 0x40;
        if (WriteToDevice(refDevice, buf, DEVICE_BUFSIZE) == kIOReturnSuccess) {
            memset(buf, 0, sizeof(buf));
            int bytes = ReadFromeDevice(refDevice, buf, DEVICE_BUFSIZE, 0.5);
            if (bytes >= 0 && buf[1] == 0x40) {
                break; // OK
            }
        }
        IOHIDDeviceClose(refDevice, kIOHIDOptionsTypeNone);
        refDevice = NULL;
    }
    if (!refDevice) {
        fprintf(stderr, "device not found\n");
        goto DONE;
    }
    
    if (doDisplay) {
        sts = Display(refDevice); // 受信モードへ
    } else {
        sts = Transfer(refDevice, ac, av); // 送信モードへ
    }
    IOHIDDeviceClose(refDevice, kIOHIDOptionsTypeNone);
    
DONE:
    if (prefDevs) {
        free(prefDevs);
    }
    if (refDevSet) {
        CFRelease(refDevSet);
    }
    if (refHidMgr) {
        IOHIDManagerClose(refHidMgr, kIOHIDOptionsTypeNone);
        CFRelease(refHidMgr);    
    }
    return sts;
}

(tanabe)
klab_gijutsu2 at 11:09│Comments(14)TrackBack(0)win | mac

トラックバックURL

この記事へのコメント

1. Posted by やもり   2013年02月25日 07:45
はじめまして。

HIDのコード参考になりました。こちらも
赤外線リモコン関係をいろいろいじってい
ますのでで、見てやってください。
2. Posted by tanabe   2013年02月25日 08:46
やもりさん、こんにちは。興味深い記事をありがとうございます。
じっくり拝見します

3. Posted by mucho   2013年08月03日 08:31
OSX関連の記事、バイナリとも大変参考になりました。
4. Posted by skykiz   2014年05月05日 07:19
4 ありがとうございます。
参考になりました。

新しいのも出たようですね。
ttp://www.omiya-giken.com/?page_id=837

まぁまぁのようです。
ttp://cubic9.com/Devel/%C5%C5%BB%D2%B9%A9%BA%EE/irMagician/
5. Posted by tanabe   2014年05月09日 10:26
skykizさん、こんにちは。興味深い情報をありがとうございます。面白そうですね。参考にさせて頂きます
6. Posted by hamatta   2014年12月01日 12:07
5 MACで使えそうなので参考にしました。
yosemiteのせいか、うまく動くとき動かない時のばらつきがありました。

よくわかっていないのですが、VID,PIDのチェックのところで、kIOHIDPrimaryUsagePageKeyもチェックするとうまくいくみたいです。
getIntProperty(refDevice, CFSTR(kIOHIDPrimaryUsagePageKey));
の戻り値が、0xFF00の以外の時をcontinueの条件に追加しました。

送信、受信とも止まることなく動くようになりました。

7. Posted by Yoseimite   2015年02月08日 20:54
Yosemite対応、エアコン対応版を作りましたのでDiffを貼っておきます。
以下のソースの差分をほぼ流用してちょっと追加しただけです。
https://github.com/Velmy/BtoIrRemocon/tree/vs2010

$ diff -w BtoIrRemocon.c BtoIrRemoconMac/BtoIrRemoconMac/BtoIrRemocon.c
63c63
< #define REMOCON_DATA_LENGTH 7
---
> #define REMOCON_DATA_LENGTH (1 + 2 + 32)
124c124
< buf[1] = 0x60; // 送信指定
---
> buf[1] = 0x61; // 送信指定
192c192
< buf[1] = 0x51;
---
> buf[1] = 0x53;
203c203
< if (buf[1] != 0x51) {
---
> if (buf[1] != 0x53) {
211c211
< buf[1] = 0x50;
---
> buf[1] = 0x52;
215c215
< if (buf[1] == 0x50 && buf[2] != 0) {
---
> if (buf[1] == 0x52 && buf[2] != 0) {
217c217
< for (i = 0; i < 7; i++) {
---
> for (i = 0; i < REMOCON_DATA_LENGTH; i++) {
240a241,242
> int uPage, myUPage = 0xFF00; // Usage Page = Microsoft
> int usage, myUsage = 0x01; // Usage = Base Up
278a281,286
> usage = getIntProperty(refDevice, CFSTR(kIOHIDPrimaryUsageKey));
> uPage = getIntProperty(refDevice, CFSTR(kIOHIDPrimaryUsagePageKey));
> if (uPage != myUPage || usage != myUsage) {
> refDevice = NULL;
> continue;
> }
$

8. Posted by yamada   2015年08月14日 18:07
5 コンソールアプリケーションを利用させて頂きました。
ありがとうございました。
9. Posted by tanabe   2015年08月14日 19:12
yamada さん、コメントをありがとうございます。記事がお役に立ち幸いです :-)
10. Posted by ブラックジャンク   2016年07月16日 21:20
ブログを見せていただきビルト済実行形式をダウンロードしたのですが解凍できずコマンド入力ができません。
この形式今でも使えるのでしょうか?
こちらのosはWindows10です。どうしてもシャープレコーダーhddの換装したいのでぜひ教えていただけないでしょうか?
11. Posted by tanabe   2016年07月16日 21:46
ブラックジャンクさん、コメントをありがとうございます。
>ビルト済実行形式をダウンロードしたのですが解凍できず
手元の環境(非 Windows10)では解凍そのものに支障は見受けられないのですが
ダウンロードされたアーカイブに問題はないでしょうか。まずその点をご確認下さい。
Windows10 での動作は未確認です。ご覧のようにソースコードをすべて公開していますので、
もし何か支障がありましたらお手元で改修の上実行形式をビルドしてお試し下さい。

12. Posted by ブラックジャンク   2016年07月18日 12:26
アドバイスありがとうございます。ダウンロード後解凍できたかはわかりませんがダウンロードしてリモコンを繋ぐとなにやら黒い画面になりwaitingと出たまま変化ありません。やはり何か手順を間違っているのでしょうか?ダウンロードしてからリモコンをUSB接続するのでしょうか?接続したままダウンロードするのでしょうか?
13. Posted by tanabe   2016年07月19日 05:28
>なにやら黒い画面になりwaitingと出たまま変化ありません
コンソールアプリケーションにあまり馴染みのない場合は
メーカー様公式の GUI アプリのほうが扱いやすいと思います。
良い展開をお祈りします。



14. Posted by やもり   2016年09月20日 16:50
こちらのページのMacのコードを利用してmrubyのmrbgemを作ってみました。最初libusbのコードをそのまま使おうかと思ったのですが、hidとしてつかまれてしまいlibusbからはアクセスできなくなるのでこちらのコードを利用させてもらいました。ありがとうございます。

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント