PLR-7のA/F値をUSB経由で取得し、CSVファイルに保存するC#アプリを作った話

前回の記事では、Sokken PLR-7 AFR ANALYZERをPCとUSBで接続し、C#アプリでリアルタイムにA/F値を取得するところまでを紹介しました。
今回はその続きとして、取得したA/F値をCSV形式で保存するロガー機能を追加したので、その内容をまとめておきます。


■ 改めて:PLR-7とのUSB接続と通信仕様

PLR-7はUSB経由でPCに接続すると、「USB Serial Port(COMポート)」として認識されます。
デバイスマネージャーで確認したCOMポート番号(例:COM9)を、アプリ内で使用します。

通信仕様は以下のとおりです:

設定項目
ボーレート115200 bps
データビット8 ビット
パリティなし
ストップビット1 ビット

■ 通信の基本的な流れ

  1. アプリ起動時にPLR-7をリモートモード(SRM,01\r\n)へ
  2. 測定モード(AOP,01\r\n)に切り替え
  3. 0.1秒ごとに RMD\r\n を送信して測定値を取得
  4. 応答(例:RMD,2,3,0,2, 120.41)の最後の値がA/F値
  5. これをTextBoxに表示し、必要に応じてログ記録

■ ロガー機能の仕様

  • 「ロガースタート」ボタンを押すと記録を開始
  • A/F値をタイムスタンプ付きで記録
  • 「ロガーストップ&保存」ボタンで記録を終了し、CSVファイルとして保存
  • 保存先はファイルダイアログでユーザーが選択可能

目次

🧾 実際のC#コード(Windowsフォーム)

使用UI要素

コントロール名前用途
TextBoxtextBox_AF_ValueA/F値のリアルタイム表示
Buttonbutton_logger_startロガー開始
Buttonbutton_logger_stop_and_saveロガー終了&CSV保存

■ Form1.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace AF計の値出力アプリ
{
    public partial class Form1 : Form
    {
        private SerialPort serialPort;
        private Timer requestTimer;
        private StringBuilder receiveBuffer = new StringBuilder();

        private bool isLogging = false;
        private List<string> logData = new List<string>();

        public Form1()
        {
            InitializeComponent();

            serialPort = new SerialPort("COM9", 115200, Parity.None, 8, StopBits.One);
            serialPort.DataReceived += SerialPort_DataReceived;

            try
            {
                serialPort.Open();
                InitializeRemoteAndMeasurementModeAsync();
            }
            catch (Exception ex)
            {
                MessageBox.Show("シリアルポート接続エラー: " + ex.Message);
            }
        }

        private async void InitializeRemoteAndMeasurementModeAsync()
        {
            try
            {
                serialPort.Write("SRM,01\r\n");
                await Task.Delay(500);

                serialPort.Write("AOP,01\r\n");
                await Task.Delay(5000);

                requestTimer = new Timer();
                requestTimer.Interval = 100;
                requestTimer.Tick += RequestTimer_Tick;
                requestTimer.Start();
            }
            catch (Exception ex)
            {
                Debug.WriteLine("初期化エラー: " + ex.Message);
            }
        }

        private void RequestTimer_Tick(object sender, EventArgs e)
        {
            if (serialPort.IsOpen)
            {
                try
                {
                    serialPort.Write("RMD\r\n");
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("送信エラー: " + ex.Message);
                }
            }
        }

        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                string incoming = serialPort.ReadExisting();
                receiveBuffer.Append(incoming);

                while (receiveBuffer.ToString().Contains("\r\n"))
                {
                    string buffer = receiveBuffer.ToString();
                    int endIndex = buffer.IndexOf("\r\n");
                    string oneLine = buffer.Substring(0, endIndex);
                    receiveBuffer.Remove(0, endIndex + 2);

                    Match match = Regex.Match(oneLine, @"RMD,\d+,\d+,\d+,\d+,\s*(-?\d+\.\d+)");

                    if (match.Success)
                    {
                        string afValue = match.Groups[1].Value;

                        Invoke(new Action(() =>
                        {
                            textBox_AF_Value.Text = afValue;

                            if (isLogging)
                            {
                                string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
                                logData.Add($"{timestamp},{afValue}");
                            }
                        }));
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("受信エラー: " + ex.Message);
            }
        }

        private void button_logger_start_Click(object sender, EventArgs e)
        {
            isLogging = true;
            logData.Clear();
        }

        private void button_logger_stop_and_save_Click(object sender, EventArgs e)
        {
            isLogging = false;

            if (logData.Count == 0)
            {
                MessageBox.Show("ログデータがありません。", "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            using (SaveFileDialog dialog = new SaveFileDialog())
            {
                dialog.Filter = "CSVファイル (*.csv)|*.csv";
                dialog.FileName = $"AF_log_{DateTime.Now:yyyyMMdd_HHmmss}.csv";

                if (dialog.ShowDialog() == DialogResult.OK)
                {
                    try
                    {
                        File.WriteAllLines(dialog.FileName, logData);
                        MessageBox.Show("ログを保存しました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show("保存に失敗しました: " + ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                }
            }
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (serialPort != null && serialPort.IsOpen)
            {
                serialPort.Close();
            }
        }

        private void textBox_AF_Value_TextChanged(object sender, EventArgs e)
        {
        }
    }
}

■ コードのポイントと補足

receiveBuffer\r\n 処理

  • PLR-7の応答が分割されて届くことを考慮して、バッファに溜めてから改行区切りで1行ずつ処理しています。

Regex によるA/F抽出

Regex.Match(oneLine, @"RMD,\d+,\d+,\d+,\d+,\s*(-?\d+\.\d+)");
  • 応答の最後の数値(A/F値)だけを抜き出しています。

CSVログ

logData.Add($"{timestamp},{afValue}");

タイムスタンプとA/F値を1行ずつ記録して、保存時に .csv 形式で出力しています。

■ 出力されるCSV形式(例)

2025-03-21 15:45:32.101,120.41
2025-03-21 15:45:32.201,120.43
...

■ まとめ

  • C#とPLR-7の組み合わせで、リアルタイムなA/Fモニタリングデータ記録が簡単に実現できる
  • USB接続、リモート・測定モード切替、測定値取得まで自動化
  • ロガー機能を使えば、CSV保存による記録や分析も容易

今後は、このCSVデータをリアルタイムグラフに表示したり、しきい値でアラートを出すなど、さらに機能を拡張していけたらと考えています。
PLR-7とPCの連携に挑戦している方の参考になれば幸いです。

ここまで読んでいただきありがとうございました。

スクールの紹介

最後に宣伝をさせてください。

技術で未来を切り拓く―あなたの夢を現実にするプログラミングスクール

Webデザインやプログラミングで成功を目指している方々にとって、このオンラインスクールは夢を叶えるための最適な場所です。皆さんのキャリアを次の段階へと引き上げるためにデザインされたこのスクールは、一人ひとりの成功を心から願い、それを実現するための全てを提供しています。ここでは、このスクールの魅力について詳しくご紹介します。

◆圧倒的な費用対効果
このオンラインプログラミングスクールは、Web系教育において最高の費用対効果を提供しています。多くの高額スクールが存在する中で、ここではリーズナブルな価格で、質の高い教材、無限のサポート、そして実際に市場で求められるスキルの習得機会を提供しています。

◆現役フリーランスの講師陣
講師たちは全員、現役のフリーランスプロフェッショナルです。市場で活躍している講師から直接、最新のトレンドや実践的なスキルを学べるのは、このスクールの大きな特徴です。

◆柔軟な学習コース
固定のコースがなく、学習者の興味やニーズに応じて自由に学習できます。進路変更も自由で、最低契約期間は1ヶ月という柔軟性を持っています。自分のペースで、自分に合った学習が可能です。

◆無制限の添削とサポート
理解できるまで、そして満足するまで、無制限に添削と質問への回答を提供しています。進路相談や技術面以外の相談にも対応しており、全面的にサポートします。

◆社長から学べる貴重な機会
デザイナー、プログラマー、ディレクター、マーケターとして豊富な経験を持つ社長から直接学べるのも、このスクールの特別な点です。他のスクールでは得られない、貴重な機会です。

◆実績作りへの徹底的なサポート
就職、転職、フリーランスとして成功するためには、高品質な実績が必要です。生徒の作品レベルを最大限に高め、市場で求められる実績を作り上げることに力を入れています。案件を取得できない生徒には、直接案件を提供することもあります。

◆メッセージからの約束
高額な授業料を支払わせて結果を出せないスクールとは違い、物理的なサポートは提供できないかもしれませんが、継続的な努力を通じて最高の結果を出せるようにサポートします。一緒に不正なスクールを撲滅し、あなたの夢を実現しましょう。

このプログラミングスクールは、Webデザインやプログラミングでの成功を目指す方々に必要な全てを備えています。今こそ、このコミュニティに参加し、あなたのキャリアを加速させる時です。

詳しくはこちら↓

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次