• 車種別
  • パーツ
  • 整備手帳
  • ブログ
  • みんカラ+

とんこつラパンのブログ一覧

2026年01月13日 イイね!

はんだごて切り忘れ防止用デッドマンタイマ「切忘 無く四郎(死郎)」DMT-KW7946 Ver.6.8

はんだごて切り忘れ防止用デッドマンタイマ「切忘 無く四郎(死郎)」DMT-KW7946 Ver.6.8https://youtu.be/ZrwKE_UywCQ?si=sGgqciGMRIiM6ifa
↑上記リンクからYouTubeで動画をご覧ください

【大幅アプデ】 はんだごて切り忘れ防止用デッドマンタイマ「切忘 無く四郎(死郎)」DMT-KW7946 Ver.6.7 【電子工作】


約5年前に自作したはんだごて切り忘れ事故防止用のデッドマン装置ですが、ここ最近ハードウェア的にもソフトウェア的にもアップデートを繰り返し大幅に進化しました。

上記はVer.6.7の機能を説明した動画です。


https://youtu.be/CLXVZr_iGvk?si=MORYFoan8NNiMT8s
【自作】 はんだごて切り忘れ防止用デッドマンタイマ 「切忘 無く四郎(死郎)」DMT-KW7946 【電子工作】

製作当初のVer.1.0の頃のものを動画にしたのが上記です。

だいぶ機能が増えてますね。

製作してからこれまで約5年間、屋内でのはんだ付け作業に活躍してくれています。

当みんカラにも何度か登場していますね。



以下Ver.6.8のソースコード全文です。

/* はんだごて切り忘れ防止用デッドマンタイマ
Ver.6.8
製作者 とんこつラパン
作成日 令和3年3月11日(2021年3月11日)

変更履歴 
R3/3/12 Ver.1.0 ヘッダ#defineに誤りがあったのを修正。タイムアウト後のブザー鳴動時間を数値で入れていたのを修正。(TIMEOUT_BZR_TIME)
R7/11/6 Ver.1.1 残り時間1分でブザーを鳴らす様に変更。(MIN_BZR_TIME) タイムアウト後のブザー鳴動時間を延長。(TIMEOUT_BZR_TIME)            
R7/11/15 Ver.1.2 タイムアウト時のブザーをビーッ・・・ビーッ・・・ビーーーーーッのの3回に変更。約1秒に1回点灯させて1分を数えるLEDの消灯時間から微調整の時間を引いて、誤差を微調整。(SEC_COUNT_ADJUST)
R7/11/16 VER.1.3 割り込み安全化
R7/11/16 Ver.1.4 TIME OUT中のブザー鳴動中のみ割り込み復帰可能(Ver.1.4)
R7/11/18 Ver.1.5 チャタリング対策・LCDちらつき低減・TIMEOUTブザー関数化
R7/11/17 Ver.1.6 TIMEOUTブザー5回に変更しブザー間の余白を#defineに追加(BZR_REST_TIME)
R7/11/19 Ver.2.0 ACS712 30A 電流センサを追加し、実測電流(A)とピーク電流(A)を取得・電流測定は100ms周期でサンプリングし、ピーク値は1秒ごとに更新
R7/11/19 Ver.2.1 過電流(3A)以上検出でAC100Vカットし、処理を中断するようにした。
R7/11/20 Ver.2.2 過電流時の表示メッセージ小変更&過電流シャットダウン時のブザーを10回で停止するように変更。(電源部ヒューズ10Aに変更し過電流判定の閾値は6Aに設定)
R7/11/20 Ver.3.0beta DS18B温度センサ(A2ピン)追加し、SSRの温度監視を追加し電流と交互表示80度以上でシャットダウン(シャットダウン時ブザー5回)
R7/11/21 Ver.3.0beta2 DS18B温度監視の見直し。一回目の温度測定で85℃を拾ってしまうので過熱異常判定の閾値をいったん86度に設定。
R7/11/21 Ver.3.0beta3 DS18B温度監視の見直し。一回目の温度測定でゴミ値(85℃)を拾ってしまう問題の対策(1回目の測定値を無視&2回連続閾値を超えた時のみ過熱異常判定)過熱異常閾値を60℃にした。警告値は50℃に設定(未使用)
R7/11/21 Ver.3.1 DS18B温度監視の見直し。ssr_temp>=TEMP_WARNで警告表示を追加。(LCD画面表示切替部に追加。)
R7/11/21 Ver.4.0 タイマ設定値をEEPROMに保存し、次回使用時、再起動時などでもタイマー設定値を保持しておくように仕様変更
R7/11/21 Ver.4.1 デバッグモード追加(起動時に黒ボタン長押しすることでデバッグモード突入(タイマー設定時間を1分から60分までの間で1分刻みで設定可能)
R7/11/22 Ver.4.2 DS18B温度監視でスタート直後に85℃を拾ってしまうことがある問題で液晶表示に!! SSR HOT !!が表示されてしまう問題を修正。SEC_COUNT_ADJUSTを微調整。
R7/11/22 Ver.4.3 DS18B温度監視でスタート直後に85℃を拾ってしまうことがある問題で液晶表示に!! SSR HOT !!が表示されてしまう問題を修正。(初回のサンプリングで-20℃85℃の範囲外だった場合無視。)
R7/11/26 Ver.4.4 DS18B温度センサーをSSRのヒートシンク取り付けビスM3と共締めできるものに変更。温度を直に拾えるようになったのでTEMP_WARN=60℃、TEMP_STOP=80℃に変更。秒カウントLEDの点滅周期が遅くなったのでSEC_COUNT_ADJUSTで微調整して対策。
R7/11/26 Ver.4.5 DS18B温度センサの処理を非ブロッキング化しSEC_COUNT_LEDの周期ズレを低減。t DS18B温度センサの初回サンプリング条件をt > -20 && t < 85から> -55 && t < 85に変更。
R7/11/26 Ver.4.6 ACS712過電流検出サンプリング周りを整理。
R7/12/4/ Ver.4.7 デバッグモード突入時にLCD2行目にバージョン情報表示。
R7/12/12 Ver.4.71 内部に使用しているArduino Nano(互換品ATMega328P)をUSB-Mini-B端子のものからUSB-Cのものに載せ換え。筐体への固定ケーブル変更、筐体加工など実施した為、記録。スケッチに変更は無し。
R7/12/16 Ver.4.8 デバッグモード突入時にブザーの吹鳴テスト実施するようにした。
R7/12/19 Ver.4.9 起動時にACS712電流センサーのゼロ点キャリブレーションを実施するようにした。
R7/12/19 Ver.5.0 電流検出で0.1Aより小さい場合には0.0Aとして表示するようにした。
R7/12/20 Ver.5.1 デバッグモード突入時に、起動時ACS712電流センサーのゼロ点キャリブレーションのオフセット値をLCDに表示するようにした。
R7/12/20 Ver.5.2 デバッグモード突入時に表示する電流センサーのゼロ点キャリブレーションのオフセット値の後に電圧換算表示もするようにした。
R7/12/21 Ver.5.3 デバッグモード突入時のACS712電流センサーキャリブレーションのオフセット値の電圧換算値が異常かどうかの判定を追加。DS18B温度センサーの値が-127℃だった場合にセンサー断線または通信異常等として異常検出するように変更。
R7/12/27 Ver.5.4 ACS712電流センサーを30A仕様から20A仕様に変更に伴い、感度調整の電流換算係数を66mV/Aから100mV/Aに変更。20A用の部分はコメントアウト。
R7/12/28 Ver.5.5 ACS712電流センサーの0点キャリブレーションのオフセット値の異常判定(デバッグモード時)の閾値を0.010から0.020に変更。デバッグモード突入の為の黒押し釦スイッチ押下時間を3秒から1秒にした。
R7/12/28 Ver.5.51 ACS712電流センサーの0点キャリブレーションのオフセット値の異常判定(デバッグモード時)の閾値を0.020から0.050に変更。
R7/12/28 Ver.5.6 デバッグモード突入する為の条件を、黒押し釦A接点(NO接点)からB接点(NC接点)に変更した。また、緑押し釦B接点(NC接点)も条件に追加した。(起動時黒押し釦か緑押し釦長押しでデバッグモード突入。)
R7/12/31 Ver.5.7 運転中に黒押しボタン押下で、表示モードを変更できるようにした。電流と温度の交互表示モードと、現在電流、現在温度の固定表示モード。表示モードはEEPROMに保存し、次回起動時も引き継ぎ。またデバッグモード突入時にディスプレイモードも表示するようにした。
R8/1/1 Ver.5.8 ディスプレイモードがDUAL(現在電流値と現在温度の固定表示モード)の時に、SSR温度がTEMP_WARNを超えても警告が出なかったのを修正(温度表示の後ろに!を表示するだけ。)
R8/1/2   Ver.5.9 ACS712過電流検出の現在電流値の表示値をRMS化、過電流シャットダウン(6A)とピーク電流の表示は瞬時値のまま。ACS712-30Aに戻したので20A用感度のほうをコメントアウトして30A用を採用。
R8/1/5 Ver.6.0 1分カウントの方式を大幅に変更。電流センサーのサンプリング周期や過電流検出シャットダウン処理などの大幅見直し。デバッグモード時に、黒押しボタンで周波数設定を50Hzから60Hzに設定変更できるようにし設定はEEPROMに保持するようにした。
R8/1/5 Ver.6.1 緑押しボタンの割り込み処理を変更。
R8/1/6 Ver.6.2 1秒カウントのISR化。
R8/1/6 Ver.6.3 SEC_COUNT_LED(緑LED点滅)の点滅ISR化。また、TIMEOUT処理中に5回のブザー鳴動中、特別に復帰を許可しているが、ブザーの鳴り終わりギリギリで緑押しボタンで運転復帰するとブザーがLOWにならずなり続けてしまう問題を修正。
R8/1/6 Ver.6.4 タイマ処理を秒カウントのみでなく分もISR化。loopの前にASCII状態遷移図を置いた。割り込み設計ポリシーコメントなどを追記した。
R8/1/7 Ver.6.41 ISRフラグ群をまとめて、フラグの意味・生成元・消費元を一覧表にしてコメントで捕捉した。
R8/1/8 Ver.6.42 関数群等を並べ替え実施。
R8/1/9 Ver.6.5 ISR内よりmillis()排除。残り時間表示時TM_leftを直接表示せず残り時間安全取得用関数を追加しTM_left競合読み書きを回避。ISRフラグ意味表などコメント類を更新。
R8/1/10 Ver.6.51 コメント類を小見直し
R8/1/10 Ver.6.6 TM_leftをloopから書き換えないように変更。コメントや設計ポリシー通りに動いていなかった部分のバグ修正。
R8/1/11 Ver.6.61 コメント、状態遷移図等を更新。
R8/1/11 Ver.6.7 黒PBでの画面表示切替時の音をブザーからスピーカーに変更し、緑PBでの生存判定、1分境界ブザー等、音色で区別できるようにした。コメント類追記。
R8/1/13 Ver.6.8 シリアルモニタでデバッグ情報を表示する様にした。
*/

// --------- バージョン情報 ---------
const uint8_t VERSION_MAJOR = 6; // メジャーバージョン
const uint8_t VERSION_MINOR = 8; // マイナーバージョン
const char VERSION_PATCH = ' '; // マイナーバージョンに続き文字がある場合(例;小パッチなどマイナーバージョンUPに満たない小変更[Ver.4.71にしたい場合ここに「1」を入力、β版、betaの場合、末尾に「b」を付加するなど)

// ---- Serial debug control ----
const bool SERIAL_DEBUG = true; // false にすると Serial 出力をまとめて消せます

// --------- 定義 ---------
#define AC100V_RY 12 // AC100V出力リレー駆動用(ケース内赤色LEDで確認可能)端子番号
#define BZR 11 // ブザー(トランジスタでマイナスコントロール)端子番号
#define SPK 10 // スピーカー端子番号
#define SEC_COUNT_LED 9 // タイマーカウント用緑LED端子番号
#define GRN_PB_A 2 // 緑押し釦SWのNO接点端子番号
#define GRN_PB_B 3 // 緑押し釦SWのNC接点端子番号
#define BLK_PB_A 4 // 黒押し釦SWのNO接点端子番号
#define BLK_PB_B 5 // 黒押し釦SWのNC接点端子番号

#define GRN_PB_A_ON 0 // 緑押し釦SWのNO接点が閉じているとき(押している時)のdigitalReadの値
#define GRN_PB_A_OFF 1 // 緑押し釦SWのNO接点が開いている時(押していない時)のdigitalReadの値
#define GRN_PB_B_ON 0 // 緑押し釦SWのNC接点が閉じている時(押していない時)のdigitalReadの値
#define GRN_PB_B_OFF 1 // 緑押し釦SWのNC接点が開いている時(押している時)のdigitalReadの値
#define BLK_PB_A_ON 0 // 黒押し釦SWのNO接点が閉じているとき(押している時)のdigitalReadの値
#define BLK_PB_A_OFF 1 // 黒押し釦SWのNO接点が開いている時(押していない時)のdigitalReadの値
#define BLK_PB_B_ON 0 // 黒押し釦SWのNC接点が閉じている時(押していない時)のdigitalReadの値
#define BLK_PB_B_OFF 1 // 黒押し釦SWのNC接点が開いている時(押している時)のdigitalReadの値

#define BEATTIME 200 // スピーカーの音を出している時間(msec)
#define MIN_BZR_TIME 500 // 残り1分時などでブザーを鳴らす時間
#define TIMEOUT_BZR_TIME 2000 // タイムアウト時ブザーを鳴らす時間
#define BZR_REST_TIME 500 // タイムアウト時ブザーの間の余拍
#define SHUTDOWN_BZR_TIME 300 // 異常検出し強制シャットダウン時のブザーを鳴らす間隔
#define SEC_LED_PULSE_MS 50 // 緑LED 点灯時間(ms)※1秒カウントの視覚用

// ---- UI sound definitions (Ver.6.7以降追加)----
#define UI_FREQ_DISPLAY 800 // 運転中 黒押しボタンでの画面表示切替時のbeep音の周波数
#define UI_TIME_DISPLAY 40 // 時間(msec)

#define UI_FREQ_SURVIVAL 1000 // 運転中 緑押しボタンでのタイマースタート及び生存判定時のbeep音の周波数
#define UI_TIME_SURVIVAL BEATTIME // 時間

#define FREQ_MINUTE_BEEP 2000 // 1分境界beep音の周波数
#define TIME_MINUTE_BEEP BEATTIME // 時間


// =========================================================
// ---- ISR flags / ISR shared variables -------------------
// ※ ISR と loop() の間で共有される変数のみ
// ※ ISR内ではフラグ操作のみ(処理は禁止)
// =========================================================

// ---- タイマ運転状態 ----
volatile bool timer_running = false; // タイマ運転中フラグ(ISRガード)

// ---- 1秒 / 分カウント関連(Timer1 ISR)----
volatile uint8_t sec_in_min = 0; // 0〜59 秒カウンタ
volatile bool minute_tick = false; // 1分経過通知フラグ

// ---- TM_left 再ロード要求(loop → ISR)----
volatile bool tm_reset_req = false;

// ---- 緑LED(SEC_COUNT_LED)制御 ----
volatile bool sec_led_pulse_req = false; // ★ ISR→loop 通知用(1Hz LEDパルス要求)
unsigned long sec_led_on_time = 0; // ★ loop側で記録(millis使用)



// ---- 生存判定(緑ボタン外部割り込み)----
volatile bool survival_irq = false; // ISR通知用(即時)
volatile bool survival_flag = false; // loop()昇格後の有効復帰フラグ
// survival_flag は「復帰要求」そのものではなく
// RUN中は延長、TIMEOUT中は復帰として上位ロジックが解釈する
// RUN中: 延長要求として解釈
// TIMEOUT中: 復帰要求として解釈


/* =========================================================
ISR / loop 共有フラグ・変数 意味表(Ver.6.5)
---------------------------------------------------------

【基本方針】
・ISR → loop 方向は「フラグ通知」
・loop → ISR 方向は「状態許可」

---------------------------------------------------------
■ フラグ・変数一覧

volatile bool timer_running
・true : Timer1 ISR が有効
・false : 時間カウント停止

volatile uint8_t TM_left
・残り時間(分)
・ISRのみが書き換える
・loopで読む場合は get_TM_left_safe() を使用

volatile uint8_t sec_in_min
・1分内の秒カウンタ(0〜59)
・ISR管理
————————————————————————————-
volatile bool tm_reset_req
・TM_left 再ロード要求フラグ(loop → ISR)

【役割】
loop() 側から「TM_left を TM_set に戻したい」ことを
Timer1 ISR に通知するための要求フラグ。

【true の意味】
・TM_left を再ロードする要求が出ている
・次回 Timer1 ISR 実行時に
TM_left = TM_set;
sec_in_min = 0;
を実行する

【false の意味】
・再ロード要求なし

【セットされる箇所】
・運転開始時(RUN 開始)
・緑PB押下による延長(RUN中)
・TIMEOUT 中の復帰操作成功時

【クリアされる箇所】
・Timer1 ISR 内で TM_left 再ロード完了後

【設計上の意図】
・TM_left を ISR 管理変数として一元化するため
・loop() からの直接書き換えを完全禁止するため
・将来の MCU 移植時(ESP32 等)でも
競合・原子性問題を発生させないため

【注意】
・tm_reset_req は「状態」ではなく「要求」を表す
・ISR 内でのみ TM_left を書き換えること
——————————————————————————————
volatile bool sec_led_pulse_req
・1HzごとにISRが立てる
・loopが検出して緑LEDを短パルス点灯させる

unsigned long sec_led_on_time
・緑LED ON 時刻(millis基準)
・loop側のみで使用(ISR使用禁止)

volatile bool survival_flag
・外部割り込み(緑PB)からの復帰要求

volatile bool timeout_interrupt_enable
【役割】
TIMEOUT状態中に「緑PBによる割り込み復帰」を
一時的に許可するためのゲートフラグ。

【true の意味】
・現在 TIMEOUT 状態である
・TIMEOUT警告ブザー鳴動中である
・緑PB割り込みによる RUN への復帰を許可する

【false の意味】
・通常運転中(RUN)
・IDLE 状態
・TIMEOUTブザー終了後
・SHUTDOWN 状態
→ 緑PB割り込みは無効(復帰不可)

【セットされる箇所】
・TIMEOUT突入時(ブザー開始直前)
timeout_interrupt_enable = true;

【クリアされる箇所】
・緑PB割り込みによる復帰成功時
・TIMEOUTブザーシーケンス完了時
・SHUTDOWN遷移時

【設計上の意図】
・復帰可能時間を TIMEOUT警告中のみに限定することで
無制限な再通電を防止し、安全側へ必ず倒す設計とする
・TIMEOUT終了後は必ず SHUTDOWN 状態へ遷移させる

【注意】
・このフラグは「状態」を表すものではなく
「割り込み許可条件」を表す制御ゲートである
・状態管理(RUN / TIMEOUT / SHUTDOWN)は
上位ロジックで別途担保すること

---------------------------------------------------------
【禁止事項】
・ISR内で millis(), delay(), LCD, I2C, Serial を使用しない
・TM_left を loop から直接書き換えない
・TM_left の再設定は tm_reset_req を介して ISR に委譲する
・loop() から TM_left を直接書き換えてはならない

========================================================= */


// ==== 前方宣言 ====
void survival_judgement();
void updateRMSWindow();
void handleBlackButton();


// --------- 表示モード定義とEEPROMアドレス定義 ----------
#define DISP_MODE_NORMAL 0 // 現在電流値&ピーク電流値/SSR温度をページ切り替えながら表示するモード
#define DISP_MODE_DUAL 1 // 現在電流値とSSR温度を表示するモード(画面ページ切替なし)

uint8_t display_mode = DISP_MODE_NORMAL; // EEPROMで保存される表示モード
uint8_t display_page = 0; // 0 / 1 ページ切替用

#define EEPROM_ADDR_DISPLAY_MODE 5


// --------- ACS712 30A 定義 ---------
#define ACS_PIN A3
#define ACS_SENS 0.066 // 66mV/A(ACS712-30A使用時はこちら)
// #define ACS_SENS 0.100 // 100mV/A(ACS712-20A使用時はこちら)

float acs_offset_adc = 512.0; // ★ 実測ゼロ点(起動時に再計算)



// --------- DS18B20 定義 ----------
#define ONE_WIRE_BUS A2 // DS18B20 を接続するピン

#include
#include

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

unsigned long last_temp_read = 0;
float ssr_temp = 0.0;


// DS18B20温度センサーの断線等異常
bool temp_sensor_error = false;

// 温度監視のしきい値
const float TEMP_WARN = 60.0; // 警告
const float TEMP_STOP = 80.0; // 強制停止



// タイマ設定値保存・電源周波数設定・画面表示モード記憶用にEEPROM使用
#include

bool debug_mode = false; // デバッグモードフラグ

// 16x2行LCDパネル使用 ライブラリ[LiquidCrystal_I2C.h] 使用
#include

LiquidCrystal_I2C lcd(0x3F, 16, 2);


uint8_t TM_set; // タイマの設定時間
volatile uint8_t TM_left; // タイマ残り時間


uint8_t SHUTDOWN_BZR_counter = 0; // 異常検出時ブザー回数のカウンター


// TIME OUT中の割り込み復帰許可フラグ
bool timeout_interrupt_enable = false;



// ---- 長押し検出 ----
unsigned long blk_press_start = 0;
bool blk_long_press_handled = false;



/* =========================================================
Timer1 Compare Match A ISR (1Hz固定)
---------------------------------------------------------
【役割】
・本装置の「時間の心臓」
・1秒周期で以下を管理する

【ISRで行う処理】
・残り時間 TM_left のデクリメント
・1分経過判定(sec_in_min)
・緑LEDの1Hzパルス要求フラグ立て

【設計ポリシー(重要)】
・ISR内では millis() / delay() / LCD / I2C / Serial を使用しない
・ISRでは「状態更新」と「フラグ通知」のみ行う
・実時間処理(millis基準)は必ず loop() 側で行う

【動作周波数】
・1Hz 固定(OCR1A設定に依存)

【注意】
・TM_left は ISR 管理変数
loop() から参照する場合は必ず
get_TM_left_safe() を使用すること
========================================================= */
ISR(TIMER1_COMPA_vect)
{
if (!timer_running) return;

/* ---- TM_left 再ロード要求 ---- */
if (tm_reset_req) {
TM_left = TM_set;
sec_in_min = 0;
tm_reset_req = false;
return;
}

/* ---- 1秒カウント ---- */
sec_in_min++;

/* ---- 緑LED 1Hzパルス要求 ---- */
sec_led_pulse_req = true;

/* ---- 1分経過判定 ---- */
if (sec_in_min >= 60) {
sec_in_min = 0;
minute_tick = true; // ← 追加:1分経過フラグを立てる

if (TM_left > 0) {
TM_left--;
}
}
}

/* ------------------------------------------
タイマー残り時間表示用にTM_left安全取得
------------------------------------------ */
// ---- ISR共有変数の安全取得 ----
uint8_t get_TM_left_safe() {
uint8_t v;
noInterrupts();
v = TM_left;
interrupts();
return v;
}


/* --------------------------------------
緑押しボタン(生存判定)割り込み処理
-------------------------------------- */
void survival_judgement() {
survival_irq = true;
}


/* ----------------------------------------------
運転中黒押しボタン処理(画面表示モード切替)
---------------------------------------------- */
void handleBlackButton() {
static bool last_state = HIGH;
bool now = digitalRead(BLK_PB_A);

if (last_state == HIGH && now == LOW) { // 押された瞬間
display_mode = (display_mode == DISP_MODE_NORMAL)
? DISP_MODE_DUAL
: DISP_MODE_NORMAL;

EEPROM.update(EEPROM_ADDR_DISPLAY_MODE, display_mode);

// フィードバック(短くbeep音)
tone(SPK, UI_FREQ_DISPLAY, UI_TIME_DISPLAY);
lcd.clear();
}

last_state = now;
}

/* -------------------------------------------------------
TIME OUT 時のブザー鳴動を1ステップだけ行う関数
return = true → 割り込みにより復帰要求が発生
------------------------------------------------------- */
bool timeout_beep_step(unsigned long on_ms, unsigned long off_ms) {

unsigned long start = millis();

// ブザー ON
digitalWrite(BZR, HIGH);
while (millis() - start < on_ms) {
if (survival_irq) {
survival_irq = false;
digitalWrite(BZR, LOW); // ---- TIMEOUTギリギリから復帰した際にブザーが鳴り続いてしまうのを防止 ----
return true; // ← 復帰
}
}
// ブザー OFF
digitalWrite(BZR, LOW);
start = millis();
while (millis() - start < off_ms) {
// TIME OUT中の復帰判定
if (survival_irq) {
survival_irq = false;
digitalWrite(BZR, LOW); // ---- TIMEOUTギリギリから復帰した際にブザーが鳴り続いてしまうのを防止 ----
return true;
}
}

return false;
}

// -------------------------------------------------------
// SSR 温度監視(1秒ごと)
// return true → 過熱で緊急停止が必要
// -------------------------------------------------------

bool first_temp_sample = true; // ★最初の1回だけ無視する
uint8_t temp_over_counter = 0; // ★連続超え回数カウンタ

bool check_ssr_temperature()
{
if (millis() - last_temp_read >= 1000) {

last_temp_read = millis();
// 1) 温度変換を開始(非同期)
sensors.setWaitForConversion(false);
sensors.requestTemperatures();
// 2) 次の周期で結果だけ読む
float t = sensors.getTempCByIndex(0);


// --- DS18B20 断線・異常チェック ---
// DS18B20はセンサー断線・未接続で-127℃となる為、-126以下を拾った場合は断線判定
if (t == DEVICE_DISCONNECTED_C || t <= -126.0) {
temp_sensor_error = true;
return true; // センサー異常で強制停止扱い
}


// ★ 最初の1回はゴミ値が出るため無視
if (first_temp_sample) {
first_temp_sample = false;

// 85℃(DS18B20の初期化直後の値) または異常な値を無視
if (t > -55 && t < 85) {
ssr_temp = t; // 初回値として採用できる
}

temp_over_counter = 0;
return false; // 判定処理は行わない
}

ssr_temp = t;

// ★ ここから判定開始 ★
if (ssr_temp >= TEMP_STOP) {
temp_over_counter++;
} else {
temp_over_counter = 0;
}

// ★ 2回連続で TEMP_STOP を超えたら異常判定 ★
if (temp_over_counter >= 2) {
return true;
}
}

return false; // 通常時
}

// --- ACS712 測定用 ---
float acs_last = 0.0;
float acs_peak = 0.0;
unsigned long last_acs_time = 0;
unsigned long last_display_switch = 0;

float acs_rms = 0.0;
unsigned long rms_last_time = 0;

// 連続サンプルでの過電流検出用(瞬時ノイズ除外)
static uint8_t oc_counter = 0; // 過電流連続検出カウンタ(単位:サンプル)


// ---- RMSサンプリング設定 ----
#define ACS_SAMPLE_INTERVAL_MS 1 // ★ 1ms固定
unsigned long last_acs_sample = 0;

// RMS窓(初期値:50Hz)
unsigned long RMS_WINDOW_MS = 200; // 50Hz: 10周期


void calibrateACSZero() {
delay(500); // ★ 電源・ADC・ACS712 安定待ち

const int samples = 500;
long sum = 0;

for (int i = 0; i < samples; i++) {
sum += analogRead(ACS_PIN);
delayMicroseconds(200);
}

acs_offset_adc = (float)sum / samples;
}


float readACS() {
int raw = analogRead(ACS_PIN);
float voltage = (raw - acs_offset_adc) * (5.0 / 1023.0);
float current = voltage / ACS_SENS;
if (current < 0) current = -current; // 符号を絶対値に
if (current < 0.1) current = 0.0; // デッドゾーン(ノイズ殺し)
return current;
}

/* --------------------------------------
電源周波数設定
-------------------------------------- */
// ---- AC周波数設定用 ----
#define EEPROM_ADDR_AC_FREQ 0x10
#define DEFAULT_AC_FREQ 50
uint8_t ac_freq_hz = 50; // 50 or 60
bool freq_setting_changed = false;
#define RMS_CYCLES 10

void updateRMSWindow() {
// 安全対策:ac_freq_hz が異常値(0など)ならデフォルトに戻す
if (ac_freq_hz != 50 && ac_freq_hz != 60) {
ac_freq_hz = DEFAULT_AC_FREQ;
}

// RMS_CYCLES 周期分の時間をミリ秒で計算(四捨五入)
// = round( RMS_CYCLES * 1000 / ac_freq_hz )
RMS_WINDOW_MS = (RMS_CYCLES * 1000UL + ac_freq_hz / 2) / ac_freq_hz;
}



/* -----------------------------
セットアップ
----------------------------- */
void setup() {

// 出力
pinMode(AC100V_RY, OUTPUT); // AC100V出力用リレーON
pinMode(BZR, OUTPUT); // ブザー
pinMode(SPK, OUTPUT); // スピーカー
pinMode(SEC_COUNT_LED, OUTPUT); // タイマーカウント用緑LED

// 入力
pinMode(GRN_PB_A, INPUT_PULLUP); // 緑スイッチNO接点
attachInterrupt(digitalPinToInterrupt(2), survival_judgement, FALLING);

pinMode(GRN_PB_B, INPUT_PULLUP); // 緑スイッチNC接点
pinMode(BLK_PB_A, INPUT_PULLUP); // 黒スイッチNO接点
pinMode(BLK_PB_B, INPUT_PULLUP); // 黒スイッチNC接点

sensors.begin(); // DS18B温度センサ

digitalWrite(AC100V_RY, LOW); // AC100Vリレー(SSR)初期化(起動時HIGHになるのを防止)
digitalWrite(BZR, LOW); // ブザー初期化(同上)
digitalWrite(SPK, LOW); // スピーカー初期化(同上)
digitalWrite(SEC_COUNT_LED, LOW); // 秒カウント緑LED初期化(同上)
temp_sensor_error = false; // 温度センサーエラー初期化
ssr_temp = 0; // SSR温度初期化

// LCD 初期化
lcd.init();
lcd.backlight();

// シリアル初期化(デバッグ用)
if (SERIAL_DEBUG) {
Serial.begin(115200);
delay(100); // シリアル安定待ち(AVRでは無くてもOK)
Serial.println();
Serial.print("Dead Man Timer ");
Serial.print(VERSION_MAJOR);
Serial.print(".");
Serial.print(VERSION_MINOR);
Serial.print(VERSION_PATCH);
Serial.println(" boot");
}

calibrateACSZero(); // ACS712のゼロ点キャリブレーション

// calibrateACSZero 実行直後に結果をログ
if (SERIAL_DEBUG) {
float offset_voltage = (acs_offset_adc - 512.0) * (5.0 / 1023.0);
Serial.print("ACS offset ADC=");
Serial.print(acs_offset_adc, 2);
Serial.print(" -> ");
Serial.print(offset_voltage, 4);
Serial.println(" V");
}

// --- Timer1 1秒割り込み設定 ---
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;

// 16MHz / 1024 / 15625 = 1Hz
OCR1A = 15624;
TCCR1B |= (1 << WGM12); // CTCモード
TCCR1B |= (1 << CS12) | (1 << CS10); // 分周 1024
TIMSK1 |= (1 << OCIE1A); // 割り込み許可
interrupts();

// 起動確認用メロディ(非リアルタイム領域)
tone(SPK, 262, BEATTIME); delay(BEATTIME);
tone(SPK, 294, BEATTIME); delay(BEATTIME);
tone(SPK, 330, BEATTIME); delay(BEATTIME);
tone(SPK, 349, BEATTIME); delay(BEATTIME);
tone(SPK, 392, BEATTIME); delay(BEATTIME);
tone(SPK, 440, BEATTIME); delay(BEATTIME);
tone(SPK, 494, BEATTIME); delay(BEATTIME);
tone(SPK, 523, BEATTIME);

// ---- デバッグモード切替:起動時に黒ボタン or 緑ボタン長押し(NC接点使用)バージョン情報表示 ----
unsigned long press_start = millis();
while ((digitalRead(BLK_PB_B) == BLK_PB_B_OFF) ||
(digitalRead(GRN_PB_B) == GRN_PB_B_OFF)) {
if (millis() - press_start > 1000) {
debug_mode = !debug_mode;
EEPROM.update(1, debug_mode);

lcd.clear();
lcd.setCursor(0, 0);
if (debug_mode) lcd.print("DEBUG MODE ON");
else lcd.print("DEBUG MODE OFF");

lcd.setCursor(0, 1);
lcd.print("Ver.");
lcd.print(VERSION_MAJOR);
lcd.print(".");
lcd.print(VERSION_MINOR);
lcd.print(VERSION_PATCH);
digitalWrite(BZR, HIGH);
delay(250);
digitalWrite(BZR, LOW);
delay(1750);
lcd.clear();
break;
}
}

// ★ DEBUG MODE時:ACS712 ゼロ点情報表示 ★
if (debug_mode) {
float offset_voltage = (acs_offset_adc - 512.0) * (5.0 / 1023.0);

lcd.clear();
lcd.setCursor(0, 0);
lcd.print("ACS OFFSET");

lcd.setCursor(0, 1);
lcd.print("ADC=");
lcd.print(acs_offset_adc, 1);
delay(1200);

lcd.clear();
lcd.setCursor(0, 0);
lcd.print("OFFSET VOLT");

lcd.setCursor(0, 1);
lcd.print(offset_voltage, 3);
lcd.print(" V");
delay(1200);

if (abs(offset_voltage) > 0.05) {
lcd.setCursor(0, 0);
lcd.print("OFFSET WARNING");
digitalWrite(BZR, HIGH);
delay(1000);
digitalWrite(BZR, LOW);
} else {
lcd.setCursor(0, 0);
lcd.print("OFFSET NORMAL");
delay(1000);
}
}

// 共通起動表示
lcd.clear();
lcd.setCursor(1, 0);
lcd.print("Dead Man Timer");
lcd.setCursor(3, 1);
lcd.print("DMT-KW7946");

// --- EEPROM から前回の TM_set を読み出す ---
TM_set = EEPROM.read(0);

if (SERIAL_DEBUG) {
Serial.print("TM_set (from EEPROM) = ");
Serial.println(TM_set);
}

if (TM_set < 5 || TM_set > 60) {
TM_set = 15;
}

// 範囲補正
if (debug_mode) {
TM_set = 1;
if (TM_set < 1 || TM_set > 60) TM_set = 5;
} else {
if (TM_set < 5 || TM_set > 60) TM_set = 15;
}

// --- 表示モード読み込み ---
display_mode = EEPROM.read(EEPROM_ADDR_DISPLAY_MODE);
if (display_mode > DISP_MODE_DUAL) {
display_mode = DISP_MODE_NORMAL;
}

if (debug_mode) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("DispMode:");
lcd.print(display_mode == DISP_MODE_NORMAL ? "NORMAL" : "DUAL");
delay(1000);
}

// EEPROMより周波数設定読み出し
uint8_t v = EEPROM.read(EEPROM_ADDR_AC_FREQ);
if (SERIAL_DEBUG) {
Serial.print("EEPROM AC_FREQ read: ");
Serial.println(v);
}
if (v == 50 || v == 60) {
ac_freq_hz = v;
} else {
ac_freq_hz = DEFAULT_AC_FREQ;
EEPROM.write(EEPROM_ADDR_AC_FREQ, ac_freq_hz);
}

updateRMSWindow();

// --- DEBUG MODE:周波数設定 ---
if (debug_mode) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("AC FREQ SET");
lcd.setCursor(0, 1);
lcd.print(ac_freq_hz);
lcd.print("Hz PRESS BLK");

// 周波数設定表示中に黒押しボタンで周波数設定変更
unsigned long t0 = millis();
while (millis() - t0 < 2000) {
if (digitalRead(BLK_PB_A) == BLK_PB_A_ON) {
delay(50);
if (digitalRead(BLK_PB_A) == BLK_PB_A_ON) {
ac_freq_hz = (ac_freq_hz == 50) ? 60 : 50;
updateRMSWindow();
// 設定変更時はEEPROMに保存
EEPROM.update(EEPROM_ADDR_AC_FREQ, ac_freq_hz);

lcd.setCursor(0, 1);
lcd.print(ac_freq_hz);
lcd.print("Hz SAVED ");
tone(SPK, 2000, 100);
delay(2000);
break;
}
}
}
}

// 再表示(起動)
lcd.clear();
lcd.setCursor(1, 0);
lcd.print("Dead Man Timer");
lcd.setCursor(3, 1);
lcd.print("DMT-KW7946");

/* -----------------------------------------------
タイマセット(黒スイッチ長押しで5分刻み)
----------------------------------------------- */
while (digitalRead(GRN_PB_A) == GRN_PB_A_OFF) {
if (digitalRead(BLK_PB_A) == BLK_PB_A_ON) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" Timer Set");
lcd.setCursor(1, 1);
lcd.print(" ");
lcd.print(TM_set);
lcd.print(" min");

delay(400);

if (digitalRead(BLK_PB_A) == BLK_PB_A_ON) {
if (debug_mode) {
// デバッグ: 1分刻み
if (TM_set < 60) TM_set++;
else TM_set = 1;
} else {
// 通常: 5分刻み
if (TM_set < 60) TM_set += 5;
else TM_set = 5;
}
delay(100);

lcd.setCursor(1, 1);
lcd.print(" ");
lcd.print(TM_set);
lcd.print(" min");
}
}
}
// ★ 変更確定 → EEPROMへ保存 ★
EEPROM.update(0, TM_set);
}



/* -----------------------------
メイン処理
----------------------------- */
void loop() {

/* ---- AC100V 出力 ON ---- */
if (SERIAL_DEBUG) {
Serial.print("RUN start: TM_set=");
Serial.print(TM_set);
Serial.print(" ac_freq_hz=");
Serial.println(ac_freq_hz);
}
tone(SPK, UI_FREQ_SURVIVAL, UI_TIME_SURVIVAL);
delay(BEATTIME);
digitalWrite(AC100V_RY, HIGH);
digitalWrite(SEC_COUNT_LED, HIGH);

lcd.clear();
lcd.setCursor(1, 0); lcd.print("AC100V OUT ON");
lcd.setCursor(2, 1); lcd.print("TIMER START");
delay(2000);
lcd.clear();
timer_running = true;
digitalWrite(SEC_COUNT_LED, LOW);

sec_in_min = 0;
minute_tick = false;


/* =================================
分カウントループ
================================= */
tm_reset_req = true; // ISR に初期ロードを依頼

// ISR主導設計のため、RUN開始時は
// TM_left がロードされるまで待つ必要がある
// ★ ISR が TM_left をロードするまで待機(最大1秒)
while (get_TM_left_safe() == 0) {
// wait: Timer1 ISR (1Hz) が必ず tm_reset_req を処理する
// ※ Timer1 ISR (1Hz) により最大1秒で必ず抜ける
}

while (get_TM_left_safe() > 0) {

unsigned long now = millis();

// ★ 割り込み受付 ★
static unsigned long last_survival_time = 0;

if (survival_irq) {
survival_irq = false;

if (SERIAL_DEBUG) {
Serial.println("IRQ: survival_irq detected");
}


if ((timer_running || timeout_interrupt_enable) && (now - last_survival_time > 50)) {
last_survival_time = now;
survival_flag = true;
}
}

/* ---- 割り込み復帰 ---- */
if (survival_flag) {
survival_flag = false;
tm_reset_req = true; // ← ISR に任せる
tone(SPK, UI_FREQ_SURVIVAL, UI_TIME_SURVIVAL);
if (SERIAL_DEBUG) {
Serial.println("Action: survival_flag -> TM reset requested");
}
}

/* ---- 緑LED 短パルス制御(ISR通知→loopで時刻管理) ---- */
if (sec_led_pulse_req) {
sec_led_pulse_req = false;
digitalWrite(SEC_COUNT_LED, HIGH);
sec_led_on_time = millis(); // ★ millis() は loop 側のみ
}

if (digitalRead(SEC_COUNT_LED) == HIGH) {
if (millis() - sec_led_on_time >= SEC_LED_PULSE_MS) {
digitalWrite(SEC_COUNT_LED, LOW);
}
}
/* ---- 分境界ブザー(RUN状態のみ) ---- */
// minute_tick は RUN 状態中のみ使用する(ISRは状態を知らないため)
if (minute_tick) {
minute_tick = false;

uint8_t tm = get_TM_left_safe();

if (SERIAL_DEBUG) {
Serial.print("Minute tick. TM_left=");
Serial.println(tm);
}

if (tm > 2) {
tone(SPK, FREQ_MINUTE_BEEP, TIME_MINUTE_BEEP);
} else if (tm == 1) {
digitalWrite(BZR, HIGH);
delay(MIN_BZR_TIME);
digitalWrite(BZR, LOW);
}
}
/* ---- ACS712 サンプリング ---- */
if (now - last_acs_sample >= ACS_SAMPLE_INTERVAL_MS) {
last_acs_sample = now;

float inst = readACS();
acs_last = inst;

if (inst > acs_peak) acs_peak = inst;

static float sum_sq = 0.0;
static int sample_count = 0;

sum_sq += inst * inst;
sample_count++;

if (now - rms_last_time >= RMS_WINDOW_MS) {
if (sample_count > 0) {
acs_rms = sqrt(sum_sq / sample_count);
}
sum_sq = 0;
sample_count = 0;
rms_last_time = now;
}

/* ---- 過電流判定 ---- */
if (acs_last >= 6.0) {
oc_counter++;
if (SERIAL_DEBUG) {
Serial.print("OC detected: acs_last=");
Serial.print(acs_last, 2);
Serial.print(" acs_peak=");
Serial.println(acs_peak, 2);
}
if (oc_counter >= 3) {
SHUTDOWN_BZR_counter = 0;
digitalWrite(AC100V_RY, LOW);
timer_running = false;
sec_in_min = 0;
digitalWrite(SEC_COUNT_LED, HIGH);

lcd.clear();
lcd.setCursor(0, 0); lcd.print("!! OC CUT 6A !!");
lcd.setCursor(0, 1); lcd.print("OUTPUT SHUTDOWN");

while (SHUTDOWN_BZR_counter < 10) {
digitalWrite(BZR, HIGH);
delay(SHUTDOWN_BZR_TIME);
digitalWrite(BZR, LOW);
delay(SHUTDOWN_BZR_TIME);
SHUTDOWN_BZR_counter++;
}
if (SERIAL_DEBUG) {
Serial.println("System SHUTDOWN due to OC. Waiting for reset.");
}
while (1) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("PeakCurrent=");
lcd.print(acs_peak, 1);
lcd.print("A");
lcd.setCursor(0, 1);
lcd.print("OUTPUT SHUTDOWN");
delay(3000);
}
}
} else {
oc_counter = 0;
}
}

/* ---- 温度監視 ---- */
if (check_ssr_temperature()) {
SHUTDOWN_BZR_counter = 0;
digitalWrite(AC100V_RY, LOW);
timer_running = false;
sec_in_min = 0;
digitalWrite(SEC_COUNT_LED, HIGH);

if (SERIAL_DEBUG) {
if (temp_sensor_error) {
Serial.println("TEMP SENSOR ERROR -> SHUTDOWN");
} else {
Serial.print("TEMP ERROR: ssr_temp=");
Serial.println(ssr_temp, 1);
}
}

lcd.clear();
if (temp_sensor_error) {
lcd.setCursor(0, 0); lcd.print("TEMP SENSOR ERR");
lcd.setCursor(0, 1); lcd.print("CHECK WIRING");
} else {
lcd.setCursor(0, 0); lcd.print(" !!TEMP ERROR!!");
lcd.setCursor(0, 1); lcd.print("OUTPUT SHUTDOWN");
}

while (SHUTDOWN_BZR_counter < 5) {
digitalWrite(BZR, HIGH);
delay(SHUTDOWN_BZR_TIME);
digitalWrite(BZR, LOW);
delay(SHUTDOWN_BZR_TIME);
SHUTDOWN_BZR_counter++;
}

while (1) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("SSR TEMP=");
lcd.print(ssr_temp, 0);
lcd.print("C");
lcd.setCursor(0, 1);
lcd.print("OUTPUT SHUTDOWN");
delay(3000);
}
}

handleBlackButton(); // 運転中黒押しボタンで表示モード切り替え


// ---- LCD 表示処理 ----
// 現在の温度状態から警告判定
bool ssr_tempwarning = (ssr_temp >= TEMP_WARN);

if (display_mode == DISP_MODE_NORMAL) {

// ==== 表示ページ自動切替(2秒)====
if (millis() - last_display_switch >= 2000) {
last_display_switch = millis();
display_page = !display_page;
lcd.clear();
}

// ====== 温度警告表示 ======
if (ssr_tempwarning) {

// 警告中も2秒周期で点滅させる
if (display_page == 0) {
lcd.setCursor(1, 0);
lcd.print("I=");
lcd.print(acs_rms, 1);
lcd.print("A ");
lcd.print("PK=");
lcd.print(acs_peak, 1);
lcd.print("A");
lcd.setCursor(3, 1);
lcd.print(get_TM_left_safe());
lcd.print(" min left");

} else {
lcd.setCursor(0, 0);
lcd.print("SSR TEMP = ");
lcd.print(ssr_temp, 0);
lcd.print("C");

lcd.setCursor(0, 1);
lcd.print("!SSR TEMP HIGH!");
}

} else {
// ===== 通常表示 =====
if (display_page == 0) {

lcd.setCursor(1, 0);
lcd.print("I=");
lcd.print(acs_rms, 1);
lcd.print("A ");

lcd.print("PK=");
lcd.print(acs_peak, 1);
lcd.print("A");

lcd.setCursor(3, 1);
lcd.print(get_TM_left_safe());
lcd.print(" min left");

} else {

lcd.setCursor(0, 0);
lcd.print("SSR TEMP = ");
lcd.print(ssr_temp, 0);
lcd.print("C");

lcd.setCursor(3, 1);
lcd.print(get_TM_left_safe());
lcd.print(" min left");
}
}

} else {
// ===== 表示固定モード(黒ボタン) =====
if (ssr_tempwarning) {
lcd.setCursor(0, 0);
lcd.print("I=");
lcd.print(acs_rms, 1);
lcd.print("A ");

lcd.print("T=");
lcd.print(ssr_temp, 0);
lcd.print("C!!");

lcd.setCursor(3, 1);
lcd.print(get_TM_left_safe());
lcd.print(" min left");
} else {
lcd.setCursor(0, 0);
lcd.print("I=");
lcd.print(acs_rms, 1);
lcd.print("A ");

lcd.print("T=");
lcd.print(ssr_temp, 0);
lcd.print("C ");

lcd.setCursor(3, 1);
lcd.print(get_TM_left_safe());
lcd.print(" min left");
}
}
/* ---- TIME OUT 判定 ---- */
if (get_TM_left_safe() == 0) {
break; // RUNループ終了 → TIMEOUT処理へ
}
}


/* ---- TIME OUT 処理 ---- */
tone(SPK, FREQ_MINUTE_BEEP, TIME_MINUTE_BEEP);
delay(BEATTIME);
timer_running = false;
sec_in_min = 0;

// TIMEOUTブザー鳴動中に限り緑押しボタンで復帰可の為、緑PB押下を促すメッセージ表示
lcd.clear();
lcd.setCursor(1, 0); lcd.print("PUSH GREEN PB");
lcd.setCursor(3, 1); lcd.print("HURRY UP!!");

digitalWrite(SEC_COUNT_LED, HIGH);
timeout_interrupt_enable = true; // TIMEOUTシーケンス中限定で復帰を許可(特別措置)

// TIMEOUTブザーシーケンス
if (timeout_beep_step(MIN_BZR_TIME, BZR_REST_TIME)) {
timeout_interrupt_enable = false;
tm_reset_req = true; // 復帰時の再ロード

if (SERIAL_DEBUG) {
Serial.println("TIMEOUT: green PB detected during beep step 1 -> RUN restart");
}
return;
}

if (timeout_beep_step(MIN_BZR_TIME, BZR_REST_TIME)) {
timeout_interrupt_enable = false;
tm_reset_req = true;

if (SERIAL_DEBUG) {
Serial.println("TIMEOUT: green PB detected during beep step 2 -> RUN restart");
}
return;
}

if (timeout_beep_step(MIN_BZR_TIME, BZR_REST_TIME)) {
timeout_interrupt_enable = false;
tm_reset_req = true;

if (SERIAL_DEBUG) {
Serial.println("TIMEOUT: green PB detected during beep step 3 -> RUN restart");
}
return;
}

if (timeout_beep_step(MIN_BZR_TIME, BZR_REST_TIME)) {
timeout_interrupt_enable = false;
tm_reset_req = true;

if (SERIAL_DEBUG) {
Serial.println("TIMEOUT: green PB detected during beep step 4 -> RUN restart");
}
return;
}

if (timeout_beep_step(TIMEOUT_BZR_TIME, BZR_REST_TIME)) {
timeout_interrupt_enable = false;
tm_reset_req = true;

if (SERIAL_DEBUG) {
Serial.println("TIMEOUT: green PB detected during FINAL beep -> RUN restart");
}
return;
}

// TIMEOUTによりSHUTDOWN状態へ遷移
timeout_interrupt_enable = false; // TIMEOUTシーケンス終了で緑押しボタンによる限定復帰許可を取り消し
digitalWrite(AC100V_RY, LOW); // AC100V出力を遮断


lcd.clear();
lcd.setCursor(1, 0); lcd.print("AC100V OUT OFF");
lcd.setCursor(3, 1); lcd.print("TIME OUT");

if (SERIAL_DEBUG) {
Serial.println("TIMEOUT -> SHUTDOWN (AC100V OUT OFF). Waiting for reset.");
}

while (1) {
lcd.setCursor(0, 1); lcd.print("PWR OFF or RESET");
delay(3000);
lcd.setCursor(0, 1); lcd.print(" TIME OUT ");
delay(3000);
}
}

/* =====================================================
割り込み設計ポリシー(Interrupt Design Policy)

【基本方針】
・割り込み処理(ISR)では「状態更新のみ」を行う
・演算・判定・表示・EEPROMアクセス・delay系処理は
一切 ISR 内では行わない
・ISRは常に最短時間で終了することを最優先とする

【Timer1 ISR(1Hz固定)】
・秒カウント/分カウント/LED短パルス生成のみ担当
・TM_left, sec_in_min, minute_tick の更新専用
・AC制御、ブザー、LCD、センサー処理は行わない

【外部割り込み(緑ボタン:生存判定)】
・割り込み内ではフラグ(survival_irq)を立てるのみ
・チャタリング対策・復帰処理は loop() 側で実施
・TIMEOUT中(ブザー中)のみ復帰可能(timeout_interrupt_enable)

【共有変数ルール】
・ISR と loop() で共有する変数は volatile 宣言必須
・複数バイト変数は「ISRで書く/loopで読む」方向に限定
・loop側での読み取りは瞬間的な不整合を許容する設計

【設計意図】
・割り込み暴走・ネスト・予期せぬ再入を防止
・タイミング依存バグの発生源を最小化
・デバッグ性と将来保守性を最優先

※ 本ポリシーを崩す変更を行う場合は、
必ず全ISRと共有変数の再レビューを行うこと
===================================================== */


/* =====================================================
動作状態遷移(Dead Man Timer State Diagram)

【状態一覧】
IDLE : 電源投入直後/設定中
RUN : タイマ運転中
TIMEOUT : 時間切れ警告状態(一時復帰可)
SHUTDOWN : 停止終端状態(原因:異常 / TIMEOUT後)

-----------------------------------------------------
+----------------+
| IDLE |
| (SETUP中) |
+----------------+
|
| 設定完了 / START(緑PB押下)
v
+----------------+
| RUN |
| (運転中) |
| |
| ・Timer1 ISR |
| 1Hz秒/分管理 |
| ・TM_left-- |
| ・緑LED点滅 |
+----------------+
| | |
| | |
| | +----------------------+
| | |
| | 過電流 / 過熱 / |
| | センサー異常 |
| v v
| +----------------+ +----------------+
| | SHUTDOWN | | SHUTDOWN |
| | (異常停止) | | (安全停止) |
| | | | |
| | ・AC100V OFF | | ・AC100V OFF |
| | ・復帰不可 | | ・無限待機 |
| | ・RESET必要 | | ・RESET必要 |
| +----------------+ +----------------+
|
|
+————————————+
| 緑PB押下
| v
| TM_leftをTM_setに戻して運転継続
| TM_left == 0
v
+----------------+
| TIMEOUT |
| (時間切れ) |
| |
| ・ブザー鳴動 |
| ・緑PBで復帰可 |
| (限定時間) |
+----------------+
|      |
|     | 緑PB押下
|     | (timeout_interrupt_enable)
|      v
|      RUN に復帰(TM_leftをTM_setに戻して運転再開)
|
v
+----------------+
| SHUTDOWN |
| (安全停止) |
| |
| ・AC100V OFF |
| ・無限待機 |
| ・RESET必要 |
+----------------+


-----------------------------------------------------
【補足説明】

・RUN
TM_set=TM_leftで運転開始し、
運転中は緑PBが押されない限り、
Timer1 ISRにより1分ごとにTM_leftをデクリメント
緑PBが押されるとTM_leftをTM_setの値に戻し運転継続(延長)

・RUN → TIMEOUT
Timer1 ISR により TM_left が 0 になった場合
TIMEOUTを知らせるブザー鳴動開始

・TIMEOUT → RUN
ブザー鳴動中のみ緑PB割り込みを許可
(timeout_interrupt_enable == true)
復帰時はTM_leftをTM_setに戻し運転継続

・TIMEOUT → SHUTDOWN
  TIMEOUTブザーシーケンス完了後、
  復帰操作(緑PB)が無かった場合は自動的にSHUTDOWNへ遷移

・RUN → SHUTDOWN
過電流 / 過熱 / センサー異常を検出した場合
→ 安全のため復帰不可

・SHUTDOWN 状態からの復帰は
電源断 または MCU RESET のみ

===================================================== */






Posted at 2026/01/13 16:48:58 | コメント(0) | トラックバック(0) | 工具
2025年09月22日 イイね!

Velenoのアルティメットフォグランププレゼントキャンペーン

この記事は、新商品リリースキャンペーン!ULTIMATEフォグランプが3名様に当たる!🎯について書いています。

欲しい車種:トヨタタンク、
欲しい形状:H16(VelenoタイプDフォグランプユニット用)、
欲しいカラー:イエロー

現在、VelenoのLEDバルブを装着しているのは

トヨタタンクのヘッドライト(プロジェクタータイプH4)、フォグランプ(ハロゲン色H16)

マシンX(-TRAIL)のヘッドライト(配線加工タイプモルターレD2S)、フォグランプにはVelenoのホワイト/イエローカラーチェンジ)

などですが、
タンクのフォグはあえての旧モデルハロゲン色のものを入れています。
しかしやはりイエローの最新バルブも欲しい!

ということでよろしくお願いします。笑

Posted at 2025/09/22 14:17:21 | コメント(0) | トラックバック(0) | クルマ
2025年08月12日 イイね!

豆タンク(仮称) 納車レディパッケージ 〜 リヤ用ドラレコ&ルームランプ編 〜

豆タンク(仮称) 納車レディパッケージ 〜 リヤ用ドラレコ&amp;ルームランプ編 〜 豆タンク(仮称)の納車前準備の続きです。

先日、フロント用のドラレコはパイオニア(カロッツェリア)楽ナビAVIC-RL801-Dに合わせてドライブレコーダーリンク機能でナビ連動させられる様に中古のND-DVR1にすると書きましたが、今度はリア用のドラレコ。



AKEEYO の激安ドライブレコーダーAKY-P1にします。

こちらの方が旧いカロッツェリアのND-DVR1よりも画質良さそうな上にND-DVR1はカメラが小さく目立たないのでND-DVR1をリヤ用にしてAKY-P1をフロント用にしようか迷いましたが、配線の取り回しなど色々考えて結局AKY-P1をリアにします。

少し大きいですがモニターレスですしバックドアに設置してもそこまで悪目立ちはしないかなと。

映像はAKEEYOのアプリからWi-Fi経由で確認・ダウンロードできます。
もちろん設定もWi-Fi経由です。

電源は常時駐車監視はしなければ付属のUSB-A to USB-Cケーブルで行けるんで、USB-C側をバックドアジャバラに通すのもそもまで苦労しないでしょう。

パイオニアのND-DVR1をリヤ用としてバックドアガラスにつけるならカメラ部分解してはんだを外してからじゃないと厳しそうだしケーブル長さも延長しないと足りなそうでしたので。。

USB電源はナビ裏あたりでACCからエーモンの USB取り出しのをつけて簡単に済ませます。



ドラレコと直接関係はありませんが、センターコンソールあたりのゴミ箱上のスイッチホールを利用して USB-C(PD)と USB-A(QC3.0)を増設してみます。











あとはルームランプのLED化は、車種別のキットでYOURS(ユアーズ)ってとこの40%、70%、100%と明るさを変更できるものを準備。

もちろんホワイトで。

一応、全部入り的なセットで、おまけでブルーのT10のLEDもついてきました。

あとはうちに来るタンクさんはG-Sということで4スピーカーではあるのですがフロントのツィーターが無いので適当なスピーカーを探してきましょう。

私的にはリヤスピーカー不要でフロント2Wayを、しっかり調整したいところですが家族の車なんでまあそこまではこだわりません。


あとはメーカーオプションのナビレディパッケージ用のステアリングリモコン左も準備してあるんで、AVIC-RL801-D用のステアリングスイッチ接続用のアダプターも用意しました。

たぶん使えると思うで集めていますが、なんせまだ現車がないんで予定変更もあり得ます。





Posted at 2025/08/12 13:09:12 | コメント(1) | トラックバック(0)
2025年08月09日 イイね!

豆タンク(仮称) 納車レディパッケージ 〜 ドラレコ編 〜

豆タンク(仮称) 納車レディパッケージ 〜 ドラレコ編 〜 豆タンク(仮称)の納車前準備を進めていますが、どうやらナビはパイオニア(カロッツェリア)の2017年モデルの8インチ楽ナビ

AVIC-RL801-D が付いている様でしたのでそれに合わせて準備しています。





このモデルはどうやら一般販売はしていない業販モデルの様なのでディーラーオプションかなんかなのでしょう。

地図やナビプログラムは2017年のままでしたのでMAPファンの地図割プラスを利用して2025年版に更新予定。




そしてこのナビはパイオニアのドラレコ
ND-DVR1
またはその後継モデル
VREC-DS600
を繋げばナビ画面でドラレコ映像が確認できたりするようなのでちと旧いですが、
家族の同意を得てND-DVR1を中古で調達



本体取り付けブラケットやカメラの両面テープ残りが酷かったので両面テープ剥がしをしておきます。



カメラはフロントガラスへ、本体はダッシュボード下あたりに取り付ける予定です。

セパレートタイプでしかもナビ連動なので映像はナビで確認、モニターは無い物です。

今時なので車両モニターでの確認に合わせてWi-Fi経由などでスマホでも見れたら良いんですが、残念ながらそういうものでは無さそうです。



SDカードはMicro-SDHCの32GBまでが正式対応の様です。

64GBのSDXCで行けた報告とか、128GBのSDXCをPC経由でI/OデータだかのHDDフォーマッタを使ってFAT32でフォーマットしたらND-DVR1で使えたとか色々情報ありましたが、

本体でフォーマットできる方が良かったのと32GBでフルHD画質?で5時間以上は録画できる様でしたのであえて冒険はせずに32GBのSDHCを用意しました。



両面テープはエーモンのガラス用をカメラに、本体にはエーモンのプラスチック用のを用意。

これをリヤ用としても良さそうですがどうですかね?とりあえずフロントに取り付ける予定ではあります。







Posted at 2025/08/09 17:06:34 | コメント(0) | トラックバック(0)
2025年08月08日 イイね!

豆タンク(仮称) 〜 納車レディパッケージ フォグランプ防水処理編 〜

豆タンク(仮称) 〜 納車レディパッケージ フォグランプ防水処理編 〜 豆タンク(仮称)の納車まで、フォグランプ後付けやヘッドライトのプロジェクター型LEDバルブなど必要なものを着々と準備しています。

フォグランプは純正ハーネス、純正デイライト、純正フォグランプユニット、フォグランプリレー、フォグランプ付き用ディマースイッチ、フォグランプベゼルなどを調達。

で、フォグランプユニットは純正は使用せず、VELENO 車種専用フォグランプユニットに、VELENOのハロゲン色H16 LEDバルブを使う予定。

まずはVELENOのフォグランプユニットの防水処理からやっておきましょう。



まずはユニット本体(ハウジング)とレンズの合わせ面に超透明コーキング剤を塗布



塗った時は白ですが固まったら限りなく透明になるのでわかりやすくてオススメです。



純正デイライトも同様にコーキング



コーキング硬化後、さらにブチルゴムテープ(今回はコニシのテープ状コークのグレー)を半分の幅で切って一周巻きました。




その後、ブチルゴムのベトベトが嫌なので防振用配線テープ(布)を巻いておきました。



フォグに使うLEDバルブは、イエローかハロゲン色か悩みましたが、私の車じゃないのでイエローだと家族がびっくりしちゃうかなと思いあえてのハロゲン色で。

通常ラインナップにないからか、箱はホワイト6300Kになってましたが、明細はハロゲン色と書いてある所にピンク色の蛍光ペンでチェックして間違い防止っぽくしてあったのでおそらく間違ってはないと思います。

アウトレット品(廃番品)なので安いのですがハロゲン色もホワイトもイエローもまだある様です。





バルブ部分からの防水もしておきます。

バルブ側にパッキンが入ってますが、その当たり面にも防水用のシリコングリスを塗布。

ただのシリコングリスではなく防水用途に特化したシリコングリスが家にあったので貰いました。



シリコングリスだけでも十分とは思いましたが、そちらにもテープ状コークを使用。

エアコンパテでもよかったんですけどね。


あとは冷却ファン部分の防水だけが心配ですが、その辺はおそらく大丈夫かなと思い様子見です。

あとは取り付け時にカプラー部やドライバーユニットの防水、振動対策などをする予定。

まだまだ納車前準備の段階でも色々ヒマが潰せます。w















Posted at 2025/08/08 13:05:29 | コメント(2) | トラックバック(0)

プロフィール

「はんだごて切り忘れ防止用デッドマンタイマ「切忘 無く四郎(死郎)」DMT-KW7946 Ver.6.8 http://cvw.jp/b/3407800/48875413/
何シテル?   01/13 16:48
とんこつラパン です。よろしくお願いします。 みんカラ久しぶり(14年以上ぶりくらい?)に始めたのでやり方を忘れてしまいました。 無言フォロー等、失礼致します。...
みんカラ新規会員登録

ユーザー内検索

<< 2026/1 >>

    123
45678910
1112 1314151617
18192021222324
25262728293031

ブログカテゴリー

リンク・クリップ

OS GARAGE OSGオリジナルリフトアップコイル(NT31系) 
カテゴリ:その他(カテゴリ未設定)
2023/04/12 02:25:06
メッキドアストライカー 
カテゴリ:その他(カテゴリ未設定)
2022/12/17 15:43:42
スズキ(純正) マルチユースバッグ 
カテゴリ:その他(カテゴリ未設定)
2022/10/11 18:17:28

愛車一覧

日産 エクストレイル マシンX(-TRAIL) (日産 エクストレイル)
2022/6/22現在、自分の名義に名義変更中です。(完了)
トヨタ タンク 豆タンク (トヨタ タンク)
パレットさんより乗り換え 豆タンクです。
スズキ セルボ セルボどーも(アントンしかお) (スズキ セルボ)
2024年10月16日譲っていただきました。 走行なんと43000km! 現在まだ名義変 ...
スズキ パレット パレットさん (スズキ パレット)
私が新車で購入しましたが親の車です。私もちょいちょいメンテナンスしているのでみんカラのガ ...

過去のブログ

2026年
01月02月03月04月05月06月
07月08月09月10月11月12月
2025年
01月02月03月04月05月06月
07月08月09月10月11月12月
2024年
01月02月03月04月05月06月
07月08月09月10月11月12月
2023年
01月02月03月04月05月06月
07月08月09月10月11月12月
2022年
01月02月03月04月05月06月
07月08月09月10月11月12月
2021年
01月02月03月04月05月06月
07月08月09月10月11月12月
ヘルプ利用規約サイトマップ
© LY Corporation