本文旨在補充《超圖解ESP32應用實作》第18章的車上診斷系統(OBD)實驗,方便沒有汽車的讀者使用ESP32當作OBD模擬器,傳遞虛擬的行車速度和引擎轉速值給另一個ESP32,並且把數值顯示在連線的手機瀏覽器。
本文的麵包板組裝電路,沿用《超圖解ESP32應用實作》第17章的「動手做17-1:ESP32 CAN匯流排通訊實驗」,加上一個類比搖桿來模擬車速和引擎轉速值:
- 水平(X)搖桿的輸入值:改變車速值0~180 (km/h)。
- 垂直(Y)搖桿的輸入值:改變引擎轉速值1500~4000 (RPM)
連接類比搖桿的ESP32開發板A,扮演車上診斷系統(OBD),執行OBD模擬程式;開發板B則是OBD診斷裝置,執行第18章「動手做18-2:在手機瀏覽器呈現即時車速和引擎轉速」的程式。

ESP32車上診動系統(OBD)模擬程式
OBD模擬器修改自coniferconifer編寫的ESP32-ECU-emulator,筆者僅刪除一些不必要的變數和前置處理器指令,然後修改CAN收發器的接腳設定,並且加入讀取搖桿數值的程式碼,請編譯底下的程式,上傳到「開發板A」備用:
#include <CAN.h>
#include <OBD2.h>
#define CTX_PIN 21 // CAN收發器傳送腳
#define CRX_PIN 22 // CAN收發器接收腳
#define IN_X 32 // 可變電阻X(水平搖桿)的輸入腳
#define IN_Y 33 // 可變電阻Y(垂直搖桿)的輸入腳
#define ECU_ID 0x7e8 //ECU id + 8
#define MIL_ON 0x80
#define DTC_CNT 0x01
uint8_t DTC[] = { MIL_ON | DTC_CNT , 0x00, 0x00} ; //A,B,C,D
uint16_t freezeDTC = 0x1234;
uint16_t fuelSystemStatus = 0x0200;
float engineEfficiency = 22.75; // %
float shortTermFuelTrimBank1 = -5.47;// %
float longTermFuelTrimBank1 = 7.2;// %
float shortTermFuelTrimBank2 = -5.47;// %
float longTermFuelTrimBank2 = 7.2;// %
uint8_t fuelPressure = 765;
uint8_t intakeManifoldAbsolutePressure = 255;
uint16_t engineRPM = 3925;
uint8_t vehicleSpeed = 120;
uint8_t timingAdvance = 8;
uint8_t airIntakeTemperature = 42 ;
float mafAirFlowRate = 6.50 ;
float throttlePosition = 5.5 ;// %
uint8_t commandedSecondaryAirStatus = 1;
uint8_t oxygenSensorsPresentIn2Banks = 3;
float oxygenSensor1 = 1.23 ; //Volt
float shortTermFuelTrim = 99.2; // %
uint8_t obdStandardThisVehicleConformsTo = 0x0a;
uint8_t oxygenSensorsPresentIn4Banks = 0x01;
uint8_t auxiliaryInputStatus = 0x01;
uint16_t distanceTraveledWithMilOn = 1000; // km DISTANCE_TRAVELED_WITH_MIL_ON
float fuelRailPressure = 5177.265; //kPa
float fuelRailGaugePressure = 655350.0; //kPa
uint8_t engineCoolantTemperature = 82;
uint8_t fuelLevel = 80; // %
float oxygenSensorFuelAir = 1.0; //ratio
float oxygenSensorFuelAirVoltage = 1.23; //volt max 8V
float controlModuleVoltage = 14.24;//Volt
float commandedEGR = 1.8; // %
float EGRError = 0.0 ; // %
float commandedEvaporativePurge = 10.0; //% COMMANDED_EVAPORATIVE_PURGE
uint8_t warmUpsSinceCodesCleared = 255; // counts WARM_UPS_SINCE_CODES_CLEARED
uint16_t distanceTraveledSinceCodesCleared = 30613; //km DISTANCE_TRAVELED_SINCE_CODES_CLEARED
float evapSystemVaporPressure = 21.34 ; // EVAP_SYSTEM_VAPOR_PRESSURE
uint8_t absoluteBarometricPressure = 215; //kPa ABSOLULTE_BAROMETRIC_PRESSURE
float catalystTemperatureBank1Sensor1 = 48.0; //dgree CATALYST_TEMPERATURE_BANK_1_SENSOR_1
float absoluteLoadValue = 19.61; //% ABSOLUTE_LOAD_VALUE
uint8_t ambientAirTemperature = 31; //degree
uint8_t fuelType = 1; //FUEL_TYPE
int i = 0;
uint8_t pidList1[4] = {0xff , 0xff, 0xff, 0xff} ;
uint8_t pidList2[4] = {0xff , 0xff, 0xff, 0xff} ;
uint8_t pidList3[4] = {0xff , 0xff, 0xff, 0xff} ;
uint8_t pidList4[4] = {0xff , 0xff, 0xff, 0xff} ;
uint8_t pidList5[4] = {0xff , 0xff, 0xff, 0xff} ;
uint8_t pidList6[4] = {0xff , 0xff, 0xff, 0xff} ;
uint8_t pidList7[4] = {0xff , 0xff, 0xff, 0xff} ;
void setPidList1_20(uint8_t pid)
{
Serial.printf("PidList1_20 %d %02x %02x %02x %02x\r\n", pid, pidList1[0], pidList1[1], pidList1[2], pidList1[3]);
CAN.beginPacket(ECU_ID);
CAN.write(0x06); //DLC 6
CAN.write(0x41);
CAN.write(pid);
CAN.write(pidList1[0]); CAN.write(pidList1[1]); CAN.write(pidList1[2]); CAN.write(pidList1[3]);
CAN.endPacket();
}
void setPidList21_40(uint8_t pid)
{
Serial.printf("PidList1_20 %d %02x %02x %02x %02x\r\n", pid, pidList2[0], pidList2[1], pidList2[2], pidList2[3]);
CAN.beginPacket(ECU_ID);
CAN.write(0x06); //DLC 6
CAN.write(0x41);
CAN.write(pid);
CAN.write(pidList2[0]); CAN.write(pidList2[1]); CAN.write(pidList2[2]); CAN.write(pidList2[3]);
CAN.endPacket();
}
void setPidList41_60(uint8_t pid)
{
Serial.printf("PidList41-60 %d %02x %02x %02x %02x\r\n", pid, pidList3[0], pidList3[1], pidList3[2], pidList3[3]);
CAN.beginPacket(ECU_ID);
CAN.write(0x06); //DLC 6
CAN.write(0x41);
CAN.write(pid);
CAN.write(pidList3[0]); CAN.write(pidList3[1]); CAN.write(pidList3[2]); CAN.write(pidList3[3]);
CAN.endPacket();
}
void setPidList61_80(uint8_t pid)
{
Serial.printf("PidList61-80 %d %02x %02x %02x %02x\r\n", pid, pidList4[0], pidList4[1], pidList4[2], pidList4[3]);
CAN.beginPacket(ECU_ID);
CAN.write(0x06); //DLC 6
CAN.write(0x41);
CAN.write(pid);
CAN.write(pidList4[0]); CAN.write(pidList4[1]); CAN.write(pidList4[2]); CAN.write(pidList4[3]);
CAN.endPacket();
}
void setPidList81_a0(uint8_t pid)
{
Serial.printf("PidList81-a0 %d %02x %02x %02x %02x\r\n", pid, pidList5[0], pidList5[1], pidList5[2], pidList5[3]);
CAN.beginPacket(ECU_ID);
CAN.write(0x06); //DLC 6
CAN.write(0x41);
CAN.write(pid);
CAN.write(pidList5[0]); CAN.write(pidList5[1]); CAN.write(pidList5[2]); CAN.write(pidList5[3]);
CAN.endPacket();
}
void setPidLista1_c0(uint8_t pid)
{
Serial.printf("PidListA1-C0 %d %02x %02x %02x %02x\r\n", pid, pidList6[0], pidList6[1], pidList6[2], pidList6[3]);
CAN.beginPacket(ECU_ID);
CAN.write(0x06); //DLC 6
CAN.write(0x41);
CAN.write(pid);
CAN.write(pidList6[0]); CAN.write(pidList6[1]); CAN.write(pidList6[2]); CAN.write(pidList6[3]);
CAN.endPacket();
}
void setPidListc1_e0(uint8_t pid)
{
Serial.printf("PidListC1-E0 %d %02x %02x %02x %02x\r\n", pid, pidList7[0], pidList7[1], pidList7[2], pidList7[3]);
CAN.beginPacket(ECU_ID);
CAN.write(0x06); //DLC 6
CAN.write(0x41);
CAN.write(pid);
CAN.write(pidList7[0]); CAN.write(pidList7[1]); CAN.write(pidList7[2]); CAN.write(pidList7[3]);
CAN.endPacket();
}
/*
Monitor status since DTCs cleared.
(Includes malfunction indicator lamp (MIL) status and number of DTCs.)
*/
void setDTC(uint8_t *DTC)
{
uint8_t *dtc = DTC;
Serial.println("DTC");
CAN.beginPacket(ECU_ID);
CAN.write(0x06); //DLC 6
CAN.write(0x41);
CAN.write(MONITOR_STATUS_SINCE_DTCS_CLEARED);
CAN.write(*dtc++);
CAN.write(*dtc++);
CAN.write(*dtc++);
CAN.write(*dtc);
CAN.endPacket();
}
void set4bytes(uint32_t data, uint8_t PID)
{
CAN.beginPacket(ECU_ID);
CAN.write(0x06); //DLC 6
CAN.write(0x41);
CAN.write(PID);
CAN.write((uint8_t)(data >> 24));
CAN.write((uint8_t)(data >> 16));
CAN.write((uint8_t)(data >> 8));
CAN.write((uint8_t)(data & 0x000000ff));
CAN.endPacket();
}
void set2bytes(uint16_t data, uint8_t PID) {
CAN.beginPacket(ECU_ID);
CAN.write(4); //DLC 4
CAN.write(0x41);
CAN.write(PID);
CAN.write((uint8_t)(data >> 8));
CAN.write((uint8_t)(data & 0x00ff));
CAN.endPacket();
}
void set1byte( uint8_t data, uint8_t PID) {
CAN.beginPacket(ECU_ID);
CAN.write(0x03); //DLC 3
CAN.write(0x41);
CAN.write(PID);
CAN.write(data);
CAN.endPacket();
}
void odb2responder(void * parameter) {
int valX, valY; // 暫存類比搖桿輸入值
while (1) {
int packetSize = CAN.parsePacket();
if (packetSize) {
if (!CAN.packetRtr()) {
if (!CAN.packetExtended()) {
uint8_t len = CAN.read();
uint8_t service = CAN.read();
uint8_t pid = CAN.read();
Serial.printf("DLC %02x 服務 %02x PID %02x\r\n", len, service, pid);
if (len != 2) break;
switch (service) {
case 0x01:
switch (pid) {
case PIDS_SUPPORT_01_20:
Serial.println("PID list requested");
setPidList1_20(pid);
break;
case PIDS_SUPPORT_21_40:
setPidList21_40(pid);
break;
case PIDS_SUPPORT_41_60:
setPidList41_60(pid);
break;
case 0x60:
setPidList61_80(pid);
break;
case 0x80:
setPidList81_a0(pid);
break;
case 0xa0:
setPidLista1_c0(pid);
break;
case 0xc0:
setPidListc1_e0(pid);
break;
case MONITOR_STATUS_SINCE_DTCS_CLEARED:
setDTC(DTC);
break;
case FREEZE_DTC:
set2bytes(freezeDTC, pid);
break;
case FUEL_SYSTEM_STATUS:
set2bytes(fuelSystemStatus, pid);
break;
case CALCULATED_ENGINE_LOAD: // engine efficiency
if (engineEfficiency > 100.0 ) engineEfficiency = 100.0;
if (engineEfficiency < 0 ) engineEfficiency = 0;
set1byte( (uint8_t)((engineEfficiency * 255.0) / 100.0), pid);
break;
case ENGINE_COOLANT_TEMPERATURE: // engine coolant temperature
if (engineCoolantTemperature < -40) engineCoolantTemperature = -40;
if (engineCoolantTemperature > 215 ) engineCoolantTemperature = 215;
set1byte(engineCoolantTemperature + 40, pid);
break;
case SHORT_TERM_FUEL_TRIM_BANK_1 :
set1byte((uint8_t)(((shortTermFuelTrimBank1 + 100.0) * 128.0) / 100.0), pid);
break;
case LONG_TERM_FUEL_TRIM_BANK_1 :
set1byte((uint8_t)(((longTermFuelTrimBank1 + 100.0) * 128.0) / 100.0), pid);
break;
case SHORT_TERM_FUEL_TRIM_BANK_2 :
set1byte((uint8_t)(((shortTermFuelTrimBank2 + 100.0) * 128.0) / 100.0), pid);
break;
case LONG_TERM_FUEL_TRIM_BANK_2 :
set1byte((uint8_t)(((longTermFuelTrimBank2 + 100.0) * 128.0) / 100.0), pid);
break;
case FUEL_PRESSURE :
set1byte(fuelPressure / 3, pid);
break;
case INTAKE_MANIFOLD_ABSOLUTE_PRESSURE:
set1byte(intakeManifoldAbsolutePressure, pid);
break;
case ENGINE_RPM: // rpm
valY = analogRead(IN_Y); // 垂直(Y)搖桿的輸入值
engineRPM = (uint16_t)map(valY, 0, 1023, 1500, 4000); // 引擎轉速
set2bytes( engineRPM, pid);
break;
case VEHICLE_SPEED: // speed
valX = analogRead(IN_X); // 水平(X)搖桿的輸入值
vehicleSpeed = (uint8_t)map(valX, 0, 1023, 0, 180); // 車速
set1byte(vehicleSpeed, pid );
break;
case TIMING_ADVANCE :
set1byte((timingAdvance + 64) * 2, pid);
break;
case AIR_INTAKE_TEMPERATURE:
set1byte(airIntakeTemperature + 40, pid );
break;
case MAF_AIR_FLOW_RATE:
set2bytes((uint16_t)(mafAirFlowRate * 100.0), pid );
break;
case THROTTLE_POSITION :
set1byte((uint8_t)((throttlePosition * 255.0) / 100.0), pid);
break;
case COMMANDED_SECONDARY_AIR_STATUS :
set1byte(commandedSecondaryAirStatus, pid );
break;
case OXYGEN_SENSORS_PRESENT_IN_2_BANKS:
set1byte( oxygenSensorsPresentIn2Banks, pid);
break;
case OXYGEN_SENSOR_1_SHORT_TERM_FUEL_TRIM:
case OXYGEN_SENSOR_2_SHORT_TERM_FUEL_TRIM:
case OXYGEN_SENSOR_3_SHORT_TERM_FUEL_TRIM:
case OXYGEN_SENSOR_4_SHORT_TERM_FUEL_TRIM:
case OXYGEN_SENSOR_5_SHORT_TERM_FUEL_TRIM:
case OXYGEN_SENSOR_6_SHORT_TERM_FUEL_TRIM:
case OXYGEN_SENSOR_7_SHORT_TERM_FUEL_TRIM:
case OXYGEN_SENSOR_8_SHORT_TERM_FUEL_TRIM:
if ( oxygenSensor1 > 1.275 ) oxygenSensor1 = 1.275;
if ( oxygenSensor1 < 0 ) oxygenSensor1 = 0.0;
if (shortTermFuelTrim < -100.0 ) shortTermFuelTrim = -100.0;
if (shortTermFuelTrim > 99.2 ) shortTermFuelTrim = 99.2;
set2bytes((uint16_t)((oxygenSensor1 * 200.0) * 256.0 + (shortTermFuelTrim + 100.0) * 128.0 / 100.0), pid);
break;
case OBD_STANDARDS_THIS_VEHICLE_CONFORMS_TO:
set1byte(obdStandardThisVehicleConformsTo, pid );
break;
case OXYGEN_SENSORS_PRESENT_IN_4_BANKS :
set1byte(oxygenSensorsPresentIn4Banks, pid );
break;
case AUXILIARY_INPUT_STATUS:
set1byte(auxiliaryInputStatus, pid );
break;
case RUN_TIME_SINCE_ENGINE_START:
set2bytes( (uint16_t)(millis() / 1000) , pid);
break;
case DISTANCE_TRAVELED_WITH_MIL_ON :
set2bytes(distanceTraveledWithMilOn , pid);
break;
case FUEL_RAIL_PRESSURE:
if ( fuelRailPressure > 5177.265 ) fuelRailPressure = 5177.265;
if ( fuelRailPressure < 0) fuelRailPressure = 0.0;
set2bytes((uint16_t)(fuelRailPressure / 0.079), pid);
break;
case FUEL_RAIL_GAUGE_PRESSURE:
if ( fuelRailGaugePressure > 655350 )fuelRailGaugePressure = 655350.0;
if ( fuelRailGaugePressure < 0 ) fuelRailGaugePressure = 0.0;
set2bytes((uint16_t)(fuelRailGaugePressure / 10), pid );
break;
case OXYGEN_SENSOR_1_FUEL_AIR_EQUIVALENCE_RATIO :
case OXYGEN_SENSOR_2_FUEL_AIR_EQUIVALENCE_RATIO :
case OXYGEN_SENSOR_3_FUEL_AIR_EQUIVALENCE_RATIO :
case OXYGEN_SENSOR_4_FUEL_AIR_EQUIVALENCE_RATIO :
case OXYGEN_SENSOR_5_FUEL_AIR_EQUIVALENCE_RATIO :
case OXYGEN_SENSOR_6_FUEL_AIR_EQUIVALENCE_RATIO :
case OXYGEN_SENSOR_7_FUEL_AIR_EQUIVALENCE_RATIO :
case OXYGEN_SENSOR_8_FUEL_AIR_EQUIVALENCE_RATIO :
set4bytes((uint32_t) (oxygenSensorFuelAir * 65535.0 / 2.0) << 16 + (uint32_t)((oxygenSensorFuelAirVoltage) * 65535.0 / 8.0), pid);
break;
case COMMANDED_EGR:
set1byte( (uint8_t)((commandedEGR * 255.0) / 100.0), pid );
break;
case EGR_ERROR :
set1byte( (uint8_t)((EGRError + 100) * 128.0 / 100.0), pid );
break;
case COMMANDED_EVAPORATIVE_PURGE:
set1byte((uint8_t)((commandedEvaporativePurge * 255.0) / 100.0), pid );
break;
case FUEL_TANK_LEVEL_INPUT: // fuel level
set1byte( (uint8_t)((fuelLevel * 255) / 100), pid );
break;
case WARM_UPS_SINCE_CODES_CLEARED :
set1byte((uint8_t)warmUpsSinceCodesCleared , pid );
break;
case DISTANCE_TRAVELED_SINCE_CODES_CLEARED:
break;
set2bytes((uint16_t) distanceTraveledSinceCodesCleared, pid);
break;
case EVAP_SYSTEM_VAPOR_PRESSURE:
if ( evapSystemVaporPressure > 8191.75 ) evapSystemVaporPressure = 8191.75;
if (evapSystemVaporPressure < -8192 )evapSystemVaporPressure = -8, 192;
set2bytes((int16_t)(evapSystemVaporPressure * 4.0), pid);
break;
case ABSOLULTE_BAROMETRIC_PRESSURE:
set1byte(absoluteBarometricPressure, pid);
break;
case CATALYST_TEMPERATURE_BANK_1_SENSOR_1 :
case CATALYST_TEMPERATURE_BANK_2_SENSOR_1 :
case CATALYST_TEMPERATURE_BANK_1_SENSOR_2 :
case CATALYST_TEMPERATURE_BANK_2_SENSOR_2 :
if (catalystTemperatureBank1Sensor1 > 6513.5) catalystTemperatureBank1Sensor1 = 6513.5;
if (catalystTemperatureBank1Sensor1 < -40.0) catalystTemperatureBank1Sensor1 = -40.0;
set2bytes( (uint16_t)((catalystTemperatureBank1Sensor1 + 40.0) * 10.0), pid);
break;
case CONTROL_MODULE_VOLTAGE:
set2bytes( (uint16_t)(controlModuleVoltage * 1000.0), pid);
break;
case ABSOLUTE_LOAD_VALUE:
if (absoluteLoadValue > 25700.0 ) absoluteLoadValue = 25700.0;
set2bytes( (uint16_t)((absoluteLoadValue * 255.0) / 100.0) , pid);
break;
case AMBIENT_AIR_TEMPERATURE :
set1byte((uint8_t) ambientAirTemperature + 40, pid);
break;
case FUEL_TYPE :
set1byte((uint8_t) fuelType, pid);
break;
default:
break;
}
break;
case 0x09:
switch (pid) {
case 0x02:
// setVIN();
break;
case 0x0a:
// setECUname();
break;
default:
break;
}
break;
default:
break;
}
}
while (CAN.available()) {
CAN.read();
}
delay(1);
}
}
}
}
void setup() {
Serial.begin(115200);
Serial.println("ESP32 ECU emulator");
CAN.setPins(CRX_PIN, CTX_PIN); // 指定CAN收發器的接腳
if (!CAN.begin(500e3)) { // 嘗試用500Kbps連線
Serial.println ("CAN初始化失敗~");
while (1); // 若初始化失敗,程式將停在這裡。
} else {
Serial.println ("CAN初始化完畢");
}
analogSetAttenuation(ADC_11db); // 設定類比輸入電壓上限3.6V
analogSetWidth(10);
xTaskCreatePinnedToCore( odb2responder, "odb2responder", 8096, NULL, 1, NULL, 1);
}
void loop() {
// 每隔一秒更新虛擬數據
fuelLevel += 1;
shortTermFuelTrimBank1 = (float)random(1, 10) - 5.0;
mafAirFlowRate = (float)random(1, 30) / 3.0;
throttlePosition += 1;
engineCoolantTemperature += 1; engineCoolantTemperature = engineCoolantTemperature % 128;
engineEfficiency += 1.0; engineEfficiency = (float)((int)engineEfficiency % 100);
delay(1000);
}
這個程式一開始宣告了一些儲存OBD參數的變數,例如:fuelSystemStatus(燃料系統狀態)、engineEfficiency(引擎效率)、fuelPressure(燃油壓力)、engineRPM(引擎轉速)、vehicleSpeed(車輛速度)…,當然這些數值都是虛構的,其中一些值在loop()中,每隔1秒更新,例如,引擎轉速和車輛速度值原本透過底下的敘述更新,筆者將它們刪除:
vehicleSpeed += 1; // 速度值+1 engineRPM += random(1, 10); // 隨機設定1~9轉速值
在setup()中,程式把一個FreeRTOS任務“odb2responder”指定給核心1執行,odb2responder任務負責持續偵聽CAN匯流排的訊息並回應相關請求,假設偵聽到 (0x7DF, 0x02, 0x01, 0x0D),代表「即時行車速度」請求的訊息,這個程式片段將被執行:
case VEHICLE_SPEED: // speed valX = analogRead(IN_X); // 讀取水平(X)搖桿的輸入值 vehicleSpeed = (uint8_t)map(valX, 0, 1023, 0, 180); // 將資料對應成0~180車速 set1byte(vehicleSpeed, pid ); // 發出1位元組資料,pid值即是0x0D break;
讀取OBD訊息的程式
開發板B執行的是「動手做18-2:在手機瀏覽器呈現即時車速和引擎轉速」單元的程式,只是調整了CAN收發器的接腳設定:
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <WebSocketsServer.h>
#include <SPIFFS.h>
#include <esp32_can.h>
#include <esp32_obd2.h>
#define INTERVAL 1000
#define CTX_PIN 21
#define CRX_PIN 22
#define LED_PIN 5
const char* ssid = "ESP32-OBDII";
const char* password = "87654321";
AsyncWebServer server(80); // 建立HTTP伺服器物件
AsyncWebSocket ws("/ws"); // 建立WebSocket物件
void onSocketEvent(AsyncWebSocket *server,
AsyncWebSocketClient *client,
AwsEventType type,
void *arg,
uint8_t *data,
size_t len)
{
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("來自%s的用戶%u已連線\n", client->remoteIP().toString().c_str(), client->id());
break;
case WS_EVT_DISCONNECT:
Serial.printf("用戶%u已離線\n", client->id());
break;
case WS_EVT_ERROR:
Serial.printf("用戶%u出錯了:%s\n", client->id(), (char *)data);
break;
case WS_EVT_DATA:
Serial.printf("用戶%u傳入資料:%s\n", client->id(), (char *)data);
break;
}
}
void notifyClients() {
JsonDocument doc;
doc["spd"] = OBD2.pidRead(VEHICLE_SPEED);
doc["rpm"] = OBD2.pidRead(ENGINE_RPM);
String output;
serializeJson(doc, output);
ws.textAll(output); // 向所有連線的用戶端傳遞JSON字串
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
if (!SPIFFS.begin(true)) {
Serial.println("無法載入SPIFFS記憶體");
return;
}
WiFi.softAP(ssid, password);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
// 設置首頁
server.serveStatic("/", SPIFFS, "/www/").setDefaultFile("index.html");
server.serveStatic("/favicon.ico", SPIFFS, "/www/favicon.ico");
server.serveStatic("/fonts/SevenSegment.woff2", SPIFFS, "/www/fonts/SevenSegment.woff2");
// 查無此頁
server.onNotFound([](AsyncWebServerRequest * req) {
req->send(404, "text/plain", "Not found");
});
ws.onEvent(onSocketEvent); // 附加事件處理程式
server.addHandler(&ws);
server.begin(); // 啟動網站伺服器
Serial.println("HTTP伺服器開工了~");
// 處理OBD(CAN)通訊
CAN0.setCANPins((gpio_num_t)CRX_PIN, (gpio_num_t)CTX_PIN);
Serial.println("嘗試連線到OBD2 CAN匯流排…");
while (1) {
// 嘗試連線到OBD2 CAN匯流排…
if (!OBD2.begin()) {
Serial.println("無法連線!");
digitalWrite(LED_PIN, LOW);
delay(500);
digitalWrite(LED_PIN, HIGH);
delay(1000);
} else {
//連線成功!
Serial.println("連線成功!");
digitalWrite(LED_PIN, LOW);
break;
}
}
}
void loop() {
static uint32_t prevTime = 0; // 前次時間,宣告成「靜態」變數。
uint32_t now = millis(); // 目前時間
if (now - prevTime >= INTERVAL) {
prevTime = now;
notifyClients(); // 向網路用戶端傳遞感測資料
}
ws.cleanupClients();
}
編譯執行此程式之前,請先上傳data資料夾裡的網頁檔。把開發板A和B都接上電腦,然後將手機或電腦的Wi-Fi連上開發板B,再開啟瀏覽器連到“192.168.4.1”,即可看到開發板A傳入的OBD模擬資料;撥動搖桿,引擎轉速和行車速度也會跟著改變。

讀取所有OBD模擬值的程式
充當OBD診斷器的開發板B,可以執行Sandeep Mistry先生編寫的OBD2_03_DataPrinter範例程式,請記得要修改CAN收發器的接腳設定。編譯上傳到開發板B,它將序列埠監視窗顯示開發板A傳入的各項虛擬參數值:


赵老师,您好, 真的非常感谢!感动了,您费心了<3
不客气!