macOSがサポートしていないスキャナにSANEを介してアクセス

　相当古いスキャナ"CanoScan N676U"を持っているのですが、もうmacOSもWindowsもサポートしていないので、使うのが結構面倒でした。使うには、VirtualBox上のUbuntu(Linux)を起動するしか方法がありませんでした。
　Ubuntuの場合、SANEを介してアクセスしているので、macOSにSANEをインストールしたら動作するのかと調べてみると、Homebrewにformulaがあるので、インストールしてみます。

```bash
$ brew install sane-backends
```

デバイスをSANEに含まれるscanimageで検索してみます。

```bash
$ scanimage -L

No scanners were identified. If you were expecting something different,
check that the scanner is plugged in, turned on and detected by the
sane-find-scanner tool (if appropriate). Please read the documentation
which came with this software (README, FAQ, manpages).
```

 まったく同じデバイスをVirtualBox上のUbuntuでは見つかるのに、macOSで見つからないというのは不思議です。何か調べる方法はないかと探してみると、SANEは、特別な環境変数を指定するとデバッグログを出力できることがわかりました。

```bash
$ export SANE_DEBUG_<バックエンド名> = <ログレベル>
```

バックエンド名は[SANE: Supported Devices](http://www.sane-project.org/sane-mfgs.html "SANE: Supported Devices")より見つけることができます。
自分のスキャナはplustekというバックエンドを使っているので、以下のように環境変数を設定します。加えて、USBバックエンドもデバッグログを出力するように設定します。

```bash
$ export SANE_DEBUG_PLUSTEK=9
$ export SANE_DEBUG_SANEI_USB=9
```

まず、デバイスのベンダーIDとプロダクトIDを取得します。

```bash
$ system_profiler SPUSBDataType

        CanoScan:

          Product ID: 0x220d
          Vendor ID: 0x04a9  (Canon Inc.)
```

デバッグログを有効にしてscanimageを再度実行してみます。

```bash
$ scanimage -L
[sanei_usb] libusb_scan_devices: device 0x04a9/0x220d at 020:018 is not configured
```

　すると、上のように"not configured"と表示されます。
　ここからはUSBの仕様の話になりますが、USBデバイスは一つ以上のConfigurationを持っており、そのどれか一つが設定されていないと使用することができません。どうもUbuntu(Linux)の場合、接続するだけConfigurationが行われるので問題なくSANEでアクセスできるのですが、macOSの場合、デバイスドライバがConfigurationを担当しているので、デバイスドライバがないデバイスだとConfigurationされていない状態になるようです。
 なので、Configurationするコマンドラインアプリを作成してみます。Swiftで作成したいところですが、USBへアクセスするのに必要となるIOKitはSwiftからアクセスするのが大変なので、Objective-Cにしました。スキャナのVendor IDとProduct IDは、コードに直接記述しています。

```Objective-C:main.m
#import <Foundation/Foundation.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/usb/IOUSBLib.h>

int main(int argc, const char *argv[]) {
  @autoreleasepool {
    UInt16 scannerVendorID = 0x04a9;
    UInt16 scannerProductID = 0x220d;
    
    CFMutableDictionaryRef matchingDict =
        IOServiceMatching(kIOUSBDeviceClassName);
    if (matchingDict == NULL) {
      NSLog(@"IOServiceMatching failed");
      return -1;
    }

    io_iterator_t iter;
    kern_return_t kr =
        IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &iter);
    if (kr != KERN_SUCCESS) {
      NSLog(@"IOServiceGetMatchingServices failed");
      return -1;
    }

    io_service_t device;
    while ((device = IOIteratorNext(iter)) != 0) {
      io_name_t name;
      kern_return_t ret = IORegistryEntryGetName(device, name);
      if (ret != kIOReturnSuccess) {
        NSLog(@"IORegistryEntryGetName failed");
        continue;
      }
      NSLog(@"Device %s", name);

      IOCFPlugInInterface **plugin;
      SInt32 score;
      ret = IOCreatePlugInInterfaceForService(
          device, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin,
          &score);
      if (ret != kIOReturnSuccess) {
        NSLog(@"IOCreatePlugInInterfaceForService failed");
        continue;
      }
      IOUSBDeviceInterface **dev;
      HRESULT result = (*plugin)->QueryInterface(
          plugin, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID), (LPVOID *)&dev);
      if (result != S_OK) {
        NSLog(@"QueryInterface failed");
        continue;
      }
      IODestroyPlugInInterface(plugin);

      UInt16 vendor;
      IOReturn ioRet = (*dev)->GetDeviceVendor(dev, &vendor);
      if (ioRet != kIOReturnSuccess) {
        NSLog(@"GetDeviceVendor failed");
        continue;
      }
      NSLog(@"Vendor: %d (%x)", (int)vendor, (int)vendor);

      UInt16 product;
      ioRet = (*dev)->GetDeviceProduct(dev, &product);
      if (ioRet != kIOReturnSuccess) {
        NSLog(@"GetDeviceProduct failed");
        continue;
      }
      NSLog(@"Product: %d (%x)", (int)product, (int)product);

      UInt8 numberOfConfigurations;
      ioRet = (*dev)->GetNumberOfConfigurations(dev, &numberOfConfigurations);
      if (ioRet != kIOReturnSuccess) {
        NSLog(@"GetNumberOfConfigurations failed");
        continue;
      }
      NSLog(@"numberOfConfigurations = %d", (int)numberOfConfigurations);

      UInt8 configNum;
      ioRet = (*dev)->GetConfiguration(dev, &configNum);
      if (ioRet != kIOReturnSuccess) {
        NSLog(@"GetConfiguration failed");
        continue;
      }
      NSLog(@"current configuration = %d", (int)configNum);

      for (int i = 0; i < numberOfConfigurations; ++i) {
        IOUSBConfigurationDescriptorPtr desc;
        ioRet = (*dev)->GetConfigurationDescriptorPtr(dev, i, &desc);
        if (ioRet != kIOReturnSuccess) {
          NSLog(@"GetConfigurationDescriptorPtr failed");
          continue;
        }
        NSLog(@"%d: configuration value = %d", i,
              (int)desc->bConfigurationValue);
      }

      if (vendor == scannerVendorID && product == scannerProductID) {
        NSLog(@"Found Image Scanner");
        if (configNum != 0) {
          NSLog(@"already configured");
        } else {
          ioRet = (*dev)->USBDeviceOpen(dev);
          if (ioRet != kIOReturnSuccess) {
            NSLog(@"USBDeviceOpen failed");
            continue;
          }

          IOUSBConfigurationDescriptorPtr desc;
          ioRet = (*dev)->GetConfigurationDescriptorPtr(dev, 0, &desc);
          if (ioRet != kIOReturnSuccess) {
            (*dev)->USBDeviceClose(dev);
            NSLog(@"GetConfigurationDescriptorPtr failed");
            continue;
          }

          ioRet = (*dev)->SetConfiguration(dev, desc->bConfigurationValue);
          if (ioRet != kIOReturnSuccess) {
            NSLog(@"SetConfiguration failed");
            (*dev)->USBDeviceClose(dev);
            continue;
          }
          NSLog(@"Configured");

          ioRet = (*dev)->USBDeviceClose(dev);
          if (ioRet != kIOReturnSuccess) {
            NSLog(@"USBDeviceClose failed");
          }
        }
      }

      IOObjectRelease(device);
      NSLog(@"-----");
    }

    /* Done, release the iterator */
    IOObjectRelease(iter);
  }
  return 0;
}
```

このアプリを起動した後で、scanimageを実行すると、今回は無事にデバイスが見つかりました。