音声認識でドローンを飛ばす

諸君 私はロボットが好きだ
諸君 私はロボットが大好きだ

ゲッターが好きだ
マジンガーが好きだ
ジャイアントロボが好きだ
ビッグオーが好きだ

ロジャースミスが「ビッグオーアクション!!」といって、轟音と共に街を吹き飛ばすのが好きだ
空中高く放り上げられたゲッターが 「チェンジゲッターワン!!」でばらばらとなって合体した時など心がおどる

ということで、音声でロボットを動かすというのは人類のロマンを果たすために音声認識でtelloを飛ばしました。
telloの動かし方は下記を参照してください。
https://needtec.sakura.ne.jp/wod07672/?p=9212

.NETで音声認識

.NETには標準で「System.Speech.Recognition」がサポートされており、音声認識が行えます。

サンプルコード全体は以下にあります。
https://github.com/mima3/Tello/tree/master/TelloSpeech

簡単な流れ

1.SpeechRecognitionEngineを作成する
2.SpeechRecognitionEngineに文法を与える
3.SpeechRecognizedイベントを処理する。

SpeechRecognitionEngineを作成する。

参照設定で「System.Speech」を追加する。
「using System.Speech.Recognition;」を宣言する。
SpeechRecognitionEngineオブジェクトを作成する。

SpeechRecognitionEngine recognizer;
// 略
recognizer = new SpeechRecognitionEngine(new System.Globalization.CultureInfo("ja-JP"));

コンストラクタにロケールを与えているので、使っているOSの言語によって、ここは変更する必要があります。

文法を作成する

ここで発音のフレーズとその意味を関連付けます。
たとえば、ロボットを発進させるとき兜甲児は「マジン、ゴー!」といいますし、ロジャースミスは「ビッグオーアクション!」といいます。
おなじ発進をおこなうという目的のための命令フレーズは主人公の数だけあるといっていいでしょう。
System.Speech.RecognitionではSemanticResultValueを用いることで、フレーズと意味を紐づけることができます。
以下は様々な発進フレーズをtelloの離着陸に関連付けたものになります。

        Choices CreateSingleCommandChoices()
        {
            Choices ret = new Choices();
            ret.Add(new SemanticResultValue("てーくおふ", "takeoff"));
            ret.Add(new SemanticResultValue("はっしん", "takeoff"));
            ret.Add(new SemanticResultValue("てろ、ごー", "takeoff"));
            ret.Add(new SemanticResultValue("てろ、あくしょん", "takeoff"));

            ret.Add(new SemanticResultValue("らんど", "land"));
            ret.Add(new SemanticResultValue("ちゃくりく", "land"));
            return ret;
        }

ChoicesオブジェクトをGrammarBulderオブジェクトに追加し、そこからGrammarオブジェクトを作成してそれをSppechRecognitionEngineにロードします。

            Choices singleCommands = CreateSingleCommandChoices();
            GrammarBuilder cmdGb = new GrammarBuilder();
            cmdGb.Append(new SemanticResultKey("singleCommands", singleCommands));
            Grammar cmdG = new Grammar(cmdGb);
            recognizer.LoadGrammar(cmdG);

ここでは作成した発進の種類について「singleCommands」というキー名で登録しています。
この「singleCommands」というキー名は音声認識に成功した場合に、なにを認識できたのか(takeoffを認識したのか、landを認識したのか)を取得するのに使用します。

SpeechRecognizedイベントを処理する

マイクからの入力でSpeechRecognitionEngineが与えられた文法を解析できた場合は、SpeechRecognizedが発火されます。
以下ではイベントの実装例をしめします。

イベントハンドラを登録する例

            // Add a handler for the speech recognized event.  
            recognizer.SpeechRecognized +=
              new EventHandler<SpeechRecognizedEventArgs>(recognizer_SpeechRecognized);

イベントを処理する例

        // Handle the SpeechRecognized event.  
        void recognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            Debug.WriteLine("SpeechRecognized...");
            if (e.Result.Semantics != null)
            {
                if (e.Result.Semantics.ContainsKey("singleCommands"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["singleCommands"].Value);

                    // telloにUDP経由でコマンドを送信する。
                    sendCmd((string)e.Result.Semantics["singleCommands"].Value);

                }
            }
        }

ここでは音声認識が正常に認識できた場合、「singleCommand」として登録した[takeoff」または「land」コマンドがtelloに送信されます。

数字を使う命令の音声解析

telloには「命令+数値」という形式のコマンドが存在します。たとえば「up 30」では30cm上昇しますし、「down 30」では30cm下降します。
これに対応するにはChoicesオブジェクトを2つ使って対応します。

まず、命令と数値のChoicesオブジェクトを作成します。

        Choices CreateNumberChoices()
        {
            Choices numbers = new Choices();
            for (int i = 0; i < 300; ++i)
            {
                numbers.Add(new SemanticResultValue(i.ToString(), i));
            }
            return numbers;
        }

        Choices CreateDirections()
        {
            Choices ret = new Choices();
            ret.Add(new SemanticResultValue("ふぉわーど", "forward"));
            ret.Add(new SemanticResultValue("すすめ", "forward"));

            ret.Add(new SemanticResultValue("ばっく", "back"));
            ret.Add(new SemanticResultValue("もどれ", "back"));

            ret.Add(new SemanticResultValue("あっぷ", "up"));
            ret.Add(new SemanticResultValue("あがれ", "up"));

            ret.Add(new SemanticResultValue("だうん", "down"));
            ret.Add(new SemanticResultValue("さがれ", "down"));

            ret.Add(new SemanticResultValue("らいと", "right"));
            ret.Add(new SemanticResultValue("みぎ", "right"));

            ret.Add(new SemanticResultValue("れふと", "left"));
            ret.Add(new SemanticResultValue("ひだり", "left"));

            ret.Add(new SemanticResultValue("かいてん", "cw"));
            ret.Add(new SemanticResultValue("まわれ", "cw"));
            ret.Add(new SemanticResultValue("ぎゃくかいてん", "ccw"));
            return ret;
        }

SemanticResultValueを追加すればするほど重くなるので、数字を3000とか10000まで認識できるようにするのは避けておいた方が無難でしょう。
以下は作成した命令と数のChoicesオブジェクトを利用して、「命令→数」の順番で認識する文法を登録した例です。
つまり「らいと ひゃく」と叫ぶと「right 100」と解釈します。

            //
            Choices numbers = CreateNumberChoices();
            Choices directions = CreateDirections();
            // 

            // Create a GrammarBuilder object and append the Choices object.
            GrammarBuilder moveGb = new GrammarBuilder();
            moveGb.Append(new SemanticResultKey("moveCommands", directions));
            moveGb.Append(new SemanticResultKey("moveCm" , numbers));

            // Create the Grammar instance and load it into the speech recognition engine.
            Grammar moveG = new Grammar(moveGb);

            // Create and load a dictation grammar.  
            recognizer.LoadGrammar(moveG);

あとはSpeechRecognized イベントを適切に処理します。

        void recognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            Debug.WriteLine("SpeechRecognized...");
            if (e.Result.Semantics != null)
            {
                if (e.Result.Semantics.ContainsKey("singleCommands"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["singleCommands"].Value);
                    sendCmd((string)e.Result.Semantics["singleCommands"].Value);

                }
                else if (e.Result.Semantics.ContainsKey("moveCommands") &&
                         e.Result.Semantics.ContainsKey("moveCm"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["moveCommands"].Value + " " + e.Result.Semantics["moveCm"].Value);
                    sendCmd((string)e.Result.Semantics["moveCommands"].Value + " " + ((int)e.Result.Semantics["moveCm"].Value).ToString());

                }
            }
        }

全体のコード

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Speech.Recognition;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TelloSpeech
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private UdpClient udpForCmd;     //コマンド結果受信用クライアント
        private UdpClient udpForStsRecv; //ステータスの結果受信用クライアント
        SpeechRecognitionEngine recognizer; //

        public MainWindow()
        {
            InitializeComponent();
        }

        private void btnConnect_Click(object sender, RoutedEventArgs e)
        {
            SetupTello();
            sendCmd("command");
            SetUpSpeech();
            btnConnect.IsEnabled = false;

        }

        // Telloとの通信を設定する
        private void SetupTello()
        {
            this.udpForCmd = new UdpClient(0);
            this.udpForStsRecv = new UdpClient(8890);

            // コマンド結果の受信処理
            Task.Run(() => {
                IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
                while (true)
                {
                    try
                    {
                        String rcvMsg = "";
                        byte[] rcvBytes = udpForCmd.Receive(ref remoteEP);
                        Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes));
                        txtResult.Dispatcher.BeginInvoke(
                            new Action(() =>
                            {
                                txtResult.Text = rcvMsg;
                            })
                        );
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }

            });

            // ステータスの受信処理
            Task.Run(() => {
                IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
                while (true)
                {
                    try
                    {
                        String rcvMsg = "";
                        byte[] rcvBytes = udpForStsRecv.Receive(ref remoteEP);
                        Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes));
                        rcvMsg = rcvMsg.Replace(";", "\r\n");
                        txbStatus.Dispatcher.BeginInvoke(
                            new Action(() =>
                            {
                                txbStatus.Text = rcvMsg;
                            })
                        );
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }

                }

            });
        }

        // コマンド送信
        private void sendCmd(string cmd)
        {
            txtCmd.Text = cmd;
            byte[] data = Encoding.ASCII.GetBytes(cmd);
            this.udpForCmd.Send(data, data.Length, "192.168.10.1", 8889);

        }

        private void SetUpSpeech()
        {
            recognizer = new SpeechRecognitionEngine(new System.Globalization.CultureInfo("ja-JP"));

            Choices singleCommands = CreateSingleCommandChoices();
            GrammarBuilder cmdGb = new GrammarBuilder();
            cmdGb.Append(new SemanticResultKey("singleCommands", singleCommands));
            Grammar cmdG = new Grammar(cmdGb);
            recognizer.LoadGrammar(cmdG);

            //
            Choices numbers = CreateNumberChoices();
            Choices directions = CreateDirections();
            // 

            // Create a GrammarBuilder object and append the Choices object.
            GrammarBuilder moveGb = new GrammarBuilder();
            moveGb.Append(new SemanticResultKey("moveCommands", directions));
            moveGb.Append(new SemanticResultKey("moveCm" , numbers));

            // Create the Grammar instance and load it into the speech recognition engine.
            Grammar moveG = new Grammar(moveGb);

            // Create and load a dictation grammar.  
            recognizer.LoadGrammar(moveG);

            // Add a handler for the speech recognized event.  
            recognizer.SpeechRecognized +=
              new EventHandler<SpeechRecognizedEventArgs>(recognizer_SpeechRecognized);

            // Configure input to the speech recognizer.  
            recognizer.SetInputToDefaultAudioDevice();

            // Start asynchronous, continuous speech recognition.  
            recognizer.RecognizeAsync(RecognizeMode.Multiple);
        }
        Choices CreateSingleCommandChoices()
        {
            Choices ret = new Choices();
            ret.Add(new SemanticResultValue("てーくおふ", "takeoff"));
            ret.Add(new SemanticResultValue("はっしん", "takeoff"));
            ret.Add(new SemanticResultValue("てろ、ごー", "takeoff"));
            ret.Add(new SemanticResultValue("てろ、あくしょん", "takeoff"));
            ret.Add(new SemanticResultValue("らんど", "land"));
            ret.Add(new SemanticResultValue("ちゃくりく", "land"));
            ret.Add(new SemanticResultValue("きろくかいし", "streamon"));
            ret.Add(new SemanticResultValue("きろくていし", "streamoff"));

            return ret;
        }
        Choices CreateNumberChoices()
        {
            Choices numbers = new Choices();
            for (int i = 0; i < 300; ++i)
            {
                numbers.Add(new SemanticResultValue(i.ToString(), i));
            }
            return numbers;
        }

        Choices CreateDirections()
        {
            Choices ret = new Choices();
            ret.Add(new SemanticResultValue("ふぉわーど", "forward"));
            ret.Add(new SemanticResultValue("すすめ", "forward"));

            ret.Add(new SemanticResultValue("ばっく", "back"));
            ret.Add(new SemanticResultValue("もどれ", "back"));

            ret.Add(new SemanticResultValue("あっぷ", "up"));
            ret.Add(new SemanticResultValue("あがれ", "up"));

            ret.Add(new SemanticResultValue("だうん", "down"));
            ret.Add(new SemanticResultValue("さがれ", "down"));

            ret.Add(new SemanticResultValue("らいと", "right"));
            ret.Add(new SemanticResultValue("みぎ", "right"));

            ret.Add(new SemanticResultValue("れふと", "left"));
            ret.Add(new SemanticResultValue("ひだり", "left"));

            ret.Add(new SemanticResultValue("かいてん", "cw"));
            ret.Add(new SemanticResultValue("まわれ", "cw"));
            ret.Add(new SemanticResultValue("ぎゃくかいてん", "ccw"));
            return ret;
        }

        // Handle the SpeechRecognized event.  
        void recognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            Debug.WriteLine("SpeechRecognized...");
            if (e.Result.Semantics != null)
            {
                if (e.Result.Semantics.ContainsKey("singleCommands"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["singleCommands"].Value);
                    sendCmd((string)e.Result.Semantics["singleCommands"].Value);

                }
                else if (e.Result.Semantics.ContainsKey("moveCommands") &&
                         e.Result.Semantics.ContainsKey("moveCm"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["moveCommands"].Value + " " + e.Result.Semantics["moveCm"].Value);
                    sendCmd((string)e.Result.Semantics["moveCommands"].Value + " " + ((int)e.Result.Semantics["moveCm"].Value).ToString());

                }
            }
        }
    }
}

まとめ

System.Speechを使用すれば簡単に音声認識プログラムがC#で実装できることを確認しました。
System.Speechについての詳細は下記を参照してください。
https://docs.microsoft.com/ja-jp/previous-versions/office/developer/speech-technologies/hh361625(v=office.14)

今回は文法をコード上に記載しましたがXMLで記載して外部ファイルとして保持することも可能のようです。
https://docs.microsoft.com/ja-jp/previous-versions/office/developer/speech-technologies/hh361594(v%3doffice.14)

VBAでInternetExplore上のJavaScriptを無理やり動かすよ!

みんな大好きInternetExplore11のJavaScriptを、みんな大好きなExcelのVBAから動かします。

なにが嬉しいの?

・現在、表示されているIEのJavaScriptの保持している変数の中身を出力できるよ!
・無理やりJavaScriptで例外を発生させたときの挙動を確かめられるよ!
・サーバー側のソースコードを書き換えずにローカル環境でJavaScriptの関数をテスト用に置き換えられるよ!

ExcelVBAなので、手足を縛ってプログラムを開発するのが性癖なマゾい会社でも使えるよ!やったぜ!

環境

InternetExploere
Windows10
Office16 Excel 32bit

なお、64bitのExcelを使っている箇所はlongでなく、longPtrとかにしないと動かないと思うけど、ぶっちゃけ64bitマシン支給するようなブルジョワジーの組織だったらVisualStudio使わせてもらってC#で書いた方がいいと思います。

サンプル

IEAuto.bas

 Option Explicit

' Microsoft HTML Object Libraryを参照設定すること

Private Declare Function EnumWindows Lib "user32" (ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long
Private Declare Function EnumChildWindows Lib "user32" (ByVal hWndParent As Long, ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long

Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Private Declare Function RegisterWindowMessage Lib "user32" Alias "RegisterWindowMessageA" (ByVal lpString As String) As Long
Private Declare Function SendMessageTimeout Lib "user32" Alias "SendMessageTimeoutA" (ByVal hwnd As Long, ByVal msg As Long, ByVal wParam As Long, ByVal lParam As Long, ByVal fuFlags As Long, ByVal uTimeout As Long, lpdwResult As Long) As Long
Private Declare Function ObjectFromLresult Lib "oleacc" (ByVal lResult As Long, riid As Any, ByVal wParam As Long, ppvObject As Object) As Long

Private mIEWndList As Collection
Private Type UUID
  Data1 As Long
  Data2 As Integer
  Data3 As Integer
  Data4(0 To 7) As Byte
End Type

' MSHTML.IHTMLDocumentのキャッシュを検索する
Public Sub RefreshBrowserCache()
    Set mIEWndList = New Collection

    Call EnumWindows(AddressOf EnumIEWndProc, 0)
End Sub
' 指定の名称を含むタイトルのブラウザを検索する
Public Function FindBrowser(ByVal title As String) As MSHTML.IHTMLDocument2
    Dim r As MSHTML.IHTMLDocument2

    If mIEWndList Is Nothing Then
        Call RefreshBrowserCache
    End If

    Set r = FindBrowserInCache(title)
    If Not r Is Nothing Then
        Set FindBrowser = r
        Exit Function
    End If

    Call RefreshBrowserCache
    Set FindBrowser = FindBrowserInCache(title)

End Function

' キャッシュの中から指定の名称を含むタイトルのブラウザを検索する
Private Function FindBrowserInCache(ByVal title As String) As MSHTML.IHTMLDocument2
    Dim d As MSHTML.IHTMLDocument2
    For Each d In mIEWndList
        If InStr(d.title, title) > 0 Then
            Set FindBrowserInCache = d
            Exit Function
        End If
    Next

End Function

' コールバック関数
Private Function EnumIEWndProc(ByVal hwnd As Long, lParam As Long) As Boolean
    Const FRAME_NAME = "IEFrame"
    Const FRAME_NAME_EDGE = "TabWindowClass" 'Edgeの場合のクラス名

    Dim strClassName As String * 128
    Dim lngRet As Long

    strClassName = ""
    Call GetClassName(hwnd, strClassName, 128)
    Dim c As String
    c = Left(strClassName, Len(Trim(strClassName)) - 1)
    If c = FRAME_NAME Or c = FRAME_NAME_EDGE Then
        EnumChildWindows hwnd, AddressOf EnumIEServerWndProc, 0
    End If
    'Debug.Print "[" & Trim(strClassName) & "_]"
    EnumIEWndProc = True
End Function

' コールバック関数
Private Function EnumIEServerWndProc(ByVal hwnd As Long, ByVal lParam As Object) As Long
    Const SERVER_NAME = "Internet Explorer_Server"
    Dim strClassName As String * 128
    Dim lngRet As Long
    Dim doc As MSHTML.IHTMLDocument2

    strClassName = ""
    Call GetClassName(hwnd, strClassName, Len(strClassName))
    If Left(strClassName, Len(Trim(strClassName)) - 1) = SERVER_NAME Then
        Set doc = GetHTMLDocument(hwnd)
        If Not doc Is Nothing Then
            mIEWndList.Add doc
        End If
    End If
    EnumIEServerWndProc = 1
End Function

Private Function GetHTMLDocument(ByVal hwnd As Long) As MSHTML.IHTMLDocument2
    Dim doc As MSHTML.IHTMLDocument2
    Dim lMsg As Long
    Dim lRet As Long
    Dim hr As Long
    Dim IID_IHTMLDocument As UUID

    With IID_IHTMLDocument
        .Data1 = &H626FC520
        .Data2 = &HA41E
        .Data3 = &H11CF
        .Data4(0) = &HA7
        .Data4(1) = &H31
        .Data4(2) = &H0
        .Data4(3) = &HA0
        .Data4(4) = &HC9
        .Data4(5) = &H8
        .Data4(6) = &H26
        .Data4(7) = &H37
    End With
    lMsg = RegisterWindowMessage("WM_HTML_GETOBJECT")
    If lMsg <> 0 Then
        SendMessageTimeout hwnd, lMsg, 0, 0, &H2, 1000, lRet
        If lRet <> 0 Then
            If ObjectFromLresult(lRet, IID_IHTMLDocument, 0, doc) = 0 Then
                Set GetHTMLDocument = doc
            End If
        End If
    End If
End Function

サンプル

Public Sub test()
    Dim b As MSHTML.IHTMLDocument3
    Dim w As Object

    Set b = IEAuto.FindBrowser("Google")
    If b Is Nothing Then
        Debug.Print "見つからない"
        Exit Sub
    End If

    Dim r As Variant
    Dim elems As MSHTML.IHTMLElementCollection
    Dim elem As MSHTML.IHTMLElement
    Set elems = b.getElementsByName("btnK")
    Debug.Print elems.Item(0).getAttribute("value")

    ' 以下のような感じでJavaScriptにアクセスできるがEdgeの場合はアクセス違反になる
    ' 管理者権限も無駄
    Dim testId As Variant
    testId = Timer
    b.parentWindow.execScript ("function x() { result = 5+5+4; console.log('TEST'); var d = document.createElement('div'); d.id = 'test_elem" & testId & "'; d.innerHTML  = result; document.getElementsByTagName('body').item(0).appendChild(d); }; x();")

    Set elem = b.getElementById("test_elem" & testId)
    Debug.Print elem.innerHTML

End Sub

解説

IEのウィンドウハンドルからMSHTML.IHTMLDocumentを取得してあとはよくあるIEの自動操作をしています。
IHTMLDocumentに変換できるウィンドウのクラス名は「Internet Explorer_Server」です。
IE11の場合は「IEFrame」クラスの子ウィンドウになっています。
Edgeの場合は「TabWindowClass」クラスの子ウィンドウになっています。

プログラムとしては「IEAuto.FindBrowser("Google")」でタイトルにGoogleを含むものを検索してIHTMLDoucmentを取得します。
あとは、exeScriptでJavaScriptをたたくことが可能ですが、関数の仕様として戻り値を取得することはできません。

そのため、JavaScriptから値をVBAに返す場合は、テスト用の要素に書き込むといいでしょう。

なお、すくなくとも当方の環境だとexeScriptはEdgeだとアクセス違反になりました。

参考

Cant create HTML document from Hwnd using C#
https://stackoverflow.com/questions/20873885/cant-create-html-document-from-hwnd-using-c-sharp

【2017年1月版】Microsoft Edgeを操作するVBAマクロ(DOM編)
https://www.ka-net.org/blog/?p=7921

Javaのバイトコードを解析した結果をクラス図で表現してみる

Javaのバイトコードを解析した結果をクラス図やコールグラフで表現する

目的

Javaのバイトコードからクラスやメソッドの情報を抜き出してSqliteに記録します。
上記の記録した情報を用いてクラス図やコールグラフを記載します。

なお、doxygen + graphviz使える人は、それを使ったほうがいいです。

使用環境

・Windows10
・Java8
・bcel-6.3.1.
・sqlite-jdbc-3.27.2.1
・plantuml.jar

使用ライブラリの解説

bcel

BCEL API(バイトコードエンジニアリングライブラリ)は、静的分析および動的なJavaクラスファイルの作成のツールキットです。
FindBugはBCELを使用してクラスファイルから静的解析を行っています。

bcel.png

https://commons.apache.org/proper/commons-bcel/manual/introduction.html

サンプルコード

JavaSE BCEL
https://hondou.homedns.org/pukiwiki/index.php?JavaSE%20BCEL

またバイトコードのバイナリをどう解釈するかは以下のcodeToStringが参考になります。
https://github.com/llmhyy/commons-bcel/blob/master/src/main/java/org/apache/bcel/classfile/Utility.java

Sqlite-JDBC

SQLiteを操作するJDBCで下記のページからダウンロードできます。

ダウンロード
https://bitbucket.org/xerial/sqlite-jdbc/downloads/

サンプルコード

package sqlitesample;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import org.sqlite.Function;

public class SqliteSample {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println("start-----");
        // load the sqlite-JDBC driver using the current class loader
        Class.forName("org.sqlite.JDBC");

        Connection connection = null;
        Statement statement = null;
        ResultSet rs = null;
        PreparedStatement pstmt = null;

        try
        {
            // create a database connection
            // Connection connection = DriverManager.getConnection("jdbc:sqlite:C:/work/mydatabase.db");
            // Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:");
            connection = DriverManager.getConnection("jdbc:sqlite:sample.db");
            connection.setAutoCommit(true);
            statement = connection.createStatement();
            statement.setQueryTimeout(30);  // set timeout to 30 sec.

            System.out.println("create table -----");
            statement.executeUpdate("drop table if exists person");
            statement.executeUpdate("create table person (id integer, name string)");
            statement.executeUpdate("insert into person values(1, 'leo')");
            statement.executeUpdate("insert into person values(2, 'yui')");
            rs = statement.executeQuery("select * from person");
            while(rs.next())
            {
                // read the result set
                System.out.println("name = " + rs.getString("name"));
                System.out.println("id = " + rs.getInt("id"));
            }
            rs.close();
            rs = null;

            System.out.println("update -----");
            pstmt = connection.prepareStatement("update person set name = ? where id = ?");
            pstmt.setString(1, "ole");
            pstmt.setInt(2, 1);
            pstmt.executeUpdate();
            rs = statement.executeQuery("select * from person");
            while(rs.next())
            {
                // read the result set
                System.out.println("name = " + rs.getString("name"));
                System.out.println("id = " + rs.getInt("id"));
            }
            rs.close();
            rs = null;

            System.out.println("Transactions -----");
            connection.setAutoCommit(false);
            statement.executeUpdate("insert into person values(3, 'zoo')");
            rs = statement.executeQuery("select * from person");
            while(rs.next())
            {
                // read the result set
                System.out.println("name = " + rs.getString("name"));
                System.out.println("id = " + rs.getInt("id"));
            }
            rs.close();
            rs = null;
            System.out.println("rollback -----");
            connection.rollback();
            rs = statement.executeQuery("select * from person");
            while(rs.next())
            {
                // read the result set
                System.out.println("name = " + rs.getString("name"));
                System.out.println("id = " + rs.getInt("id"));
            }
            rs.close();
            rs = null;

            System.out.println("function -----");
            Function.create(connection, "total", new Function() {
                @Override
                protected void xFunc() throws SQLException
                {
                    int sum = 0;
                    for (int i = 0; i < args(); i++)
                        sum += value_int(i);
                    result(sum);
                }
            });
            rs = statement.executeQuery("select total(1, 2, 3, 4, 5)");
            while(rs.next())
            {
                // read the result set
                System.out.println("total(1,2,3,4,5) = " + rs.getInt(1));
            }
            rs.close();
            rs = null;

        }
        catch(SQLException e)
        {
            // if the error message is "out of memory",
            // it probably means no database file is found
            System.err.println(e.getMessage());
        }
        finally
        {
            try
            {
                if(rs != null)
                {
                    rs.close();
                }
                if(pstmt != null)
                {
                    pstmt.close();
                }
                if(statement != null)
                {
                    statement.close();
                }
                if(connection != null)
                {
                connection.close();
                }
            }
            catch(SQLException e)
            {
                // connection close failed.
                System.err.println(e);
            }
        }
    }
}

ユーザ定義関数とかも作成できます。
その他、SQLite固有の操作については下記のテストコードが参考になります。
https://github.com/xerial/sqlite-jdbc/tree/c7c5604bcc584460268abc9a64df2953fca788d3/src/test/java/org/sqlite

PlantUML

DSLといわれる言語でUMLを含めた以下の図を記載できます。

  • シーケンス図
  • ユースケース図
  • クラス図
  • アクティビティ図(古い文法はこちら)
  • コンポーネント図
  • 状態遷移図(ステートマシン図)
  • オブジェクト図
  • 配置図
  • タイミング図
  • ワイヤーフレーム
  • アーキテクチャ図
  • 仕様及び記述言語 (SDL)
  • Ditaa
  • ガントチャート
  • マインドマップ
  • WBS図(作業分解図)
  • AsciiMath や JLaTeXMath による、数学的記法

公式でWebページから実際の記載を試せます。
RedmineのプラグインもあるのでWikiにテキストデータとして各種図を埋め込むことができます。

公式:
http://plantuml.com/ja/

ダウンロード
http://plantuml.com/ja/download

サンプル

PlantUML Cheat Sheet
https://qiita.com/ogomr/items/0b5c4de7f38fd1482a48

plantuml.jarファイルを自分のJavaのプロジェクトに組み込むことで、自分のプログラムからPNGやSVGを出力することも可能です。
http://plantuml.com/ja/api

実験結果

https://github.com/mima3/BcelToSqlite

bcelを使用してSqliteを記録する

以下のコードではJarファイルを指定して、そこに格納されているclassファイルを解析してクラス、メソッド、フィールドの情報をSQLiteに記録しています。

BcelToSqlite.java


package bcelToSqlite;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.bcel.Const;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.generic.Type;
import org.apache.bcel.util.ByteSequence;

public class BcelToSqlite {
    Connection connection = null;
    PreparedStatement pstmt = null;
    int nextClassId = 1;
    int nextMethodId = 0x10000001;
    int nextAnnotationId = 0x20000001;

    /**
     * 下記参考に実装
     * https://hondou.homedns.org/pukiwiki/index.php?JavaSE%20BCEL
     * @param args
     * @throws Exception
     */
    public static void main(String args[]) throws Exception
    {
        String srcPath;
        srcPath = ".\\lib\\bcel-6.3.1.jar";
        BcelToSqlite thisClass = new BcelToSqlite();
        thisClass.startWalk(new File(srcPath));
        System.out.println(srcPath + " -> output.sqlite");
    }

    private void executeSql(String sql) throws SQLException {
        pstmt = connection.prepareStatement(sql);
        pstmt.executeUpdate();
        pstmt.close();
        pstmt = null;
    }
    private void executeSql(String sql, Object args[]) throws SQLException {
        pstmt = connection.prepareStatement(sql);
        int ix = 1;
        for (Object obj : args) {
            try {
                pstmt.setInt(ix, (int)Integer.parseInt(obj.toString()));
            } catch (NumberFormatException ex) {
                pstmt.setString(ix, obj.toString());
            }

            ++ix;
        }
        pstmt.executeUpdate();
        pstmt.close();
        pstmt = null;
    }

    private void startWalk(File path) throws Exception {
        try {
            connection = DriverManager.getConnection("jdbc:sqlite:output.sqlite");
            connection.setAutoCommit(true);
            executeSql("drop table if exists class");
            executeSql("create table class (id int primary key, name string , access_flags int, super_class_name string)");

            executeSql("drop table if exists interface");
            executeSql("create table interface (class_id int, interface_name string)");

            executeSql("drop index if exists index_interface");
            executeSql("create index index_interface on interface(class_id)");

            executeSql("drop table if exists method");
            executeSql("create table method (id int primary key, class_id int, name string, fullname string, access_flag int, return_type string, byte_code string)");

            executeSql("drop table if exists method_parameter");
            executeSql("create table method_parameter (method_id int, seq int, param_type string)");

            executeSql("drop index if exists index_method_parameter");
            executeSql("create index index_method_parameter on method_parameter(method_id)");

            executeSql("drop table if exists method_depend");
            executeSql("create table method_depend (method_id int, called_method string, opecode int)");

            executeSql("drop index if exists index_method_depend");
            executeSql("create index index_method_depend on method_depend(method_id)");

            executeSql("drop table if exists field");
            executeSql("create table field (id int primary key, class_id int, name string, fullname string, access_flag int, type string)");

            executeSql("drop table if exists anotation");
            executeSql("create table anotation (id int primary key, refid int, type string)");

            executeSql("drop index if exists index_anotation");
            executeSql("create index index_anotation on anotation(refid)");

            connection.setAutoCommit(false);
            dirWalk(path);
            connection.commit();

        }
        finally
        {
            if(pstmt != null)
            {
                pstmt.close();
            }
            if(connection != null)
            {
                connection.close();
            }
        }
    }
    private void dirWalk(File path) throws Exception {
        if (path.isDirectory()) {
            for (File child : path.listFiles()) {
                dirWalk(child);
            }
        } else if (path.getName().endsWith("jar") || path.getName().endsWith("zip")) {
            jarWalk(path);
        } else if (path.getName().endsWith("class")) {
            JavaClass javaClass = new ClassParser(path.getAbsolutePath()).parse();
            classWalk(javaClass);
        }
    }

    private void jarWalk(File jarFile) throws Exception {
        try (JarInputStream jarIn = new JarInputStream(new FileInputStream(jarFile));) {
            JarEntry entry;
            while ((entry = jarIn.getNextJarEntry()) != null) {
                if (!entry.isDirectory()) {
                    String fileName = entry.getName();
                    if (fileName.endsWith("class")) {
                        JavaClass javaClass = new ClassParser(jarFile.getAbsolutePath(), fileName).parse();
                        classWalk(javaClass);
                    }
                }
            }
        }
    }

    private void classWalk(final JavaClass javaClass) throws SQLException, ClassNotFoundException, IOException {
        System.out.println(javaClass.getClassName());

        executeSql(
            "insert into class values(?, ?, ?, ?)",
            new Object[] {
                nextClassId,
                javaClass.getClassName(),
                javaClass.getAccessFlags(),
                javaClass.getSuperclassName()
            }
        );

        // メソッドの取得
        final org.apache.bcel.classfile.Method[] methods = javaClass.getMethods();
        for (org.apache.bcel.classfile.Method method : methods) {
            methodWalk(nextClassId, javaClass, method);
        }

        Field[] fields = javaClass.getFields();
        for (Field field : fields) {
            fieldWalk(nextClassId, javaClass, field);
        }

        // インターフェイスの取得
        for (JavaClass i : javaClass.getAllInterfaces()) {
            if (i.getClassName().equals(javaClass.getClassName())) {
                continue;
            }
            executeSql(
                "insert into interface values(?, ?)",
                new Object[] {
                        nextClassId,
                    i.getClassName()
                }
            );
        }

        // アノテーション
        anotationWalk(nextClassId, javaClass.getAnnotationEntries());

        if (nextClassId % 500 == 0) {
            connection.commit();
        }

        // コミット
        ++nextClassId;
    }

    private void anotationWalk(final int refId, final AnnotationEntry[] annotations) throws SQLException {
        for (AnnotationEntry a : annotations) {
            executeSql(
                    "insert into anotation values(?, ?, ?)",
                    new Object[] {
                            nextAnnotationId,
                            refId,
                            a.getAnnotationType()
                    }
                );
        }
        ++nextAnnotationId;

    }

    private void methodWalk(final int classId, final JavaClass javaClass, final org.apache.bcel.classfile.Method method) throws SQLException, IOException {
        String code = getCode(method);
        executeSql(
            "insert into method values(?, ?,  ?, ?, ?, ?, ?)",
            new Object[] {
                    nextMethodId,
                    classId,
                    method.getName(),
                    javaClass.getClassName() + "." + method.getName() + " " + method.getSignature(),
                    method.getAccessFlags(),
                    method.getReturnType().toString(),
                    code
                }
        );

        int seq = 1;
        for(Type p : method.getArgumentTypes()) {
            executeSql(
                "insert into method_parameter values(?, ?, ?)",
                new Object[] {
                    nextMethodId,
                    seq,
                    p.toString()
                }
            );
            ++seq;
        }
        if (method.getCode() != null) {
            ByteSequence stream = new ByteSequence(method.getCode().getCode());
            for (int i = 0; stream.available() > 0 ; i++) {
                analyzeCode(nextMethodId, stream, method.getConstantPool());
            }
        }

        // アノテーション
        anotationWalk(nextMethodId, method.getAnnotationEntries());

        ++nextMethodId;
    }

    private void fieldWalk(final int classId, final JavaClass javaClass, final org.apache.bcel.classfile.Field field) throws SQLException, IOException {

        executeSql(
            "insert into field values(?, ?, ?, ?, ?, ?)",
            new Object[] {
                    nextMethodId,
                    classId,
                    field.getName(),
                    javaClass.getClassName() + "." + field.getName() + " " + field.getSignature(),
                    field.getAccessFlags(),
                    field.getType().toString()
                }
        );

        // アノテーション
        anotationWalk(nextMethodId, field.getAnnotationEntries());

        ++nextMethodId;
    }

    private  boolean wide = false; /* The `WIDE' instruction is used in the
     * byte code to allow 16-bit wide indices
     * for local variables. This opcode
     * precedes an `ILOAD', e.g.. The opcode
     * immediately following takes an extra
     * byte which is combined with the
     * following byte to form a
     * 16-bit value.
     */

    /**
     * 以下参考に実装
     * commons-bcel/src/main/java/org/apache/bcel/classfile/Utility.java
     * codeToString
     * @param bytes
     * @param constant_pool
     * @throws IOException
     * @throws SQLException
     * @throws ClassFormatException
     */
    public  void analyzeCode(final int methodId,  final ByteSequence bytes, final ConstantPool constant_pool) throws IOException, ClassFormatException, SQLException {
        final short opcode = (short) bytes.readUnsignedByte();
        int default_offset = 0;
        int low;
        int high;
        int npairs;
        int index;
        int vindex;
        int constant;
        int[] match;
        int[] jump_table;
        int no_pad_bytes = 0;
        int offset;
        final boolean verbose = true;
        final StringBuilder buf = new StringBuilder(Const.getOpcodeName(opcode));
        /* Special case: Skip (0-3) padding bytes, i.e., the
         * following bytes are 4-byte-aligned
         */
        if ((opcode == Const.TABLESWITCH) || (opcode == Const.LOOKUPSWITCH)) {
            final int remainder = bytes.getIndex() % 4;
            no_pad_bytes = (remainder == 0) ? 0 : 4 - remainder;
            for (int i = 0; i < no_pad_bytes; i++) {
                byte b;
                if ((b = bytes.readByte()) != 0) {
                    System.err.println("Warning: Padding byte != 0 in "
                            + Const.getOpcodeName(opcode) + ":" + b);
                }
            }
            // Both cases have a field default_offset in common
            default_offset = bytes.readInt();
        }
        switch (opcode) {
            /* Table switch has variable length arguments.
             */
            case Const.TABLESWITCH:
                low = bytes.readInt();
                high = bytes.readInt();
                offset = bytes.getIndex() - 12 - no_pad_bytes - 1;
                default_offset += offset;
                buf.append("\tdefault = ").append(default_offset).append(", low = ").append(low)
                        .append(", high = ").append(high).append("(");
                jump_table = new int[high - low + 1];
                for (int i = 0; i < jump_table.length; i++) {
                    jump_table[i] = offset + bytes.readInt();
                    buf.append(jump_table[i]);
                    if (i < jump_table.length - 1) {
                        buf.append(", ");
                    }
                }
                buf.append(")");
                break;
            /* Lookup switch has variable length arguments.
             */
            case Const.LOOKUPSWITCH: {
                npairs = bytes.readInt();
                offset = bytes.getIndex() - 8 - no_pad_bytes - 1;
                match = new int[npairs];
                jump_table = new int[npairs];
                default_offset += offset;
                buf.append("\tdefault = ").append(default_offset).append(", npairs = ").append(
                        npairs).append(" (");
                for (int i = 0; i < npairs; i++) {
                    match[i] = bytes.readInt();
                    jump_table[i] = offset + bytes.readInt();
                    buf.append("(").append(match[i]).append(", ").append(jump_table[i]).append(")");
                    if (i < npairs - 1) {
                        buf.append(", ");
                    }
                }
                buf.append(")");
            }
                break;
            /* Two address bytes + offset from start of byte stream form the
             * jump target
             */
            case Const.GOTO:
            case Const.IFEQ:
            case Const.IFGE:
            case Const.IFGT:
            case Const.IFLE:
            case Const.IFLT:
            case Const.JSR:
            case Const.IFNE:
            case Const.IFNONNULL:
            case Const.IFNULL:
            case Const.IF_ACMPEQ:
            case Const.IF_ACMPNE:
            case Const.IF_ICMPEQ:
            case Const.IF_ICMPGE:
            case Const.IF_ICMPGT:
            case Const.IF_ICMPLE:
            case Const.IF_ICMPLT:
            case Const.IF_ICMPNE:
                buf.append("\t\t#").append((bytes.getIndex() - 1) + bytes.readShort());
                break;
            /* 32-bit wide jumps
             */
            case Const.GOTO_W:
            case Const.JSR_W:
                buf.append("\t\t#").append((bytes.getIndex() - 1) + bytes.readInt());
                break;
            /* Index byte references local variable (register)
             */
            case Const.ALOAD:
            case Const.ASTORE:
            case Const.DLOAD:
            case Const.DSTORE:
            case Const.FLOAD:
            case Const.FSTORE:
            case Const.ILOAD:
            case Const.ISTORE:
            case Const.LLOAD:
            case Const.LSTORE:
            case Const.RET:
                if (wide) {
                    vindex = bytes.readUnsignedShort();
                    wide = false; // Clear flag
                } else {
                    vindex = bytes.readUnsignedByte();
                }
                buf.append("\t\t%").append(vindex);
                break;
            /*
             * Remember wide byte which is used to form a 16-bit address in the
             * following instruction. Relies on that the method is called again with
             * the following opcode.
             */
            case Const.WIDE:
                wide = true;
                buf.append("\t(wide)");
                break;
            /* Array of basic type.
             */
            case Const.NEWARRAY:
                buf.append("\t\t<").append(Const.getTypeName(bytes.readByte())).append(">");
                break;
            /* Access object/class fields.
             */
            case Const.GETFIELD:
            case Const.GETSTATIC:
            case Const.PUTFIELD:
            case Const.PUTSTATIC:
                index = bytes.readUnsignedShort();
                buf.append("\t\t").append(
                        constant_pool.constantToString(index, Const.CONSTANT_Fieldref)).append(
                        verbose ? " (" + index + ")" : "");

                executeSql(
                        "insert into method_depend values(?,?,?)",
                        new Object[] {
                            methodId,
                            constant_pool.constantToString(index, Const.CONSTANT_Fieldref),
                            opcode
                        }
                    );
                break;
            /* Operands are references to classes in constant pool
             */
            case Const.NEW:
            case Const.CHECKCAST:
                buf.append("\t");
                //$FALL-THROUGH$
            case Const.INSTANCEOF:
                index = bytes.readUnsignedShort();
                buf.append("\t<").append(
                        constant_pool.constantToString(index, Const.CONSTANT_Class))
                        .append(">").append(verbose ? " (" + index + ")" : "");

                executeSql(
                        "insert into method_depend values(?,?,?)",
                        new Object[] {
                            methodId,
                            constant_pool.constantToString(index, Const.CONSTANT_Class),
                            opcode
                        }
                    );
                break;
            /* Operands are references to methods in constant pool
             */
            case Const.INVOKESPECIAL:
            case Const.INVOKESTATIC:
                index = bytes.readUnsignedShort();
                final Constant c = constant_pool.getConstant(index);
                // With Java8 operand may be either a CONSTANT_Methodref
                // or a CONSTANT_InterfaceMethodref.   (markro)
                buf.append("\t").append(
                        constant_pool.constantToString(index, c.getTag()))
                        .append(verbose ? " (" + index + ")" : "");
                executeSql(
                    "insert into method_depend values(?,?,?)",
                    new Object[] {
                        methodId,
                        constant_pool.constantToString(index, c.getTag()),
                        opcode
                    }
                );
                break;
            case Const.INVOKEVIRTUAL:
                index = bytes.readUnsignedShort();
                buf.append("\t").append(
                        constant_pool.constantToString(index, Const.CONSTANT_Methodref))
                        .append(verbose ? " (" + index + ")" : "");

                executeSql(
                        "insert into method_depend values(?,?,?)",
                        new Object[] {
                            methodId,
                            constant_pool.constantToString(index, Const.CONSTANT_Methodref),
                            opcode
                        }
                    );
                break;
            case Const.INVOKEINTERFACE:
                index = bytes.readUnsignedShort();
                final int nargs = bytes.readUnsignedByte(); // historical, redundant
                buf.append("\t").append(
                        constant_pool
                                .constantToString(index, Const.CONSTANT_InterfaceMethodref))
                        .append(verbose ? " (" + index + ")\t" : "").append(nargs).append("\t")
                        .append(bytes.readUnsignedByte()); // Last byte is a reserved space
                executeSql(
                        "insert into method_depend values(?,?,?)",
                        new Object[] {
                            methodId,
                            constant_pool.constantToString(index, Const.CONSTANT_InterfaceMethodref),
                            opcode
                        }
                    );
                break;
            case Const.INVOKEDYNAMIC:
                index = bytes.readUnsignedShort();
                buf.append("\t").append(
                        constant_pool
                                .constantToString(index, Const.CONSTANT_InvokeDynamic))
                        .append(verbose ? " (" + index + ")\t" : "")
                        .append(bytes.readUnsignedByte())  // Thrid byte is a reserved space
                        .append(bytes.readUnsignedByte()); // Last byte is a reserved space

                executeSql(
                        "insert into method_depend values(?,?,?)",
                        new Object[] {
                            methodId,
                            constant_pool.constantToString(index, Const.CONSTANT_InvokeDynamic),
                            opcode
                        }
                    );
                break;
            /* Operands are references to items in constant pool
             */
            case Const.LDC_W:
            case Const.LDC2_W:
                index = bytes.readUnsignedShort();
                buf.append("\t\t").append(
                        constant_pool.constantToString(index, constant_pool.getConstant(index)
                                .getTag())).append(verbose ? " (" + index + ")" : "");
                break;
            case Const.LDC:
                index = bytes.readUnsignedByte();
                buf.append("\t\t").append(
                        constant_pool.constantToString(index, constant_pool.getConstant(index)
                                .getTag())).append(verbose ? " (" + index + ")" : "");
                break;
            /* Array of references.
             */
            case Const.ANEWARRAY:
                index = bytes.readUnsignedShort();
                buf.append("\t\t<").append(
                        Utility.compactClassName(constant_pool.getConstantString(index,
                                Const.CONSTANT_Class), false)).append(">").append(
                        verbose ? " (" + index + ")" : "");
                break;
            /* Multidimensional array of references.
             */
            case Const.MULTIANEWARRAY: {
                index = bytes.readUnsignedShort();
                final int dimensions = bytes.readUnsignedByte();
                buf.append("\t<").append(
                        Utility.compactClassName(constant_pool.getConstantString(index,
                                Const.CONSTANT_Class), false)).append(">\t").append(dimensions)
                        .append(verbose ? " (" + index + ")" : "");
            }
                break;
            /* Increment local variable.
             */
            case Const.IINC:
                if (wide) {
                    vindex = bytes.readUnsignedShort();
                    constant = bytes.readShort();
                    wide = false;
                } else {
                    vindex = bytes.readUnsignedByte();
                    constant = bytes.readByte();
                }
                buf.append("\t\t%").append(vindex).append("\t").append(constant);
                break;
            default:
                if (Const.getNoOfOperands(opcode) > 0) {
                    for (int i = 0; i < Const.getOperandTypeCount(opcode); i++) {
                        buf.append("\t\t");
                        switch (Const.getOperandType(opcode, i)) {
                            case Const.T_BYTE:
                                buf.append(bytes.readByte());
                                break;
                            case Const.T_SHORT:
                                buf.append(bytes.readShort());
                                break;
                            case Const.T_INT:
                                buf.append(bytes.readInt());
                                break;
                            default: // Never reached
                                throw new IllegalStateException("Unreachable default case reached!");
                        }
                    }
                }
        }
    }

    private String getCode(org.apache.bcel.classfile.Method method) {
        if (method.getCode() == null) {
            return "";
        }
        return method.getCode().toString();
    }

}

bcelの解析結果からクラス図を作成する

bcelの解析結果を格納したSqliteからクラス図を作成しています。
plantUML.png

SqliteToGraph.java


package sqliteToGraph;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.apache.bcel.Const;

import net.sourceforge.plantuml.FileFormat;
import net.sourceforge.plantuml.FileFormatOption;
import net.sourceforge.plantuml.SourceStringReader;

public class SqliteToGraph {
    static final int MAX_MEMBER_SIZE = 10;

    public class ClassData {
        private int id;
        private String name;
        private String packageName;
        private String className;
        private int accessFlg;
        private String superClassName;

        public ClassData(ResultSet rs) throws SQLException {
            id = rs.getInt("id");
            name = rs.getString("name");
            accessFlg = rs.getInt("access_flags");
            superClassName = rs.getString("super_class_name");

            int ix = name.lastIndexOf(".");
            className = name.substring(ix + 1);
            packageName = name.substring(0, ix);
        }

        public int getId() {
            return id;
        }
        public String getName() {
            return name;
        }
        public String getPackageName() {
            return packageName;
        }
        public String getClassName() {
            return className;
        }
        public int getAccessFlg() {
            return accessFlg;
        }
        public String getSuperClassName() {
            return superClassName;
        }
    }

    public static void main(String[] args) throws SQLException, IOException{
        SqliteToGraph sg = new SqliteToGraph();
        String dbPath = "..\\bcelToSqlite\\output.sqlite";
        String path = "test_class.svg";
        sg.parse(dbPath, path);
        System.out.println("class:" + dbPath + "->" + path);
    }
    public void parse(String dbPath, String path) throws SQLException, IOException {
        Connection connection = null;
        ResultSet rs = null;
        PreparedStatement pstmt = null;
        PreparedStatement pstmtMethod = null;
        StringBuilder sb = new StringBuilder();
        try
        {
            // create a database connection
            // Connection connection = DriverManager.getConnection("jdbc:sqlite:C:/work/mydatabase.db");
            // Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:");
            connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
            pstmt = connection.prepareStatement("select id, name, access_flags, super_class_name from class order by id");
            rs = pstmt.executeQuery();
            HashMap<Integer, ClassData> mapClass = new HashMap<Integer, ClassData>();

            while(rs.next())
            {
                SqliteToGraph.ClassData data = new SqliteToGraph.ClassData(rs);
                mapClass.put(data.id, data);
            }
            rs.close();
            pstmt.close();
            pstmt = null;

            ////
            sb.append("@startuml\n");
            sb.append("left to right direction\n");

            pstmt = connection.prepareStatement("select id, name , access_flag, type from field where class_id = ?");
            pstmtMethod = connection.prepareStatement("select distinct name , access_flag from method where class_id = ?");

            for(Integer key : mapClass.keySet()) {
                String prefix = "class";
                if ((mapClass.get(key).getAccessFlg() & Const.ACC_INTERFACE) == Const.ACC_INTERFACE) {
                    prefix = "interface";
                }
                sb.append("  " + prefix +" \"" + mapClass.get(key).name + "\" {" + "\n");

                // field
                List<String> list = new ArrayList<String>();
                pstmt.setInt(1, mapClass.get(key).id);
                rs = pstmt.executeQuery();
                while(rs.next())
                {
                    list.add(rs.getString("name"));
                }
                for(int i = 0; i < list.size()  ; ++i)  {
                    sb.append("    " + list.get(i) + " \n");
                    if (i > MAX_MEMBER_SIZE) {
                        sb.append("    ... \n");
                        break;
                    }
                }
                rs.close();
                list = new ArrayList<String>();

                // method
                pstmtMethod.setInt(1, mapClass.get(key).id);
                rs = pstmtMethod.executeQuery();
                while(rs.next())
                {
                    if ((rs.getInt("access_flag") & Const.ACC_PUBLIC) == Const.ACC_PUBLIC) {
                        list.add(rs.getString("name"));
                    }
                }
                rs.close();
                for(int i = 0; i < list.size()  ; ++i)  {
                    sb.append("    " + list.get(i) + "() \n");
                    if (i > MAX_MEMBER_SIZE) {
                        sb.append("    ...() \n");
                        break;
                    }
                }
                sb.append("  }\n");
                if (checkSuperClassName(mapClass.get(key).getSuperClassName())) {
                    sb.append("  " + mapClass.get(key).getSuperClassName() + " <|-- " + mapClass.get(key).name + "\n");
                }
            }
            pstmt.close();
            pstmt = null;

            pstmt = connection.prepareStatement("select class_id, interface_name from interface");
            rs = pstmt.executeQuery();
            while(rs.next())
            {
                if (checkSuperClassName(rs.getString("interface_name"))) {
                    sb.append("  " + rs.getString("interface_name") + " <|.. " + mapClass.get(rs.getInt("class_id")).name + "\n");
                }
            }
            pstmt.close();
            pstmt = null;
            sb.append("@enduml\n");

            writeSvg(sb.toString(), path);
        }
        finally
        {
            try
            {
                if(rs != null)
                {
                    rs.close();
                }
                if(pstmt != null)
                {
                    pstmt.close();
                }
                if(pstmtMethod != null)
                {
                    pstmtMethod.close();
                }
                if(connection != null)
                {
                connection.close();
                }
            }
            catch(SQLException e)
            {
                // connection close failed.
                System.err.println(e);
            }
        }

    }
    private boolean checkSuperClassName(String superClassName) {
        if (superClassName.startsWith("java.")) {
            return false;
        }
        if (superClassName.startsWith("javax.")) {
            return false;
        }
        return true;
    }

    private static void writeSvg(String source, String path) throws IOException {
        SourceStringReader reader = new SourceStringReader(source);
        final ByteArrayOutputStream os = new ByteArrayOutputStream();
        // Write the first image to "os"
        @SuppressWarnings("deprecation")
        String desc = reader.generateImage(os, new FileFormatOption(FileFormat.SVG));
        os.close();

        final String svg = new String(os.toByteArray(), Charset.forName("UTF-8"));
        File out = new File(path);
        PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(out)));
        pw.write(svg);
        pw.close();

    }
}

bcelの解析結果からコールグラフを作成する

bcelの解析結果を格納したSqliteから指定のメソッドのコールグラフを作成しています。

planguml2.png

DependMethod.java

package sqliteToGraph;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import net.sourceforge.plantuml.FileFormat;
import net.sourceforge.plantuml.FileFormatOption;
import net.sourceforge.plantuml.SourceStringReader;

public class DependMethod {
    public static void main(String[] args) throws SQLException, IOException{
        DependMethod dm = new DependMethod();
        String dbPath = "..\\bcelToSqlite\\output.sqlite";
        String path = "test_depend.svg";
        String methodName = "org.apache.bcel.util.ClassPath.SYSTEM_CLASS_PATH";
        dm.parse(dbPath, path, methodName);
        System.out.println("depend:" + dbPath + "->" + path);
    }
    Connection connection = null;
    PreparedStatement pstmtLike = null;
    PreparedStatement pstmtEqual = null;

    class TreeItem {
        private String methodName;
        private List<TreeItem> children = new ArrayList<TreeItem>();
        public TreeItem(String m) {
            methodName = m;
        }
        public List<TreeItem> GetChildren() {
            return children;
        }
    }
    List<TreeItem> root = new ArrayList<TreeItem>();
    HashMap<String, TreeItem> map = new HashMap<String, TreeItem>();
    List<String> rectangles = new ArrayList<String>();

    public void parse(String dbPath, String path, String methodName) throws SQLException, IOException {
        ResultSet rs = null;
        try
        {
            connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
            pstmtLike = connection.prepareStatement("select  distinct method_depend.called_method from method inner join method_depend on method.id = method_depend.method_id where method_depend.called_method like ?");
            pstmtLike.setString(1, "%" + methodName + "%");
            rs = pstmtLike.executeQuery();

            while(rs.next())
            {
                TreeItem item = new TreeItem(rs.getString("called_method"));
                root.add(item);
                map.put(item.methodName, item);
            }
            rs.close();

            pstmtEqual = connection.prepareStatement("select  distinct method.fullname as call_method from method inner join method_depend on method.id = method_depend.method_id where method_depend.called_method like ?");
            for (TreeItem item : root) {
                walkDependency(item.methodName);
            }
            StringBuilder sbDef = new StringBuilder();
            StringBuilder sbArrow = new StringBuilder();
            StringBuilder sb = new StringBuilder();
            rectangles = new ArrayList<String>();

            sb.append("@startuml\n");
            for (TreeItem item : root) {
                drawDependency(item, sbDef, sbArrow);
            }
            sb.append(sbDef.toString());
            sb.append(sbArrow.toString());

            sb.append("@enduml\n");
            System.out.println(sb.toString());
            writeSvg(sb.toString() + "\n" + sb.toString(), path);

        }
        finally
        {
            try
            {
                if(rs != null)
                {
                    rs.close();
                }
                if(pstmtLike != null)
                {
                    pstmtLike.close();
                }
                if(pstmtEqual != null)
                {
                    pstmtEqual.close();
                }
                if(connection != null)
                {
                connection.close();
                }
            }
            catch(SQLException e)
            {
                // connection close failed.
                System.err.println(e);
            }
        }

    }

    void walkDependency(String calledMethod) throws SQLException {
        pstmtEqual.setString(1, calledMethod);
        ResultSet rs = pstmtEqual.executeQuery();

        List<String> list = new ArrayList<String>();

        while(rs.next())
        {
            String callMethod = rs.getString("call_method");
            if (callMethod.equals(calledMethod)) {
                // 再起呼び出し対策
                continue;
            }
            list.add(callMethod);
            if (!map.containsKey(callMethod)) {
                TreeItem item = new TreeItem(callMethod);
                map.put(item.methodName, item);
                map.get(calledMethod).GetChildren().add(item);
            }
        }
        rs.close();

        for (String callMethod : list) {
            walkDependency(callMethod);
        }

    }
    void drawDependency(TreeItem item, StringBuilder sbDef, StringBuilder sbArrow) {
        if (!rectangles.contains(item.methodName)) {
            rectangles.add(item.methodName);
            sbDef.append("rectangle \"" + item.methodName + "\" as " + makeAlias(item.methodName) + "\n");
        }
        for (TreeItem child : item.GetChildren()) {
            sbArrow.append(makeAlias(item.methodName) + "<--" + makeAlias(child.methodName) + "\n");
            drawDependency(child, sbDef, sbArrow);
        }

    }
    private String makeAlias(String name) {
        name = name.replaceAll("/", "_");
        name = name.replaceAll(" ", "_");
        name = name.replaceAll("<", "_");
        name = name.replaceAll(">", "_");
        name = name.replaceAll("\\$", "_");
        name = name.replaceAll(";", "_");
        name = name.replaceAll("\\(", "_");
        name = name.replaceAll("\\)", "_");
        name = name.replaceAll("\\[", "_");
        name = name.replaceAll("\\]", "_");
        return name;
    }

    private static void writeSvg(String source, String path) throws IOException {
        SourceStringReader reader = new SourceStringReader(source);
        final ByteArrayOutputStream os = new ByteArrayOutputStream();
        // Write the first image to "os"
        @SuppressWarnings("deprecation")
        String desc = reader.generateImage(os, new FileFormatOption(FileFormat.SVG));
        os.close();

        final String svg = new String(os.toByteArray(), Charset.forName("UTF-8"));
        File out = new File(path);
        PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(out)));
        pw.write(svg);
        pw.close();

    }
}

あとがき

DoxygenとGraphVizを使えば、こんなことしなくてもいいです。
Doxygenは結果をXMLを吐くこともできるので、解析も楽だと思います。

ただ、bcelはfindbug等のよくつかわれるツールに入っているので、インターネット禁止縛りプレイを楽しんでいるところでも、利用できます。
この時は、Sqliteでなくテキストに吐き出してからAccessにつっこんで使用するということをやっていました。

また、解析結果をDatabaseに格納しておくと、依存関係の調査等で役に立ったりします。

WordPressでPlantUMLを利用して図を描画する

目的

WordPressでPlantUMLを利用して図を作成します。
なおこの際、日ノ本言葉を使うものとします。
plantumlphp.png

環境

PHP 7.3
Wordpress 5.2

PlantUML Renderer

PlantUML RendererはWordPressのプラグインとして以下に公開されています。
https://ja.wordpress.org/plugins/plantuml-renderer/

WordPressのソースブロックに以下を記載します。

PlantUML Syntax:
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response

Alice -> Bob: Another authentication Request
Alice <-- Bob: another authentication Response

以下のような画像が表示されます。

plantumlphp0.png

ただし当然の権利のように日ノ本言葉を記載すると文字化けします。

仕組み

PlantUML Renderのソースは以下にあります。
https://plugins.trac.wordpress.org/browser/plantuml-renderer/trunk/public/class-plantuml-renderer-public.php

            public function pumlr_shortcode_handler( $atts, $content = null ) {
                    $new_content = str_replace( '>','>',$content );                                      // Right angle bracket.
                    $new_content = str_replace( '<','<',$new_content );                  // Left angle bracket.
                    $new_content = str_replace( '–','--',$new_content );      // En-dash  (WP converts "--" to one en-dash).
                    $new_content = str_replace( '—','--',$new_content );      // Em-dash  (WP converts "--" to one em-dash).
                    $new_content = str_replace( '“','"',$new_content );         // Curly left double quote
                    $new_content = str_replace( '”','"',$new_content );         // Curly right double quote
                    $new_content = str_replace( '”','"',$new_content );         // Curly right double quote
                    $remove_array = array( '<p>', '</p>', '<br />', '<br/>' );              // Remove HTML tags.
                    $new_content = str_replace( $remove_array, '', $new_content );
                    $img = '<p><img src=http://www.plantuml.com/plantuml/img/';
                    $img .= $this->encodep( $new_content );
                    $img .= ' alt="PlantUML Syntax:' . $content . '" usemap="#plantuml_map">';

                    // Get the image map, if our syntax has plantuml link syntax.
                    $linkpos = strpos( $new_content, '[[' );
                    if ( false !== $linkpos ) {
                            $response = wp_remote_get( 'http://www.plantuml.com/plantuml/map/' . $this->encodep( $new_content ) );
                            if ( is_array( $response ) ) {
                                    $body = $response['body'];
                                    $img .= $body;
                            }
                    }
                    $img .= '</p>';
                    return $img;
            }
            /**
             * Encode our plantuml syntax - See PlantUML PHP Doc for details - where this was lifted from.
             *
             * @param    string $text Our text to encode.
             * @since    1.0.0
             */
            private function encodep( $text ) {
                    $data = utf8_encode( $text );
                    $compressed = gzdeflate( $data, 9 );
                    return $this->encode64( $compressed );
            }
  1. UTF-8でエンコードする
  2. 圧縮する
  3. base64で表現する
  4. http://www.plantuml.com/plantuml/map/3の結果」を呼び出す
  5. 画像が作成されるのでimgタグで表示する

この仕組みは以下のページに記載されており、プラグインはそれを忠実に実装しています。
http://plantuml.com/ja/code-php

上記ページからたどれるPHPのサンプルでは同様に化けます。

JavaScriptでの実装

PlantUMLの公式ページでPHPではなく、JavaScriptで同じことをしている箇所があります。
http://plantuml.com/en/demo-javascript-synchronous

このページでは日本語が化けることはありません。
どんな実装をしているか見てみましょう。

demo-javascript-synchronous

function compress(a){
  a=unescape(encodeURIComponent(a));
  GID("im").src="http://www.plantuml.com/plantuml/img/"+encode64(zip_deflate(a,9))
};

encode64とzip_deflateはsynchro.jsに実装されていてPHP側の実装と同様です。
問題はこのパラメータに与えるaにあります。

ここではaをencodeURIComponentをしたあと、unescapeを実行しています。
unescapeはASCII以外対応していないので日本語は文字化けします。
そして、その化けた内容をそのまま利用しています。

つまり、PHPのutf8_encodeが不要ということになります。

修正内容

インストールしたプラグインのファイルを直接修正して対応します。
このファイルは以下に存在してました。
/ワードプレスのパス/wp-content/plugins/plantuml-renderer/public

utf8_encodeを除去します

class-plantuml-renderer-public.php

    /**
     * Encode our plantuml syntax - See PlantUML PHP Doc for details - where this was lifted from.
     *
     * @param    string $text Our text to encode.
     * @since    1.0.0
     */
    private function encodep( $text ) {
        //$data = utf8_encode( $text );
        $data = $text; // http://plantuml.com/en/demo-javascript-synchronous のJSではunescape(encodeURIComponent(a))でASCII以外は化けているデータを送ってうまくいっている
        $compressed = gzdeflate( $data, 9 );
        return $this->encode64( $compressed );
    }

また、「"」が混ざった場合、imgタグのaltが上手く表示できなくなるのでそれもあわせてなおします。ここでは全角に変換してます。

class-plantuml-renderer-public.php

    /**
     * Our lovely shortcode.
     *
     * @param    array  $atts    Attributes passed into shortcode.
     * @param    string $content Content within the shortcode tags.
     * @since    0.0.3
     */
    public function pumlr_shortcode_handler( $atts, $content = null ) {
        $new_content = str_replace( '>','>',$content );                   // Right angle bracket.
        $new_content = str_replace( '<','<',$new_content );           // Left angle bracket.
        $new_content = str_replace( '–','--',$new_content );  // En-dash  (WP converts "--" to one en-dash).
        $new_content = str_replace( '—','--',$new_content );  // Em-dash  (WP converts "--" to one em-dash).
        $new_content = str_replace( '“','"',$new_content );    // Curly left double quote
        $new_content = str_replace( '”','"',$new_content );    // Curly right double quote
        $new_content = str_replace( '”','"',$new_content );    // Curly right double quote
        $remove_array = array( '<p>', '</p>', '<br />', '<br/>' );      // Remove HTML tags.
        $new_content = str_replace( $remove_array, '', $new_content );
        $img = '<p><img src=http://www.plantuml.com/plantuml/img/';
        $img .= $this->encodep( $new_content );
        // "を全角に変換して表示する
        $img .= ' alt="PlantUML Syntax:' . str_replace( '"', '”', $content) . '" usemap="#plantuml_map">';

        // Get the image map, if our syntax has plantuml link syntax.
        $linkpos = strpos( $new_content, '[[' );
        if ( false !== $linkpos ) {
            $response = wp_remote_get( 'http://www.plantuml.com/plantuml/map/' . $this->encodep( $new_content ) );
            if ( is_array( $response ) ) {
                $body = $response['body'];
                $img .= $body;
            }
        }
        $img .= '</p>';
        return $img;
    }

結果

上記2つの対応により、日ノ本言葉がつかえるようになります。
plantumlphp.png

改造案

いまのままだと外部のURLに頼ってしまうので自前のサーバにPlantUMLのJARを配置した方がいいでしょう。
以下のページが参考になりそうです。
http://dora.bk.tsukuba.ac.jp/~takeuchi/?%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%2Fpukiwiki%2Fuml.inc.php

茜ちゃんをPowerShellで弄る

目的

VOICEROID2の琴葉茜ちゃんをPowerShellでしゃべらせます。

000097.jpg

この際、なるべくどこでも動くように、UIAutoMationを使用します。

環境:

 
OS Window
VOICEROIDE2 2.0.1.0
PSVersion 5.1.17134.590
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.17134.590
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1

実装しよう

古式ゆかしいWindowsアプリの実装ではないので、Spyでは茜ちゃんの画面構成を調べることはできません。
Inspect.exeを使用して、茜ちゃんを隅々まで調べてから実装しましょう。

Inspect
https://docs.microsoft.com/en-us/windows/desktop/winauto/inspect-objects

C#での実装

PowerShellでいきなり実装するのは辛いので、まずC#で実装してみます。

//using System.Windows.Automation;
// UIAutomationを使用します。
        private void button3_Click(object sender, EventArgs e)
        {
            AutomationElement mainForm = null;
            String message = "茜ちゃん、かわいい!やったー!";
            foreach (var p in Process.GetProcesses())
            {
                if (p.MainWindowTitle.Contains("VOICEROID2"))
                {
                    mainForm = AutomationElement.FromHandle(p.MainWindowHandle);
                }
            }
            if (mainForm == null)
            {
                Debug.WriteLine("起動してない");
                return;
            }
            {
                var elems = mainForm.FindAll(
                    TreeScope.Element | TreeScope.Descendants,
                    new PropertyCondition(AutomationElement.ClassNameProperty, "TextBox"));

                ValuePattern txtboxName = elems[0].GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
                txtboxName.SetValue(message);
            }
            {
                var elems = mainForm.FindAll(
                    TreeScope.Element | TreeScope.Descendants,
                    new PropertyCondition(AutomationElement.ClassNameProperty, "Button"));

                InvokePattern btn = elems[0].GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
                btn.Invoke();
            }

            AutomationElementCollection stsMessage;
            do
            {
                Thread.Sleep(500);
                stsMessage = mainForm.FindAll(
                 TreeScope.Element | TreeScope.Descendants,
                    new PropertyCondition(AutomationElement.NameProperty, "テキストの読み上げは完了しました。"));

            } while (stsMessage.Count == 0);
        }

※ボタンの増減で動かなくなるコードです

PowerShellでの実装

続いてPowerShellで実装します。

voiceroid.ps1

$message = $Args[0]
$target = Get-Process | Where-Object {$_.MainWindowTitle.StartsWith("VOICEROID2") -eq $True} | Select-Object 
if ($target.Length -eq 0) {
  Write-Error "VOICEROID2を起動してください"
  exit 1
}
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
$mainForm = [System.Windows.Automation.AutomationElement]::FromHandle($target.MainWindowHandle)

# テキストの検索
$cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ClassNameProperty, "TextBox")
$elems = $mainForm.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
$elem = $elems[0].GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) -as [System.Windows.Automation.ValuePattern]
$elem.SetValue($message);

# ボタン押下
$cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ClassNameProperty, "Button")
$elems = $mainForm.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
$invElm = $elems[0].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
$invElm.Invoke()

# 読み上げ中は待機
$cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, "テキストの読み上げは完了しました。")
do
{
  Start-Sleep -m 500 
  $elems = $mainForm.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}
while ($elems.Count -eq 0)

PowerShellのスクリプトファイルを実行するには管理者権限を用いて実行ポリシーを変更するか、以下のようにバッチファイル経由で実行ポリシーを指定して起動します。

powershell -ExecutionPolicy RemoteSigned ./voiceroid.ps1 茜ちゃん!可愛い!やったぁ!
powershell -ExecutionPolicy RemoteSigned ./voiceroid.ps1 働きたくない・・・
powershell -ExecutionPolicy RemoteSigned ./voiceroid.ps1 せやなー

問題

…実は、UIAutomationでは自動操作できない箇所があります。
ボイスとチューニングにあるタブの中身のコントロールは操作できません。

UIAutomation で タブの内側の要素が取れない
https://teratail.com/questions/53276

上記のページに記載してあるとおりアプリの作り方によって、タブの中身の情報がとれなくなるようです。

なお、偉大なる先駆者様はFriendlyというライブラリを使って解決しているようです。

TTSController
https://github.com/mikoto2000/TTSController

Friendly
https://github.com/Codeer-Software/Friendly

PowerShellのUIAutomationは複雑怪奇なり

どぼぢでうごかないのぉおおおお!!C#では動いたでしょう!!!!!

PowerShellのUIAutomationを使ってウィンドウの操作していると、たまにコントロールの操作に失敗することがあります。
C#やVB.NET,さらにはVBAでは動作しているのにも関わらず、この事象は発生します。

動かないパターン

まず、Win32で作成した画面を用意します。ここでは以下のような画面を用意しました。

UIAutomation1png.png 

ID1000のEditControlにテキストを入力するようにC#で実装してみます。

var root = AutomationElement.RootElement;
var cond1 = new PropertyCondition(AutomationElement.NameProperty, "NativeTest");
var mainForm = root.FindFirst(TreeScope.Element | TreeScope.Children, cond1);

var cond2 = new PropertyCondition(AutomationElement.AutomationIdProperty, "1000");
var edit = mainForm.FindFirst(
    TreeScope.Element | TreeScope.Descendants, 
    cond2);
ValuePattern editValue = edit.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
editValue.SetValue("csharsssssp");

C#側は上記のコードで期待通り動作します。
次にPowerShellでC#と同様の実装をして実行してみます。

test1.ps1

Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
$cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, "NativeTest")
$mainForm = $rootElement.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Children, $cond)
$cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::AutomationIdProperty, "1000")
$edit = $mainForm.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
$txt = $edit.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
$txt.SetValue("Test2")

上記のPowerShellを実行すると下記のエラーが発生します。

"1" 個の引数を指定して "GetCurrentPattern" を呼び出し中に例外が発生しました: "サポートされていないパターンです。"
発生場所 C:\dev\ps\voiceroid\test2.ps1:30 文字:1
+ $txt = $edit.GetCurrentPattern([System.Windows.Automation.ValuePatter ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : InvalidOperationException

この事象はWin32の画面、たとえば、「名前を付けて保存」で表示されるようなコモンダイアログでも発生します。
…VOICEROID2つかって茜ちゃんとキャッキャウフフしてたらハマった。

VBAの実装方法

なおVBAでは以下のように実装します。
これでも動くので、PowerShellとUIAutomationの相性の問題と考えられます。

' UIAutomationClient(UIAutomationCore.dll) 参照
Public Sub test()
    Dim uia As UIAutomationClient.CUIAutomation
    Set uia = New UIAutomationClient.CUIAutomation

    Dim root As IUIAutomationElement
    Set root = uia.GetRootElement()

    Dim cndMainForm As IUIAutomationCondition
    Set cndMainForm = uia.CreatePropertyCondition(UIA_NamePropertyId, "NativeTest")

    Dim mainForm As IUIAutomationElement
    Set mainForm = root.FindFirst(TreeScope_Element Or TreeScope_Children, cndMainForm)

    Dim cndEdit As IUIAutomationCondition
    Set cndEdit = uia.CreatePropertyCondition(UIA_AutomationIdPropertyId, "1000")

    Dim edit As IUIAutomationElement
    Set edit = mainForm.FindFirst(TreeScope_Element Or TreeScope_Descendants, cndEdit)

    Debug.Print edit.CurrentClassName

    Dim editValue As IUIAutomationValuePattern
    Set editValue = edit.GetCurrentPattern(UIA_ValuePatternId)
    Call editValue.SetValue("vba set")
End Sub

参考:
起動中のMicrosoft EdgeからタイトルとURLを取得するVBAマクロ(UI Automation編)
https://www.ka-net.org/blog/?p=6076

なぜエラーとなるのか

FindFirstで取得された内容の差異

まずPowerShellのFindFirstで取得した変数をダンプしてみましょう。

## 略
$edit = $mainForm.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
# 変数のダンプ
$edit.Current
#
$txt = $edit.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
$txt.SetValue("Test2")

$edit.Currentの値は以下のようになります。

ControlType          : System.Windows.Automation.ControlType
LocalizedControlType : ウィンドウ
Name                 : csharsssssp
AcceleratorKey       :
AccessKey            :
HasKeyboardFocus     : False
IsKeyboardFocusable  : False
IsEnabled            : True
BoundingRectangle    : 428,436,151,33
HelpText             :
IsControlElement     : True
IsContentElement     : True
LabeledBy            :
AutomationId         : 1000
ItemType             :
IsPassword           : False
ClassName            : Edit
NativeWindowHandle   : 6556200
ProcessId            : 9748
IsOffscreen          : False
Orientation          : None
FrameworkId          : Win32
IsRequiredForForm    : False
ItemStatus           :

次にInspectで該当のコントロールを確認します。
UIAutomation2png.png

LocalizedControlTypeの値がPowerShellでは「ウィンドウ」、Inspectでは「編集」になっています。
つまり、PowerShellの中では該当コントロールがただのウィンドウのため、ValuePatternに変換できず、エラーとなったと推測できます。

この差異はC#では「UIAutomationClientsideProviders.dll」が読み込まれますが、PowerShellでは読み込まれないことによって生じる差異です。

読み込まれたDLLの確認

VisualStudioでは、デバッグ中にDLLが読み込まれると出力ウィンドウに表示されます。
これを利用してC#で読み込まれたDLLを確認してみます。

RootElementを取得する直前の出力ウィンドウが以下の状態だとします。
UIAutomation3png.png

RootElementを取得した時点で以下のように「UIAutomationClientsideProviders.dll」が読み込まれます。
UIAutomation4png.png

次にFindFirstを行う前の状態は以下のようになります。変化はありません。
UIAutomation5png.png

FindFirstを実行することで「UIAutomationClientsideProviders.resources.dll」が読み込まれます。
UIAutomation6png.png

「UIAutomationClientsideProviders.resources.dll」の役割については、おそらく、LocalizedControlTypeを言語環境に合わせて設定するためのリソース情報を扱うためと思われます。

次にPowerShellで読み込まれているDLLを確認します。
これには下記のコマンドを使用します。

[System.AppDomain]::CurrentDomain.GetAssemblies()  | Sort-Object -Property FullName | %{$_.FullName + "`t" + $_.Location}

Sort-Object以降は出力を見やすくしているため記載しています。
このコードをPowerShellのGetCurrentPatternの前に入れた際の結果は以下の通りになります。

FullName Location
Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
Microsoft.Management.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.Management.Infrastructure\v4.0_1.0.0.0__31bf3856ad364e35\Microsoft.Management.Infrastructure.dll
Microsoft.PowerShell.Commands.Utility, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.PowerShell.Commands.Utility\v4.0_3.0.0.0__31bf3856ad364e35\Microsoft.PowerShell.Commands.Utility.dll
Microsoft.PowerShell.ConsoleHost, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.PowerShell.ConsoleHost\v4.0_3.0.0.0__31bf3856ad364e35\Microsoft.PowerShell.ConsoleHost.dll
Microsoft.PowerShell.ConsoleHost.resources, Version=3.0.0.0, Culture=ja, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.PowerShell.ConsoleHost.resources\v4.0_3.0.0.0_ja_31bf3856ad364e35\Microsoft.PowerShell.ConsoleHost.resources.dll
Microsoft.PowerShell.Security, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.PowerShell.Security\v4.0_3.0.0.0__31bf3856ad364e35\Microsoft.PowerShell.Security.dll
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll
mscorlib.resources, Version=4.0.0.0, Culture=ja, PublicKeyToken=b77a5c561934e089 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\mscorlib.resources\v4.0_4.0.0.0_ja_b77a5c561934e089\mscorlib.resources.dll
System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll
System.Configuration.Install, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Configuration.Install\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.Install.dll
System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll
System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 C:\WINDOWS\Microsoft.Net\assembly\GAC_64\System.Data\v4.0_4.0.0.0__b77a5c561934e089\System.Data.dll
System.DirectoryServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.DirectoryServices\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.DirectoryServices.dll
System.Management, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Management\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Management.dll
System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll
System.Management.Automation.resources, Version=3.0.0.0, Culture=ja, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Management.Automation.resources\v4.0_3.0.0.0_ja_31bf3856ad364e35\System.Management.Automation.resources.dll
System.Numerics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Numerics\v4.0_4.0.0.0__b77a5c561934e089\System.Numerics.dll
System.Transactions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 C:\WINDOWS\Microsoft.Net\assembly\GAC_64\System.Transactions\v4.0_4.0.0.0__b77a5c561934e089\System.Transactions.dll
System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll
UIAutomationClient, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll
UIAutomationProvider, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\UIAutomationProvider\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationProvider.dll
UIAutomationTypes, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\UIAutomationTypes\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationTypes.dll
WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35 C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll

UIAutomationClientsideProvidersが読み込まれていないことが確認できると思います。

UIAutomationClientsideProvidersってなんだ?

UIAutomationは以下のようなアーキテクトになっています。
https://docs.microsoft.com/ja-jp/windows/desktop/WinAuto/architecture-and-interoperability

UIAutomation7png.png

操作されるアプリケーション側をサーバー、アプリケーションを操作する側がクライアントとなります。
サーバーとクライアントはともにUIAutomationCore.dllをよみこみ、プロセス間通信をもちいてプロパティ値を検索するなどしています。

Win32 コントロールは、UIAutomationClientsideProviders.dll のクライアント側プロバイダーによって Microsoft UI オートメーション に公開されています。
本来、UIAutomationClientsideProviders.dllは、UI オートメーション クライアント アプリケーションで使用するために、自動的に登録されるはずですが、今回はされなかったため、Win32のコントロールが操作できなかったことになります。

UI オートメーションによる標準コントロールのサポート
https://docs.microsoft.com/ja-jp/dotnet/framework/ui-automation/ui-automation-support-for-standard-controls

対策

まず、単純にAdd-Typeで読み込んでもダメでした。
次に、この事象は何人かの人がはまっており、CodeProjectには、この対応策が記載されています。

Using WPF UI Automation with PowerShell
https://www.codeproject.com/Tips/110824/Using-WPF-UI-Automation-with-PowerShell

$source = @"
using System;
using System.Windows.Automation;
namespace UIAutTools
{
    public class Element
    {
        public static AutomationElement RootElement
        {
            get
            {
                return AutomationElement.RootElement;
            }
        }
    }
}
"@
Add-Type -TypeDefinition $source -ReferencedAssemblies( `
    "UIAutomationClient", "UIAutomationTypes")
$root = [UIAutTools.Element]::RootElement

これは簡単にいうとPowerShellの型の取り扱いの問題で発生しているっぽいから、C#で記載してしちゃえばいいじゃないという考えです。
この考えを取り入れて最初のコードを直した物が以下になります。

test3.ps1

Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes

$source = @"
using System;
using System.Windows.Automation;
namespace UIAutTools
{
    public class Element
    {
        public static AutomationElement RootElement
        {
            get
            {
                return AutomationElement.RootElement;
            }
        }
    }
}
"@
Add-Type -TypeDefinition $source -ReferencedAssemblies("UIAutomationClient", "UIAutomationTypes")

$rootElement = [UIAutTools.Element]::RootElement

$cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, "NativeTest")
$mainForm = $rootElement.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Children, $cond)

$cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::AutomationIdProperty, "1000")
$edit = $mainForm.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
$edit.Current
$txt = $edit.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
$txt.SetValue("Test3")

この実装は、Windows7(英語OS)+PowerShell2.0の環境においては期待通り動作しました。

しかしながら、Windows10(日本語OS)+Powershell5.0の環境においては、変わらずUIAutomationClientsideProvidersは読み込まれません。
そこで、さらに操作対象のウィンドウをFindFirstで取得するところまでC#で記載します。
※ちょうどC#でUIAutomationClientsideProvidersResourceまで読み込んだ時点です。

test4.ps1

Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes

$source = @"
using System;
using System.Windows.Automation;
namespace UIAutTools
{
    public class Element
    {
        public static AutomationElement RootElement
        {
            get
            {
                return AutomationElement.RootElement;
            }
        }
        public static AutomationElement GetMainWindowByTitle(string title) {
            PropertyCondition cond = new System.Windows.Automation.PropertyCondition(System.Windows.Automation.AutomationElement.NameProperty, title);
            return RootElement.FindFirst(TreeScope.Element | TreeScope.Children, cond);
        }
    }
}
"@
Add-Type -TypeDefinition $source -ReferencedAssemblies("UIAutomationClient", "UIAutomationTypes")

$mainForm = [UIAutTools.Element]::GetMainWindowByTitle("NativeTest")

$cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::AutomationIdProperty, "1000")
$edit = $mainForm.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
$edit.Current
$txt = $edit.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
$txt.SetValue("Test4")

これにより、UIAutomationClientsideProvidersが読み込まれ、期待通り動作します。

注意
Add-TypeでC#のコードを追加した場合、PowerShellを起動しなおさないかぎり、そのコードは残り続けます。

また、その影響か、一度、うまくいかないコードを動かすと、正しく動くコードを実行してもPowerShellを起動しなおさない限り正常に動作しません。

まとめ

UIAutomationを使用する際、Win32やWPFといった古いウィンドウを操作する場合、C#として実装しないと正常に動作しない場合があります。
また、新しめのアプリケーションでもメッセージボックスや名前を付けて保存などの共通のダイアログはWin32で表示される場合があるので、今回の事象に遭遇する場合があります。

また、PowerShellのUIAutomationのライブラリは存在してますが、実は基本部分をC#で実装してDLLを提供している形なので、今回の罠にはハマりません。
https://archive.codeplex.com/?p=uiautomation

なお、何故PowerShellだとUIAutomationClientsideProvidersが呼ばれないかはソースを見てみましたが全くわからんかったです。

AutomationElement.cs
https://referencesource.microsoft.com/#UIAutomationClient/System/Windows/Automation/AutomationElement.cs

UiaCoreAPI.cs : このAPI経由でUIAutomationCore.dllを操作している。
https://referencesource.microsoft.com/#UIAutomationClient/MS/Internal/Automation/UiaCoreAPI.cs

PowerShellのコード
https://github.com/PowerShell/PowerShell

意識が高くないVisualStudioを使用した単体テストの自動化

はじめに

この記事ではVisualStudioを使用して、どのように単体テストを自動化していくかを記述します。

この記事では「単体テストの自動化?なにそれおいしいの?」とかいう感じの組織で、現実的な妥協点を探っていくという、縛りゲーをやるクソゲーマー向けて、ファミ通程度の攻略情報をお届けするのが目的です。

あと「単体テスト」を自動でやるという枝葉の話でしかないので、「テスト?なにそれおいしいの?」とか「品質管理?わいには関係ないで」とかいう方は「自動」云々でなく、まず基本的なソフトウェア技法から調べた方がよろしいです。

「まっとうなソフトウェアテストの知識」を持っている人が、ツールを駆使して、作業の効率化をもとめ品質を上げるってのが重要で、「まっとうなソフトウェアテストの知識」がない人が、手順だけを自動化するのは本質を踏み外しています。

JSTQBの教科書とか、ソフトウェアテストの本とかでもアマゾンで買ったほうが有益でしょう。

自動化を目的にするのはやめましょう。

単体テストの自動化の目的

「単体テスト」をテストコードによって実現します。
流れとしては以下の全てをテストコードにより実現します。

・テスト対象が「どこ」でも「何時」でも実行できるように実行できる前提を作る
・テスト対象を実行する
・テスト対象が正しく動作したか検証する

これにより、回帰テストの効率が高まります。
たとえば、別の機能を修正した場合、すくなくともテストコードが記述している箇所の挙動は保障されます。
また、人力で実行した場合、人間は疲労等で結果の判定のミスをします。
手動の単体テストで合格していたはずの項目があとになって動いてなかった経験はいくらでもあるでしょう。すくなくとも、テストコードは何回実行しても、書かれた通りにしか動かないので、疲れてミスもしませんし、証跡として無駄なスクリーンキャプチャーをとるより効率的ですし、意味もあります。

しかし、あくまで単体テストです。結合テストは別に考えましょう。
単体テストを自動化して完全に動いても、それによって品質が高いと等価にはなりません。
テストコードを書くことは重要ですが、それに過度の期待を持つべきではないというのも重要です。

たとえば画面の使い勝手や、同時実効性の問題は単体テストで検出するのはかなり厳しいです。

この前提は管理する側含めて全員の共有にした方が良いです。
「単体テストを自動化したから結合テストの工数を削っていいよね」とかいう楽観主義は防ぐようにしてください。
特に前線から離れた管理職は夢見がちな都合のいい楽観主義を現実と思い込む傾向があるので、ここは釘を強くさしておいてください。

「単体テストはあくまで単体テスト。結合テストは別で考えろ」

テスト対象が「どこ」でも「何時」でも実行できるように実行できる前提を作る

作成した単体テストが「どこ」でも「何時」でも実行できるようにするというのが重要です。
たとえば、現在日付に依存するようなテストや、テストコードを書いた担当者の端末でしか動作しないようなテストは負債になります。

この工程では、それを防ぐために現在日付を取得する関数が常に同じ日付を返すように調整したり、どこの端末でもテスト対象が動作するように、ファイルやデータを読み込めるようにしたりします。

つまり、テスト対象が依存している機能がテストに都合のいい動作をするように設定します。
VisualStudioの場合は、MoqやMicrosoftFakesを使用して、依存した機能をテストに都合のいいように設定するとよろしいでしょう。

MSTestによるユニットテストの自動化 - Moq
http://qiita.com/mima_ita/items/55394bcc851eb8b6dc24#moq

Microsoft Fakesを利用したテストコードの記述
http://qiita.com/mima_ita/items/9ebb0f40d3209f33a45d

また、よくあることですが、ある特定のテストを実行した後だと動かないということが多々あります。
この工程では、前後で如何なるテストが行われても動くような状況を作成してください。

テスト対象を実行する

GUIのテスト対象を実行するのは前段階が色々困難ですが、単体テストの場合、そのテスト対象の関数のみを実行すればいいのでなんとでもなります。

特に.NETの場合はリフレクションが使用できるので、プライベートメソッドだろが、プライベートプロパティだろうがアクセスすることが可能です。

VisualStudioのProfessional以上の場合、テストコードを記述するプロジェクトが作成できるので以下を参考に対象の関数を実行するといいでしょう。
プライベートへのアクセスはPrivateObjectを使用してもいいですし、リフレクションを使用してもいいでしょう。

MSTestによるユニットテストの自動化
http://qiita.com/mima_ita/items/55394bcc851eb8b6dc24

C# で private メソッドを呼んでみる
http://normalian.hatenablog.com/entry/20090124/1232782230

テスト対象が正しく動作したか検証する

テスト対象が正しく動作したかの検証も重要です。
良く見るのが、テストの実行まではテストコードに書いて、実行結果を目視で確認するというやり方です。
これは古式ゆかしいデバッグ方法ですが、自動化とはいいません。

MSTestには様々なAssert関数が用意されているので、それらを使用して実行結果を検証しましょう。

様々なAssert機能
http://qiita.com/mima_ita/items/55394bcc851eb8b6dc24#%E6%A7%98%E3%80%85%E3%81%AAassert%E6%A9%9F%E8%83%BD

また、オブジェクトの内容をチェックする際は、それぞれのプロパティをAssertで愚直チェックをしてもいいですが、オブジェクトをJSONに変換してチェックする手もあります。

        public class TestData12
        {
            public class TestDataItem
            {
                public int key { set; get; }
                public string name { set; get; }
            }

            public string GroupName { set; get; }
            public List<TestDataItem> Items { set; get; }
        }

        [TestMethod]
        public void TestMethod12()
        {
            var expData = new TestData12();
            expData.GroupName = "Gp1";
            expData.Items = new List<TestData12.TestDataItem>();
            expData.Items.Add(new TestData12.TestDataItem
            {
                key = 1111,
                name = "item1"
            });
            expData.Items.Add(new TestData12.TestDataItem
            {
                key = 2222,
                name = "item1"
            });
            string json = Newtonsoft.Json.JsonConvert.SerializeObject(expData);
            Assert.AreEqual(
                "{\"GroupName\":\"Gp1\",\"Items\":[{\"key\":1111,\"name\":\"item1\"},{\"key\":2222,\"name\":\"item1\"}]}",
                json
            );
        }

これを実行するにはNewtonsoft.Jsonが必要になります。
http://www.newtonsoft.com/json

ここで紹介した方法はあくまで基本です。
個々のプロジェクト用に必要な検証用のユーティリティ関数を作成するといいでしょう。

どれを自動化すべきか?

意識が高い系の記事なら、「全部、カバレッジ100%に決まっているだろ」とか言いだしますが、ここでは「工数と開発者のスキルしだい」という面白くもない解答になります。

単体テストの自動化自体がはじめての場合

単体テストの自動化が初めての場合は、簡単にできて効果の高い箇所をテストするといいでしょう。
これは他のクラスになるべく依存していない、ユーティリティ関数のテストです。

static classの中から、なるべく多く機能で使用されているところから選定するといいでしょう。

共通的な機能あるいは業務的に重要な機能

次に単体テストを自動化して効果のある機能は多くの機能から呼ばれている機能です。
多くのクラスのベースとなるクラスや、多くのクラスから参照される機能が対象になるでしょう。

単体テストを自動化することにより、仮にその共通機能に修正が入っても、他の機能に影響を与えないような修正になっていることを保証する回帰試験が容易になります。

また、品質の確保においてシビアなものを要求される重要な機能においても単体テストを自動化するのは有効です。

テストコードで保障されていない箇所を調べてテストコードを書く

VisualStudo2013のPremium以上もしくはVisualStudio2015のEnterpriseのエディションにはコードカバレッジを計測する機能がついています。
これにより、どの機能がテストコードにより実行されていないか調べることが可能です。

(1)[テスト]→[コードカバレッジの分析]→[すべてのテスト]を選択
vs002.png

(2)コードカバレッジの結果が以下のように表示される。
vs003.png

関数単位で未カバー(実行していない)箇所が出力される。

(3)結果の関数をクリックすると実行済みの行は青、未実行の行は赤で表示される。
vs004.png

これらの機能を利用して、未実行の行を探して、そこを通るようにテストコードを書いていきます。

品質が悪そうな個所を探してテストコードを書く

ある程度、経験を積むとマズイ書き方をしているコードはわかります。
たとえば、以下のようなコードはヤバい臭いがぷんぷんとします。

for(int i = 0; i < x.Count;++i)
{
  for(int j = 0; j < y.Count;++j)
  {
    if(x[i].name == "XXX")
    {
       if (y[j].name == "ZZZ")
       {
          hoge1();
          if (hoge2() == 3)
          {
             hoge4();
          }
          else
          {
            if (hoge3() == 3)
            {
               if (hoge4() == 5)
               {
                  hoge6();
               }
               else if (a==5)
               {
          if(x)
                 {
                 }
                 else
                 {
                 }
               }
            }
          }
          hoge5();
       }
       else if (y[j].name = "dddd")
       {
          hoge1();
          if (hoge2() == 3)
          {
             hoge4();
          }
          else
          {
            if (hoge3() == 3)
            {
               if (hoge4() == 5)
               {
                  hoge6();
               }
               else if (a==5)
               {
               }
            }
          }
          hoge5();
       }
    }
  }
}

しかしながら、「クソコードだから直せよ、凸助」というと刃傷沙汰になります。
そういう、マズイ書き方をしている箇所を定量的に見つける手段として、「サイクロマティック複雑度」という指標が存在します。

循環的複雑度(Cyclomatic complexity)
https://ja.wikipedia.org/wiki/%E5%BE%AA%E7%92%B0%E7%9A%84%E8%A4%87%E9%9B%91%E5%BA%A6

簡単にいうと、ループや分岐が多いほどこの複雑度は高くなります。
1関数で25を超える場合、複雑度が極めて大きく、バグが潜んでいる可能性が高いです。

VisualStudioでは以下のようにサイクロマティック複雑度を計測します。

(1)[分析]→[ソリューションのコードメトリックスを計算]
vs005.png

(2)コードメトリックスの結果が表示される。
vs006.png

サイクロマティック複雑度は25未満に抑えるように実装するのが筋ですが、直せない場合は、これを超える箇所を集中的にテストコードを記述しましょう。
(だいたいバグがでてくるはず)

自動テストはいつ書いて実行すべきか?

実装しながらテストコードを書いたほうが楽です。
すくなくともテストコードが書けるレベルのコードになります。
そしてテストコードは最低毎日実行して、必ず全てのテストコードが合格するように監視してください。

人間は安易な方向に、あっさり流されます。

いくら、「どの環境でもテストが動くようにしろ」と言っても自分の環境でしか動かないテストコードはコミットされますし、「テストコードを動かして合格してからコミットしろ」といってもテストを不合格にするような安易な修正がコミットされます。

これはなにをどうやっても防げません。

そしてそれを放置すると、動かないテストが大量に作成されて、単体テストの自動化が形骸化します。最悪[Ignore]属性を付与して無視する形でいいので、テストが合格しつづけることだけは死守しましょう。

「毎日テストを実行して、その結果が合格になるように維持し続ける」ということだけは、なにがなんでも死守してください。

コマンドラインからビルドしてテストを実行する方法

毎日実行しつづけるのに手でやるのはしんどいです。
なので、コマンドラインから実行できるようにして、タスクスケジューラーかなにかで毎日実行するといいでしょう。

(1)MSBuildを使用してソリューションをビルドする

C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild TestSample.sln

(2)コマンドプロンプトからテストを実行する。

# 環境変数の設定
"C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\VsDevCmd.bat"

# テストの実行
vstest.console.exe UnitTestProject1\bin\Debug\UnitTestProject1.dll

コマンド ラインからの VSTest.console の使用
https://msdn.microsoft.com/ja-jp/library/jj155800.aspx

可能ならJenkinsを使用するといいですが、手段は問わないので毎日実行して結果を監視しましょう。

どう説得するか?

テストコードを書くことを認めさせる方法としていくつかのケースが考えられます。
ここでは、その説得方法を考えてみましょう。

テストコード書かないと単体テストができないケース

このパターンが一番説得しやすいです。
なぜならば、手動でテストができず、テストコードでしかテストできないので反論がしようがありません。
代替手段のないことを提案するのが一番楽です。

外部の機能に依存しており、それが完成していない場合

たとえば、外部の機能に依存しており、そのシステムが現在開発中で手動でテストできないケースを考えてみましょう。

従来であれば、結合テストまで先送りして地獄を味わいます。
しかし、テストコードを書くならば、外部のシステムのインターフェイスをMoqまたはMicrosfotFakesで偽装して単体テストを行うことが可能になります。

結合テストでのリスクを事前に減らせるというのは大きな説得材料になるでしょう。

大量のパターンがある場合

たとえばある関数の引数が取りうるパターンが数百、数千あったとします。
これは手動で実行するのは無理です。

しかしながら、テストコードであれば、パラメータを機械的に作成してテストコードを実行するだけなので有効です。

関連する機能、もしくは自機能に変更が予測されテストを何回も行う必要があるケース

次善のパターンです。
このケースをもって説得する場合、以下の工数の勝負になります。

手動テストの工数:「一回の手動テストに係る工数」×「予測される変更回数」
自動テストの工数:「テストコードを記述する工数」

これを算出して金額の差額で、勝負しましょう。

品質で勝負するケース

これは分の悪い勝負です。

テストコードを書いた方が品質が良くなる傾向がありますが、それを費用体効果で表すのはかなり難しいですし、事前に説得する材料を持ってくるのは無理でしょう。

ここで勝負するには、テストコードを書いた機能と書かなかった機能でバグ数/step数で比較することになりますが、この時点で手遅れなので、一手遅れます。

FAQ

よくある質問をまとめます。

他の開発者がテストコードを書いてくれません。

作業者向けの答え

そういうもんです。あきらめましょう。
無理やり書かせても、やっつけ仕事で意味のない仕事をあげてくるだけです。

ただし、他人が書かないのは、貴方が書かない理由にはなりません。
少なくとも貴方は書けばいいです。
貴方が書き続ける限りいずれ、カバレッジは100%に近づきます。

管理者向けの答え

泣き言いっていないで、テストコードを書ける人間を調達するか、教育しましょう。
学習期間を用意する、テストコードを書くための工数を用意する、テストコードが正しくかけたかチェックする工数を確保するのは管理職の職責です。

管理者がテストコードを書く工数を確保してくれません。

作業者向けの答え

そういうもんです。あきらめましょう。
本当に、そのテストコードが必要だと思うなら作業進捗を過小報告して裏で書きましょう。

残念ながら、まっとうじゃない状況で、まっとうなことをしたかったら、まっとうじゃない手段をとるしかありません。

管理者向けの答え

確保しましょう。
どうしても確保できんのなら、優先度を考えて効果の高い部分だけ適用するのはどうでしょうか?

それでも私は周りにテストコードを書かせたいのです。

作業者向けの答え

権限のない人間が他人の行動を決めることはできません。

テストコードを書かない機能にバグがないなら、それはそれでうまくいっているので、無理にうまくいっている事を変更する必要がないでしょう。わりきりましょう。

バグが大量にあるなら、バグ報告をして、「品質に問題があるので是正処置をとる必要があります」という方向にもっていきましょう。

この際、テストコードがある機能のバグ件数/Step数と比較してやれば、管理職はテストコードを書かないとまずいんじゃないかと思い始めます。
その事実を10か20も積み重ねれば、組織としてテストコードを書くようになるか、テストコードを書ける奴を雇ってくるでしょう。

管理者向けの答え

こんなもん読んでないで、さっさと人を教育するか適切な人雇う手筈を考えた方がいいでしょう。
貴方の組織の問題は貴方でないと解決できません。

テストコード書いてくれるのですが、合格しないコードをコミットしやがります

作業者向けの答え

そういうもんです。あきらめましょう。

毎日、全体メールで「偉い管理職」でもまぜて不合格の旨を通知しつづけましょう。
そのうち折れて直してきます。直さなくても管理職が問題視して勝手に対応してくれます。

放置されるようなら徐々に上役をCCにまぜていくといいでしょう。最後の偉い人まで行って放置されるなら、その組織の命数はもう長くないので、「プログラマがプロジェクトを救おうなどとおこがましいとは思わんかね?」とでも、ご自身に言い聞かせて、辞める準備をしましょう。

管理者向けの答え

テストが通らないコードはコミットさせないという運用ルールを作って周知します。

可能なら、テストコードが適切に動作しているかチェックする人間を任命しましょう。
当然、通常の作業量を減らすべきです。

テストコードが書きずらいです。

作業者向けの答え

GUI、マルチスレッドなどはたしかにテストコードは書きずらいので、そういう単体テストに向かないところはテストコードを書かないのも手です。

そういう理由がないのに、テストコードが書きずらいのは単純に実装がへたくそなだけです。
自分が書いた実装ならテストが書けるように直しましょう。

他人が書いた実装なら、バグを淡々と報告して、「作り直した方がはやい」ということを上に理解させましょう。
たぶん、そういう実装を仕上げてくる方の成果物の品質は極めて悪いので、簡単にバグを見つけることができます。

管理職向けの答え

特別の理由がない場合、実装が不味い可能性があります。
サイクロマティック複雑度の計測をしてみてください。
異常な値が出た場合は、残念ながらその機能はバグを含んでいる可能性が極めて高いです。
別の人間にプロダクトコードの修正をできる権限をあたえて見直しをさせた方がいいでしょう。

単体テストを自動化する意味がわかりません

作業者向けの答え

作りっぱなしで、終われる仕事だけしていればいいというなら、その考えもよろしいんじゃないでしょうか。

管理職向けの答え

しょせん、コストと効果のトレードオフにしかすぎません。
変更が発生する頻度と1回のテストの手間で、解答がかわります。
プロジェクトの性質によっては、その答えになるのも已む得ないでしょう。

ただし、もし、あらゆる状況で意味がないというお考えならば、そもそも「ソフトウェア」の管理には向いていない疑義があります。

ニート翔ぶ~C#でドローンを飛ばす~

昨今、退職エントリーが流行っているので、昨年、勢いで会社を辞めてニートになった記念に何か書こうと思います。
おっさんは明るい未来に羽ばたくことはできませんでしたので、せめて、ドローンぐらいは明るい未来を羽ばたかせてみせよう、そう思ってこの記事を書いてみました。

Telloとは

Telloは小型のドローンでカメラもついており、Android,iPadといった携帯端末で操作が可能です。
https://www.ryzerobotics.com/jp/tello

この度は無収入のくせに以下のセットを購入しました。
https://www.amazon.co.jp/gp/product/B07979Q4YS

注意事項:
・充電用のUSBはついてこないので自前でそろえる必要があります。
DSC_0032.JPG
機体にささないと充電できません。ただし別売りのバッテリーケースを購入すれば機体にささずに充電が可能のようですが、おっさんは無職なので購入してまでの検証はしてません。

・ハードウェアの性能としてはカメラがついているので動画撮影が可能です。つまり、住宅地で飛ばすと覗きとまちがわれるので気をつけましょう。おっさんは無職なのでポリス沙汰になると無職で全国デビューになるので細心の注意しないといけません。

・wifiでつなげて機体の操作をする必要があるため、操作側のリモコンはLANカードが2枚差しでないとインターネットにつなげながらの操作は行えません。おっさんはノートPCを10年ぶりくらいに有線のLANにつなげて作業しました。

・羽に指が当たると、そこそこ痛いので、慣れない間は軍手をして操作したほうがいいです。たぶん、大型のドローンの羽だったら、ドローンのかわりに指が飛んでいたと思います。

・可能なら外の広いところで運転した方が安全です。おっさんは引きこもりなので家でやりましたが、5回ほど壁にあたり墜落しました。

Tello SDK

TelloはSDKが提供されており、UDP経由で以下のことが行えます。
・機体の操作。
・機体の情報取得(傾きとか温度とかバッテリー情報)
・カメラからの撮影情報の取得

UDPなので基本的に無線LANがつながればどんなプラットフォームでも動作させることができますが、検索してでてくるMacのPythonかC/C++でやった方が絶対にいいです。
ジャイアントロボのように音声で操作しようと思って、音声認識が簡単にできる.NETで始めたら、えらい苦労しました。

また、SDKではなくて、バイナリデータを送信してSDKに書かれていない操作もできるようですが、ここでは割愛します。

Tello SDK 1.3.0.0

以下はTelloSDK1.3.0.0をそれっぽく翻訳したものです。

1. 概要

Tello SDKはWi-Fi UDPポートを介して航空機に接続し、ユーザーはテキストコマンドでドローンを制御することができます。 Tello3.pyファイルをダウンロードするにはここをクリックしてください。

2. アーキテクチャ

Wi-Fiを使用してTelloとPC、Mac、またはモバイルデバイスとの間の通信を確立します。

コマンドの送信と応答の受信

 Tello IP: 192.168.10.1 UDP PORT:8889 <<-->> PC/Mac/Mobile
 注意1:同じポートを介してTelloとメッセージを送受信するように、PC、Mac、またはモバイルデバイスでUDPクライアントを設定します。

 注意2:他のコマンドを送信する前に、"command"コマンドをUDP ポート8889を介してTelloに送信してTelloのSDKモードを開始します。

Telloステータスの受信

 Tello IP: 192.168.10.1 ->> PC/Mac/Mobile UDP Server: 0.0.0.0 UDP PORT:8890

 注意3:PC、Mac、またはモバイルデバイスにUDPサーバーをセットアップし、UDP PORT 8890を介してIP 0.0.0.0からのメッセージを聞きます。まだ行っていない場合は、注意2を実行して状態データの受信を開始してください。**

Telloビデオストリームの受信

 Tello IP: 192.168.10.1 ->> PC/Mac/Mobile UDP Server:0.0.0.0 UDP PORT:11111
 注意4:PC、Mac、またはモバイルデバイスにUDPサーバーをセットアップし、UDP PORT 11111を介してIP 0.0.0.0からメッセージを受信します。
 注意5:もし実行していないなら注意2を実行してください。その後、UDP PORT 8889を介して「streamon」コマンドをTelloに送信してストリーミングを開始します。

3. TELLOコマンドの種類と結果

このSDKには3つの基本的なコマンドタイプが含まれています。

コントロールコマンド(xxx)
・コマンドの実行が成功した場合、"ok"が戻ります
・成功しなかった場合、"error"または有益な結果コードが戻ります

リードコマンド(xxx?)
・サブパラメータの現在値を戻します。

設定コマンド(xxx a)は、新しいサブパラメータ値を設定しようとします
・コマンドの実行が成功した場合、"ok"が戻ります
・成功しなかった場合、"error"または有益な結果コードが戻ります

4. Tello コマンド

コントロールコマンド

コマンド 説明 考えられる応答
command SDKモードに入る ok
error
takeoff Telloが自動で離陸する ok
error
land Telloが自動で着陸する ok
error
streamon ビデオストリームをON ok
error
streamoff ビデオストリームをOFF ok
error
emergency 全てのモータを停止 ok
error
up x Tello が x cm上昇
x:20-500
ok
error
down x Tello が x cm下降
x:20-500
ok
error
left x Tello が x cm左へ
x:20-500
ok
error
right x Tello が x cm右へ
x:20-500
ok
error
forward x Tello が x cm前進
x:20-500
ok
error
back x Tello が x cm後退
x:20-500
ok
error
cw x Tello が 時計回りに x度 回転
x:1-3600
ok
error
ccw x Tello が 反時計回りに x度 回転
x:1-3600
ok
error
flip x Tello が x 方向に宙返りをする
l:(left)
r(right)
f(foward)
b(back)
ok
error
go x y z speed Tello が x y z の方向へ speed(cm/s)の速度で飛ぶ x:20-500 ※訳注 前進する xで後退
y:20-500 ※訳注 左へ -y で右へ
z:20-500
speed:10-100
ok
error
curve x1 y1 z1 x2 y2 z2 speed Telloが現在位置とspeed(cm/s)とともに定義された2つの座標を曲線状に飛びます。もし円弧の半径が0.5-10meterの範囲外の場合、レスポンスはfalseとなります。
x1, x2: 20-500
y1, y2: 20-500
z1, z2: 20-500
speed: 10-60
x/y/z は同時に-20~20の間にはできません
ok
error

設定コマンド

コマンド 説明 考えられる応答
speed x 速度 x cm/sを設定する
x: 10-100
ok
error
rc a b c d 4つのチャネルを通してRCコントロールを送信する
a: left/right (-100~100)
b: forward/backward (-100~100)
c: up/down (-100~100)
d: yaw (-100~100)
ok
error
wifi ssid pass Wi-FiのSSIDとpasswordを設定する ok
error

リードコマンド

コマンド 説明 考えられる応答
speed? 現在の速度(cm/s)を取得 x: 1-100
battery? 現在のバッテリーのパーセンテージを取得 x: 1-100
time? 現在の飛行時間を取得 time
height? 現在の高さ(cm)を取得 x: 0-3000
temp? 現在の温度(℃)を取得 x : 0-90
attitude? IMU(慣性計測装置) の姿勢情報を取得 pitch roll yaw
baro? バロメータ(気圧計)の値(m)を取得 x
acceleration? IMU角加速度データを取得する(0.001g) x y z
tof? TOFからの距離(cm)を取得する x:30-1000
wifi? Wi-FiのSNRを取得する snr

訳者注
pitch roll yaw:参考:https://algorithm.joho.info/robotics/roll-pitch-yaw-matrix/
Tof:Time of Flightのことと思われる
SNR:信号対雑音比。SN比が高ければ伝送における雑音の影響が小さく、SN比が小さければ影響が大きい。

5. Telloステータス

データ型:String
Example:
“pitch:%d;roll:%d;yaw:%d;vgx:%d;vgy%d;vgz:%d;templ:%d;temph:%d;tof:%d;h:%d;bat:%d;baro: %.2f; time:%d;agx:%.2f;agy:%.2f;agz:%.2f;\r\n”

説明
o pitch: ピッチ角
o roll: ロール角
o yaw: ヨー角
o vgx: Speed x,
o vgy: Speed y,
o vgz: Speed z,
o templ: 最も低い温度, 摂氏℃
o temph: 最も高い温度、摂氏℃
o tof: TOF distance, cm
o h: Height, cm
o bat: 現在のバッテリーのパーセンテージ, %
o baro: バロメーター測定, cm
o time: モータの時間,
o agx: 加速度x,
o agy: 加速度y,
o agz: 加速度z,

6 安全機能

もしTelloが15秒間なにもコマンドを受信しなければ、自動で着陸をします

7 TelloのWi-fiリセット

電源ON状態のTelloに5秒間の長押しをするとインジケータライトが消えて黄色に点滅します。 インジケータランプが黄色のライトを点滅させると、Wi-Fi SSIDとパスワードは工場出荷時の設定にリセットされ、デフォルトではパスワードは設定されません。

.NETで操作してみる。

おとなしく、Mac+Pythonで動かした方がいいです。やっている人がいっぱいいます。
それでもやるなら、以下を参考にしてみてください。

事前準備

・ステータス取得のための8890とビデオストリーム取得のためのポート11111を開けておきます。
 つながらない場合は、アプリケーション固有のファイアウォールの設定も確認してください。 
 Telloとのネットワークはパブリックのネットワークになっているはずなので、パブリックの設定もちゃんとみましょう(2敗)
 settei.png

・WireShark等でネットワークの電文をみれるようにしておきます。
 https://www.wireshark.org/
 問題の切り分けにやくに立ちます。

・ffmpegを用意する。
 Telloからのビデオ情報を表示するのに使用します。
 また、自前でデコードする場合もffmpegのAPIを使用しないと厳しいです。
 https://www.ffmpeg.org/

簡単なTelloプログラミング

画面
gamen.png

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Net;//for UDP
using System.Net.Sockets; //for UDP
using System.Threading;//for Interlocked
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;

namespace TelloSample
{
    public partial class Form1 : Form
    {
        private UdpClient udpForCmd;     //コマンド結果受信用クライアント
        private UdpClient udpForStsRecv; //ステータスの結果受信用クライアント

        public Form1()
        {
            InitializeComponent();
        }

        // コマンドの結果更新用
        private delegate void DelegateUpdateCmdResult(String ret);

        // ステータスの更新用
        private delegate void DelegateUpdateSts(String sts);

        // コマンド結果を更新。ワーカスレッドからの場合はメインスレッドで実行
        private void UpdateCmdResult(String ret)
        {
            if (this.InvokeRequired)
            {
                Object[] param = new Object[1] { ret };

                this.Invoke(new DelegateUpdateCmdResult(this.UpdateCmdResult), param);
                return;
            }
            this.txtRet.Text = ret;
            this.btnCmd.Enabled = true;
        }

        // ステータスを更新。ワーカスレッドからの場合はメインスレッドで実行
        private void UpdateSts(String sts)
        {
            if (this.InvokeRequired)
            {
                Object[] param = new Object[1] { sts };

                this.Invoke(new DelegateUpdateSts(this.UpdateSts), param);
                return;
            }
            this.txtSts.Text = sts;
        }

        // Telloとの通信を設定する
        private void SetupTello()
        {
            this.udpForCmd = new UdpClient(0);
            this.udpForStsRecv = new UdpClient(8890);

            // コマンド結果の受信処理
            Task.Run(() => {
                IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
                while (true)
                {
                    try
                    {
                        String rcvMsg = "";
                        byte[] rcvBytes = udpForCmd.Receive(ref remoteEP);
                        Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes));
                        this.UpdateCmdResult(rcvMsg);
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }

            });

            // ステータスの受信処理
            Task.Run(() => {
                IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
                while (true)
                {
                    try
                    {
                        String rcvMsg = "";
                        byte[] rcvBytes = udpForStsRecv.Receive(ref remoteEP);
                        Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes));
                        rcvMsg = rcvMsg.Replace(";", "\r\n");
                        this.UpdateSts(rcvMsg);
                    }
                    catch (Exception ex)
                    { 
                        Debug.WriteLine(ex.Message);
                    }

                }

            });

        // コマンド送信
        private void sendCmd(string cmd)
        {
            byte[] data = Encoding.ASCII.GetBytes(cmd);
            this.udpForCmd.Send(data, data.Length, "192.168.10.1", 8889);

        }

        // 開始ボタン
        private void btnStart_Click(object sender, EventArgs e)
        {
            SetupTello();

            this.txtRet.Text = "";
            this.btnCmd.Enabled = false;

            sendCmd("command");
        }

        // コマンド送信ボタン押下
        private void btnCmd_Click(object sender, EventArgs e)
        {
            this.txtRet.Text = "";
            this.btnCmd.Enabled = false;
            sendCmd(this.txtCmd.Text);
        }

    }
}

ビデオについて

streamon コマンドを送信するとポート11111にビデオの情報が受信できます。
これを表示するにはffmpegのffplayを使用するといいでしょう。

ffplay -probesize 32 -sync ext udp://127.0.0.1:11111

ウィンドウが起動して現在のカメラが表示されます。

camera.png

よくある問題

・コマンドを受け付けない
無線LANでつながっているかを確認する。
充電されているか確認する。USBさして青ランプが点灯されたらフル充電である。
WireSharkでパケットの送受信がされているか確認する。
送受信のポートが開いているか確認。規定値だとパブリックネットワークなので注意。

・カメラが受信できない。
11111ポートが開いているか見直す。
WireSharkでパケットが届いているか確認する。

・ステータスが受信できない。
8890ポートが開いているか見直す。

・たまにコマンドの応答結果がとれない。
UDPなので仕様だと思われます。okが必ずくるという前提は多分まずいかもです。

.NETで自力でビデオデータを取り扱いたい。

OpenCVSharpを使えば簡単にできます・・・(震え)

・OpenCVSharp
OpenCvSharp3-AnyCPUとOpenCvSharp4.runtime.winをNuGetで取得していました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenCvSharp;

namespace OpenCVVideo
{
    class Program
    {
        static void Main(string[] args)
        {
            var camera = VideoCapture.FromFile("udp://127.0.0.1:11111");
            using (var normalWindow = new Window("normal"))
            {
                var normalFrame = new Mat();
                var srFrame = new Mat();
                while (true)
                {
                    camera.Read(normalFrame);
                    if (normalFrame.Empty())
                        break;

                    normalWindow.ShowImage(normalFrame);
                    int key = Cv2.WaitKey(100);
                    if (key == 27) break;   // ESC キーで閉じる
                }
            }

        }
    }
}

OpenCVのVideoCapture.FromFileはファイルといいつつ、UDPからのストリームもとれます。
まちがっても自分でUDPで11111ポートを監視してデコードしようとしてはいけません。
以下にその愚かな例をしめしますが、おとなしくOpenCVを使いましょう。

愚かにも自前でUDPの11111ポートを監視した例

簡単な流れとしては以下の通りになります。

  1. streamonコマンドを送信
  2. ポート11111を監視
  3. 1460バイトとどいている間は後続のパケットがあるので受信しつづける。1460以外のデータ長がきたら、いままできたぶんとまとめてffmpegのAPIを使用してデコードする。
  4. デコード結果をffmpegの関数を使用してRGBに変換する
  5. OpenCVにかませるため、OpenCVの関数をつかってBGRに変換する
  6. OpenCVでビデオつくったりする。

この挙動は下記を参考にしました。
https://github.com/dji-sdk/Tello-Python/blob/master/Tello_Video/tello.py
https://github.com/dji-sdk/Tello-Python/tree/master/Tello_Video/h264decoder

.NETでつらいのは3と4です。
これをおこなうにはネイティブのDLLでffmpegのAPIを使い実行した結果をC#に渡す必要があります。

.NETでTelloのビデオを扱うために必要なライブラリ

・ffmpeg
https://ffmpeg.zeranoe.com/builds/
devにincludeファイルとlibファイル、sharedにdllがあるのでそれぞれダウンロードしました。
このライブラリはtelloから取得したh264形式をRGBに変換するために使用します。
ソースコードから自前でコンパイルもできますが、MSYS2をいれたりして結構、手間がかかるのでFormアプリでいいなら、おとなしく配布されているものを使った方がいいと思います。(一敗)

h264decorderの移植

先に紹介したPythonでh264のデコードをするためのコードを.NETでやるために改造しました。
https://github.com/mima3/Tello/tree/master/h264decoder

おそらく、メモリ解放処理がうまくできていない気がするので参考程度にしてください。

h264decoderの使用

.NETでネイティブのDLLを使用する場合は、32bitか64bitかは意識してください。
今回は64bitで動かすためにAnyCPUをx64に変更するか、32bitを優先するフラグをオフにする必要があります。
もし、今まで動いていたアプリケーションが起動すらしない場合、DLLが32bitと64bitで混在している可能性を疑ってみてください。

以下に移植したh264decoderを使用して動画を作成する実装例を記載します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Net;//for UDP
using System.Net.Sockets; //for UDP
using System.Threading;//for Interlocked
using System.Diagnostics;
using OpenCvSharp;
using System.IO;
using System.Runtime.InteropServices;

namespace TelloSample
{
    public partial class Form1 : Form
    {
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        public struct H264DecoderResult
        {
            public int w;
            public int h;
            public int size;
            public IntPtr buff;
        }
        [DllImport("h264decoder.dll", EntryPoint = "InitH264Decoder")]
        static extern void _InitH264Decoder();

        [DllImport("h264decoder.dll", EntryPoint = "TermH264Decoder")]
        static extern void _TermH264Decoder();

        [DllImport("h264decoder.dll", EntryPoint = "DecodeH264")]
        static extern bool _DecodeH264(IntPtr buff, int size, ref H264DecoderResult outbuff);

        [DllImport("h264decoder.dll", EntryPoint = "GetH264DecoderLastError")]
        public static extern IntPtr GetH264DecoderLastError();

        [DllImport("h264decoder.dll", EntryPoint = "FreeData")]
        public static extern void FreeData(IntPtr data);

        [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
        public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);

        private UdpClient udpForCmd;     //コマンド結果受信用クライアント
        private UdpClient udpForStsRecv; //ステータスの結果受信用クライアント
        private UdpClient udpForVideo;   //ビデオストリームの受信用

        public Form1()
        {
            InitializeComponent();
        }

        // コマンドの結果更新用
        private delegate void DelegateUpdateCmdResult(String ret);

        // ステータスの更新用
        private delegate void DelegateUpdateSts(String sts);

        // コマンド結果を更新。ワーカスレッドからの場合はメインスレッドで実行
        private void UpdateCmdResult(String ret)
        {
            if (this.InvokeRequired)
            {
                Object[] param = new Object[1] { ret };

                this.Invoke(new DelegateUpdateCmdResult(this.UpdateCmdResult), param);
                return;
            }
            this.txtRet.Text = ret;
            this.btnCmd.Enabled = true;
        }

        // ステータスを更新。ワーカスレッドからの場合はメインスレッドで実行
        private void UpdateSts(String sts)
        {
            if (this.InvokeRequired)
            {
                Object[] param = new Object[1] { sts };

                this.Invoke(new DelegateUpdateSts(this.UpdateSts), param);
                return;
            }
            this.txtSts.Text = sts;
        }

        // Telloとの通信を設定する
        private void SetupTello()
        {
            this.udpForCmd = new UdpClient(0);
            this.udpForStsRecv = new UdpClient(8890);
            this.udpForVideo = new UdpClient(11111);

            // コマンド結果の受信処理
            Task.Run(() => {
                IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
                while (true)
                {
                    try
                    {
                        String rcvMsg = "";
                        byte[] rcvBytes = udpForCmd.Receive(ref remoteEP);
                        Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes));
                        this.UpdateCmdResult(rcvMsg);
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }

            });

            // ステータスの受信処理
            Task.Run(() => {
                IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
                while (true)
                {
                    try
                    {
                        String rcvMsg = "";
                        byte[] rcvBytes = udpForStsRecv.Receive(ref remoteEP);
                        Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes));
                        rcvMsg = rcvMsg.Replace(";", "\r\n");
                        this.UpdateSts(rcvMsg);
                    }
                    catch (Exception ex)
                    { 
                        Debug.WriteLine(ex.Message);
                    }

                }

            });

            // ビデオストリームの受信処理
            Task.Run(() => {
                IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
                byte[] packetData = new byte[0];
                int cnt = 0;
                _InitH264Decoder();
                var fourcc = VideoWriter.FourCC('m', 'p', '4', 'v');
                var video = new VideoWriter("test.mp4", fourcc, 20, new OpenCvSharp.Size(960, 720) );

                while (true)
                {
                    try
                    {

                        byte[] rcvBytes = udpForVideo.Receive(ref remoteEP);
                        int l = packetData.Length;
                        Array.Resize<byte>(ref packetData, l + rcvBytes.Length);
                        Array.Copy(rcvBytes, 0, packetData, l, rcvBytes.Length);
                        if (rcvBytes.Length != 1460)
                        {

                            int size = Marshal.SizeOf(packetData[0]) * packetData.Length;
                            IntPtr inPtr = Marshal.AllocHGlobal(size);
                            Marshal.Copy(packetData, 0, inPtr, packetData.Length);

                            H264DecoderResult decret = new H264DecoderResult();
                            Debug.WriteLine("DO DECODE");
                            if (_DecodeH264(inPtr, packetData.Length, ref decret))
                            {
                                Debug.WriteLine("DO DECODE,,,,ok");
                                var mat = new Mat(decret.h, decret.w, MatType.CV_8UC3);
                                CopyMemory(mat.Data, decret.buff, (uint)decret.size);
                                var matCv = new Mat();
                                Cv2.CvtColor(mat, matCv, ColorConversionCodes.RGB2BGR);
                                video.Write(matCv);
                                FreeData(decret.buff);
                            }
                            else
                            {
                                Debug.Write(Marshal.PtrToStringAnsi(GetH264DecoderLastError()));
                            }
                            Marshal.FreeHGlobal(inPtr);

                            packetData = new byte[0];
                            ++cnt;

                        }
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }

                }
                // 後片付けの方法はあとで考える。(呼ばれない)
                _TermH264Decoder();
                video.Release();
            });

        }

        // コマンド送信
        private void sendCmd(string cmd)
        {
            byte[] data = Encoding.ASCII.GetBytes(cmd);
            this.udpForCmd.Send(data, data.Length, "192.168.10.1", 8889);

        }

        // 開始ボタン
        private void btnStart_Click(object sender, EventArgs e)
        {
            SetupTello();

            this.txtRet.Text = "";
            this.btnCmd.Enabled = false;

            sendCmd("command");
        }

        // コマンド送信ボタン押下
        private void btnCmd_Click(object sender, EventArgs e)
        {
            this.txtRet.Text = "";
            this.btnCmd.Enabled = false;
            sendCmd(this.txtCmd.Text);
        }

    }
}

とりあえず64bitで動くソースは以下に置いておきます。
https://github.com/mima3/Tello

メモリ解放関係がだいぶ怪しいので、とりあえず動かす用としてくださいというか、そもそもOpenCVでやった方がはるかに楽です・・・orz

最後に

ここではTelloの最低限の機能を.NETで実装した例をしめしました。
基本的に文字のコマンドを送信するだけで、制御できますが、h264のデコード処理はネィティブのDLLを作ってOpenCVがとれるようにする必要があります。
そこを超えてしまえば、あとは.NETのライブラリを色々と利用してにTelloを活用する道筋が見えるかと思います。(たとえば、音声認識で飛行させるとか・・・)

まぁ、.NETでTelloを使う道筋はみえても、おっさんの人生の道筋はみえないね。しかたないね。

VBScriptでエンコードを指定してファイルを連結する

目的

VBScriptでエンコードを指定してファイルを連結するスクリプトを記述する

スクリプト

jointext.vbs


'* 指定ディレクトリの検索を行う
'* @param [in] searchPath 検索対象のパス
'* @param [out] fileList  パスを格納するコレクション
'* @param [in] targetExt  検索対象の拡張子
'* @param [in] excludeDir 検査対象のディレクトリ名
Sub SearchFile(ByVal searchPath , ByRef fileList, ByRef targetExt, ByRef excludeDir)
    Dim fso
    Dim oFile
    Dim oDir
    Dim oSubDir
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set oDir = fso.GetFolder(searchPath)

    For Each oSubDir In oDir.SubFolders
        If Not excludeDir.Contains(oSubDir.Name) Then
            SearchFile searchPath & oSubDir.Name & "\", fileList, targetExt, excludeDir
        End If
    Next

    For Each oFile In oDir.Files
        If targetExt.Contains(fso.GetExtensionName(oFile.Name)) Then
            fileList.add searchPath & oFile.Name
        End If
    Next

End Sub

'* エンコードを指定してファイルを読んで出力ストリームに書き込む
'* @param[in] path 入力パス
'* @param[in] enc  エンコード
'* @param[out] outStream 出力ストリーム
Function ReadAndAppend(ByVal path, Byval enc, Byref outStream)
    Dim inStream
    Set inStream = CreateObject("ADODB.Stream")
    inStream.type = 2            '1:バイナリデータ 2:テキストデータ
    inStream.charset = enc       '入力ファイルの文字コード設定
    inStream.open
    inStream.LoadFromFile path   '入力ファイルを読み込む

    outStream.WriteText "// copy... " & path, 1
    outStream.WriteText inStream.ReadText(-1), 1   'WriteTextの第二引数:0:文字列のみ書き込む 1:文字列+改行を書き込む

    inStream.Close
    Set inStream = Nothing

End Function

Dim objParm
Dim searchPath
Dim fileList
Dim excludeDir
Dim targetExt
Dim enc
Dim outputFileName
Set fileList = CreateObject("System.Collections.ArrayList")
Set excludeDir = CreateObject("System.Collections.ArrayList")
Set targetExt = CreateObject("System.Collections.ArrayList")
Set objParm = Wscript.Arguments
If objParm.Count < 3 Then
   WScript.echo "CScript jointext 検査対象のフォルダ 出力パス エンコード"
   WScript.echo "例: CScript jointext.vbs C:\dev\WSH\jointext\js\ out2.txt UTF-8"
   WScript.Quit
End If

targetExt.add "js"

excludeDir.add ".svn"
excludeDir.add ".git"

searchPath = objParm(0)
outputFileName = objParm(1)
enc = objParm(2)

SearchFile searchPath, fileList, targetExt, excludeDir

fileList.Sort

Dim outStream
Set outStream =CreateObject("ADODB.Stream")
outStream.type = 2
outStream.charset = enc '出力ファイルの文字コード設定
outStream.open 

Dim f
For Each f In fileList
   WScript.Echo f
   ReadAndAppend f, enc, outStream
Next

outStream.SaveToFile outputFileName, 2
outStream.Close
Set outStream = Nothing

実行例

CScript jointext.vbs C:\dev\WSH\jointext\js\ out2.txt UTF-8"

C:\dev\WSH\jointext\js\ 以下のJavaScriptを連結して出力する。
この時、.gitや.svnはフォルダは見ない。

C++のsortとstable_sortの違い

概要

sortには同位の場合に、その順番を保障する安定したソートと保障していない安定しないソートの二種類がある。

STLで提供されているsortは安定しないソートで、stable_sortは安定しているソートである。

sortで安定したソートを実現したい場合は、比較用の関数で同位だったら、元の並び順で比較してやれば安定する。

検証コード


# include <iostream>  // cout, endl
# include <algorithm> // stable_sort, for_each
# include <vector>
# include <time.h>     // for clock()
# include <string>

using namespace std;
struct test
{
    int order;
    int key;
};

bool CustPredicate(test elem1, test elem2)
{
    if (elem1.key > elem2.key)
        return false;

    if (elem1.key < elem2.key)
        return true;
    return false; // trueにするとDebugでAssert。同値の場合、順序が逆になる
    // https://support.microsoft.com/en-us/kb/949171
}

bool CustPredicate2(test elem1, test elem2)
{
    if (elem1.key > elem2.key)
        return false;

    if (elem1.key < elem2.key)
        return true;
    // 同じ場合は元の並び順で決める
    return elem1.order < elem2.order;
}

vector<test> createList(size_t sz)
{
    std::vector<test> list;
    // データ数が少ないと、安定してないソートでも安定しているように見えるので注意
    for (size_t i = 0; i < sz; ++i){
        test t;
        t.order = i;
        t.key = i % 3;
        list.push_back(t);
    }
    return list;
}

int main() {
    {
        vector<test> list = createList(32);
        cout << "\n不安定なソート 32件-----------------------------------\n";
        sort(list.begin(), list.end(), &CustPredicate);
        for (size_t i = 0; i < list.size(); ++i){
            cout << "(" << list[i].key << ":" << list[i].order << ")";
        }
    }
    {
        vector<test> list = createList(33);
        cout << "\n不安定なソート 33件-----------------------------------\n";
        sort(list.begin(), list.end(), &CustPredicate);
        for (size_t i = 0; i < list.size(); ++i){
            cout << "(" << list[i].key << ":" << list[i].order << ")";
        }
    }
    {
        vector<test> list = createList(32);
        cout << "\n安定なソート 33件-----------------------------------\n";
        stable_sort(list.begin(), list.end(), &CustPredicate);
        for (size_t i = 0; i < list.size(); ++i){
            cout << "(" << list[i].key << ":" << list[i].order << ")";
        }
    }
    {
        vector<test> list = createList(33);
        cout << "\n安定なソート 33件-----------------------------------\n";
        stable_sort(list.begin(), list.end(), &CustPredicate);
        for (size_t i = 0; i < list.size(); ++i){
            cout << "(" << list[i].key << ":" << list[i].order << ")";
        }
    }
    {
        vector<test> list = createList(33);
        cout << "\n不安定なソートを比較関数でなんとかする 33件-----------------------------------\n";
        stable_sort(list.begin(), list.end(), &CustPredicate2);
        for (size_t i = 0; i < list.size(); ++i){
            cout << "(" << list[i].key << ":" << list[i].order << ")";
        }
    }
    size_t sort_size = 500000;
    {
        vector<test> list = createList(sort_size);
        cout << "\n不安定ソートの速度-----------------------------------\n";
        clock_t start = clock();    // スタート時間
        sort(list.begin(), list.end(), &CustPredicate2);
        clock_t end = clock();     // 終了時間
        std::cout << "\nduration = " << (double)(end - start) / CLOCKS_PER_SEC << "sec.\n";
    }
    {
        vector<test> list = createList(sort_size);
        cout << "\n安定ソートの速度-----------------------------------\n";
        clock_t start = clock();    // スタート時間
        stable_sort(list.begin(), list.end(), &CustPredicate2);
        clock_t end = clock();     // 終了時間
        std::cout << "\nduration = " << (double)(end - start) / CLOCKS_PER_SEC << "sec.\n";
    }
    {
        vector<test> list = createList(sort_size);
        cout << "\n比較関数で安定させるソート-----------------------------------\n";
        clock_t start = clock();    // スタート時間
        sort(list.begin(), list.end(), &CustPredicate2);
        clock_t end = clock();     // 終了時間
        std::cout << "\nduration = " << (double)(end - start) / CLOCKS_PER_SEC << "sec.\n";
    }
    cout << endl << endl;

}

結果

VS2013 + Windows7


不安定なソート 32件-----------------------------------
(0:0)(0:3)(0:6)(0:9)(0:12)(0:15)(0:18)(0:21)(0:24)(0:27)(0:30)
(1:1)(1:4)(1:7)(1:10)(1:13)(1:16)(1:19)(1:22)(1:25)(1:28)(1:31)
(2:2)(2:5)(2:8)(2:11)(2:14)(2:17)(2:20)(2:23)(2:26)(2:29)
不安定なソート 33件-----------------------------------
(0:0)(0:21)(0:30)(0:3)(0:12)(0:27)(0:6)(0:15)(0:24)(0:9)(0:18)
(1:1)(1:4)(1:7)(1:10)(1:13)(1:16)(1:19)(1:22)(1:25)(1:28)(1:31)
(2:14)(2:23)(2:8)(2:17)(2:26)(2:5)(2:20)(2:29)(2:2)(2:11)(2:32)
安定なソート 33件-----------------------------------
(0:0)(0:3)(0:6)(0:9)(0:12)(0:15)(0:18)(0:21)(0:24)(0:27)(0:30)
(1:1)(1:4)(1:7)(1:10)(1:13)(1:16)(1:19)(1:22)(1:25)(1:28)(1:31)
(2:2)(2:5)(2:8)(2:11)(2:14)(2:17)(2:20)(2:23)(2:26)(2:29)
安定なソート 33件-----------------------------------
(0:0)(0:3)(0:6)(0:9)(0:12)(0:15)(0:18)(0:21)(0:24)(0:27)(0:30)
(1:1)(1:4)(1:7)(1:10)(1:13)(1:16)(1:19)(1:22)(1:25)(1:28)(1:31)
(2:2)(2:5)(2:8)(2:11)(2:14)(2:17)(2:20)(2:23)(2:26)(2:29)(2:32)
不安定なソートを比較関数でなんとかする 33件-----------------------------------
(0:0)(0:3)(0:6)(0:9)(0:12)(0:15)(0:18)(0:21)(0:24)(0:27)(0:30)
(1:1)(1:4)(1:7)(1:10)(1:13)(1:16)(1:19)(1:22)(1:25)(1:28)(1:31)
(2:2)(2:5)(2:8)(2:11)(2:14)(2:17)(2:20)(2:23)(2:26)(2:29)(2:32)
不安定ソートの速度-----------------------------------

duration = 0.12sec.

安定ソートの速度-----------------------------------

duration = 0.081sec.

比較関数で安定させるソート-----------------------------------

duration = 0.109sec.

VisualStudioのC++だとsort関数は32件までは挿入ソートをしているので安定している。
それいこうは、クイックソート。
stable_sortも32件までは挿入ソートで、それ以降はマージソート
速度的には、stable_sortの方が早い。そのかわり、マージソートなのでメモリを消費している。

stable_sortでassertが出る場合

vs2005以降でstable_sortを使うとAssertがでる場合がある。
これは同値の場合にtrueを返してしまっているため。
https://support.microsoft.com/en-us/kb/949171

trueを返すと同位の場合、逆順にならんでしまう。

Debian7 + g++ 4.7.4

不安定なソート 32件-----------------------------------
(0:24)(0:0)(0:15)(0:18)(0:12)(0:21)(0:9)(0:6)(0:27)(0:3)(0:30)
(1:25)(1:16)(1:22)(1:28)(1:19)(1:31)(1:13)(1:10)(1:7)(1:4)(1:1)
(2:17)(2:14)(2:20)(2:11)(2:23)(2:8)(2:26)(2:5)(2:29)(2:2)
不安定なソート 33件-----------------------------------
(0:0)(0:18)(0:15)(0:21)(0:12)(0:24)(0:9)(0:27)(0:6)(0:30)(0:3)
(1:25)(1:28)(1:22)(1:19)(1:31)(1:1)(1:16)(1:13)(1:10)(1:7)(1:4)
(2:17)(2:14)(2:20)(2:11)(2:23)(2:8)(2:26)(2:5)(2:29)(2:2)(2:32)
安定なソート 33件-----------------------------------
(0:0)(0:3)(0:6)(0:9)(0:12)(0:15)(0:18)(0:21)(0:24)(0:27)(0:30)
(1:1)(1:4)(1:7)(1:10)(1:13)(1:16)(1:19)(1:22)(1:25)(1:28)(1:31)
(2:2)(2:5)(2:8)(2:11)(2:14)(2:17)(2:20)(2:23)(2:26)(2:29)
安定なソート 33件-----------------------------------
(0:0)(0:3)(0:6)(0:9)(0:12)(0:15)(0:18)(0:21)(0:24)(0:27)(0:30)
(1:1)(1:4)(1:7)(1:10)(1:13)(1:16)(1:19)(1:22)(1:25)(1:28)(1:31)
(2:2)(2:5)(2:8)(2:11)(2:14)(2:17)(2:20)(2:23)(2:26)(2:29)(2:32)
不安定なソートを比較関数でなんとかする 33件-----------------------------------
(0:0)(0:3)(0:6)(0:9)(0:12)(0:15)(0:18)(0:21)(0:24)(0:27)(0:30)
(1:1)(1:4)(1:7)(1:10)(1:13)(1:16)(1:19)(1:22)(1:25)(1:28)(1:31)
(2:2)(2:5)(2:8)(2:11)(2:14)(2:17)(2:20)(2:23)(2:26)(2:29)(2:32)
不安定ソートの速度-----------------------------------

duration = 0.256623sec.

安定ソートの速度-----------------------------------

duration = 0.253698sec.

比較関数で安定させるソート-----------------------------------

duration = 0.252346sec.

g++の場合、件数が32件でも不安定になっているので、おそらく、件数によるアルゴリズムの切り替えはしてないくさい。(あるいはもっと件数が小さい)

そして、stable_sortとsortの速度差がVisualStudioC++より小さい。

おそらく、処理系によって実装の方法が違っているっぽい。