デジカメ(Nicon D3300)をPCにUSB接続すると外部ドライブとして認識せずにリムーバブル記憶域になってしましまい単純なファイルコピーコマンドが使えません。
調べたところD3300はWindows Portable Device(WPD)というデバイスらしくそれ用のコードでないといけないようでその方法を紹介します。
実装内容
今回やりたいことは以下になり、すべてのコードが実行後にアプリを終了させたかったためConsoleアプリとして作成しました。
今回のソースはこちらのGitにアップしておきました。
- PCに接続された12台のD3300からローカルの指定したフォルダに写真をコピー
- コピーが終了したらD3300(SDカード)内のファイルを削除
- ファイル削除が終わったらアプリ終了
1台のカメラ接続でローカルに写真をコピー
WPD: Transferring Contentから『WPD: Enumerating Content Source Code』をダウンロードします。この記事にコピーの手順が書いてありますが、ちょっとわかりにくい部分もあるんで簡単に説明します。
まず PortableDevices.cs にコピーの関数を追加します。
PortableDevices.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public void DownloadFile(PortableDeviceFile file, string saveToPath) { IPortableDeviceContent content; this._device.Content(out content); IPortableDeviceResources resources; content.Transfer(out resources); PortableDeviceApiLib.IStream wpdStream; uint optimalTransferSize = 0; var property = new _tagpropertykey(); property.fmtid = new Guid(0xE81E79BE, 0x34F0, 0x41BF, 0xB5, 0x3F, 0xF1, 0xA0, 0x6A, 0xE8, 0x78, 0x42); property.pid = 0; resources.GetStream(file.Id, ref property, 0, ref optimalTransferSize, out wpdStream); System.Runtime.InteropServices.ComTypes.IStream sourceStream = (System.Runtime.InteropServices.ComTypes.IStream)wpdStream; var filename = Path.GetFileName(file.Name); FileStream targetStream = new FileStream(Path.Combine(saveToPath, filename), FileMode.Create, FileAccess.Write); unsafe { var buffer = new byte[1024]; int bytesRead; do { sourceStream.Read(buffer, 1024, new IntPtr(&bytesRead)); targetStream.Write(buffer, 0, 1024); } while (bytesRead > 0); targetStream.Close(); } Marshal.ReleaseComObject(sourceStream); Marshal.ReleaseComObject(wpdStream); } |
unsafe を使っているので Menu > Project > PortableDevices Properties... > Build の Allow unsafe code にチェックを入れてください。
次に Program.cs にDownloadFile関数を呼び出すコードを追加します。ハイライン部分がそれになります。これで1台のデジカメで写真のコピーができるはずです。
Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public static void DisplayFolderContents(PortableDevice device, PortableDeviceFolder folder) { foreach (var item in folder.Files) { Console.WriteLine(item.Name); if (item is PortableDeviceFile) { device.DownloadFile((PortableDeviceFile)item, @"F:\t_tmp\"); } if (item is PortableDeviceFolder) { DisplayFolderContents(device, (PortableDeviceFolder) item); } } } |
複数台のカメラに対応したコピー
ソースを読んだ感じこのコードのままで複数台対応できるはずなんですが、なぜか2台目以降を認識してくれなかったため PortableDevices.cs のRefresh関数を変更します。(以下は2台を認識させるためのコードです。)
PortableDevices.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public void Refresh() { this._deviceManager.RefreshDeviceList(); var deviceIds = new string[2]; deviceIds[0] = @"\\?\usb#vid_04b0&pid_0433#*******#{6ac27878-a6fa-4155-ba85-f98f491d4f33}"; deviceIds[1] = @"\\?\usb#vid_04b0&pid_0433#*******#{6ac27878-a6fa-4155-ba85-f98f491d4f33}"; foreach (var deviceId in deviceIds) { Add(new PortableDevice(deviceId)); } } |
ハイライトになっている部分の説明
- vid_04b0 -> ベンダーID
- pid_0433 -> プロダクトID
- ******* -> 端末固有ID(この記事ではIDは伏せておきます)
- {6ac27878-a6fa-4155-ba85-f98f491d4f33 -> 詳しくはわかりませんが外部デバイスであることを示しているようです。
手っ取り早くこれらを調べるにはRefresh関数のオリジナルコードに Console.Writeline を追加して調べてみるのがいいです。一応書いておくと以下のように。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public void Refresh() { this._deviceManager.RefreshDeviceList(); // Determine how many WPD devices are connected var deviceIds = new string[1]; uint count = 1; this._deviceManager.GetDevices(ref deviceIds[0], ref count); // Retrieve the device id for each connected device deviceIds = new string[count]; this._deviceManager.GetDevices(ref deviceIds[0], ref count); Console.WriteLine("deviceIds0: " + deviceIds[0]); foreach (var deviceId in deviceIds) { Add(new PortableDevice(deviceId)); } } |
D3300のSDカードからコピーした写真を削除
WPD: Transferring Contentのコメント欄にChristopheさんが書き込みをしています。次のURLからダウンロードしてください。
http://dl.dropbox.com/u/40603470/WPDDeletingContent.zip
PortableDevices.cs を開けてDeleteFile関数とStringToPropVariant関数をコピーしてきてそれをPortableDevicesプロジェクトの PortableDevices.cs に追加します。
Program.csのDisplayFolderContents関数にDeleteFile関数を呼び出すコードを追加します。そうするとDisplayFolderContents関数は以下の様なコードになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public static void DisplayFolderContents(PortableDevice device, PortableDeviceFolder folder) { foreach (var item in folder.Files) { Console.WriteLine(item.Name); if (item is PortableDeviceFile) { device.DownloadFile((PortableDeviceFile)item, @"F:\t_tmp\"); } if (item is PortableDeviceFile) { device.DeleteFile((PortableDeviceFile)item); } if (item is PortableDeviceFolder) { DisplayFolderContents(device, (PortableDeviceFolder) item); } } } |
これでコピー後にファイルが削除されているはずです。
並列処理にする
このままだと12台コピーし終わるまでにかなり時間がかかってしまいますので Program.cs のMain関数内を並列処理に書き換えました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static void Main() { var collection = new PortableDeviceCollection(); collection.Refresh(); Parallel.ForEach(collection, device => { device.Connect(); Console.WriteLine(device.FriendlyName); var folder = device.GetContents(); foreach (var item in folder.Files) { DisplayObject(device, item); } device.Disconnect(); }); } |
コメントを残す