スイスポ(ZC33S)のICWSをESP32でブースト連動オート化するコントローラーを自作 - スイスポくんのカスタム日記

スイスポ(ZC33S)のICWSをESP32でブースト連動オート化するコントローラーを自作

前回の記事でICWS(インタークーラーウォータースプレー)の配管とノズルを取り付けましたが、噴射のタイミングを制御する仕組みがありませんでした。今回はESP32とADS1115を使って、ブースト圧が一定値を超えたら自動でポンプを駆動するコントローラーを自作しました。ブースト計に繋がっているセンサーの信号線に分岐して割り込み、スマホからWiFiで接続してしきい値やキャリブレーションをブラウザ上で調整できるようにしています。回路図・部品表・実際に書き込んだソースコード全文をまとめます。

ZC33SのICWSをブースト圧で自動噴射させる#

理想としては「ブーストが0.5barを超えたら噴射する」という制御でした。配管と配線はすでに終わっているので、あとはブースト圧を読んで閾値を超えたらポンプの12Vをオンにする「頭脳」だけが必要な状態です。

選択肢は大きく2つありました。ホブスイッチ(調整式圧力スイッチ)とリレーを使う無コードの構成は頑丈で壊れにくい一方、閾値固定・常時噴射でヒステリシスや間欠噴射ができません。マイコン制御ならしきい値・ヒステリシス・間欠噴射・キャリブレーションまで自由に作り込めます。

ホースの小容量なリアウォッシャータンクを流用しているので、ブーストがかかっている間ずっと噴射し続けるとすぐに水切れします。 閾値を超えている間だけ間欠的に噴射する制御にしたかったので、マイコン制御(ESP32)を選びました。

設計方針:ADS1115でブースト計のセンサー信号を横取りする#

ESP32はWiFiでの設定変更ができる点が魅力ですが、内蔵ADCはノイズが多く非線形なので、ブースト圧のようなアナログ信号を精度良く読むには向いていません。そこで、外付けの高精度ADCであるADS1115(16bit I2C)を組み合わせました。

ブースト検出用のセンサーですが、新たに追加購入はしていません。すでに取り付けてあるAutoGauge製ブーストメーターのセンサー信号線(白線、3線式アナログ電圧出力)に、ADS1115を高インピーダンスで並列に接続しています。 ADS1115の入力インピーダンスは非常に高く、47kΩ×2の分圧を通しても流れる電流は3V時で約32µAしかないため、ブーストメーター本体の指示にはほぼ影響しません。これで専用のMAPセンサー代(¥1,500〜5,000)が浮きました。

回路図:ADS1115でブースト信号を読み、MOSFETでポンプを駆動#

ICWS ESP32コントローラーの回路図

大きく3つのブロックに分かれています。

  • ①電源系統: 車のACC(+12V)を、MCU用(1Aヒューズ)とポンプ用(7.5Aヒューズ)に分けて別々に保護しています。MCU側はmini-360バックコンバータで5Vちょうどに調整してからESP32のVINへ。ポンプはモーターなので始動時に定常の2〜3倍の電流が流れることがあるため、5Aだと切れる(ニューサンスブロー)恐れがあり7.5Aにしました。
  • ②ブースト信号入力: ブーストメーターの白線を47kΩ×2で分圧し、中点をADS1115のA0に接続しています。ADS1115はI2C(SDA=GPIO21、SCL=GPIO22)でESP32と通信します。
  • ③出力: GPIO25からMOSFET(IRLZ44N)のゲートを駆動してポンプをON/OFFします。ゲートには10kΩのプルダウン抵抗が必須です。 これがないと、ESP32がフリーズしたり電源が落ちたりしたときにゲートの電位が不定になり、意図せず噴射し続ける可能性があります。ポンプにはフライバックダイオード(SR54F)を並列に入れて、モーターの誘導性キックバックからMOSFETを保護しています。

冬場など、そもそもICWS自体を使いたくない場合のために、ACCからバックコンバータへの給電ラインにマスタートグルスイッチを入れてあります。これをOFFにすればソフトウェアとは無関係にMCUの電源ごと落ちるので、確実に噴射しません。

部品表とコスト目安#

部品用途目安価格購入先
ESP32 開発ボード(WROOM-32D 38pin)メインマイコン¥837AliExpress
ADS1115モジュール16bit I2C ADC¥314AliExpress
mini-360 バックコンバータ12V→5V降圧¥470(5個入り)AliExpress
抵抗キット(300pcs/30種類、E12)各所抵抗値¥405AliExpress
ロジックレベルMOSFET IRLZ44Nポンプ駆動数十円〜秋月電子
SR54F(SMBショットキー)フライバック/逆接保護兼用¥250(10個)秋月電子
ヒューズホルダー+ミニ平型7.5Aポンプ系統保護¥749(5個セット、防水)Amazon
トグルSW・プッシュSW・LED赤緑マスターSW/手動噴射/状態表示数百円千石電商
プロジェクトボックス収納ケース数百円秋月電子/マルツ

抵抗値は、100Ω(MOSFETゲート直列)・10kΩ(ゲートプルダウン、省略不可)・47kΩ×2(白線の分圧)・220Ω(緑LED)・330Ω(赤LED)を使っています。当初は33kΩ×2で分圧する想定でしたが、買った抵抗キットに33kΩが無かったため47kΩ×2に変更しました。 分圧比が変わってもソフト側のキャリブレーションで吸収されるので、コード変更は不要です。

合計は、全部新規で買うと約6,000〜8,000円、ESP32や工具類が手持ちにあれば4,000円前後に収まる見積もりでした。実際にはAliExpressでまとめ買いして送料無料に乗せたり、mini-360が5個入りで余ったりするので、次のプロジェクト用の予備も確保できました。

ブレッドボード試作からユニバーサル基板・防水ケースへ#

まずはブレッドボードで試作します。ロジック側だけを先に組んで、ブースト読み・0.5bar判定・LED・プッシュスイッチが動くことを確認してから、最後に12V/5Aのポンプ回路を太い配線で追加しました。ブレッドボードの接点は5Aを流すと発熱して接触不良の原因になるので、ポンプ回路だけはブレッドボード外で配線しています。

ICWS ESP32コントローラー - ブレッドボード試作、ADS1115とMOSFET周りの配線

SR54Fダイオードは表面実装(SMB)パッケージで脚が無いので、そのままではブレッドボードに挿せません。両端に単線をはんだ付けして脚を作ってから挿しています。 極性は帯マーク側がカソードです。

ICWS ESP32コントローラー - ブレッドボード全体、赤と青のLEDが状態表示

ロジックの動作を確認できたら、ESP32とADS1115をユニバーサル基板にはんだ付けして実装します。

ICWS ESP32コントローラー - ユニバーサル基板にESP32とADS1115を実装

最後にプロジェクトボックスに収めます。ミニ平型ヒューズとホルダーも一緒に収納しました。

ICWS ESP32コントローラー - プロジェクトボックスに収納、ヒューズホルダーと基板

ICWS ESP32コントローラー - ケースを閉じる直前の配線状態

ちなみに組み立て中、フラッシュ書き込みができなくなるトラブルに遭遇しました。ログに「フラッシュが読めていない」と出ていたのを頼りに、テスターで基板を当たっていくと5V↔CMD間のはんだブリッジ(意図しないはんだの橋渡し)を発見。除去したら無事に書き込めるようになりました。ユニバーサル基板の手はんだでは、こういう物理的な短絡が原因になることもあるようです。

WiFi設定ページでしきい値とキャリブレーションを調整#

起動するとESP32が「ICWS-ZC33S」というWiFiアクセスポイントを立てるので、スマホから接続してhttp://192.168.4.1を開くと設定ページが表示されます。ダークテーマで、現在のブースト値・状態(IDLE/ARMED/SPRAY)・ピーク値がリアルタイムに表示されます。

設定できる項目は次のとおりです。

  • Boost ON / Boost OFF: 噴射を開始する閾値と、停止する閾値(ヒステリシス)。閾値付近でのバタつきを防ぎます。
  • Spray ON / Spray OFF: 間欠噴射のON時間・OFF時間。タンク容量が小さいので、常時噴射ではなく間欠にして水を節約しています。
  • Capture 0 bar: エンジン停止・キーONの状態(インマニ=大気圧)でボタンを押すと、そのときの電圧を0bar基準として保存します。
  • Capture slope: 既知のブースト値で安定させた状態でボタンを押すと、傾き(mV/bar)を計算して保存します。ZC33Sだと1.0barを維持し続けるのが難しいので、known欄の初期値は0.64barにしています。

設定値はESP32のNVS(フラッシュ内の独立領域)に保存されるので、スケッチを書き込み直しても消えません。「Erase All Flash Before Sketch Upload」を有効にしない限り、キャリブレーションはそのまま引き継がれます。

ESP32のソースコード全文#

実際に書き込んで動いているソースコードです。ライブラリはAdafruit ADS1X15(+ Adafruit BusIO)を使っています。WiFi/WebServer/Preferencesは ESP32 core に含まれるものです。

/*
Intercooler Water Spray Controller / ZC33S
------------------------------------------------------------
MCU : ESP32 (Arduino core "ESP32 Dev Module")
Boost in : AutoGauge 548 boost sensor WHITE wire (signal, ~1V @0bar)
tapped via 1k series -> ADS1115 A0. Sensor BLACK -> common GND.
RED is left alone (the gauge powers the sensor). READ-ONLY: never
drive voltage onto WHITE.
Pump out : logic-level MOSFET low-side. Gate needs an external 10k pulldown
to GND so the pump stays OFF whenever the MCU is unpowered.
Flyback diode across the pump. Fuse (5-7.5A) on the pump 12V.
LEDs : RED = hardware only, 5V rail -> 330ohm -> LED -> GND (power on/off)
GREEN= GPIO -> 220ohm -> LED -> GND (lit while spraying; case A:
mirrors the pump, so it pulses with interval bursts)
Master : a toggle on the 12V(ACC) feed to the buck cuts MCU power = OFF.
No software involvement; gate pulldown guarantees no spray.
Config : phone -> WiFi AP "ICWS-ZC33S" -> http://192.168.4.1
Thresholds / interval / calibration are editable and persisted
to NVS (survive power cycles). No logging.
Libraries: Adafruit ADS1X15 (+ Adafruit BusIO). WiFi/WebServer/Preferences
are part of the ESP32 core.
------------------------------------------------------------
CALIBRATION (boost_bar = (mV - v0mV) / slopeMvBar):
1) Key ON, engine OFF (manifold = atmosphere = 0 bar). Open the page,
tap "Capture 0 bar" -> sets v0mV.
2) Do a pull to a boost the AutoGauge gauge clearly shows (use its
peak-recall). The page holds Peak mV. Then set slope by hand:
slopeMvBar = (peakMv - v0mV) / peakBoost, type it into "slope",
or while sitting at a known steady boost use "Capture slope point".
*/
#include <Wire.h>
#include <Adafruit_ADS1X15.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
// ---------- Pin map (ESP32) ----------
const int PIN_MOSFET = 25; // -> gate (EXTERNAL 10k pulldown to GND required)
const int PIN_LED_GREEN = 26; // -> 220ohm -> green LED -> GND (spray active)
const int PIN_BTN = 27; // momentary push to GND (INPUT_PULLUP)
// I2C: SDA=21, SCL=22 -> ADS1115 (addr 0x48)
// ---------- WiFi AP ----------
const char* AP_SSID = "ICWS-ZC33S";
const char* AP_PASS = "spray12345"; // must be >= 8 chars
Adafruit_ADS1115 ads;
WebServer server(80);
Preferences prefs;
// ---------- Config (persisted in NVS) ----------
struct Config {
float boostOn; // bar, arm threshold
float boostOff; // bar, disarm threshold (hysteresis)
uint32_t onMs; // interval spray ON (set offMs=0 for continuous)
uint32_t offMs; // interval spray OFF
float v0mV; // white-wire mV at 0 bar (gauge)
float slopeMvBar; // mV per bar
} cfg;
void loadCfg() {
prefs.begin("icws", false);
cfg.boostOn = prefs.getFloat("boostOn", 0.40f);
cfg.boostOff = prefs.getFloat("boostOff", 0.20f);
cfg.onMs = prefs.getUInt ("onMs", 3000);
cfg.offMs = prefs.getUInt ("offMs", 1000);
cfg.v0mV = prefs.getFloat("v0mV", 456.2f); // from screenshot; recalibrate with real sensor in car
cfg.slopeMvBar = prefs.getFloat("slope", 456.2f); // from screenshot; recalibrate with real sensor in car
prefs.end();
}
void saveCfg() {
prefs.begin("icws", false);
prefs.putFloat("boostOn", cfg.boostOn);
prefs.putFloat("boostOff", cfg.boostOff);
prefs.putUInt ("onMs", cfg.onMs);
prefs.putUInt ("offMs", cfg.offMs);
prefs.putFloat("v0mV", cfg.v0mV);
prefs.putFloat("slope", cfg.slopeMvBar);
prefs.end();
}
// ---------- Runtime state ----------
bool adsOk = false;
float lastMv = 0, lastBoost = 0, peakMv = 0;
bool armed = false, sprayPhaseOn = true, spraying = false;
uint32_t lastPhase = 0, lastSample = 0;
float readMv() {
if (!adsOk) return 0;
int16_t raw = ads.readADC_SingleEnded(0);
return ads.computeVolts(raw) * 1000.0f;
}
float toBoost(float mv) {
if (cfg.slopeMvBar < 1.0f) return 0;
return (mv - cfg.v0mV) / cfg.slopeMvBar;
}
// ---------- Web ----------
String page() {
String s = F(
"<!doctype html><html><head><meta charset=utf-8>"
"<meta name=viewport content='width=device-width,initial-scale=1'>"
"<title>ICWS</title><style>"
"*{box-sizing:border-box}"
"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;"
"margin:0;background:#0b0f14;color:#e6edf3;-webkit-font-smoothing:antialiased}"
".wrap{max-width:480px;margin:0 auto;padding:18px 14px 40px}"
"header{display:flex;align-items:baseline;gap:10px;margin:6px 2px 16px}"
".logo{font-size:22px;font-weight:800;letter-spacing:.5px;"
"background:linear-gradient(90deg,#22d3ee,#38bdf8);-webkit-background-clip:text;"
"background-clip:text;-webkit-text-fill-color:transparent}"
".sub{font-size:11px;color:#7d8da1;text-transform:uppercase;letter-spacing:2px}"
".card{background:#11171f;border:1px solid #1e2733;border-radius:16px;padding:16px;margin-bottom:14px}"
".card h3{margin:0 0 10px;font-size:11px;letter-spacing:2px;text-transform:uppercase;color:#7d8da1;font-weight:600}"
".live{text-align:center}"
".status{display:inline-block;font-size:11px;font-weight:700;letter-spacing:2px;padding:4px 14px;border-radius:999px;margin-bottom:4px}"
".s-idle{background:#1b2430;color:#7d8da1}"
".s-armed{background:#3a2a06;color:#fbbf24}"
".s-spray{background:#06363f;color:#22d3ee;animation:pulse 1s infinite}"
"@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}"
".boost{font-size:54px;font-weight:800;line-height:1;margin:6px 0;font-variant-numeric:tabular-nums}"
".boost .unit{font-size:18px;color:#7d8da1;font-weight:600;margin-left:6px}"
".bar{position:relative;height:10px;background:#1b2430;border-radius:999px;margin:14px 0 10px;overflow:hidden}"
".fill{position:absolute;left:0;top:0;bottom:0;width:0;border-radius:999px;transition:width .25s ease}"
".fill.s-idle{background:#475569}.fill.s-armed{background:#f59e0b}"
".fill.s-spray{background:linear-gradient(90deg,#22d3ee,#38bdf8)}"
".mark{position:absolute;top:-3px;width:2px;height:16px;background:#e6edf3;opacity:.55}"
".meta{display:flex;justify-content:space-between;font-size:13px;color:#9fb0c3}"
".meta b{color:#e6edf3}"
"label{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:14px;padding:9px 0;border-bottom:1px solid #161d27}"
"label:last-child{border-bottom:0}"
".ig{display:flex;align-items:center;gap:8px}"
".u{color:#7d8da1;font-size:12px}"
"input{background:#0b0f14;border:1px solid #28323f;color:#e6edf3;border-radius:10px;padding:9px 11px;width:96px;font-size:15px;text-align:right;font-variant-numeric:tabular-nums}"
"input:focus{outline:none;border-color:#22d3ee;box-shadow:0 0 0 3px rgba(34,211,238,.15)}"
"button{font-family:inherit;cursor:pointer;border:0;border-radius:11px;padding:11px 14px;font-size:14px;font-weight:600}"
".primary{width:100%;background:linear-gradient(90deg,#22d3ee,#38bdf8);color:#04121a;font-weight:800;letter-spacing:.5px}"
".ghost{background:transparent;border:1px solid #28323f;color:#9fb0c3;margin-top:12px}"
".cap{background:#1b2430;color:#e6edf3;border:1px solid #28323f}"
".full{width:100%}"
".row{display:flex;gap:8px;align-items:center}"
".row input{width:64px}.row button{flex:1}"
"form{margin:0}"
"</style></head><body><div class=wrap>"
"<header><div class=logo>&#9889; ICWS</div><div class=sub>ZC33S&nbsp;Water&nbsp;Spray</div></header>"
"<section class='card live'>"
"<div class=status id=status>--</div>"
"<div class=boost><span id=boost>--</span><span class=unit>bar</span></div>"
"<div class=bar><div class=fill id=fill></div><div class=mark id=mark></div></div>"
"<div class=meta><span id=mv>-- mV</span><span>Peak <b id=peak>--</b> mV</span></div>"
"<form method=POST action=/resetpk><button class=ghost>reset peak</button></form>"
"</section>"
"<form method=POST action=/set>"
"<section class=card><h3>Thresholds</h3>");
s += "<label><span>Boost ON</span><span class=ig><input name=boostOn value=" + String(cfg.boostOn,2) + "><span class=u>bar</span></span></label>";
s += "<label><span>Boost OFF</span><span class=ig><input name=boostOff value=" + String(cfg.boostOff,2) + "><span class=u>bar</span></span></label>";
s += "<label><span>Spray ON</span><span class=ig><input name=onMs value=" + String(cfg.onMs) + "><span class=u>ms</span></span></label>";
s += "<label><span>Spray OFF</span><span class=ig><input name=offMs value=" + String(cfg.offMs) + "><span class=u>ms</span></span></label>";
s += F("</section><section class=card><h3>Calibration</h3>");
s += "<label><span>v0 @0&nbsp;bar</span><span class=ig><input name=v0mV value=" + String(cfg.v0mV,1) + "><span class=u>mV</span></span></label>";
s += "<label><span>slope</span><span class=ig><input name=slope value=" + String(cfg.slopeMvBar,1) + "><span class=u>mV/bar</span></span></label>";
s += F("</section><button class=primary type=submit>Save</button></form>"
"<section class=card><h3>Quick Calibrate</h3>"
"<form method=POST action=/zero><button class='cap full'>Capture 0&nbsp;bar &middot; set v0 = now</button></form>"
"<form method=POST action=/cal class=row style='margin-top:10px'>"
"<span class=u>known</span><input name=kb value=1.0><span class=u>bar</span>"
"<button class=cap>Capture slope</button></form>"
"</section>"
"<script>var ON=");
s += String(cfg.boostOn,2);
s += F(";function pct(b){var lo=-1.0,hi=2.0,p=(b-lo)/(hi-lo)*100;return Math.max(0,Math.min(100,p));}"
"document.getElementById('mark').style.left=pct(ON)+'%';"
"setInterval(function(){fetch('/live').then(r=>r.json()).then(j=>{"
"document.getElementById('boost').textContent=j.boost.toFixed(2);"
"document.getElementById('mv').textContent=j.mv.toFixed(0)+' mV';"
"document.getElementById('peak').textContent=j.peak.toFixed(0);"
"var c=j.spray?'s-spray':(j.armed?'s-armed':'s-idle');"
"var st=document.getElementById('status');"
"st.textContent=j.spray?'SPRAY':(j.armed?'ARMED':'IDLE');st.className='status '+c;"
"var f=document.getElementById('fill');f.style.width=pct(j.boost)+'%';f.className='fill '+c;"
"}).catch(e=>{});},250);"
"</script></div></body></html>");
return s;
}
void handleRoot() { server.send(200, "text/html", page()); }
void handleLive() {
String j = "{\"mv\":" + String(lastMv,1) + ",\"boost\":" + String(lastBoost,3)
+ ",\"peak\":" + String(peakMv,1)
+ ",\"armed\":" + (armed ? "true" : "false")
+ ",\"spray\":" + (spraying ? "true" : "false") + "}";
server.send(200, "application/json", j);
}
float argF(const char* n, float d) { return server.hasArg(n) ? server.arg(n).toFloat() : d; }
void handleSet() {
cfg.boostOn = argF("boostOn", cfg.boostOn);
cfg.boostOff = argF("boostOff", cfg.boostOff);
cfg.onMs = (uint32_t)argF("onMs", cfg.onMs);
cfg.offMs = (uint32_t)argF("offMs", cfg.offMs);
cfg.v0mV = argF("v0mV", cfg.v0mV);
cfg.slopeMvBar = argF("slope", cfg.slopeMvBar);
if (cfg.boostOff > cfg.boostOn) cfg.boostOff = cfg.boostOn; // keep hysteresis sane
saveCfg();
server.sendHeader("Location", "/"); server.send(303, "text/plain", "");
}
void handleZero() {
cfg.v0mV = readMv(); saveCfg();
server.sendHeader("Location", "/"); server.send(303, "text/plain", "");
}
void handleCal() {
float kb = argF("kb", 0);
if (kb > 0.05f) {
float sl = (readMv() - cfg.v0mV) / kb;
if (sl > 100.0f) { cfg.slopeMvBar = sl; saveCfg(); }
}
server.sendHeader("Location", "/"); server.send(303, "text/plain", "");
}
void handleResetPk() {
peakMv = lastMv;
server.sendHeader("Location", "/"); server.send(303, "text/plain", "");
}
void setup() {
Serial.begin(115200);
pinMode(PIN_MOSFET, OUTPUT); digitalWrite(PIN_MOSFET, LOW);
pinMode(PIN_LED_GREEN, OUTPUT); digitalWrite(PIN_LED_GREEN, LOW);
pinMode(PIN_BTN, INPUT_PULLUP);
loadCfg();
Wire.begin(21, 22);
adsOk = ads.begin(); // ADS1115 @ 0x48
if (adsOk) ads.setGain(GAIN_TWOTHIRDS); // +-6.144V (headroom; add divider if signal nears 5V)
else Serial.println("ADS1115 not found");
WiFi.mode(WIFI_AP);
WiFi.softAP(AP_SSID, AP_PASS);
WiFi.setTxPower(WIFI_POWER_8_5dBm); // 約8.5dBm ≈ 最大出力の約10%
server.on("/", handleRoot);
server.on("/live", handleLive);
server.on("/set", HTTP_POST, handleSet);
server.on("/zero", HTTP_POST, handleZero);
server.on("/cal", HTTP_POST, handleCal);
server.on("/resetpk", HTTP_POST, handleResetPk);
server.begin();
}
void loop() {
server.handleClient();
uint32_t now = millis();
if (now - lastSample >= 20) { // sample ~50 Hz
lastSample = now;
lastMv = readMv();
lastBoost = toBoost(lastMv);
if (lastMv > peakMv) peakMv = lastMv;
if (lastBoost >= cfg.boostOn) armed = true; // hysteresis
else if (lastBoost <= cfg.boostOff) armed = false;
}
if (armed) { // interval duty machine
if ( sprayPhaseOn && (now - lastPhase >= cfg.onMs)) { sprayPhaseOn = false; lastPhase = now; }
if (!sprayPhaseOn && (now - lastPhase >= cfg.offMs)) { sprayPhaseOn = true; lastPhase = now; }
} else {
sprayPhaseOn = true; lastPhase = now; // next arm starts on a spray burst
}
bool autoSpray = armed && sprayPhaseOn;
bool manual = (digitalRead(PIN_BTN) == LOW); // held = force spray, ignores boost
spraying = autoSpray || manual;
digitalWrite(PIN_MOSFET, spraying ? HIGH : LOW);
digitalWrite(PIN_LED_GREEN, spraying ? HIGH : LOW); // case A: green mirrors the pump
}

WiFiの電波は、車内でスマホと繋がれば十分なので出力を約10%(WIFI_POWER_8_5dBm)まで絞っています。混信や消費電流を減らす狙いです。

デモ動画:ICWSの状態表示と実際のブースト計測#

ICWSの状態表示(IDLE/ARMED/SPRAY)と、実際のブースト計測の様子を撮った動画です。

まとめ:ブースト連動の自動噴射までは完成、ノイズ対策は次の宿題#

ESP32とADS1115で、ICWSをブースト圧に連動して自動噴射するコントローラーを自作できました。ブーストメーターのセンサー信号を横取りして専用センサーを増やさずに済ませたので、部品代は全部新規で約6,000〜8,000円に収まっています。WiFi経由でしきい値やキャリブレーションをブラウザから調整できるので、実走しながら数値を詰めていけるのも便利です。

ただし正直に書いておくと、ブーストをかけていないのにたまに誤って噴射することがあり、原因はセンサーのノイズによる瞬間的なスパイクだと分かっています。しきい値超えが150ミリ秒連続したときだけ本物のブーストと判定するソフト側の対策は設計済みですが、この記事のソースコードにはまだ反映していません。次に車をいじるタイミングで書き込む予定です。

あわせて読みたい#

コメント