
スイスポ(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でポンプを駆動

大きく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) | メインマイコン | ¥837 | AliExpress |
| ADS1115モジュール | 16bit I2C ADC | ¥314 | AliExpress |
| mini-360 バックコンバータ | 12V→5V降圧 | ¥470(5個入り) | AliExpress |
| 抵抗キット(300pcs/30種類、E12) | 各所抵抗値 | ¥405 | AliExpress |
| ロジックレベル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を流すと発熱して接触不良の原因になるので、ポンプ回路だけはブレッドボード外で配線しています。

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

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

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


ちなみに組み立て中、フラッシュ書き込みができなくなるトラブルに遭遇しました。ログに「フラッシュが読めていない」と出ていたのを頼りに、テスターで基板を当たっていくと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>⚡ ICWS</div><div class=sub>ZC33S Water 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 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 bar · 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ミリ秒連続したときだけ本物のブーストと判定するソフト側の対策は設計済みですが、この記事のソースコードにはまだ反映していません。次に車をいじるタイミングで書き込む予定です。
あわせて読みたい
関連記事
「エンジン」の人気記事





