Newer
Older
TIASShot / TIASshot / UI / Form1.HwTest.cs
#if HWTEST
#if !SELFTEST
#error HWTEST は SELFTEST の部品(SelfTestConfig・SelfTestReport・SelfTestPump・閉じる処理の段階の記録)を使うため,SELFTEST と同時に定義すること
#endif
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

namespace TIASshot {
    public partial class Form1 {

        /// <summary>
        /// 実機テスト用コンストラクタの目印型(HWTEST 限定).
        /// </summary>
        internal sealed class HwTestMarker {
            internal static readonly HwTestMarker Instance = new HwTestMarker();
            private HwTestMarker() { }
        }

        // 実機テストのモードか(Form1_Load の自動接続をしない・ShowError をモーダル表示しない・メモリ観測窓を開かない)
        private bool _hwTestMode = false;

        // ShowImage に届いたフレームの数(コールバックスレッドが書き,UI スレッドが読む)
        private long _hwTestFrames = 0;
        private int _hwTestFrameWidth = 0;
        private int _hwTestFrameHeight = 0;
        // フレームの記録中の例外(最初の 1 件)
        private string _hwTestFrameError = null;

        // 平均輝度・飽和画素の割合の測定の要求と結果(_hwTestSampleLock で保護)
        private readonly object _hwTestSampleLock = new object();
        private int _hwTestSamplesWanted = 0;
        private readonly List<HwTestFrameStats> _hwTestSamples = new List<HwTestFrameStats>();

        // ShowError に渡されたメッセージ(_hwTestSampleLock で保護)
        private readonly List<string> _hwTestErrors = new List<string>();

        // 直近の接続失敗の理由(_hwTestSampleLock で保護)
        private readonly List<string> _hwTestConnectErrors = new List<string>();

        /// <summary>
        /// 実機テスト用コンストラクタ(HWTEST 限定).通常のコンストラクタ(InitializeComponent を含む)を通したうえで,
        /// 実機テストのモードにする.表示(Show)すると Form1_Load が走るが,自動接続はしない(HwTestConnect で機種を指定して接続する).
        /// </summary>
        internal Form1(HwTestMarker marker) : this() {
            _hwTestMode = true;
            Text += " [HWTEST]";
        }

        // ================================================================================
        // カメラ・光源の接続
        // ================================================================================

        /// <summary>接続されている Lumenera カメラの台数(SDK が無い・列挙に失敗した場合は例外)</summary>
        internal static int HwTestCountLucam() {
            return Lucam.HwTestCountCameras();
        }

        /// <summary>見つかった ImagingSource カメラの名前(IC Imaging Control の列挙)</summary>
        internal string[] HwTestListIScam() {
            return icImagingControl1.Devices.Select(d => d.Name).ToArray();
        }

        /// <summary>
        /// 指定した機種だけを接続する(通常起動の Form1_Load と同じ接続処理を使う.ただし Lucam 優先の自動選択はしない).
        /// 接続できたら現在のカメラ(_camera)にする.
        /// </summary>
        /// <returns>接続できたカメラ.できなければ null</returns>
        internal CameraBase HwTestConnect(HwTestCameraKind kind) {
            var camera = kind == HwTestCameraKind.Lucam ? ConnectLucam() : ConnectIScam();
            _camera = camera;
            return camera;
        }

        /// <summary>
        /// 接続後の画面・光源・副モニタ・本モニタの再描画タイマーの開始(通常起動の Form1_Load と同じ処理).
        /// </summary>
        internal void HwTestStartSession() {
            StartSession();
        }

        partial void OnConnectFailed(string device, string error) {
            lock (_hwTestSampleLock) {
                _hwTestConnectErrors.Add($"{device}: {error}");
            }
        }

        /// <summary>接続失敗の理由を取り出す(古い順.取り出した分は消す)</summary>
        internal List<string> HwTestTakeConnectErrors() {
            lock (_hwTestSampleLock) {
                var result = _hwTestConnectErrors.ToList();
                _hwTestConnectErrors.Clear();
                return result;
            }
        }

        /// <summary>現在のカメラ(未接続なら null)</summary>
        internal CameraBase HwTestCamera => _camera;

        /// <summary>現在のカメラを切断する(閉じる処理を通さない.再接続の検査・後始末用)</summary>
        internal void HwTestDisconnectCamera() {
            _camera?.Disconnect();
        }

        /// <summary>光源のポートが開いているか</summary>
        internal bool HwTestLightIsOpen => _light.IsOpen;

        /// <summary>光源の点灯状態(画面の照明ボタンが管理する状態)</summary>
        internal bool HwTestLightOn => _isLightOn;

        /// <summary>照明ボタンの表示文</summary>
        internal string HwTestLightButtonText => btnLightSW.Text;

        /// <summary>照明ボタンを押す(通常の操作と同じ処理.校正中は何もしない)</summary>
        internal void HwTestClickLightButton() {
            btnLightSW_Click(btnLightSW, EventArgs.Empty);
        }

        /// <summary>光源のポートを閉じる(閉じる処理と同じ LightSource.Close.消灯はしない)</summary>
        internal void HwTestCloseLight() {
            _light.Close();
        }

        /// <summary>
        /// 光源のポートを開き直して点灯する(Form1_Load と同じ手順).画面の照明ボタンの状態も点灯に合わせる.
        /// </summary>
        /// <returns>開けたら true</returns>
        internal bool HwTestReopenLight() {
            if (!_light.Open()) return false;
            _light.SetLight();
            _isLightOn = true;
            btnLightSW.Text = "照明 OFF";
            btnLightSW.Enabled = true;
            return true;
        }

        /// <summary>
        /// ウォッチドッグ(別スレッド)からの光源のポートの緊急クローズ(タイムアウト時の後始末).
        /// 消灯はしない(終了時と同じく光源は点いたまま残る).LightSource.Close はクローズの失敗をログに残して戻る.
        /// </summary>
        internal void HwTestEmergencyCloseLight() {
            _light.Close();
        }

        // ================================================================================
        // 閉じる処理
        // ================================================================================

        /// <summary>
        /// ユーザーが閉じようとしたときの処理(Form1_FormClosing)を呼ぶ(UI スレッドで呼ぶこと).
        /// 閉じると決まった場合は後始末(閉じ始めのフラグ → タイマー停止 → カメラ切断 → 光源)まで行う.
        /// フォームは破棄しない(後続の再接続の検査で使う).
        /// </summary>
        /// <returns>閉じると決まったら true,取り消されたら(保存中)false</returns>
        internal bool HwTestRequestClose() {
            var e = new FormClosingEventArgs(CloseReason.UserClosing, false);
            Form1_FormClosing(this, e);
            return !e.Cancel;
        }

        /// <summary>閉じるときの後始末が通過した段階(SELFTEST の記録を使う)</summary>
        internal IReadOnlyList<string> HwTestCloseSteps => SelfTestCloseSteps;

        /// <summary>閉じ始めたか</summary>
        internal bool HwTestIsClosing => _isClosing;

        /// <summary>本モニタの再描画タイマーが動作中か</summary>
        internal bool HwTestRefreshTimerRunning => _mainRefreshTimer.Enabled;

        /// <summary>本モニタ(picDisplay)に表示中の画像</summary>
        internal Image HwTestDisplayedImage => picDisplay.Image;

        /// <summary>
        /// 閉じる処理で後始末まで済んだ後に,次の検査のために画面の状態を接続直後に戻す(閉じる処理を 2 回以上検査するため).
        /// 閉じ始めのフラグを下ろし(以降の ShowMessage・EnableShots を通常どおり効かせる),撮影ボタンを無効にし,
        /// 本モニタの再描画タイマーを動かし直し,光源のポートを開き直して点灯する(StartSession と同じ状態.
        /// StartSession を 2 回呼ぶとタイマーの Tick の登録と副モニタが重複するため,ここで必要な分だけ戻す).
        /// カメラは接続しない(呼び出し側が HwTestConnect で接続する).
        /// </summary>
        /// <returns>光源のポートを開き直せたら true</returns>
        internal bool HwTestResumeAfterClose() {
            _isClosing = false;
            EnableShots(false);
            _mainRefreshTimer.Start();
            // 開いたままのポートを開き直すと SerialPort が例外を投げるため,閉じているときだけ開く
            if (_light.IsOpen) return true;
            return HwTestReopenLight();
        }

        // ================================================================================
        // 撮影
        // ================================================================================

        /// <summary>データ名(txtDataName.保存フォルダ名の一部)を設定する</summary>
        internal void HwTestSetDataName(string dataName) {
            txtDataName.Text = dataName;
        }

        /// <summary>連続撮影の枚数・間隔(ms)の入力欄を設定する</summary>
        internal void HwTestSetMultiShots(int numShots, int intervalMs) {
            txtNumMultiShots.Text = numShots.ToString(System.Globalization.CultureInfo.InvariantCulture);
            txtMultiShotsInterval.Text = intervalMs.ToString(System.Globalization.CultureInfo.InvariantCulture);
        }

        /// <summary>撮影ボタン(単発・連続)が両方とも有効か</summary>
        internal bool HwTestShotsEnabled => btnShotOne.Enabled && btnShotMulti.Enabled;

        /// <summary>ステータス表示(txtMessage)の現在値</summary>
        internal string HwTestMessage => txtMessage.Text;

        /// <summary>単発撮影のボタンを押す(通常の操作と同じ処理.撮影は UI スレッドで同期に行われ,保存は保存スレッドで続く)</summary>
        internal void HwTestClickShotOne() {
            btnShot1_Click(btnShotOne, EventArgs.Empty);
        }

        /// <summary>連続撮影のボタンを押す(通常の操作と同じ処理)</summary>
        internal void HwTestClickShotMulti() {
            btnShotMulti_Click(btnShotMulti, EventArgs.Empty);
        }

        // ================================================================================
        // フレームの観測
        // ================================================================================

        /// <summary>ShowImage に届いたフレームの数</summary>
        internal long HwTestFrameCount => Interlocked.Read(ref _hwTestFrames);

        /// <summary>最後に届いたフレームの大きさ</summary>
        internal Size HwTestLastFrameSize => new Size(Volatile.Read(ref _hwTestFrameWidth), Volatile.Read(ref _hwTestFrameHeight));

        /// <summary>フレームの記録中に起きた例外(無ければ null)</summary>
        internal string HwTestFrameError => Volatile.Read(ref _hwTestFrameError);

        /// <summary>ShowError に渡されたメッセージ(古い順のコピー)</summary>
        internal IReadOnlyList<string> HwTestErrors {
            get { lock (_hwTestSampleLock) return _hwTestErrors.ToArray(); }
        }

        /// <summary>
        /// 次に届く count 枚のフレームの平均輝度・飽和画素の割合の測定を要求する(それまでの測定結果は捨てる).
        /// </summary>
        internal void HwTestRequestSamples(int count) {
            lock (_hwTestSampleLock) {
                _hwTestSamples.Clear();
                _hwTestSamplesWanted = count;
            }
        }

        /// <summary>測定済みのフレーム数</summary>
        internal int HwTestSampleCount {
            get { lock (_hwTestSampleLock) return _hwTestSamples.Count; }
        }

        /// <summary>測定結果を取り出す(未達の要求は取り消す)</summary>
        internal List<HwTestFrameStats> HwTestTakeSamples() {
            lock (_hwTestSampleLock) {
                _hwTestSamplesWanted = 0;
                var result = _hwTestSamples.ToList();
                _hwTestSamples.Clear();
                return result;
            }
        }

        /// <summary>
        /// ShowImage(カメラのコールバックスレッド)から呼ばれるフレームの記録.入力 bmp は読むだけ(所有権は呼び出し側).
        /// 例外をコールバックへ伝えるとプレビューが止まるため,記録して戻る(実機テストの結果で FAIL として報告する).
        /// </summary>
        private void HwTestOnFrame(Bitmap bmp) {
            if (!_hwTestMode) return;
            try {
                Interlocked.Increment(ref _hwTestFrames);
                Volatile.Write(ref _hwTestFrameWidth, bmp.Width);
                Volatile.Write(ref _hwTestFrameHeight, bmp.Height);

                bool wanted;
                lock (_hwTestSampleLock) wanted = _hwTestSamplesWanted > 0;
                if (!wanted) return;

                var stats = MeasureFrameStats(bmp);
                lock (_hwTestSampleLock) {
                    if (_hwTestSamplesWanted > 0) {
                        _hwTestSamples.Add(stats);
                        _hwTestSamplesWanted--;
                    }
                }
            } catch (Exception ex) {
                Debug.WriteLine($"[HWTEST] フレームの記録に失敗しました: {ex}");
                Interlocked.CompareExchange(ref _hwTestFrameError, $"{ex.GetType().FullName}: {ex.Message}", null);
            }
        }

        /// <summary>
        /// フレームの平均輝度と飽和画素の割合を求める.間引いた行だけを複製して純粋関数(HwTestJudge.FrameStats)に渡す.
        /// </summary>
        private static HwTestFrameStats MeasureFrameStats(Bitmap bmp) {
            var bytesPerPixel = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;
            if (bytesPerPixel < 1 || bytesPerPixel > 4) {
                throw new NotSupportedException($"平均輝度を求められない画素形式です: {bmp.PixelFormat}");
            }
            var step = HwTestThresholds.PixelStep;
            var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
            try {
                var rowCount = (bmp.Height + step - 1) / step;
                var rowBytes = bmp.Width * bytesPerPixel;
                var rows = new byte[(long)rowCount * rowBytes];
                for (var r = 0; r < rowCount; r++) {
                    var src = IntPtr.Add(data.Scan0, r * step * data.Stride);
                    Marshal.Copy(src, rows, r * rowBytes, rowBytes);
                }
                return HwTestJudge.FrameStats(rows, rowCount, bmp.Width, rowBytes, bytesPerPixel, step, HwTestThresholds.SaturatedPixelLevel);
            } finally {
                bmp.UnlockBits(data);
            }
        }

        /// <summary>ShowError の実機テスト版(モーダル表示すると無人実行が止まるため記録のみ行う)</summary>
        private void HwTestRecordError(string msg) {
            Debug.WriteLine($"[HWTEST] ShowError: {msg}");
            lock (_hwTestSampleLock) {
                _hwTestErrors.Add(msg);
            }
        }
    }
}
#endif