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でコーディングしたらどうなるか?という試験的なアプリを作ってみました。

アプリを雑なイメージにするとこんな感じです。

f:id:bamch0h:20190924231444p:plain

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#

  1. クラスライブラリ(.Net Standard) のプロジェクトを作成
  2. NuGetで、GrpcGrpc.ToolsGoogle.Protobuf をインストール
  3. proto ファイルをプロジェクトに追加
  4. csproj に以下の行を追加 (PackageReferenceのItemGroupとは別に作成する)
  <ItemGroup>
    <Protobuf Include="helloworld.proto" Link="helloworld.proto" />
  </ItemGroup>
  1. ビルドを実行すると、dllが作成される。

NOTE

.Net Framework を使ってWPFアプリを作成する場合は、上記で作成したクラスライブラリのほかに、GrpcGoogle.Protobuf をパッケージに追加する必要があるので、NuGetを使ってインストールしておくこと。.Net Standard の場合は クラスライブラリの中のパッケージを使用するようで、新たに追加する必要はなかった。

最終動作イメージ

以下のような動作となる。意外とシームレス?

f:id:bamch0h:20190924234054g:plain

microsoft/vs-streamjsonrpc のバージョンが 2 になっていたので使ってみた。

ほんとにさわりだけ使ってみただけ、使用感はさほど変わってなかったけど、golang 使わずに C#だけで作ったら NamedPipe の使い方がよくわからず躓いたのでメモ代わりに記事を書く。

登場人物は3人、TargetClientServer

Client 部分はほとんど 以前の記事(http://bamch0h.hatenablog.com/entry/2018/12/26/012124) と変わらない。

Server 部分は今回新規に書き起こした。 JsonRpc.Attach の第二引数に RPC用のクラス Target を指定することで、public メソッドを公開してくれるようだ。他にも色々公開の仕方があるみたいだけど、今回は割愛。

ServerAttach の次に、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バインディングサンプルを書きました。

github.com

今は、同じフォルダにある opcua.lua というフォルダを読むことしかできませんがOPC UA のクライアントのサンプルとしては十分だと思います。

Lua は NLua を使っていて、OPC UA クライアントは OPC UA Foundation 公式のライブラリを使っています。

github.com

github.com

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 を作りました

OPC UA クライアント Jupiter を作りました。

github.com

OPC UA Foundation が提供している OOS のライブラリを使って作ってあるので、ある程度の信頼性はあります。

やれることは、

くらいの単純なものです。 よかったら、つかってください。

https://raw.githubusercontent.com/bamchoh/jupiter/cb04e6b2c9e4657c5c781cfeb50f4c5813e9ee4e/example.gif

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