open62541のサンプルをMinGWで動かす

今日は open62541 のサーバーのサンプルを MinGW で動かす。

ファイル構成はこんな感じとする

./
├ open62541 // git clone https://github.com/open62541/open62451 でとってきたファイル群
└ myServer.c // 今回動かすサーバーサンプル

まずは ライブラリをビルド

git clone https://github.com/open62541/open62541
cd open62541
mkdir build
cd build
cmake .. -G "MSYS Makefiles"

MINGW Makefiles を使うとなぜかエラーになったので、 MSYS Makefiles を使った

プロジェクトルートに戻って、 myServer.c を作成する

#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>

#include <signal.h>
#include <stdlib.h>

static volatile UA_Boolean running = true;
static void stopHandler(int sig) {
  UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
  running = false;
}

int main(void) {
  signal(SIGINT, stopHandler);
  signal(SIGTERM, stopHandler);

  UA_Server *server = UA_Server_new();
  UA_ServerConfig_setDefault(UA_Server_getConfig(server));

  UA_StatusCode retval = UA_Server_run(server, &running);

  UA_Server_delete(server);
  return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

んで、ビルド。include するディレクトリが多いので注意が必要なのと、mingwの場合は ws2_32 の指定が必要。というか、Windowsだったら、ws2_32の指定が必要なのかな。

gcc -I ./open62541/include -I ./open62541/plugins/include -I ./open62541/build/src_generated -I ./open62541/arch -I ./open62541/deps -L ./open62541/build/bin -std=c99 myServer.c -lopen62541 -lws2_32 -o myServer

これで、myServer.exe が出来上がる。

このサンプルでは、オリジナルのノードデータを追加していないので、クライアントから見えるのはサーバーのシステム情報ノードのみ。

open62541のサンプルを動かす

前回の記事でopen62541のビルドに成功して、 open62541.lib が生成できるのは確認できた。 けど、これができたからといって実際には使えないので、このライブラリを使ってexeを作る必要がある。

んで、githubのページのREADMEにはサンプルが示してあって、今回はそれを動かしてみる。

https://github.com/open62541/open62541/tree/v1.1-dev#examples

#include <stdio.h>
#include <open62541/client.h>
#include <open62541/client_config_default.h>
#include <open62541/client_highlevel.h>

int main(int argc, char* argv[])
{
    /* Create a client and connect */
    UA_Client* client = UA_Client_new();
    UA_ClientConfig_setDefault(UA_Client_getConfig(client));
    UA_StatusCode status = UA_Client_connect(client, "opc.tcp://localhost:4840");
    if (status != UA_STATUSCODE_GOOD) {
        UA_Client_delete(client);
        return status;
    }

    /* Read the value attribute of the node. UA_Client_readValueAttribute is a
    * wrapper for the raw read service available as UA_Client_Service_read. */
    UA_Variant value; /* Variants can hold scalar values and arrays of any type */
    UA_Variant_init(&value);
    status = UA_Client_readValueAttribute(client, UA_NODEID_STRING(1, "the.answer"), &value);
    if (status == UA_STATUSCODE_GOOD &&
        UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_INT32])) {
        printf("the value is: %i\n", *(UA_Int32*)value.data);
    }

    /* Clean up */
    UA_Variant_deleteMembers(&value);
    UA_Client_delete(client); /* Disconnects the client internally */
    return status;
}

ちなみに、私はC言語初心者なので、大分基本的な部分から入っていくことになる。

今回は v1.1-dev タグを使っていく。 v1.0 タグだとなぜかビルドできなかった。

git checkout refs/tags/v1.1-dev

open62541.lib のビルド方法は前回の記事を参照のこと。今回は Release - x64 でビルドしたものを使用。

C++のコンソールソリューションを作成する。

上のコードをコピペする。

インクルードパスが足りていないので、足す。

  • <open62541のgitリポジトリがクローンされているパス>\arch
  • <open62541のgitリポジトリがクローンされているパス>\build\src_generated
  • <open62541のgitリポジトリがクローンされているパス>\plugins\include
  • <open62541のgitリポジトリがクローンされているパス>\include

まだエラーがでる。error C2664: 'UA_LocalizedText UA_LOCALIZEDTEXT(char *,char *)': 引数 1 を 'const char [6]' から 'char *' へ変換できません。 とかいうの。

なので、 その他のオプション のところに /Zc:strictStrings- を足す。

エラーは出なくなるが、リンカが通らないので、リンカオプションを足す。

まずは、 追加のライブラリ ディレクトリopen62541.lib があるディレクトリを指定する。

追加の依存ファイルに oepn62541.lib を足す。ただ、これだけだとまだ足りなくて、 ws2_32.lib も必要なので併記しておく。

これでビルドは通るようになる。

ただ、リンクワーニング LINK : warning LNK4098: defaultlib 'LIBCMT' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。 が表示される。

ランタイムライブラリ/MT にすればこの問題は解決する。(この解決策がいいのかは不明)

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 でも同じように作れば簡単にできるとおもいます。