銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

Q#プロジェクトでC#のクラスが不要になりました

しばらくQ#の記事を書いていなかったのですが、最近のQ#の更新でC#のクラスが不要になっていたので久しぶりに記事を書きたいと思います。

少し古い記事ですが、Q#プロジェクトというのは、プロジェクトとしては.NET Coreのプロジェクトで、エントリポイントとなるMainメソッドを含むC#クラスが存在し、そこから呼び出されるQ#のソースが存在する構造でした。

tech.tanaka733.net

ですが、今年4月にリリースされたVersion 0.11.2004.2825からこのC#クラスが不要になりました。

例えば最新のQ# SDKでプロジェクトを作るとこのような構造のProgram.qsファイルとcsprojファイルのみのプロジェクトが作られます。

namespace Quantum.QSharpCompilerExtensionLab {

    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;

    
    @EntryPoint()
    operation HelloQ () : Unit {
        Message("Hello quantum world!");
    }
}

この@EntryPoint()というのがC#のMainメソッドに相当するものになります。これがどのように動くのかが気になるので、コンパイル(トランスパイル)結果を見てみましょう。最新のバージョンでもQ#コードはいったんC#にトランスパイルされてからコンパイルされます。C#としての出力は、/obj/qsharp/src以下にあります。

f:id:tanaka733:20200607212741p:plain

このProgram.g.csのほうが以前と同じQ#コードをC#コードにトランスパイルしたもののようです。もう一つのProgram.EntryPoint.g.csのほうがMainメソッドを含むエントリーポイントのためのコンパイル結果です。実際にはこのようになっています。

//------------------------------------------------------------------------------
// <auto-generated>                                                             
//     This code was generated by a tool.                                       
//     Changes to this file may cause incorrect behavior and will be lost if    
//     the code is regenerated.                                                 
// </auto-generated>                                                            
//------------------------------------------------------------------------------
namespace Quantum.QSharpCompilerExtensionLab
{
    using System;
    using Microsoft.Quantum.Core;
    using Microsoft.Quantum.Intrinsic;
    using Microsoft.Quantum.Simulation.Core;

    internal class __QsEntryPoint__ : Microsoft.Quantum.CsharpGeneration.EntryPointDriver.IEntryPoint<QVoid, QVoid>
    {
        public string Summary => "";
        public System.Collections.Generic.IEnumerable<System.CommandLine.Option> Options => new System.CommandLine.Option[] { };
        public string DefaultSimulator => "QuantumSimulator";
        public EntryPointInfo<QVoid, QVoid> Info => Quantum.QSharpCompilerExtensionLab.HelloQ.Info;
        public IOperationFactory CreateDefaultCustomSimulator() => throw new InvalidOperationException();
        public QVoid CreateArgument(System.CommandLine.Parsing.ParseResult parseResult) => QVoid.Instance;
        private static async System.Threading.Tasks.Task<int> Main(string[] args) => await new Microsoft.Quantum.CsharpGeneration.EntryPointDriver.Driver<Quantum.QSharpCompilerExtensionLab.HelloQ, QVoid, QVoid>(new __QsEntryPoint__()).Run(args);
    }
}

MainメソッドはDriver.Runというメソッドを呼び出しており、このDriverクラスが以前のQ#プロジェクトで書いていたC#クラス内の処理を行っているように見えます。また、SummaryやDefaultSimulatorみたいな項目もありますが、現状@EntryPointにパラメーターを渡せないで、この辺はまだ変更できなさそうです。もしかすると今後のバージョンアップで機能追加されるかもしれません。

Visual Studio Live Shareを使ってオンラインハンズオンをやってみた

本記事はMicrosoft MVPブログ企画の記事として投稿しています。その他の記事はこちらからご覧ください。

昨日、オンラインでTeamsとVisual Studio Live Shareを使ったオンラインハンズオンを開催しました。

csharp-tokyo.connpass.com

Visual Studio Live Shareを使ってコードを見せながらオンラインハンズオンをやるのは割と便利だったのですが、少し注意しないといけない点もあったのでまとめてみます。Live Shareの概要についてはこちらを。

docs.microsoft.com

Live Shareはペアプロみたいな用途を想定されていると思われるかもしれませんが、読み取り専用アクセスでも共有できるのでハンズオンのような講義用途も想定されています。

docs.microsoft.com

よかった点

Windows, Mac, Linuxで見ることができる

Visual StudioおよびVisual Studio Codeでお互いに共有できるのでいずれの環境でも見ることができます。またプレビュー版ですがブラウザからアクセスすることができます。

docs.microsoft.com

.NET Coreというクロスプラットフォームな開発ツールのハンズオンだと、参加する人は自分の普段使っているデバイスを使いたいわけですが、それを実現することができます。

いま見てほしいコードを見せられる

TeamsなどWeb会議ツールを併用すれば、今説明しているコードそのものを見せることは可能です。IDEで共有することによって、さらに見ているコード近辺の気になる部分であったり、参照関係にあるコードなどを自分の好きなタイミングで見ることができます。この時説明している人のコードを見失っても、参加者一覧の中の説明している人をダブルクリックすることでそのコードに戻ることができます。

f:id:tanaka733:20200523004257p:plain

docs.microsoft.com

準備が簡単

今までも、クラウド環境などを使って同一環境を複数準備して、それぞれにログインしてもらってハンズオンを行うという方法はありました。ただ、クラウドのコストがかかったり、環境を作るのが手間だったりしました。Live Shareであれば、基本的には自分がハンズオンの手順通りにコードを書いていくだけで、それをそのまま共有することができます。今回用意したのは、このリポジトリだけです。

github.com

自分が動かしているWebアプリに参加者からアクセスしてもらえる

共有ローカルサーバーという機能があって、自分がプロジェクトをデバッグ起動したとき、そのポートに参加者の方がアクセスするようにできます。

f:id:tanaka733:20200523004717p:plain

docs.microsoft.com

また今回あまり活用できませんでしたが、シェルを共有する方法もあります。

docs.microsoft.com

注意しないといけない点

デフォルト5名、最大30名まで

デフォルトだと5名までしか参加できません。設定変更で最大30名になります。

docs.microsoft.com

今回は、テスト接続時に5名までの上限にぶつかって、設定確認したところ30名への引き上げがすでに有効になっていたので、動かないのではと不安になりました。結局Visual Studioを再起動したら5名以上参加できました。

音声は別途共有が必要

まもなく音声やチャット共有がサポートされるらしいのですが、またリリースされていなかったので今回はTeams会議を併用しました。

docs.microsoft.com

参加者の方が困ったときのヘルプ方法を用意しておく

オフラインだと会場をスタッフが見て回ってうまくいっているか確認できますが、オンラインだとこちらから確認する方法がありません。ですので、参加している方が困ったときにすぐヘルプを求める方法を用意しておくのがよいです。Live Shareを逆向きに(参加者の方が自分の画面をスタッフに共有する)するのもありですし、zoomのブレイクアウトセッションが使えるのであればデスクトップを共有することも可能です。(Live Shareだとコードを直接編集できる、デスクトップ共有だとLive Shareに行く前のトラブルシューティングも対応できる、と異なったメリットがあります) なお、参加者の方がLive Shareする場合、MicrosoftアカウントもしくはGitHubアカウントでログインしておく必要があります。

まとめ

今回は初回ということもありうまくいかなかった点もありますが、改善していけばかなりオンラインハンズオンも十分に運用できる見込みが立ちました。参加していただいた方、ありがとうございました。

C# でUSB-IO2.0 (AKI)を操作する (1) LED点灯

さて準備ができたので、C#からLEDを点灯させたいと思います。

tech.tanaka733.net

VBですがサンプルコードがあるのでそれを参考にできるのと、こちらに仕様があります。

km2net.com

がHIDについてあまり詳しくない私のような場合はPythonでの操作について書かれたこちらの記事の方がわかりやすかったです。

qiita.com

さて、VBのサンプルコードではhid.dllなどのAPIを直接実行しているので、C#からP/Invokeすれば同じことができるはずです。が、そんなコードを一から書きたくないと思って検索するとHidLibraryというライブラリがあるのを見つけました。なんと、.NET Standard対応までされています。

github.com

というわけでHidLibraryを使ってコードを書いていきます。今後複雑な操作をさせるために操作用のクラスUSBIO2Deviceを作ることにします。

HidDevices.EnumerateでVendorIdとProductIdを指定すると、指定した接続デバイスの一覧が返ってきます。VendorIdとProductIdの値は仕様書に書いてあります。SendAndReceiveメソッドでJ1側の端子とJ2側の端子に出力される値を指定しています。

using HidLibrary;
using System;
using System.Linq;

namespace HIDLabs.AkiUSBIO2.Library
{
    public class USBIO2Device : IDisposable
    {
        private const int VendorId = 0x1352;
        private const int ProductId = 0x121;

        public static HidDevice[] ListDevices() => HidDevices.Enumerate(VendorId, ProductId).ToArray();

        public static USBIO2Device Connect(HidDevice device) => new USBIO2Device(device);

        public static USBIO2Device Connect(HidDevice device)
        {
            return new USBIO2Device(device);
        }

        public static USBIO2Device Connect()
        {
            var device = HidDevices.Enumerate(VendorId, ProductId).FirstOrDefault();
            if (device == null)
            {
                throw new InvalidOperationException("Deviceが見つかりません");
            }
            return new USBIO2Device(device);
        }

        bool disposed = false;
        readonly HidDevice device;

        private USBIO2Device(HidDevice device)
        {
            this.device = device;
        }

        public void Dispose()
        {
            // Dispose of unmanaged resources.
            Dispose(true);
            // Suppress finalization.
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposed)
                return;

            if (disposing)
                device.Dispose();

            disposed = true;
        }

        public (byte j1, byte j2) SendAndReceive(byte j1, byte j2)
        {
            var sendData = new byte[64];
            sendData[1] = 0x20;
            sendData[2] = 0x1;
            sendData[3] = j1;
            sendData[4] = 0x2;
            sendData[5] = j2;
            var res = device.Write(sendData);
            if (!res)
                throw new InvalidOperationException("送信に失敗しました");

            var receiveData = device.Read();
            if (receiveData.Status != HidDeviceData.ReadStatus.Success)
                throw new InvalidOperationException($"受信に失敗しました: {receiveData.Status}");
            return (receiveData.Data[2], receiveData.Data[3]);
        }
    }
}

J1の0番端子の出力をOFF、ON繰り返すコードがこのようになります。すべてONが0xFFで、n番端子をOFFにするためには0xFF XOR 2^(n+1)の値を書き込めばよいです。なお、HidLibraryにはHIDに対するほかの操作ができるメソッドが用意されていますが、USB-IO2.0に実行すると対応していないというエラーがかえってきました。

using HIDLabs.AkiUSBIO2.Library;
using System;
using System.Threading.Tasks;

namespace HIDLabs.AkiUSBIO2.ConsoleApp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Hello USB IO2.0!");

            using var device = USBIO2Device.Connect();

            for (int i = 0; i < 10; i++)
            {
                device.SendAndReceive(0xFF ^ 1, 0xFF);
                await Task.Delay(TimeSpan.FromMilliseconds(100));
                device.SendAndReceive(0xFF, 0xFF);
                await Task.Delay(TimeSpan.FromMilliseconds(100));
            }
            device.SendAndReceive(0, 0);
        }
    }
}

こんな感じで実際に点滅します。(パラメーターがサンプルコードと違う気がしますが...)