open62541のcmakeをMSVC用に実行する
open62541 は OPC UA の標準規格版なのかな?
なんしか、C言語を使って OPC UA を使うことができるみたい。
MSVCでビルドするのに、CMAKEを使うんだけど、CMAKEよくわからん勢なのでメモ。
Building open62541 — open62541 1.0.0-2-gf6f855d0 documentation
上記の公式のビルド方法では、
cd <path-to>\open62541 mkdir build cd build <path-to>\cmake.exe .. -G "Visual Studio 14 2015"
と記載があったけど、今使ってるのが、Visual Studio 2019 なのでどうしよう?
という感じだった。
結論としては、単純に、CMAKEのオプションを変えればいいだけだったみたい。
x86 Native Tools COmmand Prompt for VS 2019
を実行して
Github(https://github.com/open62541/open62541) からクローンし、
git checkout v1.0
した後、以下を実行
mkdir build cd build cmake .. -G "Visual Studio 16 2019"
v1.0
をチェックアウトしたのは、master
ブランチがビルドできなかったから。
cmake ができると、buildフォルダにソリューションファイルができているので
それを Visual Studio 2019で起動して、ビルドを実行すると bin
フォルダにライブラリファイルが生成されている。
これを使って自作アプリに組み込んだりできるんだと思うけど、今はまだそこまでいってない。
Windows 上に Python3.7 embeddable と pip をインストールする
なんか、普通にダウンロードしてきて pip をインストールするだけだとうまく動かなかった。
具体的には .\Scripts\pip.exe -V
を実行すると以下のエラーが発生して pip
が使えない。
Traceback (most recent call last): File "D:\obj\windows-release\37win32_Release\msi_python\zip_win32\runpy.py", line 193, in _run_module_as_main File "D:\obj\windows-release\37win32_Release\msi_python\zip_win32\runpy.py", line 85, in _run_code File "C:\dev\python374\Scripts\pip.exe\__main__.py", line 5, in <module> ModuleNotFoundError: No module named 'pip'
なので、色々調べて試してみた結果を残しておく。
基本的には、以下のURLに記載されていることを実行すればうまくいくようだった。
超軽量、超高速な配布用Python「embeddable python」 - Qiita
ただ、3.6環境なので、3.7環境でも同じかどうか試してみたいと思う。
今回は自身のPCにすでにPython3.7を入れてしまっていたので、Dockerを使って環境を構築する。
Docker上でWindowsコンテナを作成するやり方は以下の記事を参考にすること
DockerでWindowsコンテナを起動する為に必要な設定 - bamch0h’s diary
今回は powershell
を使う、 cmd
だと Webからファイルをダウンロードしたり、zipファイルを解凍したりすることを
コマンドライン上からするのは難しそうだったので、 powershell
を使うことにした。
Vim のインストール
後でファイルを編集しないといけないのでVimをインストールする。 netupvim
便利。
PS C:\dev> wget https://github.com/koron/netupvim/releases/download/v1.4.1/netupvim-v1.4.1.zip -o netupvim.zip PS C:\dev> Expand-Archive -Path .\netupvim.zip -DestinationPath vim PS C:\dev> cd vim PS C:\dev\vim> .\netupvim.exe PS C:\dev\vim> $Env:Path += ";C:\dev\vim"
Python のインストール
PS C:\dev> wget https://www.python.org/ftp/python/3.7.4/python-3.7.4-embed-win32.zip -o python374.zip PS C:\dev> Expand-Archive -Path .\python374.zip -DestinationPath python374
python37._pth の編集
PS C:\dev> cd .\python374\ PS C:\dev\python374> vim .\python37._pth
以下のように編集。最下行の #import site
のコメントアウトを解除するだけ。
python37.zip . # Uncomment to run site.main() automatically import site
pip の編集
PS C:\dev\python374> wget "https://bootstrap.pypa.io/get-pip.py" -O "get-pip.py" PS C:\dev\python374> .\python.exe get-pip.py
pip が使えるかのチェック
PS C:\dev\python374> .\python.exe -m pip install numpy
最後に Successfully installed numpy-1.17.2
のような表示がでていれば pip
が動いてる。
まとめ
Windows 10 で
python-embeddable
を使ってpip
を使うにはpython37._pth
の編集が必要。3.6で発生していた問題は3.7でも発生するようなので注意。
試験的にC#(WPF) と Go を GRPC を用いて連携させる
最近、趣味のコーディングは Go を使ってやることが多かったりして、Goでなんでも書きたい欲が強くなるんだけども、やはりGUIアプリを作成するときにはWindowsならC#一択になる傾向があるのが、私の最近なのですが、C#で記述する部分をできるだけなくして、それ以外をGoで書けないかなぁと模索している中で、MVVMのM(Model)の部分をGoで記述して、V-VMの部分のみをWPFで記述し、間をプロセス間通信でどうにかすれば、WebアプリのJavascriptとバックエンドの関係のように記述できないかなぁ?と思ったりしています。
なので、今回はプロセス間通信にGRPCを使って、GUI部分をC#で、ロジック部分をGoでコーディングしたらどうなるか?という試験的なアプリを作ってみました。
アプリを雑なイメージにするとこんな感じです。
C#側をGRPCのクライアント、Go側をサーバーにします。アプリ側にはクライアントを2つ用意しておきます。GRPCの通信インターフェースには SayHello()
と StreamMessage()
があり、SayHello()で任意の文字列を送ると、送った文字列の頭にHello
を返して送り返してきます。StreamMessage()
はストリームで繋がっているインターフェースで、このインターフェースを使って、SayHello()
されたときの返信文字列を別のチャンネルにも流しています。例えば、C#側にはクライアントCH1とCH2があります。CH1のSayHello()を呼び出すと、CH2のStreamMessage()と経由して、CH1のメッセージが届きます。逆に、CH2のSayHello()を呼び出すと、CH1のStreamMessage()を経由してCH2のメッセージが届きます。
proto
ファイルは以下のようになっています。
// Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; option objc_class_prefix = "HLW"; package helloworld; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} rpc StreamMessage (OpenRequest) returns (stream Message) {} } // The request message containing the user's name. message HelloRequest { string name = 1; string message = 2; } // The response message containing the greetings message HelloReply { string message = 1; } message OpenRequest { string name = 1; } message Message { string text = 1; }
Go側の実装はこんな感じ。
package main import ( "context" "errors" "fmt" "log" "net" pb "./helloworld" "google.golang.org/grpc" ) const ( address = ":50051" ) // server is used to implement helloworld.GreeterServer. type server struct { Streams map[string]pb.Greeter_StreamMessageServer Channels map[string]chan string } // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v, %v", in.GetName(), in.GetMessage()) if _, ok := s.Streams[in.GetName()]; !ok { return nil, errors.New("stream does not open") } if ch, ok := s.Channels[in.GetName()]; ok { ch <- "Hello " + in.GetMessage() } return &pb.HelloReply{Message: "Hello " + in.GetMessage()}, nil } func (s *server) StreamMessage(in *pb.OpenRequest, stream pb.Greeter_StreamMessageServer) error { if _, ok := s.Streams[in.GetName()]; !ok { if s.Streams == nil { s.Streams = make(map[string]pb.Greeter_StreamMessageServer) } s.Streams[in.GetName()] = stream if s.Channels == nil { s.Channels = make(map[string]chan string) } s.Channels[in.GetName()] = make(chan string, 1) } log.Println(in.GetName()) for { greeting := <-s.Channels[in.GetName()] for ch, stream := range s.Streams { if ch == in.GetName() { continue } msg := pb.Message{ Text: greeting, } if err := stream.Send(&msg); err != nil { log.Println(err) return err } } } return nil } func main() { lis, err := net.Listen("tcp", fmt.Sprintf("%s", address)) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) fmt.Println("start server") if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
C#側の実装はこんな感じ。
using System; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.ComponentModel; using System.Runtime.CompilerServices; using Grpc.Core; using Helloworld; namespace WpfApp9 { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new MainWindowVM(); } private void Tb1_TextChanged(object sender, TextChangedEventArgs e) { var vm = (MainWindowVM)this.DataContext; var tb = (TextBox)sender; vm.SayHello("ch1", tb.Text); } private void Tb2_TextChanged(object sender, TextChangedEventArgs e) { var vm = (MainWindowVM)this.DataContext; var tb = (TextBox)sender; vm.SayHello("ch2", tb.Text); } } public class MainWindowVM : INotifyPropertyChanged { #region Const const string CH1_NAME = "ch1"; const string CH2_NAME = "ch2"; #endregion #region Fields Channel channel1; Channel channel2; Greeter.GreeterClient greeter1; Greeter.GreeterClient greeter2; string _ch1_greeting; string _ch2_greeting; string _ch1_subscription; string _ch2_subscription; #endregion #region Events public event PropertyChangedEventHandler PropertyChanged; #endregion #region Constructor public MainWindowVM() { _ch1_greeting = ""; _ch1_subscription = ""; channel1 = new Channel("127.0.0.1:50051", ChannelCredentials.Insecure); greeter1 = InitChannel(channel1, CH1_NAME, new Action<string>((text) => Channel1Subscription = text)); _ch2_greeting = ""; _ch2_subscription = ""; channel2 = new Channel("127.0.0.1:50051", ChannelCredentials.Insecure); greeter2 = InitChannel(channel2, CH2_NAME, new Action<string>((text) => Channel2Subscription = text)); } #endregion #region Properties public string Channel1Greeting { get { return _ch1_greeting; } set { if (_ch1_greeting == value) return; _ch1_greeting = value; NotifyPropertyChanged(); } } public string Channel1Subscription { get { return _ch1_subscription; } set { if (_ch1_subscription == value) return; _ch1_subscription = value; NotifyPropertyChanged(); } } public string Channel2Greeting { get { return _ch2_greeting; } set { if (_ch2_greeting == value) return; _ch2_greeting = value; NotifyPropertyChanged(); } } public string Channel2Subscription { get { return _ch2_subscription; } set { if (_ch2_subscription == value) return; _ch2_subscription = value; NotifyPropertyChanged(); } } #endregion #region Method public void SayHello(string ch, string message) { HelloReply reply; switch(ch) { case CH1_NAME: reply = greeter1.SayHello(new HelloRequest { Name = CH1_NAME, Message = message}); Channel1Greeting = string.Format("Greeting: {0}", reply.Message); break; case CH2_NAME: reply = greeter2.SayHello(new HelloRequest { Name = CH2_NAME, Message = message}); Channel2Greeting = string.Format("Greeting: {0}", reply.Message); break; } } private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private Greeter.GreeterClient InitChannel(Channel ch, string name, Action<string> act) { var g = new Greeter.GreeterClient(ch); Task.Run(async () => { using (var stream = g.StreamMessage(new OpenRequest() { Name = name })) { try { while (await stream.ResponseStream.MoveNext()) { var message = stream.ResponseStream.Current; act(message.Text); } } catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); } } }); return g; } #endregion } public class SayHelloCommand : ICommand { private static readonly Action EmptyExecute = () => { }; private static readonly Func<bool> EmptyCanExecute = () => true; private Action execute; private Func<bool> canExecute; public SayHelloCommand(Action execute, Func<bool> canExecute) { this.execute = execute ?? EmptyExecute; this.canExecute = canExecute ?? EmptyCanExecute; } public void Execute() { this.execute(); } public bool CanExecute() { return this.canExecute(); } bool ICommand.CanExecute(object parameter) { return this.CanExecute(); } public event EventHandler CanExecuteChanged; public void RaiseCanExecuteChanged() { this.CanExecuteChanged?.Invoke(this, EventArgs.Empty); } void ICommand.Execute(object parameter) { this.Execute(); } } }
XAMLはこんな感じ。
<Window x:Class="WpfApp9.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp9" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <StackPanel> <TextBlock Text="Channel1" /> <DockPanel> <TextBlock Text="Text Box" Margin="0,0,10,5" /> <TextBox TextChanged="Tb1_TextChanged" /> </DockPanel> <DockPanel> <TextBlock Text="Greeting itself" Margin="0,0,10,5" /> <TextBlock Text="{Binding Channel1Greeting}" Background="AliceBlue"/> </DockPanel> <DockPanel> <TextBlock Text="Greeting from other" Margin="0,0,10,5" /> <TextBlock Text="{Binding Channel1Subscription}" Background="#FF8D9AC7" /> </DockPanel> <Border Margin="10"/> <TextBlock Text="Channel2" /> <DockPanel> <TextBlock Text="Text Box" Margin="0,0,10,5" /> <TextBox TextChanged="Tb2_TextChanged" /> </DockPanel> <DockPanel> <TextBlock Text="Greeting itself" Margin="0,0,10,5" /> <TextBlock Text="{Binding Channel2Greeting}" Background="AliceBlue"/> </DockPanel> <DockPanel> <TextBlock Text="Greeting from other" Margin="0,0,10,5" /> <TextBlock Text="{Binding Channel2Subscription}" Background="#FF8D9AC7"/> </DockPanel> </StackPanel> </Window>
protoファイルのビルド
Go編
protoファイルがある場所で以下のコマンドを実行。helloworld
のフォルダが既にあることが前提。
proto -I . helloworld.proto --go_out=plugins:grpc:helloworld
C#側
- クラスライブラリ(.Net Standard) のプロジェクトを作成
- NuGetで、
Grpc
とGrpc.Tools
とGoogle.Protobuf
をインストール - proto ファイルをプロジェクトに追加
- csproj に以下の行を追加 (PackageReferenceのItemGroupとは別に作成する)
<ItemGroup> <Protobuf Include="helloworld.proto" Link="helloworld.proto" /> </ItemGroup>
- ビルドを実行すると、dllが作成される。
NOTE
.Net Framework を使ってWPFアプリを作成する場合は、上記で作成したクラスライブラリのほかに、Grpc
と Google.Protobuf
をパッケージに追加する必要があるので、NuGetを使ってインストールしておくこと。.Net Standard の場合は クラスライブラリの中のパッケージを使用するようで、新たに追加する必要はなかった。
最終動作イメージ
以下のような動作となる。意外とシームレス?
microsoft/vs-streamjsonrpc のバージョンが 2 になっていたので使ってみた。
ほんとにさわりだけ使ってみただけ、使用感はさほど変わってなかったけど、golang 使わずに C#だけで作ったら NamedPipe の使い方がよくわからず躓いたのでメモ代わりに記事を書く。
登場人物は3人、Target
と Client
と Server
Client
部分はほとんど 以前の記事(http://bamch0h.hatenablog.com/entry/2018/12/26/012124) と変わらない。
Server
部分は今回新規に書き起こした。 JsonRpc.Attach
の第二引数に RPC用のクラス Target
を指定することで、public メソッドを公開してくれるようだ。他にも色々公開の仕方があるみたいだけど、今回は割愛。
Server
の Attach
の次に、rpc.Completion
でタスクの終了を待っている。これをしないと、Client
が要求を投げるとServer
側がすぐさま終了してしまうようだった。応答を返して、Client
がちゃんとパイプをクローズしてくれるまで待つためにこの行を入れている。(これに気付くまでに結構時間が吸われたので辛かった。。。)
このプログラムを実行すると、1+2 の結果である 3
が表示される。
using System; using System.Threading.Tasks; using System.IO.Pipes; using StreamJsonRpc; namespace ConsoleApp5 { public class Target { public int Add(int a, int b) { return a + b; } } public class Client { public async Task Start(string pipename) { var stream = new NamedPipeClientStream(".", pipename, PipeDirection.InOut, PipeOptions.Asynchronous); stream.Connect(); using (var rpc = JsonRpc.Attach(stream)) { int ret = await rpc.InvokeAsync<int>("Add", 1, 2); Console.WriteLine(ret); } } } public class Server { public void Start(string pipename) { var stream = new NamedPipeServerStream(pipename, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); stream.WaitForConnection(); using (var rpc = JsonRpc.Attach(stream, new Target())) { rpc.Completion.Wait(); } } } class Program { static void Main(string[] args) { string pipename = "winiotestpipe"; var ts = Task.Run(() => new Client().Start(pipename)); var tc = Task.Run(() => new Server().Start(pipename)); Task.WhenAll(tc, ts).Wait(); Console.ReadLine(); } } }
OPCUAクライアントライブラリのLuaバインディングサンプル書いた
OPC UA クライアント ライブラリの Luaバインディングサンプルを書きました。
今は、同じフォルダにある opcua.lua
というフォルダを読むことしかできませんがOPC UA のクライアントのサンプルとしては十分だと思います。
Lua は NLua を使っていて、OPC UA クライアントは OPC UA Foundation 公式のライブラリを使っています。
opcua:open("opc.tcp://localhost:62541/Quickstarts/ReferenceServer") local t = opcua:read({ "ns=2;s=Scalar_Simulation_Int16", "ns=2;s=Scalar_Simulation_Int32", "ns=2;s=Scalar_Simulation_Int64"}) for k,v in pairs(t) do print(string.format("%s = %s", k,v)) end
こんな感じで読み出しができます。
今回は .Net Core で作成しましたが、おそらく .Net Framework でも同じように作れば簡単にできるとおもいます。
フリーの OPC UA クライアント Jupiter を作りました
DockerでWindowsコンテナを起動する為に必要な設定
Docker for Windows では、 Windows コンテナが起動できるようになっています。
開発者に朗報! Windows 10でWindows Serverコンテナが実行可能に (1/2):企業ユーザーに贈るWindows 10への乗り換え案内(42) - @IT
https://hub.docker.com/_/microsoft-windows
ただ、私の環境では公式のドキュメントに書いてある通りに docker run mcr.microsoft.com/windows:1903
を実行すると no matching manifest for unknown in the manifest list entries.
と表示され起動できない。
Windows コンテナは Docker を Windows コンテナ用に設定する必要があるみたい。
常駐タスクアイコンの中から、 docker for windows
を右クリックして Switch to windows containers
を選択しておくこと。
あと、 エクスペリメンタル機能を有効にしておく必要がある。
同じように docker for windows
のアイコンを右クリックして Settings
から Daemon
を選択して Experimental features
のチェックを入れる。
いろんなサイトではこれで有効になるようだが、私の環境では無理だった。
私の場合は、エクスペリメンタル機能を有効にするために、%USERPROFILE%
の下にある .docker/config.json
の中に "experimental": "enabled"
を追加した。
あと、Windows コンテナは ホストの Windows OS と同じ環境で動作するため ホストのOSをゲストのOSと同じにしておく必要があるようだ。
私の環境は WIndows 10 Pro 1803 だったため、ちゃんと起動できなかった。 Windows Update を行って Windows 10 Pro 1903 にしておく必要がある。
これらをすべて実行し、 docker for windows
を再起動したのち、再度 docker run mcr.microsoft.com/windows:1903
を実行すると実行できる。
ただ、実行してもすぐに終了してしまうので、以下のようにインタラクティブモードにしたほうがいい。
docker run -it --name wincont01 --isolation=hyperv mcr.microsoft.com/windows:1903 cmd