【技術】AndroidアプリをWindowsアプリに変換

2025年07月14日

QRコード、Bluetooth、Websサーバー、時々ポーティング

0. はじめに

少し期間が開いてしまいましたが、

前年度最後に技術要素がぎっしり詰まった楽しい開発を自由に推進させて頂きました。

それぞれの要素に関して1つずつ解き明かしたいと考えています。

開発の「きも」は現行システム運用中デバイスやサーバーはそのままに
通信用のアプリをごっそり異なる環境に移植するというものでした。
詳細は明かせないので簡単に説明すると、「QRコード」を読取って、サーバーに送信し
リアルタイムに管理システムで使用するアプリケーションの移植でした。

図1. システム全体概略図

   マウスポインタを画像に合わせて更新後のシステムを覗いてみましょう!

運用中デバイスは、Androidアプリで実現しています。これをWindowsで全く同じ動作が可能なアプリ開発でした。

前任者が開発したAndroidアプリのソースコードもあり、楽勝案件ガッツポーズと思われるかも知れません。

実際は、ポーティングというより、”お師匠さんAndroidアプリ“と同じ動作する。”弟子Windowsアプリ“を育てあげる事でした。

『ベスト・キッド』という映画があったと思いますが、師匠の思考はJavaでAndroidライブラリ利用の塊で

できたアプリ弟子の思考はC#で全てWindows環境のアプリという違いがありました。

Androidアプリアプリお師匠さんには、「アプリの動作」その所作「Data Format」その精神のみ参考にさせて頂きました。


1. QRコード(※QRコードはデンソーウェーブの登録商標です)

 実は、勇み足でOpenCVというライブラリでWindows上でQRコード読み書きできるテストも済ませてました。
実際はIoTデバイスでQRコードを読み取りデータ化された情報を受取りWebサーバーに送るだけなので
全く不要でしたが、~6H位でQRコード読取りできました。 QRコードに関しては、成果物に1行も含まれていません。(-_-);
ですが、このブログ”ポンチ絵中”の「会社ホームページ」のQRコードは、このソフトで作ったと記憶しています。



2. Bluetooth

2.1 SPP

 上記QRコードを読取ったIoTデバイスからは、BluetoothのSPPというプロファイルを使用して
Bluetooth上でシリアルポートをシミュレーションしてあたかもシリアルポート上で
送信/ 受信しているかの様な魔法を実現してくれるものです。

このトリックによりシリアルポートプログラムはそのままです。このプログラムからは違いを認識できません

IoT I/Fサンプル


 このコードだけでは動作しないかも知れません、実コードから省略している部分や書換えた部分があります。
ですので、動作無保証ですし、このコード使用で損害に合っても賠償しません参考まで



/*!
 *  @file       SerialPort.cs
 *  @brief      ウエアラブル端末レシーバタスク
 *  @author     (株)スマートエイジングサポート
 *  @date       2025/02/05
 * 
 *  @details    ウエアラブル達末からの受信コマンドを処理するクラス
 *              呼出サーバーから返信されるステータスコードをウエアラブル端末に返信する。
 *  @note
 */

using System.Text;
using System.IO.Ports;
using System.Threading;
using System.Net;

using System.Runtime.ConstrainedExecution;
using System.Text.RegularExpressions;
using System.Runtime.CompilerServices;

/*!
 *  @class      SerialPort
 *  @brief      ウエアラブルI/Fクラス
 *  @author     (株)スマートエイジングサポート
 *  @date       2025/02/05
 * 
 *  @note
 */
public class SerialPort
{
    public string                   name    { get; private set; }                               //!<    Bluetooth デバイス名
    private SerialPort              port                            = new SerialPort();	        //!<    シリアルポート
    public volatile DataList        recv;                                                       //!<    受信用バッファ
    public Int32                    error   { get; private set; }   = 0;                        //!<    エラー発生カウント数

    /*!
     *  @fn    bool Open(string portname)
     *  
     *  シリアルポートをオープンする。
     *  対象はBluetooth(SPP)が使用するシリアルポートであるが、
     *  その他のデバイスが存在する(仮想)シリアルポート名でも良い(主にデバッグ用に使用する。)
     *  
     *  @brief  シリアルポートオープン
     *  @param  [in] string     portname   
     *  @return オープン結果(true: 成功、 false: 失敗)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    public bool Open(string portname)
    {
        if ( port.IsOpen) {
            // シリアルポートは既にオープンしています。
            if ( port.PortName != portname) {
                // シリアルポート名が異なります。
                port.Close();
                // シリアルポートがクローズするまで待ちます。
                while ( port.IsOpen) {
                    Thread.Sleep(10);
                }
            } else {
                return true;
            }
        }
        try {
            // シリアルポートのパラメータ設定
            port.PortName       = portname;
            port.Encoding       = System.Text.Encoding.UTF8;
            port.BaudRate       = 115200;
            port.Handshake      = Handshake.None;
            port.ReadTimeout    = 100;
            port.WriteTimeout   = 100;
            
            port.Open();                                // デバイスオープン
        } catch (UnauthorizedAccessException ex) {      // デバイスアクセス権限で例外発生
            Console.WriteLine("Error: Port access denied. " + ex.Message);
            return false;
            
        } catch (IOException ex) {                      // デバイスアクセスで例外発生(別アプリで使用中等)
            Console.WriteLine("Error: IO exception occurred. " + ex.Message);
            return false;
            
        } catch (InvalidOperationException ex) {        // デバイスに対する無効な操作例外発生
            Console.WriteLine("Error: Invalid operation. " + ex.Message);
            return false;
            
        } catch (Exception ex) {                        // その他の例外発生
            Console.WriteLine("Error: Exception. " + ex.Message);
            return false;
        }

        port.DiscardInBuffer();                         // 受信バッファからデータを削除します。
        port.DiscardOutBuffer();                        // 送信バッファからデータを削除します。

        port.DataReceived   += Port_DataReceived;       // 受信イベントハンドラを登録します。
        port.ErrorReceived  += Port_ErrorReceived;      // 受信エラーイベントハンドラを登録します。
        port.PinChanged     += Port_PinChanged;         // 制御状態変化イベントハンドラを登録します。
        return true;
    }

    /*!
      *  @fn    bool IsOpen(string portname)
      *  
      *  シリアルポートのオープン状態取得
      *  
      *  @brief  シリアルポート状態
      *  @return オープン状態(true: Open、 false: Close)
      * 
      *  @author     (株)スマートエイジングサポート
      *  @date       2025/02/13
      * 
      *  @note
      * 
      */
    public bool IsOpen()
    {
        return port.IsOpen;
    }

    /*!
      *  @fn    string PortName()
      *  
      *  オープンしているポート名取得
      *  
      *  @brief  シリアルポート名取得
      *  @return シリアルポート名
      * 
      *  @author     (株)スマートエイジングサポート
      *  @date       2025/02/13
      * 
      *  @note
      * 
      */
    public string PortName()
    {
        string name = port.PortName;
        return name;
    }

    /*!
     *  @fn    bool Close()
     *  
     *  シリアルポートをクローズする。
     *  対象はBluetooth(SPP)が使用するシリアルポートであるが、
     *  その他のデバイスが存在する(仮想)シリアルポート名でも良い(主にデバッグ用に使用する。)
     *  
     *  @brief  シリアルポートクローズ
     *  @return クローズ結果(true: 成功、 false: 失敗)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note   現状必ずtrueを返す。
     * 
     */
    public bool Close()
    {
        if ( !port.IsOpen) {
                                                         // シリアルポートはオープンしていません。
            /*
             *   Note :
             *
             *     元々処理する内容がありましたが開発成果に関わるので削除しました。
             */
        } else {
            port.DataReceived   -= Port_DataReceived;   // 受信イベントハンドラを解除します。
            port.ErrorReceived  -= Port_ErrorReceived;  // 受信エラーイベントハンドラを解除します。
            port.PinChanged     -= Port_PinChanged;     // 制御状態変化イベントハンドラを解除します。
            port.DiscardInBuffer();                     // 受信データバッファのデータを破棄します。
            port.Close();                               // シリアルポートをクローズします。
            while ( port.IsOpen ) {                     // クローズするまで待ちます。
                Thread.Sleep(10);
            }
        }
        return true;
    }

    /*!
     *  @fn   void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
     *  
     *  受信データをアプリケーション受信バッファ(recv :データリスト)に書込む。
     *  例外が発生したら処理を中断する。
     *  
     *  
     *  @brief  シリアルポート受信ハンドラ
     *  @return なし
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note 
     * 
     */
    private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        SerialPort  serial  = (SerialPort)sender;
        Int32       count   = serial.BytesToRead;
        int         b       = -1;
        char[]      chars   = new char[count];

        Console.WriteLine($"Bluetooth 受信カウントは、{count}です。");
        for ( Int32 i = 0; i< count; i++ ) {
            try {
                b = serial.ReadByte();
                if ( b == -1 )      break;
                chars[i] = (char)b;
            } catch ( InvalidOperationException ex ) {
                Console.WriteLine("Invalid operation. " + ex.Message);
                break;

            } catch ( TimeoutException ex ) {
                Console.WriteLine("Error: Timeout. " + ex.Message);
                break;

            } catch ( Exception ex ) {
                Console.WriteLine("Error: Exception. " + ex.Message);
                break;

            }
        }
        string  line = new string(chars);
        //
        //  ToDo: Readデータ取扱い
        //
        //   このサンプルでは、Readデータを文字列化しているが
        //   その後の処理は未定義なので、ここで適切に処理して下さい。
        // 

    }

    /*!
     *  @fn   void Port_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
     *  
     *  シリアル受信で発生したエラーを記録する
     *  
     *  @brief  シリアルポート受信エラーハンドラ
     *  @return なし
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note 
     * 
     */
    private void Port_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
    {
        SerialPort  serial  = (SerialPort)sender;
        SerialError err     = e.EventType;
        
        this.error++;
        switch ( err ) {
            case SerialError.RXOver:
                Console.WriteLine("Error: An input buffer overflow has occurred. ");
                Console.WriteLine("There is either no room in the input buffer, ");
                Console.WriteLine(" or a character was received after the end-of-file (EOF) character.");
                break;

            case SerialError.Overrun:
                Console.WriteLine("Error: A character-buffer overrun has occurred. The next character is lost.");
                break;

            case SerialError.RXParity:
                Console.WriteLine("Error: The hardware detected a parity error.");
                break;

            case SerialError.Frame:
                Console.WriteLine("Error: The hardware detected a framing error.");
                break;

            case SerialError.TXFull:
                Console.WriteLine("Error: The application tried to transmit a character, but the output buffer was full.");
                break;

            default:
                Console.WriteLine("Error: Unkown.");
                break;
        }
    }

    /*!
     *  @fn   void Port_PinChanged(object sender, SerialPinChangedEventArgs e)
     *  
     *  シリアルポートの制御信号の変化を記録する。
     *  
     *  @brief  シリアルポート制御信号変化ハンドラ
     *  @return なし
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note 
     * 
     */
    private void Port_PinChanged(object sender, SerialPinChangedEventArgs e)
    {
        SerialPort      serial = (SerialPort)sender;
        SerialPinChange change = e.EventType;

        switch ( change ) {
            case SerialPinChange.CtsChanged:
                Console.WriteLine("Changed: The Clear to Send (CTS) signal changed state.");
                Console.WriteLine("This signal is used to indicate whether data can be sent over the serial port.");
                break;

            case SerialPinChange.DsrChanged:
                Console.WriteLine("Changed: The Data Set Ready (DSR) signal changed state.");
                Console.WriteLine("This signal is used to indicate whether the device on the serial port is ready to operate.");
                break;

            case SerialPinChange.CDChanged:
                Console.WriteLine("Changed: The Carrier Detect (CD) signal changed state.");
                Console.WriteLine("This signal is used to indicate whether a modem is connected to a working phone line and a data carrier signal is detected.");
                break;

            case SerialPinChange.Break:
                Console.WriteLine("Changed: A break was detected on input.");
                break;

            case SerialPinChange.Ring:
                Console.WriteLine("Changed: A ring indicator was detected.");
                break;

            default:
                Console.WriteLine("Changed: Unkown.");
                break;
        }
    }
}

2.2 規格バージョンの壁


 …ですが、Bluetooth規格のバージョンが上がってしまい、「SPP」はレガシーな物に分類され直接サポートされなくなりました。

SPPを全てBluetoothのハード(あるいはデバイスドライバ)を叩いて構築するのは大変ですし、
そもそもそんなことしていたら開発期間足りません。

SPPに関して調査してみると、何と「Microsoft」自身がSPPを作ってWindowsに標準実装したデバイスドライバがありました。
このドライバーを利用する事ができました。(^o^)v


2.3 技術情報の壁


 世間一般のインターネットにあるSPP 記事といえば、規格バージョンが古い記事か、
中途半端な情報で現行Windows10ではまともに動作しない様なものばかり…

自分で、仮説を立て実験しながら開発成果物に合う方法を探り当てるしかありませんでした。
試行するにも対向機材がなくとりあえず(スマートフォンを使用して)別プロファイルにて
ペアリングや接続確認等を手を変え品を変え、試行する日々もありました。

 試行錯誤する際は、GUI版ではなくCUI版で実施しています。
動作させたその場で、コンソールに情報をそのまま表示できるので様々な実験を繰返すには
GUIより効率良いです。 (作り捨てはもったいないので)後に簡易スクリプト機能を搭載して、
IoTデバイスの代わりにターゲットソフトにデータを送りつけるソフトに作り変えて
同じテストを何度でも繰返し実現できる自動テスト実施に使用しました。

 IoTデバイスを入手してからは、1日もかからず、常に通信できるまでに持っていけました。
技術者的には「理論」・「ドキュメント」より「Run」(=動作させて確認)で試行錯誤していたので、
「野良」仕事感満載で遠回りした感は否めませんが、自身のスキル向上の糧にはなりました。
Bluetooth関連書籍を自身で購入して眺めましたが、開発に全く寄与する部分はありませんでした。
開発期間内に必要十分な仕様/ 機能に仕上げる事ができたのは、地道な「野良」仕事の賜物と考えています。

 この開発ではAndroidアプリとWindowsアプリでのBluetoothに関するアプローチの違いや
Windows上でのSPPに関するノウハウを蓄積できました。

ペアリングや接続などでBluetoothの状態変化をハンドリングしてGUIに反映させたり、
その状態になる様にユーザにダイアログで指示を表示するなどBluetooth関連で必要とされる
イベントハンドリングなど一通りできる様になりました。 このノウハウはSPP以外でも応用可能です。

Bluetoothハンドリングサンプル


 このコードだけでは動作しませんし、実コードから省略している部分や書換えた部分があります。
ですので、動作無保証ですし、このコード使用で損害に合っても賠償しません参考まで



/*!
 *  @file       BTSerial.cs
 *  @brief      Bluetoothデバイスマネージャ
 *  @author     (株)スマートエイジングサポート
 *  @date       2025/02/05
 * 
 *  @details    Bluetoothデバイスを検索して使用するシリアルポート名を取得する。
 *              指定したBluetoothデバイス名の接続を監視しその状態変化を収集する。
 *  @note
 */

using System;
using System.Management;
using System.Text.RegularExpressions;
using Windows.Devices.Bluetooth.Rfcomm;
using Windows.Devices.Bluetooth;
using Windows.Devices.Enumeration;


/*!
 *  @class      BluetoothSerial
 *  @brief      Bluetoothシリアルクラス
 *  @author     (株)スマートエイジングサポート
 *  @date       2025/02/05
 * 
 *  @note
 * 
 */
public class BluetoothSerial
{
public  static Guid                      serialID { get; }  = new Guid( @"00001101-0000-1000-8000-00805F9B34FB" );//!< SPPプロトコルのGUID
private static List<BluetoothDevice>     tgt                = new List<BluetoothDevice>();                        //!< 監視Bluetoothデバイス
private static Dictionary<string, bool>  devPr              = new Dictionary<string, bool>();                     //!< deviceからペアリング状態取得
private static Dictionary<string, bool>  devST              = new Dictionary<string, bool>();                     //!< deviceから接続状態取得
private static Dictionary<string, ulong> devAd              = new Dictionary<string, ulong>();                    //!< deviceからアドレス取得
public  static string                    spp                = RfcommServiceId.SerialPort.AsString();              //!< SPPプロトコル文字列
public         RfcommServiceId           sppId              = RfcommServiceId.SerialPort;                         //!< SPPプロトコルサービスID
public  static string                    noDevice           = "<No device>";                                      //!< デバイスなし文字列

    /*!
     *  @fn   static bool IsBluetoothDevice(in string deviceId)
     *  
     *  引数deviceIdで渡されたデバイスIDからBluetoothデバイスか判定する。
     *  
     *  @brief  Bluetoothデバイス判定
     *  @param  [in] string     deviceId     デバイスID
     *  @return 判定結果  (true : Blutoothデバイス、 false: それ以外)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    private static bool IsBluetoothDevice(in string deviceId)
    {
        return Regex.IsMatch(deviceId, @"^BTHENUM.*$");
    }

    /*!
     *  @fn   static async Task GetBluetoothDevice(string target)
     *  
     *  引数targetで渡されたデバイス名からBluetoothデバイスを取得しtgtに設定する。
     *  
     *  @brief  Bluetoothデバイス取得タスク
     *  @param  [in] List   target     Bluetoothデバイス名
     *  @return 取得結果  (true : 取得済、 false: 未取得)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    public static async Task GetBluetoothDevice(List target)
    {
        bool result = false;
        string deviceSelector = BluetoothDevice.GetDeviceSelector();
        DeviceInformationCollection devices = await DeviceInformation.FindAllAsync(deviceSelector);
        foreach ( var deviceInfo in devices ) {
            BluetoothDevice device = await BluetoothDevice.FromIdAsync(deviceInfo.Id);
            if ( device != null ) {
                if (target.Contains(deviceInfo.Name)) {
                    Console.WriteLine($"Bluetoothデバイス\"{deviceInfo.Name}\"が見つかりました。");
                    tgt.Add(device);
                    result  = true;
                }
                var sdp = device.SdpRecords;
            }
        }
        return result;
    }

    /*!
     *  @fn   static async Task GetBluetoothPaired(string target)
     *  
     *  引数targetで渡されたデバイス名からBluetoothデバイスのペアリング状態を取得する
     *  
     *  @brief  Bluetoothデバイスペアリング状態取得タスク
     *  @param  [in] List   target     Bluetoothデバイス名
     *  @return 取得結果  (true : 取得済、 false: 未取得)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    public static async Task GetBluetoothPaired(List target)
    {
        devPr.Clear();
        bool result = false;
        string deviceSelector = BluetoothDevice.GetDeviceSelector();
        DeviceInformationCollection devices = await DeviceInformation.FindAllAsync(deviceSelector);
        foreach ( var deviceInfo in devices ) {
            BluetoothDevice device = await BluetoothDevice.FromIdAsync(deviceInfo.Id);
            if ( device != null ) {
                if ( target.Contains(deviceInfo.Name) ) {
                    devPr.Add(device.Name, device.DeviceInformation.Pairing.IsPaired);
                    if ( device.DeviceInformation.Pairing.IsPaired ) {
                        Console.WriteLine($"Bluetoothデバイス\"{deviceInfo.Name}\"はペアリング済です。");
                        result  = true;
                    } else {
                        Console.WriteLine($"Bluetoothデバイス\"{deviceInfo.Name}\"はペアリングしていません。");
                    }
                }
            }
        }
        return result;
    }

    /*!
     *  @fn   static string GetSerialport()
     *  
     *  デバイス情報からBluetooth(SPP)が使用するシリアルポート名を取得する。
     *  
     *  @brief  Bluetoothで使用するシリアルポート名取得
     *  @param  [in] string     target     Bluetoothデバイス名
     *  @return 取得結果  (true : 取得済、 false: 未取得)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note 最初にマッチングしたシリアルポート名を取得するのでBluetoothで使用するシリアルポートは1つ限定とする。
     * 
     */
    public static string GetSerialport()
    {
        string  comPortName                     = noDevice;

        try {
            Regex                       regexPortName  = new Regex(@"(COM\d+)");
            ManagementObjectSearcher    searchSerial   = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity");
            ManagementObjectCollection  serials        = searchSerial.Get();

            foreach ( ManagementObject obj in serials ) {
                string? name        = obj["Name"]           as string;  // デバイスマネージャーに表示されている機器名
                string? classGuid   = obj["ClassGuid"]      as string;  // GUID
                string? devicePass  = obj["DeviceID"]       as string;  // デバイスインスタンスパス

                if ( name != null && classGuid != null && devicePass != null ) {
                    if ( devicePass.Contains("{00001101-0000-1000-8000-00805F9B34FB}") && IsBluetoothDevice(devicePass) ) {
                        Match   result      = regexPortName.Match(name);
                        if ( result.Success ) {
                            comPortName     = result.Groups[1].ToString();
                            break;
                        }
                    }
                }
            }
        } catch( Exception ex ) {
            Console.WriteLine(ex.ToString() );
        }
        return comPortName;
    }

    /*!
     *  @fn    bool IsPaired(in string name)
     *  
     *  前回収集した情報からname(Bluetoothデバイス)のペアリング状態を取得する。
     *  
     *  @brief  Bluetoothデバイスペアリング状態取得
     *  @param  [in] string     name     Bluetoothデバイス名
     *  @return 接続状態  (true : ペアリング済、 false: 未ペアリング)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/13
     * 
     *  @note
     * 
     */
    public bool IsPaired(in string name)
    {
        if ( ( name == null ) || ( name.Length == 0 ) ) return false;
        if ( devPr.ContainsKey(name) )                  return devPr[name];
        return false;
    }

    /*!
     *  @fn    bool IsConnected(in string name)
     *  
     *  イベントハンドラから収集した情報からname(Bluetoothデバイス)の接続状態を取得する。
     *  
     *  @brief  Bluetoothデバイス接続状態取得
     *  @param  [in] string     name     Bluetoothデバイス名
     *  @return 接続状態  (true : 接続中、 false: 未接続)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    public bool IsConnected(in string name)
    {
        if (( name == null ) || (name.Length == 0)) return false;
        if (devST.ContainsKey(name))                return devST[name];
        return false;
    }

    /*!
     *  @fn    ulong GetAddress(in string name)
     *  
     *  イベントハンドラから収集した情報からname(Bluetoothデバイス)のアドレスを取得する。
     *  
     *  @brief  Bluetoothデバイスアドレス取得
     *  @param  [in] string     name     Bluetoothデバイス名
     *  @return Bluetoothアドレス  (但し、0 : 未取得)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    public ulong GetAddress(in string name)
    {
        if ( ( name == null ) || ( name.Length == 0 ) ) return 0;
        if ( devAd.ContainsKey(name))                   return devAd[name];
        return 0;
    }

    /*!
     *  @fn    static string GetFormattedAddress(in string name)
     *  
     *  イベントハンドラから収集した情報からname(Bluetoothデバイス)のアドレスを取得する。
     *  
     *  @brief  Bluetoothデバイスアドレス取得
     *  @param  [in] string     name     Bluetoothデバイス名
     *  @return Bluetoothアドレス文字列 (但し、"00:00:00:00:00:00:00" : 未取得)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/14
     * 
     *  @note
     * 
     */
    public static string GetFormattedAddress(in string name)
     {
        string  adr    =  "00:00:00:00:00:00";

        if ( ( name == null ) || ( name.Length == 0 ) ) return adr;
        if ( devAd.ContainsKey(name) ) {
            ulong   raw     = devAd[name];

            adr  = raw.ToString("X12");

            adr = adr.Insert(10, ":");
            adr = adr.Insert( 8, ":");
            adr = adr.Insert( 6, ":");
            adr = adr.Insert( 4, ":");
            adr = adr.Insert( 2, ":");
        }
        return adr;
    }

    /*!
     *  @fn    string? GetDeviceName(ulong address)
     *  
     *  イベントハンドラから収集した情報からaddress(Bluetoothデバイスアドレス)を元にBluetoothデバイス名を取得する。
     *  
     *  @brief  Bluetoothデバイス名取得
     *  @param  [in] ulong address     Bluetoothデバイスアドレス
     *  @return Bluetoothデバイス名  (但し、該当アドレスの登録が無ければnullとなる)
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    public string? GetDeviceName(ulong address)
    {
        return devAd.FirstOrDefault(x => x.Value.Equals(address)).Key;
    }

    /*!
     *  @fn    static void Device_ConnectionStatusChanged(BluetoothDevice sender, object args)
     *  
     *  イベントハンドラから収集した情報からaddress(Bluetoothデバイスアドレス)を元にBluetoothデバイス名を取得する。
     *  
     *  @brief  Bluetooth接続状態変化イベントハンドラ
     *  @param  [in] BluetoothDevice sender     Bluetoothデバイス(送信元)
     *  @param  [in] object          args       イベント付加情報(未使用)
     *  @return なし
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    private static void Device_ConnectionStatusChanged(BluetoothDevice sender, object args)
    {
        var     device  = (BluetoothDevice)sender;
        if ( device != null ) {
            Console.WriteLine($" {device.Name} ({device.BluetoothAddress:X}): {device.ConnectionStatus.ToString()}");

            bool stat = (device.ConnectionStatus == BluetoothConnectionStatus.Connected)? true: false;

            if ( devST.ContainsKey(device.Name) ) {
                devST[device.Name] = stat;
                devAd[device.Name] = device.BluetoothAddress;
            } else {
                devST.Add(device.Name, stat);
                devAd.Add(device.Name, device.BluetoothAddress);
            }
            Console.WriteLine($"接続状態が変化しました。 {device.Name} ({GetFormattedAddress(device.Name)}): ({device.ConnectionStatus.ToString()})");
            if (stat) {
                Console.WriteLine($"接続しました。{device.Name} ({GetFormattedAddress(device.Name)}");
            } else {
                Console.WriteLine($"切断しました。{device.Name} ({GetFormattedAddress(device.Name)}");
            }
        }
    }

    /*!
     *  @fn     static void AddConnectionStatusChecker()
     *  
     *  イベントハンドラをtgtデバイスに登録する。
     *  
     *  @brief  Bluetooth接続状態変化イベントハンドラ登録
     *  @return なし
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    public static void AddConnectionStatusChecker()
    {
        foreach (BluetoothDevice device in tgt) {
            device.ConnectionStatusChanged += Device_ConnectionStatusChanged;
        }
    }

    /*!
     *  @fn     static void DelConnectionStatusChecker()
     *  
     *  イベントハンドラをtgtデバイスから削除する。
     *  
     *  @brief  Bluetooth接続状態変化イベントハンドラ削除
     *  @return なし
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     * 
     */
    public static void DelConnectionStatusChecker()
    {
        foreach ( BluetoothDevice device in tgt ) {
            device.ConnectionStatusChanged -= Device_ConnectionStatusChanged;
        }
    }

}

Andowid端末やWindowsでのBluetoothデバイス関連開発なら、設計、開発、そして動作検証まで弊社にご相談下さい。

3. Webサーバー

 最終的には、本物のWebサーバに接続してデータのやり取りをするのですが、クローズドシステムの為、
私の会社からはそのWebサーバにアクセスする事はできません。
しかしながら、Bluetoothで受信したデータをWebサーバに送信してそのレスポンスを受信します。
受信したレスポンスを加工して、oTデバイスに状態を送信(返信)する必要があります。
さらに、Webサーバー通信に関してタイムウントやリトライの機能要件もあります。

   …さてどうしましょうか?

WindowsにはIISという「Webサーバー」がフリーで付属しています。
他にも仮想環境でLinuxを動かしRedmine運用をする為に私自身がApache立ち上げています。
これらは、全て会社ローカルなので、セキュリティー対策は最小限に抑えていて、自由に色々試行できます。
Redmineは、自身の業務で欠かせないツールとなっていますので、これが何かの拍子に動かなくなると困りますので
Linux環境のApacheの方がやりやすいのですが、IISに頑張ってもらう事にしました。
(社外からアクセスできる様にするなら頑張ってセキュリティー対策しないといけません。) こちらも都合良くクローズド環境です。

では、どうやって「本物Webサーバ」の真似をするかです。
今回はIISを選択したこともあり、CGI(Common Gateway Interface)の仕組みを使いC++でハンドリングするバイナリ作ってインストールしました。
当初はバッチファイル経由でバイナリを呼出す計画でしたが、バッチファイルの途中でフリーズする現象に遭遇
目的はWebサーバーとの通信テストを速やかに実行する為でしたので割切りしています。(よそ様に公開する事もないので問題なし。)

開発ターゲットの為にいくつか応答の性質が異なるCGIを作りました。 Webサーバーへの送信はHTTPHTTPのPOSTを使用しています。
ここでの送信データはJSONフォーマットを使用しています。(別に怖くないです。プレーンテキストベースのフォーマットです。)


3.1 共通仕様


全てのCGIで以下の機能を持たせます。

  1. 受信したデータを時間情報と共にログ化して残します。(全て)
  2. 応答時のクライアント表示ページにその情報を掲載します。(仕様上応答を返さないもの有)

3.2 個別仕様


個別にCGIで以下の機能(応答)を持たせます。

  1. 受信したデータを評価してフォーマットやデータを検査しその結果(OK/NG)を返します。/li>
  2. 受信したデータに依存せずOKを返します。(レスポンス速度等を計測する目的)/li>
  3. 受信したデータに依存せずNGを返します。(リトライタイミングや回数などの検査目的)
  4. 受信したデータに依存せず様々な応答をランダムに返します(あるいは返しません)。(総合テストで異常動作をしないかを検証する目的)
  5. 受信したデータは記録しますが、レスポンスを返しません。(サーバが見つからない場合の動作検証する目的)

個別仕様の最後のCGI(ダンマリモード)は、IISがエラーが発生して意図した動作を実現できないので
実際は使用しませんでした。
(記録は残りませんが)無効なURLで代用しました。



4. Webクライアント

 開発ターゲット側はWebクライアントとしてWebサーバに接続しにいきます。簡単な仕組みなのでフルスクラッチでも動作可能ですが。
デバッグや単体テスト等及び改変のリスクを低減する目的で、C#専用で一般公開されているライブラリを使用して実現しました。
この部分は、単純なPOSTだけですので私がコーディングしたのは数10行で、ほぼサンプルと同様です。
一部AIにコーディングもしてもらいましたが、間違っていた部分もあったので比較対象とし、全面的にコーディングし直しました。
インターネット検索でこのライブラリに関してコーディングの仕方でメモリーリークが…とありましたが、
手元で確認の為に単純なPOST繰返しを1日以上動作させて メモリリソース状況を確認しましたが
その(メモリーリーク)痕跡はありませんでした。


Webクライアントサンプル


このコードだけでは動作ないかも知れません、実コードから省略している部分や書換えた部分があります。
ですので、動作無保証ですし、このコード使用で損害に合っても賠償しません参考まで



/*!
 *  @file       WebClient.cs
 *  @brief      HTTP接続クライアント
 *  @author     (株)スマートエイジングサポート
 *  @date       2025/02/05
 * 
 *  @details    HTTPクライアントタスク
 *  @note
 * 
 */

using System.Net;
using System.Text;

using static System.Net.WebRequestMethods;

public class WebApiClient
{
    private readonly  HttpClient            Client;                                 //!<    タスクで使用するHttpClientを1つのみにする為に使用

    /*!
     *  @fn     WebApiClient()
     *  @brief  WebApiClientクラスコンストラクター(使用禁止)
     * 
     *  @note 引数のないものは外部から使用できない様にする。
     * 
     */
    private WebApiClient()
    {

    }

    /*!
     *  @fn WebApiClient(Int32  msec)
     *  @brief  WebApiClientクラスコンストラクター
     *  @param  [in] Int32      msec   タイムアウト[msec]
     * 
     *  HttpClientHandlerをリダイレクト禁止にして生成し、そのハンドラを使用してHttpClientを生成する。
     *  生成したClientインスタンスにタイムアウトを設定する。
     * 
     *  @note
     */
    public WebApiClient(Int32  msec)
    {
        var handler = new HttpClientHandler
        {
            AllowAutoRedirect = false // リダイレクトをしないように設定
        };
        Client          = new HttpClient(handler);
        Client.Timeout  = TimeSpan.FromMilliseconds(msec);
    }

    /*!
     *  @fn   async Task PostAsync(string url, string infomation, Int32 retry)
     *  
     *  呼出サーバーに予めJSONフォーマット化してある情報をPOSTしてそのステータスコードを返す。
     *  タイムアウトやキャンセル以外の例外が発生した場合は、ステータスコードHttpStatusCode.InternalServerErrorを返して終了する。
     *
     *  @brief  非同期呼出サーバPOSTタスク
     *  @param  [in] string     url         呼出サーバーURL
     *  @param  [in] string     informarion JSONフォーマットで記述された送信情報
     *  @param  [in] Int32      retry       リトライ回数
     *  @return ステータスコード
     * 
     *  @author     (株)スマートエイジングサポート
     *  @date       2025/02/05
     * 
     *  @note
     */
    public async Task PostAsync(string url, string infomation, Int32 retry)
    {
        Console.WriteLine("Begin");
        int             value       = 250;
        Color           status      = Color.FromArgb(255,value,value);
        for ( Int32 count = 0; count <= retry; count++) {
            try {
                //HTTP POST要求を送信する
                var data = new StringContent(infomation, Encoding.UTF8, mediaType: "application/json");
                Console.WriteLine("POST");
                var response = await Client.PostAsync(url, data);
                response.EnsureSuccessStatusCode();
                string body = await response.Content.ReadAsStringAsync();  
                Console.WriteLine($"Statuscode : {response.StatusCode.ToString()} ");
                Console.WriteLine("End(Sucsess).");
                return response.StatusCode;
                
            } catch ( InvalidOperationException ex ) {  // 無効な操作
                Console.WriteLine(ex.ToString());
                break;
                
            } catch ( HttpRequestException ex ) {       // 送信リクエスト失敗
                Console.WriteLine(ex.ToString());
                break;

            } catch ( TaskCanceledException ex ) {      // タイムアウト(含むリトライ処理)
                Console.WriteLine(ex.ToString());
                if (count >= retry) {
                    return HttpStatusCode.GatewayTimeout;
                }
                continue;
                
            } catch ( UriFormatException ex ) {         // URI無効
                Console.WriteLine(ex.ToString());
                break;
                
            } catch ( Exception ex ) {                  // その他の例外全て
                Console.WriteLine(ex.ToString());
                break;
                
            }
        }
        Console.WriteLine("End(Failed)");
        return HttpStatusCode.InternalServerError;
    }
}
Andowid端末やWindowsPCでのWebクライアント関連開発なら、設計、開発、そして動作検証まで弊社にご相談下さい。


5.1 実環境での2つの大きな課題

5.1.1 Bluetooth(SerialPort)が使えない課題

 Windowsにインストールされているセキュリティ対策ソフトの影響でBluetoothでSPPを設定して使用する事ができませんでした。
所謂仮想シリアルポートですから、ここから入出力が出来たら、情報漏洩は発生します。
これを防ぐため、全てのオフィスで使用するWindowsには、それを防止するセキュリティソフトが導入されていました。

この仕組み自体に「穴あけ」するのは本末転倒ですので開発ソフトウエア導入先で他のセキュリティ対策を選択頂きました。
無事フィールドテストもパスして受入テストも一発でパスできました。


5.1.2 HiDPI 問題

 高解像度(特にWindowsタブレット系)でのGUI表示がウィンドウが極端に小さいだけでなく、文字部分が大きく表示され
GUIが崩れてしまいました。単に小さいだけならスケーリング調整で済みますし、テキストやUIがぼやけるのであれば
取り敢えずその状態で運用頂いて対策講じるとかできるのですが、GUI崩壊というかっこ悪い状態に最終段で発覚しました。
それまでは、自身の開発環境PC上で動作させていたのでDPIの違いによる不具合は気が付きませんでした。

対策は、C#のデザイナーである設定をONするだけです。フォント表示がメインでメイン画面の背景に壁紙の様にリソースを
導入しているだけなので、以下を実施しただけで済みました。

・ AutoScaleModeプロパティをDpiモードにする。


マウスポインタを画像に合わせると設定変更後を確認できます。

図1. システム全体概略図

今時、DPI(1インチにどれだけドットが打てるか?で画面のGUIを制御するなんて、私が大学時代の初代MacintoshやImageWriterを
思い出しました(72 DPI! WYSIWYG)。ぼやけてもよければアフィン変換したら一発で終了ですし、
超解像度技術で解像度低いのから高いのに変換してもそんなに頑張らなくても人間の目には綺麗に見える様にできます。
今時なにやっているのだろうか? 私。


以上です。

【技術】オムロン製PLCラダープログラム設計

2025年05月09日

オムロン製PLCラダープログラムを設計しました。

【設計ソフト/機器環境】
・オムロンPLC CJ2M-CPU34
・オムロンPLC CX-Programmer ver 9.7

【外部接続機器】※イーサネット接続
・安川製PLC(安川製サーボモーター) MPX1312
・IAI製コントローラ(シリンダー)RCON/RSEL
・オムロン製温度調節器  NX-EIC202
・オムロン製安全セーフティユニット GC-1000
・八紘電機製タッチパネル V9100iSD

【機器構成】
1オムロンPLC⇔安川PLC間でサーボモーターを制御
2オムロンPLC⇔安全PLC間でセーフティ回路を構成
3オムロンPLC⇔タッチパネル間の制御
4オムロンPLC⇔温度調節器の制御

【技術】PLCとロボットの相互通信プログラム

2025年04月24日

キーエンス製PLCとロボットの相互通信プログラムを設計しました。

【設計,デバックソフト/機器環境】
・キーエンスPLC KV-8000S0
・キーエンスPLCソフト KV STUDIO ver.11G
・キーエンスタッチパネル VT5-W07(イーサネット接続)
・キーエンスタッチパネルソフト VT STUDIO ver.8G
・キーエンス製安全セーフティユニット GC-1000(イーサネット接続)
・キーエンス製安全セーフティユニットソフト GC-CONFIGURATOR ver+1.3.0.1

【外部接続機器】※イーサネット接続
・三菱電機製PLC Q03UDVCPU
・三菱電気製サーボ  HG- K R 0 5 3 等
・IAI製ロボット,シリンダー  RCP6-SA4C-WA-35P-10-50 等

【機器構成】
1キーエンスPLC⇔三菱製サーボモーターの制御
2キーエンスPLC⇔IAI製ロボット,シリンダー間の制御
3キーエンスPLC⇔タッチパネル間の制御
4キーエンスPLC⇔安全PLC間でセーフティ回路を構成