C#とExcel VBA間のデータ交換

 弊局JG6JAVは、通常運用記録とコンテスト運用もExcelを使っている。特に無線機の周波数とモードを自動的に記録することは重要だ。今までいろいろな手段でそれを行っていた。けれども時々問題発生していたのでもっと確実且つリアルタイムにその情報を取得する手段を模索していた。そこで最近頼りにしているChat-GPTに問い合わせたらlocalhost webサーバを使うという手法を提案された。そのwebサーバにExcel VBAからアクセスされたらその時点の最新情報を取得することができる。ということでさっそく組み込んでみたらストレスなく動作してくれている。IC7851RC2の全ソースコードは長いので今回取り組んだ部分のソースを何かの参考になればよいと思い公開する。

C# IC7851RC.exe IC-7851制御情報交換部
// Rig Info Data Class
public static class RigInfoData
{
    public static int RunNr { get; set; }
    public static bool Run { get; set; }
    public static decimal VFO_Freq { get; set; }
    public static string LogMode { get; set; }
    public static int RFPower { get; set; }
    public static string Callsign { get; set; }
    public static string GetString()
    {
        return $"{RunNr};{Run};{VFO_Freq};{LogMode};{RFPower};{Callsign}";
    }
}
//
// Start_RigServer のRigInfoを更新する。
private void Send_RigInfo(int nr, bool run, string callsign = "")
{
    RigInfoData.RunNr = nr;
    RigInfoData.Run = run;
    RigInfoData.Callsign = callsign;
}
//
// 変更後: 戻り値を Task にし、async void は避ける
bool _isRigServerRunning = false;
CancellationTokenSource _cts = new ();
// Excel用 無線機情報サーバー アプリケーション起動時に起動
private async Task Start_RigServer()
{
    // サーバーが既に実行中の場合は起動しない
    if (_isRigServerRunning) return;
    _cts = new CancellationTokenSource();
    var token = _cts.Token;
    _ = Task.Run(() => RunServerAsync(token));
}
//
private async Task RunServerAsync(CancellationToken token)
{
    var listener = new HttpListener();
    listener.Prefixes.Add("http://localhost:8080/");
    listener.Start();
    try
    {
        while (!token.IsCancellationRequested) // 停止指示がない間ループを継続
        {
            var contextTask = listener.GetContextAsync();
            await Task.WhenAny(contextTask, Task.Delay(-1, token));
            if (token.IsCancellationRequested)
            {
                break;
            }
            var ctx = contextTask.Result;
            if (ctx.Request.RawUrl == "/riginfo") // Excel へ Rig Info 送信
            {
                RigInfoData.VFO_Freq = VFO_Freq.Value; // 周波数
                RigInfoData.LogMode = LogMode; // ログ用のモード名
                RigInfoData.RFPower = RI.RFP; // 出力
                var RigInfo = RigInfoData.GetString();
                byte[] buf = Encoding.UTF8.GetBytes(RigInfo);
                ctx.Response.StatusCode = 200;
                ctx.Response.ContentType = "text/plain; charset=utf-8";
                ctx.Response.ContentEncoding = Encoding.UTF8;
                // キャッシュを無効化
                ctx.Response.Headers["Cache-Control"] =
                    "no-store, no-cache, must-revalidate, max-age=0";
                ctx.Response.Headers["Pragma"] = "no-cache";
                ctx.Response.Headers["Expires"] = "-1";
                // 本文書き込み
                ctx.Response.OutputStream.Write(buf, 0, buf.Length);
                ctx.Response.OutputStream.Flush();
                ctx.Response.OutputStream.Close();
                ctx.Response.Close();
                this.BeginInvoke(() => StatusLabel.Text =
                    $"◎Rig Server Sent {RigInfo}");
            }
            if (ctx.Request.RawUrl == "/cw") // Excel からメッセージ受信
            {
                await HandleCwFromExcelAsync(ctx);
            }
        }
    }
    catch (HttpListenerException)
    {
        StatusLabel.Text = "Start_RigServer Http Listener Exception error";
    }
    catch (Exception ex)
    {
        StatusLabel.Text = $"Start_RigServer : Error {ex.Message}";
    }
    finally
    {
        if (listener.IsListening)
        {
            listener.Stop();
        }
        listener.Close();
        _isRigServerRunning = false;
        _cts.Dispose();
    }
}
//
// Excel から送られたCWメッセージ送信と無線機の設定変更、内部変数変更。
private async Task HandleCwFromExcelAsync(HttpListenerContext ctx)
{
    string msg;
    using (var sr = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
    {
        msg = await sr.ReadToEndAsync();
    }
    // UIスレッドで SendCW 実行
    BeginInvoke(new Action(() =>
    {
        var data = msg.Split(';');
        switch (data[0])
        {
            case "#Set":
                Change_RigSettings(data);
                break;
            case "#OnKey":
                OnKeyControl(data[1]);
                break;
            case "#RunStatus":
                Set_RunStatus(data[1] == "%Y", false);
                break;
            default:
                SendCW(msg); // CW電文送出
                break;
        }
    }));
    byte[] res = Encoding.UTF8.GetBytes("OK");
    ctx.Response.ContentType = "text/plain";
    ctx.Response.ContentLength64 = res.Length;
    await ctx.Response.OutputStream.WriteAsync(res, 0, res.Length);
    ctx.Response.Close();
    //
    void Change_RigSettings(string[] data) 
    {
        for (int i = 1; i < 4; i++)
        {
            switch (i)
            {
                case 1: // RF Power
                    Set_RigPower(int.Parse(data[i]));
                    break;
                case 2: // Scope Scroll BSN
                    switch (data[2])
                    {
                        case "A":
                            Set_BSN(1, (3, 2), 2, 4);
                            break;
                        case "B":
                            Set_BSN(1, (3, 2), 2, 2);
                            break;
                        case "C":
                            Set_BSN(2, (4, 3), 3, 3);
                            break;
                        case "D":
                            Set_BSN(0, (3, 1), 1, 1);
                            break;
                    }
                    break;
                case 3: // Contest CQ Message => TextBox F5msgに転記  
                    if (data[3].Length > 0) { F5msg.Text = data[3]; }
                    break;
            }
        }
    }
    //
    void OnKeyControl(string key)
    {
        modifierKeys = key.Substring(0, 1) switch
        {
            "%" => "alt",
            "+" => "shit",
            "^" => "ctrl",
            _ => ""
        };
        if (modifierKeys.Length > 0)
        {
            key = key.Substring(1);
        }
        (var btn, var tb) = key switch
        {
            "{F1}" => (F1, F1msg),
            "{F2}" => (F2, F2msg),
            "{F3}" => (F3, F3msg),
            "{F4}" => (F4, F4msg),
            "{F5}" => (F5, F5msg),
            "{F6}" => (F6, F6msg),
            "{F7}" => (F7, F7msg),
            "{F8}" => (F8, F8msg),
            _ => (null, null)
        };
        if (btn != null)
        {
            TabControl1.SelectedIndex = 0;
            F_button_click(btn, MouseEventArgs.Empty);
            return;
        }
        if (key == "{TAB}" && modifierKeys == "^") { key = "{F10}"; } // Ctrl + Tab 
        switch (key)
        {
            case "{ESC}":
            case "{F9}":
                InterruptStop(); // CW Transmission interrupt stop
                return;
            case "{F10}":
                SendCW(SendMsg.Text);
                return;
        }
    }
}
//
// CancellationTokenSource をキャンセルし、RunServerAsync を中断させる
public void Stop_RigServer()
{
    if (_isRigServerRunning && _cts != null)
    {
        _cts.Cancel();
    }
}

この方法だとExcel VBAからアクセスするだけで無線機の周波数とモードの情報を取得できる。Excelからlocalhostへデータが書き込まれるとその内容により即応してCW送信、無線機の設定変更などが行われる。

Excel VBA jg6jav_log.xlsm 情報取得部分
' C# 起動中の RigServerにアクセス
Function HttpGetUTF8(url As String) As String
    Dim xml As Object
    Set xml = CreateObject("MSXML2.XMLHTTP")
    xml.Open "GET", url, False
    xml.send

    Dim stream As Object
    Set stream = CreateObject("ADODB.Stream")
    stream.Type = 1 'binary
    stream.Open
    stream.Write xml.responseBody
    stream.Position = 0

    stream.Type = 2 'text
    stream.Charset = "UTF-8"

    HttpGetUTF8 = stream.ReadText
    stream.Close
End Function
'
' RigServer で取得したデータを必要データに転記
Sub GetRigInfo(Optional Ret As Boolean = False)
    Dim txt As String
    txt = HttpGetUTF8("http://localhost:8080/riginfo")
    Dim Rcv As Variant
    Rcv = Split(txt, ";")
    Dim i As Integer
    Dim RigInfo As Range
    Set RigInfo = Sheets("Settings").Range("RigInfo")
    With RigInfo.Cells(1, 1)
        For i = 0 To 5
            .Offset(i, 0).Value = Rcv(i)
        Next
    End With
    Dim Nr As Integer
    Dim sta As Boolean
    Dim Callsign As String
    With RigInfo
        Nr = .Cells(1, 1).Value '// マクロ選択番号
        sta = .Cells(2, 1).Value '// Bloolean
        rigFreq = .Cells(3, 1).Value '// Public 周波数情報
        rigMode = .Cells(4, 1).Value '// Public モード
        rigPower = .Cells(5, 1).Value '// Public 出力
        Callsign = .Cells(6, 1).Value '// メモリーのコールサイン
    End With
    rigBand = Band(rigFreq)
    If Ret Then Exit Sub
    Select Case Nr
    Case 1
        Call RcvRigInfo(Callsign)
    Case 2
        Call StatusCheck(sta)
    End Select
End Sub
IC7851RC2.exeからExcel VBA マクロ起動
// Run status 設定
private void Set_RunStatus(bool sw, bool post = true)
{
    Running = sw;
    RunFreq = Running ? VFO_Freq.Value : 0M;
    if (post) { RunExcelMacro(1, Running); }
}
//
// Excel マクロ実行
void RunExcelMacro(int RunNr, bool args1 = false, string args2 = "")
{
    Excel.Application xl =
      (Excel.Application)Marshal.GetActiveObject("Excel.Application");
    if (xl == null) return;
    var macroName = RunNr switch 
    {
        1 => "StatusCheck",
        2 => "RcvRigInfo",
        _ => ""
    };
    if (macroName.Length < 1) { return; }
    try
    {
        xl.Run(macroName, RunNr == 1 ? args1 : args2);
    }
    catch (Exception ex)
    {
        StatusLabel.Text =
            $"[RunExcelMacro] {macroName}({args1},{args2}) Error: {ex.Message}";
    }
}

Excel VBA CW 送出用文字列渡し部分
Sub SendMsg(ByVal msg As String)
    Dim http As Object
    Set http = CreateObject("MSXML2.XMLHTTP")
    http.Open "POST", "http://localhost:8080/cw", False
    http.setRequestHeader "Content-Type", "text/plain; charset=utf-8"
    http.send msg
End Sub
'
'---// ID 送信 //---
Sub SendMyID()
    SendMsg("JG6JAV")
End Sub
'
'---// IC7851RCへSendkey //---
Sub IC7851(SKey As String)
    SendMsg ("#OnKey;" & SKey)
End Sub
'
'---// IC7851RC 出力100Wに設定 //---
Private Sub RF100W()
    Call SendMsg("#Set;100;D;")
End Sub

 ログもMySQLなどに移行してすべてC#で構成するとよいのだろうけれどもExcelは何かと使い勝手が良いのでなかなか思い切れないでいる。だからといってアマチュア無線局の皆さんの間で多く使われているアプリケーションを使う気にはなれない。

コメント

  1. フリーソフトってどうしてもサポートというか、いつまで使えるか、常に不安を感じてて。
    自分で組んでみるかと思って、MS-DOS時代に使い倒した桐というDBソフトを購入。
    QSL印刷用もコンテスト用もやっぱり国内産有名フリーソフトに全く敵う気がせず挫折。
    フリーソフトを使わせていただいています。

    ホントは、有料でいいのでどっかの会社組織がやってくれればいいなぁと思ってるんですが。
    どこもやらないでしょうねぇ…。

    • フリーソフトは悪くはないのですが、自分で設計してないので操作を覚えるが面倒なのでログ関係は自前構築しています。なのでゴリゴリの自分用仕様です。