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スキルを習得する(PR)

ここまで読んでいただきありがとうございます。 最後に宣伝をさせてください。

「副業を始めたいが、何から手をつければいいかわからない」「独学でスキルはついたが、収益化できていない」という悩みを持つ方には、マンツーマン指導のWebスクール**「メイカラ」**が適しています。

このスクールは、単に技術を教えるだけでなく、**「副業として具体的にどう稼ぐか」**という実務直結のノウハウ提供に特化している点が特徴です。

講師陣は、実際に「副業Webライターから1年で独立して月収100万円」を達成したプロや、現役で利益を出し続けているブロガーなど、確かな実績を持つプレイヤーのみで構成されています。そのため、机上の空論ではない、現場で通用する戦術を学ぶことができます。

副業に特化した強み

  • 最短ルートの提示: 未経験からでも実績を出せるよう、マンツーマンで指導。
  • AI活用の習得: 副業の時間対効果を最大化するための、正しいAI活用スキルも網羅。
  • 案件獲得のチャンス: 運営がWebマーケティング会社であるため、実力次第で社内案件の紹介など、仕事に直結する可能性があります。

受講者の多くは、「在宅でできる仕事を探している」「副業を頑張りたい」という20代・30代・40代が中心です。

受講前には、講師による無料説明が行われます。無理な勧誘はなく、自分に合った副業スタイルやプランを相談できるため、まずは話を聞いてみることから始めてみてはいかがでしょうか。

ブログで稼ぎたいなら「メイカラ」

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

この記事を書いた人

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

目次