Add OBD-II K-line support (ISO 9141/14230) with scanner example
New protocol handler alongside BMW I/K-Bus: - KLineObd2: 5-baud slow init, fast init (TiniPulse), request/response with half-duplex echo clearing, PID convenience wrapper - Obd2Pids.h: ~20 common PIDs with SAE J1979 decode helpers - obd2_scanner.cpp: polls RPM, speed, coolant, throttle, voltage Build config changes: - config.h: KLINE_* defaults (10400/8N1/MOD256/no idle detect) - platformio.ini: build_src_filter separates sketches, new [env:obd2-scanner] environment with KLINE_TX_INVERT=0
This commit is contained in:
parent
1464fcabe6
commit
8ba53630c0
@ -58,3 +58,39 @@
|
|||||||
|
|
||||||
// Maximum raw message size (source + length + up to 36 body bytes)
|
// Maximum raw message size (source + length + up to 36 body bytes)
|
||||||
#define IBUS_MAX_MSG 40
|
#define IBUS_MAX_MSG 40
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// OBD-II K-line configuration (ISO 9141 / ISO 14230)
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
// --- UART pin assignments (defaults to same as IBUS — shared hardware) ---
|
||||||
|
#ifndef KLINE_RX_PIN
|
||||||
|
#define KLINE_RX_PIN IBUS_RX_PIN
|
||||||
|
#endif
|
||||||
|
#ifndef KLINE_TX_PIN
|
||||||
|
#define KLINE_TX_PIN IBUS_TX_PIN
|
||||||
|
#endif
|
||||||
|
#ifndef KLINE_LED_PIN
|
||||||
|
#define KLINE_LED_PIN IBUS_LED_PIN
|
||||||
|
#endif
|
||||||
|
#ifndef KLINE_UART_NUM
|
||||||
|
#define KLINE_UART_NUM IBUS_UART_NUM
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --- Protocol constants ---
|
||||||
|
#ifndef KLINE_BAUD
|
||||||
|
#define KLINE_BAUD 10400
|
||||||
|
#endif
|
||||||
|
#ifndef KLINE_FRAMING
|
||||||
|
#define KLINE_FRAMING SERIAL_8N1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// TX inversion: 1 for optocoupler (PC817), 0 for transistor circuit
|
||||||
|
#ifndef KLINE_TX_INVERT
|
||||||
|
#define KLINE_TX_INVERT 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ISO 9141 default ECU address for 5-baud slow init
|
||||||
|
#ifndef KLINE_INIT_ADDR
|
||||||
|
#define KLINE_INIT_ADDR 0x33
|
||||||
|
#endif
|
||||||
|
|||||||
294
firmware/lib/AutoWire/KLineObd2.cpp
Normal file
294
firmware/lib/AutoWire/KLineObd2.cpp
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
// OBD-II K-line protocol handler (ISO 9141 / ISO 14230)
|
||||||
|
// 5-baud slow init, fast init, request/response with echo clearing.
|
||||||
|
|
||||||
|
#include "KLineObd2.h"
|
||||||
|
#include "Obd2Pids.h"
|
||||||
|
|
||||||
|
KLineObd2::KLineObd2(KLineTransport& transport)
|
||||||
|
: _transport(transport),
|
||||||
|
_initialized(false),
|
||||||
|
_kw1(0),
|
||||||
|
_kw2(0),
|
||||||
|
_testerAddr(obd2::TESTER_ADDR),
|
||||||
|
_ecuAddr(0) {}
|
||||||
|
|
||||||
|
// --- 5-baud slow init (ISO 9141-2) ---
|
||||||
|
// Bit-bang the target address at 5 baud on the TX pin.
|
||||||
|
// The UART peripheral must be detached from the TX pin during this sequence,
|
||||||
|
// then re-attached for normal 10400 baud communication.
|
||||||
|
//
|
||||||
|
// Sequence:
|
||||||
|
// 1. TX pin LOW for 200ms (start bit at 5 baud)
|
||||||
|
// 2. 8 data bits of targetAddr, LSB first, 200ms each
|
||||||
|
// 3. TX pin HIGH for 200ms (stop bit)
|
||||||
|
// 4. Re-attach UART, wait for ECU response
|
||||||
|
// 5. ECU sends: 0x55 (sync), keyword1, keyword2
|
||||||
|
// 6. Tester sends: inverted keyword2
|
||||||
|
// 7. ECU sends: inverted tester address (0xCC for 0x33)
|
||||||
|
|
||||||
|
bool KLineObd2::slowInit(uint8_t targetAddr) {
|
||||||
|
int8_t txp = _transport.txPin();
|
||||||
|
uint8_t unum = _transport.uartNum();
|
||||||
|
|
||||||
|
// Detach UART from TX pin so we can bit-bang
|
||||||
|
uart_set_pin((uart_port_t)unum, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE,
|
||||||
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||||
|
|
||||||
|
// Configure TX pin as GPIO output, start HIGH (idle)
|
||||||
|
pinMode(txp, OUTPUT);
|
||||||
|
digitalWrite(txp, HIGH);
|
||||||
|
delay(300); // W0: idle before init (>300ms per ISO 9141)
|
||||||
|
|
||||||
|
// Bit-bang target address at 5 baud
|
||||||
|
sendByte5Baud(targetAddr);
|
||||||
|
|
||||||
|
// Re-attach UART to TX pin
|
||||||
|
uart_set_pin((uart_port_t)unum, txp, UART_PIN_NO_CHANGE,
|
||||||
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||||
|
|
||||||
|
// Wait for ECU sync byte (0x55) — W1: 20-300ms per ISO 9141
|
||||||
|
int sync = readByteTimeout(300);
|
||||||
|
if (sync != obd2::SYNC_BYTE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read keyword bytes — W2: 5-20ms between bytes
|
||||||
|
int kw1 = readByteTimeout(50);
|
||||||
|
if (kw1 < 0) return false;
|
||||||
|
_kw1 = (uint8_t)kw1;
|
||||||
|
|
||||||
|
int kw2 = readByteTimeout(50);
|
||||||
|
if (kw2 < 0) return false;
|
||||||
|
_kw2 = (uint8_t)kw2;
|
||||||
|
|
||||||
|
// W3: 25-50ms before tester responds
|
||||||
|
delay(30);
|
||||||
|
|
||||||
|
// Send inverted keyword2 back to ECU
|
||||||
|
uint8_t invKw2 = ~_kw2;
|
||||||
|
_transport.serial()->write(invKw2);
|
||||||
|
_transport.serial()->flush();
|
||||||
|
|
||||||
|
// Clear our own echo (half-duplex bus)
|
||||||
|
clearEcho(1);
|
||||||
|
|
||||||
|
// W4: ECU confirms by sending inverted target address
|
||||||
|
int ack = readByteTimeout(50);
|
||||||
|
if (ack < 0) return false;
|
||||||
|
|
||||||
|
// The ack should be the complement of the target address
|
||||||
|
if ((uint8_t)ack != (uint8_t)~targetAddr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ecuAddr = targetAddr;
|
||||||
|
_initialized = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fast init (ISO 14230-2, KWP2000) ---
|
||||||
|
// 25ms LOW + 25ms HIGH "TiniPulse" on TX, then StartComm request.
|
||||||
|
|
||||||
|
bool KLineObd2::fastInit() {
|
||||||
|
int8_t txp = _transport.txPin();
|
||||||
|
uint8_t unum = _transport.uartNum();
|
||||||
|
|
||||||
|
// Detach UART from TX pin for the wake-up pulse
|
||||||
|
uart_set_pin((uart_port_t)unum, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE,
|
||||||
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||||
|
|
||||||
|
pinMode(txp, OUTPUT);
|
||||||
|
digitalWrite(txp, HIGH);
|
||||||
|
delay(300); // idle before pulse
|
||||||
|
|
||||||
|
// TiniPulse: 25ms LOW + 25ms HIGH
|
||||||
|
digitalWrite(txp, LOW);
|
||||||
|
delay(25);
|
||||||
|
digitalWrite(txp, HIGH);
|
||||||
|
delay(25);
|
||||||
|
|
||||||
|
// Re-attach UART
|
||||||
|
uart_set_pin((uart_port_t)unum, txp, UART_PIN_NO_CHANGE,
|
||||||
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||||
|
|
||||||
|
// Send StartCommunication request (KWP2000 service 0x81)
|
||||||
|
// Format: [fmt+len, target, source, service_id]
|
||||||
|
uint8_t startComm[] = {
|
||||||
|
0xC1, // format: functional addressing, 1 data byte
|
||||||
|
obd2::FUNC_ADDR, // target: functional address
|
||||||
|
obd2::TESTER_ADDR, // source: tester
|
||||||
|
0x81 // StartCommunication service
|
||||||
|
};
|
||||||
|
uint8_t checksum = KLineTransport::checksumMod256(startComm, 4);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < 4; i++) {
|
||||||
|
_transport.serial()->write(startComm[i]);
|
||||||
|
}
|
||||||
|
_transport.serial()->write(checksum);
|
||||||
|
_transport.serial()->flush();
|
||||||
|
|
||||||
|
// Clear echo (5 bytes: 4 data + 1 checksum)
|
||||||
|
clearEcho(5);
|
||||||
|
|
||||||
|
// Read StartComm positive response
|
||||||
|
// Expected: [fmt+len, source, target, 0xC1, kw1, kw2, checksum]
|
||||||
|
uint8_t resp[7];
|
||||||
|
uint8_t respLen = readResponse(resp, sizeof(resp), 300);
|
||||||
|
if (respLen < 4) return false;
|
||||||
|
|
||||||
|
// Positive response service ID is request + 0x40
|
||||||
|
// Find the service byte — it's after header bytes
|
||||||
|
// For physical addressing response: [fmt, target, source, 0xC1, kw1, kw2, chk]
|
||||||
|
bool foundResponse = false;
|
||||||
|
for (uint8_t i = 0; i < respLen; i++) {
|
||||||
|
if (resp[i] == 0xC1) { // StartComm positive response
|
||||||
|
if (i + 2 < respLen) {
|
||||||
|
_kw1 = resp[i + 1];
|
||||||
|
_kw2 = resp[i + 2];
|
||||||
|
foundResponse = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundResponse) return false;
|
||||||
|
|
||||||
|
_ecuAddr = obd2::FUNC_ADDR;
|
||||||
|
_initialized = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Request/response ---
|
||||||
|
|
||||||
|
void KLineObd2::sendRequest(const uint8_t* data, uint8_t len) {
|
||||||
|
uint8_t checksum = KLineTransport::checksumMod256(data, len);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
_transport.serial()->write(data[i]);
|
||||||
|
}
|
||||||
|
_transport.serial()->write(checksum);
|
||||||
|
_transport.serial()->flush();
|
||||||
|
|
||||||
|
// Clear echo: data bytes + checksum byte
|
||||||
|
clearEcho(len + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t KLineObd2::readResponse(uint8_t* buf, uint8_t maxLen, uint16_t timeoutMs) {
|
||||||
|
uint8_t count = 0;
|
||||||
|
unsigned long start = millis();
|
||||||
|
|
||||||
|
// Read first byte (format/header) to determine message length
|
||||||
|
int first = readByteTimeout(timeoutMs);
|
||||||
|
if (first < 0) return 0;
|
||||||
|
buf[count++] = (uint8_t)first;
|
||||||
|
|
||||||
|
// Determine expected length from format byte
|
||||||
|
// Bits 6-7: address mode (00=no addr, 10=phys, 11=func)
|
||||||
|
// Bits 0-5: data length (0 = length in separate byte)
|
||||||
|
uint8_t fmt = (uint8_t)first;
|
||||||
|
uint8_t addrBytes = (fmt & 0x80) ? 2 : 0; // target + source if addressing present
|
||||||
|
uint8_t dataLen = fmt & 0x3F;
|
||||||
|
|
||||||
|
// Read address bytes if present
|
||||||
|
for (uint8_t i = 0; i < addrBytes && count < maxLen; i++) {
|
||||||
|
int b = readByteTimeout(timeoutMs - (millis() - start));
|
||||||
|
if (b < 0) return count;
|
||||||
|
buf[count++] = (uint8_t)b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If dataLen == 0, next byte is the actual length
|
||||||
|
if (dataLen == 0 && count < maxLen) {
|
||||||
|
int lenByte = readByteTimeout(timeoutMs - (millis() - start));
|
||||||
|
if (lenByte < 0) return count;
|
||||||
|
buf[count++] = (uint8_t)lenByte;
|
||||||
|
dataLen = (uint8_t)lenByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data bytes + checksum
|
||||||
|
uint8_t remaining = dataLen + 1; // +1 for checksum
|
||||||
|
for (uint8_t i = 0; i < remaining && count < maxLen; i++) {
|
||||||
|
uint16_t elapsed = millis() - start;
|
||||||
|
if (elapsed >= timeoutMs) return count;
|
||||||
|
int b = readByteTimeout(timeoutMs - elapsed);
|
||||||
|
if (b < 0) return count;
|
||||||
|
buf[count++] = (uint8_t)b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t KLineObd2::requestPid(uint8_t mode, uint8_t pid,
|
||||||
|
uint8_t* response, uint8_t maxLen) {
|
||||||
|
if (!_initialized) return 0;
|
||||||
|
|
||||||
|
// ISO 14230 format: physical addressing, 2 data bytes
|
||||||
|
uint8_t req[] = {
|
||||||
|
(uint8_t)(0x82), // fmt: physical addressing, 2 data bytes
|
||||||
|
_ecuAddr, // target
|
||||||
|
_testerAddr, // source
|
||||||
|
mode, // service/mode
|
||||||
|
pid // PID
|
||||||
|
};
|
||||||
|
|
||||||
|
sendRequest(req, sizeof(req));
|
||||||
|
|
||||||
|
uint8_t raw[16];
|
||||||
|
uint8_t rawLen = readResponse(raw, sizeof(raw));
|
||||||
|
if (rawLen < 5) return 0; // minimum: fmt + target + source + response_mode + pid
|
||||||
|
|
||||||
|
// Find response data — skip header, copy from response_mode onward
|
||||||
|
// Response mode = request mode + 0x40
|
||||||
|
uint8_t respMode = mode + 0x40;
|
||||||
|
for (uint8_t i = 0; i < rawLen; i++) {
|
||||||
|
if (raw[i] == respMode && (i + 1) < rawLen && raw[i + 1] == pid) {
|
||||||
|
// Copy data bytes after mode+pid, excluding checksum
|
||||||
|
uint8_t dataStart = i + 2;
|
||||||
|
uint8_t dataEnd = rawLen - 1; // exclude checksum
|
||||||
|
uint8_t dataLen = 0;
|
||||||
|
for (uint8_t j = dataStart; j < dataEnd && dataLen < maxLen; j++) {
|
||||||
|
response[dataLen++] = raw[j];
|
||||||
|
}
|
||||||
|
return dataLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Private helpers ---
|
||||||
|
|
||||||
|
void KLineObd2::sendByte5Baud(uint8_t data) {
|
||||||
|
int8_t txp = _transport.txPin();
|
||||||
|
|
||||||
|
// Start bit (LOW)
|
||||||
|
digitalWrite(txp, LOW);
|
||||||
|
delay(200);
|
||||||
|
|
||||||
|
// 8 data bits, LSB first
|
||||||
|
for (uint8_t i = 0; i < 8; i++) {
|
||||||
|
digitalWrite(txp, (data >> i) & 0x01 ? HIGH : LOW);
|
||||||
|
delay(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop bit (HIGH)
|
||||||
|
digitalWrite(txp, HIGH);
|
||||||
|
delay(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLineObd2::clearEcho(uint8_t count) {
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
readByteTimeout(KLINE_ECHO_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int KLineObd2::readByteTimeout(uint16_t timeoutMs) {
|
||||||
|
unsigned long start = millis();
|
||||||
|
while (millis() - start < timeoutMs) {
|
||||||
|
_transport.drainUart();
|
||||||
|
if (_transport.rxAvailable() > 0) {
|
||||||
|
return _transport.rxRead();
|
||||||
|
}
|
||||||
|
delayMicroseconds(100); // prevent busy-wait burning CPU
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
68
firmware/lib/AutoWire/KLineObd2.h
Normal file
68
firmware/lib/AutoWire/KLineObd2.h
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
// OBD-II K-line protocol handler (ISO 9141 / ISO 14230)
|
||||||
|
// Uses KLineTransport for UART, ring buffers, and bus management.
|
||||||
|
// Handles 5-baud slow init, fast init, request/response framing,
|
||||||
|
// and half-duplex echo clearing.
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "KLineTransport.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
|
||||||
|
#ifndef KLINE_RESPONSE_TIMEOUT_MS
|
||||||
|
#define KLINE_RESPONSE_TIMEOUT_MS 2000
|
||||||
|
#endif
|
||||||
|
#ifndef KLINE_ECHO_TIMEOUT_MS
|
||||||
|
#define KLINE_ECHO_TIMEOUT_MS 100
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class KLineObd2 {
|
||||||
|
public:
|
||||||
|
KLineObd2(KLineTransport& transport);
|
||||||
|
|
||||||
|
// ISO 9141 5-baud slow init — bit-bangs target address at 5 baud,
|
||||||
|
// then reads sync (0x55) and keyword bytes from ECU.
|
||||||
|
// Returns true if ECU responds with valid sync + keywords.
|
||||||
|
bool slowInit(uint8_t targetAddr = 0x33);
|
||||||
|
|
||||||
|
// ISO 14230 fast init — 25ms LOW + 25ms HIGH TiniPulse,
|
||||||
|
// then reads StartComm response.
|
||||||
|
bool fastInit();
|
||||||
|
|
||||||
|
// Send an OBD-II request. Frames the message and discards echo bytes.
|
||||||
|
// data[] should contain: [header, source, target, mode, pid, ...]
|
||||||
|
// For simple PID requests, use requestPid() instead.
|
||||||
|
void sendRequest(const uint8_t* data, uint8_t len);
|
||||||
|
|
||||||
|
// Read response from ECU. Blocks until response received or timeout.
|
||||||
|
// Returns number of bytes read, or 0 on timeout.
|
||||||
|
uint8_t readResponse(uint8_t* buf, uint8_t maxLen,
|
||||||
|
uint16_t timeoutMs = KLINE_RESPONSE_TIMEOUT_MS);
|
||||||
|
|
||||||
|
// Convenience: send a mode/PID request and read the response.
|
||||||
|
// Returns response data length (excluding header/checksum), or 0 on failure.
|
||||||
|
uint8_t requestPid(uint8_t mode, uint8_t pid,
|
||||||
|
uint8_t* response, uint8_t maxLen);
|
||||||
|
|
||||||
|
bool isInitialized() const { return _initialized; }
|
||||||
|
|
||||||
|
// Protocol info from init handshake
|
||||||
|
uint8_t keyword1() const { return _kw1; }
|
||||||
|
uint8_t keyword2() const { return _kw2; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Bit-bang a single byte at 5 baud (200ms per bit) on TX pin
|
||||||
|
void sendByte5Baud(uint8_t data);
|
||||||
|
|
||||||
|
// Discard echo bytes from half-duplex bus
|
||||||
|
void clearEcho(uint8_t count);
|
||||||
|
|
||||||
|
// Read a single byte with timeout
|
||||||
|
int readByteTimeout(uint16_t timeoutMs);
|
||||||
|
|
||||||
|
KLineTransport& _transport;
|
||||||
|
bool _initialized;
|
||||||
|
uint8_t _kw1;
|
||||||
|
uint8_t _kw2;
|
||||||
|
uint8_t _testerAddr; // our address (0xF1 = external test equipment)
|
||||||
|
uint8_t _ecuAddr; // ECU address from init response
|
||||||
|
};
|
||||||
141
firmware/lib/AutoWire/Obd2Pids.h
Normal file
141
firmware/lib/AutoWire/Obd2Pids.h
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
#pragma once
|
||||||
|
// OBD-II PID constants and decode helpers (SAE J1979)
|
||||||
|
// Covers the most common Mode 01 (current data) PIDs.
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
namespace obd2 {
|
||||||
|
|
||||||
|
// --- Diagnostic modes ---
|
||||||
|
constexpr uint8_t MODE_CURRENT = 0x01; // current data
|
||||||
|
constexpr uint8_t MODE_FREEZE = 0x02; // freeze frame
|
||||||
|
constexpr uint8_t MODE_DTC = 0x03; // stored DTCs
|
||||||
|
constexpr uint8_t MODE_CLEAR_DTC = 0x04; // clear DTCs
|
||||||
|
constexpr uint8_t MODE_O2_TEST = 0x05; // O2 sensor monitoring
|
||||||
|
constexpr uint8_t MODE_TEST_RESULTS = 0x06; // on-board monitoring test results
|
||||||
|
constexpr uint8_t MODE_PENDING_DTC = 0x07; // pending DTCs
|
||||||
|
constexpr uint8_t MODE_CONTROL = 0x08; // control of on-board system
|
||||||
|
constexpr uint8_t MODE_VEHICLE_INFO = 0x09; // vehicle information (VIN, etc.)
|
||||||
|
constexpr uint8_t MODE_PERMANENT_DTC = 0x0A; // permanent DTCs
|
||||||
|
|
||||||
|
// --- Mode 01 PIDs ---
|
||||||
|
constexpr uint8_t PID_SUPPORTED_01_20 = 0x00; // supported PIDs [01-20]
|
||||||
|
constexpr uint8_t PID_DTC_STATUS = 0x01; // monitor status since DTCs cleared
|
||||||
|
constexpr uint8_t PID_FUEL_STATUS = 0x03; // fuel system status
|
||||||
|
constexpr uint8_t PID_ENGINE_LOAD = 0x04; // calculated engine load (%)
|
||||||
|
constexpr uint8_t PID_COOLANT_TEMP = 0x05; // engine coolant temperature (C)
|
||||||
|
constexpr uint8_t PID_SHORT_FUEL_TRIM_1 = 0x06; // short term fuel trim bank 1 (%)
|
||||||
|
constexpr uint8_t PID_LONG_FUEL_TRIM_1 = 0x07; // long term fuel trim bank 1 (%)
|
||||||
|
constexpr uint8_t PID_INTAKE_PRESSURE = 0x0B; // intake manifold pressure (kPa)
|
||||||
|
constexpr uint8_t PID_RPM = 0x0C; // engine RPM
|
||||||
|
constexpr uint8_t PID_SPEED = 0x0D; // vehicle speed (km/h)
|
||||||
|
constexpr uint8_t PID_TIMING_ADVANCE = 0x0E; // timing advance (degrees BTDC)
|
||||||
|
constexpr uint8_t PID_INTAKE_TEMP = 0x0F; // intake air temperature (C)
|
||||||
|
constexpr uint8_t PID_MAF_RATE = 0x10; // MAF air flow rate (g/s)
|
||||||
|
constexpr uint8_t PID_THROTTLE = 0x11; // throttle position (%)
|
||||||
|
constexpr uint8_t PID_OBD_STANDARD = 0x1C; // OBD standards compliance
|
||||||
|
constexpr uint8_t PID_RUN_TIME = 0x1F; // run time since engine start (s)
|
||||||
|
constexpr uint8_t PID_SUPPORTED_21_40 = 0x20; // supported PIDs [21-40]
|
||||||
|
constexpr uint8_t PID_FUEL_LEVEL = 0x2F; // fuel tank level input (%)
|
||||||
|
constexpr uint8_t PID_BARO_PRESSURE = 0x33; // barometric pressure (kPa)
|
||||||
|
constexpr uint8_t PID_SUPPORTED_41_60 = 0x40; // supported PIDs [41-60]
|
||||||
|
constexpr uint8_t PID_CONTROL_VOLTAGE = 0x42; // control module voltage (V)
|
||||||
|
constexpr uint8_t PID_FUEL_TYPE = 0x51; // fuel type
|
||||||
|
constexpr uint8_t PID_OIL_TEMP = 0x5C; // engine oil temperature (C)
|
||||||
|
constexpr uint8_t PID_FUEL_RATE = 0x5E; // engine fuel rate (L/h)
|
||||||
|
|
||||||
|
// --- K-line protocol constants ---
|
||||||
|
constexpr uint8_t SYNC_BYTE = 0x55; // ECU sync response
|
||||||
|
constexpr uint8_t DEFAULT_TARGET = 0x33; // ISO 9141 default ECU address
|
||||||
|
constexpr uint8_t TESTER_ADDR = 0xF1; // external test equipment
|
||||||
|
constexpr uint8_t FUNC_ADDR = 0x33; // functional addressing
|
||||||
|
|
||||||
|
// --- ISO 14230 (KWP2000) header formats ---
|
||||||
|
constexpr uint8_t KWP_FMT_NO_ADDR = 0x00; // no address info
|
||||||
|
constexpr uint8_t KWP_FMT_PHYS_ADDR = 0x80; // physical addressing
|
||||||
|
constexpr uint8_t KWP_FMT_FUNC_ADDR = 0xC0; // functional addressing
|
||||||
|
|
||||||
|
// --- Decode helpers ---
|
||||||
|
// All formulas from SAE J1979 / ISO 15031-5
|
||||||
|
|
||||||
|
// PID 0x04: Calculated engine load (0-100%)
|
||||||
|
inline float decodeEngineLoad(uint8_t a) {
|
||||||
|
return a * (100.0f / 255.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x05: Engine coolant temperature (-40 to 215 C)
|
||||||
|
inline int16_t decodeCoolantTemp(uint8_t a) {
|
||||||
|
return (int16_t)a - 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x06/0x07: Short/long term fuel trim (-100 to 99.2%)
|
||||||
|
inline float decodeFuelTrim(uint8_t a) {
|
||||||
|
return (a / 1.28f) - 100.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x0B: Intake manifold pressure (0-255 kPa)
|
||||||
|
inline uint8_t decodeIntakePressure(uint8_t a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x0C: Engine RPM (0-16383.75 RPM) — 2 bytes
|
||||||
|
inline float decodeRpm(uint8_t a, uint8_t b) {
|
||||||
|
return ((a * 256.0f) + b) / 4.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x0D: Vehicle speed (0-255 km/h)
|
||||||
|
inline uint8_t decodeSpeed(uint8_t a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x0E: Timing advance (-64 to 63.5 degrees BTDC)
|
||||||
|
inline float decodeTimingAdvance(uint8_t a) {
|
||||||
|
return (a / 2.0f) - 64.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x0F: Intake air temperature (-40 to 215 C)
|
||||||
|
inline int16_t decodeIntakeTemp(uint8_t a) {
|
||||||
|
return (int16_t)a - 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x10: MAF air flow rate (0-655.35 g/s) — 2 bytes
|
||||||
|
inline float decodeMafRate(uint8_t a, uint8_t b) {
|
||||||
|
return ((a * 256.0f) + b) / 100.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x11: Throttle position (0-100%)
|
||||||
|
inline float decodeThrottle(uint8_t a) {
|
||||||
|
return a * (100.0f / 255.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x1F: Run time since engine start (0-65535 s) — 2 bytes
|
||||||
|
inline uint16_t decodeRunTime(uint8_t a, uint8_t b) {
|
||||||
|
return (uint16_t)(a * 256) + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x2F: Fuel tank level input (0-100%)
|
||||||
|
inline float decodeFuelLevel(uint8_t a) {
|
||||||
|
return a * (100.0f / 255.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x33: Barometric pressure (0-255 kPa)
|
||||||
|
inline uint8_t decodeBaroPressure(uint8_t a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x42: Control module voltage (0-65.535 V) — 2 bytes
|
||||||
|
inline float decodeControlVoltage(uint8_t a, uint8_t b) {
|
||||||
|
return ((a * 256.0f) + b) / 1000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x5C: Engine oil temperature (-40 to 210 C)
|
||||||
|
inline int16_t decodeOilTemp(uint8_t a) {
|
||||||
|
return (int16_t)a - 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID 0x5E: Engine fuel rate (0-3212.75 L/h) — 2 bytes
|
||||||
|
inline float decodeFuelRate(uint8_t a, uint8_t b) {
|
||||||
|
return ((a * 256.0f) + b) / 20.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace obd2
|
||||||
@ -1,4 +1,5 @@
|
|||||||
; BMW I/K-Bus Interface — ESP32 + PC817 Optocoupler
|
; AutoWire — Multi-protocol automotive bus interface (ESP32)
|
||||||
|
; BMW I/K-Bus (optocoupler) + OBD-II K-line (transistor/optocoupler)
|
||||||
; Based on muki01/I-K_Bus (MIT), ported for ESP32 with R2=220 fix
|
; Based on muki01/I-K_Bus (MIT), ported for ESP32 with R2=220 fix
|
||||||
|
|
||||||
[platformio]
|
[platformio]
|
||||||
@ -17,10 +18,15 @@ build_flags =
|
|||||||
-DIBUS_IDLE_TIMEOUT_US=1500
|
-DIBUS_IDLE_TIMEOUT_US=1500
|
||||||
-DIBUS_PACKET_GAP_MS=10
|
-DIBUS_PACKET_GAP_MS=10
|
||||||
|
|
||||||
|
; ===================================================================
|
||||||
|
; BMW I/K-Bus sniffer environments (main.cpp)
|
||||||
|
; ===================================================================
|
||||||
|
|
||||||
; --- ESP32 (classic, dual-core) ---
|
; --- ESP32 (classic, dual-core) ---
|
||||||
; GPIO 16/17 are UART2 defaults, free on most devkits
|
; GPIO 16/17 are UART2 defaults, free on most devkits
|
||||||
[env:esp32dev]
|
[env:esp32dev]
|
||||||
board = esp32dev
|
board = esp32dev
|
||||||
|
build_src_filter = +<main.cpp>
|
||||||
build_flags =
|
build_flags =
|
||||||
${env.build_flags}
|
${env.build_flags}
|
||||||
-DIBUS_RX_PIN=16
|
-DIBUS_RX_PIN=16
|
||||||
@ -32,6 +38,7 @@ build_flags =
|
|||||||
; GPIO 4/5 are general-purpose; GPIO 8 is onboard LED on most C3 devkits
|
; GPIO 4/5 are general-purpose; GPIO 8 is onboard LED on most C3 devkits
|
||||||
[env:esp32-c3]
|
[env:esp32-c3]
|
||||||
board = esp32-c3-devkitm-1
|
board = esp32-c3-devkitm-1
|
||||||
|
build_src_filter = +<main.cpp>
|
||||||
build_flags =
|
build_flags =
|
||||||
${env.build_flags}
|
${env.build_flags}
|
||||||
-DIBUS_RX_PIN=4
|
-DIBUS_RX_PIN=4
|
||||||
@ -43,9 +50,28 @@ build_flags =
|
|||||||
; GPIO 15/16 free on S3-DevKitM; GPIO 48 is RGB LED
|
; GPIO 15/16 free on S3-DevKitM; GPIO 48 is RGB LED
|
||||||
[env:esp32-s3]
|
[env:esp32-s3]
|
||||||
board = esp32-s3-devkitm-1
|
board = esp32-s3-devkitm-1
|
||||||
|
build_src_filter = +<main.cpp>
|
||||||
build_flags =
|
build_flags =
|
||||||
${env.build_flags}
|
${env.build_flags}
|
||||||
-DIBUS_RX_PIN=15
|
-DIBUS_RX_PIN=15
|
||||||
-DIBUS_TX_PIN=16
|
-DIBUS_TX_PIN=16
|
||||||
-DIBUS_LED_PIN=48
|
-DIBUS_LED_PIN=48
|
||||||
-DIBUS_UART_NUM=1
|
-DIBUS_UART_NUM=1
|
||||||
|
|
||||||
|
; ===================================================================
|
||||||
|
; OBD-II K-line scanner environment (obd2_scanner.cpp)
|
||||||
|
; ===================================================================
|
||||||
|
|
||||||
|
; Uses same ESP32 devkit — different sketch and protocol config.
|
||||||
|
; KLINE_TX_INVERT=0 for transistor circuit (Tucker k-line-board).
|
||||||
|
; Set to 1 if using PC817 optocoupler interface.
|
||||||
|
[env:obd2-scanner]
|
||||||
|
board = esp32dev
|
||||||
|
build_src_filter = +<obd2_scanner.cpp>
|
||||||
|
build_flags =
|
||||||
|
${env.build_flags}
|
||||||
|
-DIBUS_RX_PIN=16
|
||||||
|
-DIBUS_TX_PIN=17
|
||||||
|
-DIBUS_LED_PIN=2
|
||||||
|
-DIBUS_UART_NUM=1
|
||||||
|
-DKLINE_TX_INVERT=0
|
||||||
|
|||||||
144
firmware/src/obd2_scanner.cpp
Normal file
144
firmware/src/obd2_scanner.cpp
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// OBD-II K-line Scanner — ESP32 + transistor/optocoupler interface
|
||||||
|
// Polls common PIDs (RPM, speed, coolant temp) and prints to serial.
|
||||||
|
//
|
||||||
|
// Hardware: Transistor circuit (Tucker k-line-board) or PC817 optocoupler.
|
||||||
|
// Set KLINE_TX_INVERT=1 in build flags for optocoupler, 0 for transistor.
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "config.h"
|
||||||
|
#include "KLineTransport.h"
|
||||||
|
#include "KLineObd2.h"
|
||||||
|
#include "Obd2Pids.h"
|
||||||
|
|
||||||
|
KLineTransport transport;
|
||||||
|
KLineObd2* scanner = nullptr;
|
||||||
|
|
||||||
|
static unsigned long lastPollMs = 0;
|
||||||
|
static const unsigned long POLL_INTERVAL_MS = 500;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(500);
|
||||||
|
|
||||||
|
Serial.println();
|
||||||
|
Serial.println("=== OBD-II K-line Scanner ===");
|
||||||
|
Serial.print("Bus UART: ");
|
||||||
|
Serial.print(KLINE_BAUD);
|
||||||
|
Serial.println(" 8N1");
|
||||||
|
Serial.print("RX pin: GPIO ");
|
||||||
|
Serial.print(KLINE_RX_PIN);
|
||||||
|
Serial.print(" TX pin: GPIO ");
|
||||||
|
Serial.println(KLINE_TX_PIN);
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
KLineConfig config;
|
||||||
|
config.baud = KLINE_BAUD;
|
||||||
|
config.framing = KLINE_FRAMING;
|
||||||
|
config.idleTimeoutUs = 0; // master/slave — no idle detection needed
|
||||||
|
config.idleCheckUs = 0;
|
||||||
|
config.packetGapMs = 50; // P3: inter-message timing
|
||||||
|
config.txInvert = KLINE_TX_INVERT;
|
||||||
|
config.checksumType = CHECKSUM_MOD256;
|
||||||
|
|
||||||
|
transport.begin(Serial1, KLINE_RX_PIN, KLINE_TX_PIN, KLINE_LED_PIN,
|
||||||
|
KLINE_UART_NUM, config);
|
||||||
|
|
||||||
|
scanner = new KLineObd2(transport);
|
||||||
|
|
||||||
|
// Try slow init first (ISO 9141-2)
|
||||||
|
Serial.print("Slow init (0x");
|
||||||
|
Serial.print(KLINE_INIT_ADDR, HEX);
|
||||||
|
Serial.print(")... ");
|
||||||
|
|
||||||
|
if (scanner->slowInit(KLINE_INIT_ADDR)) {
|
||||||
|
Serial.println("OK");
|
||||||
|
Serial.print("Keywords: ");
|
||||||
|
Serial.print(scanner->keyword1(), HEX);
|
||||||
|
Serial.print(" ");
|
||||||
|
Serial.println(scanner->keyword2(), HEX);
|
||||||
|
} else {
|
||||||
|
Serial.println("failed");
|
||||||
|
Serial.print("Fast init... ");
|
||||||
|
if (scanner->fastInit()) {
|
||||||
|
Serial.println("OK");
|
||||||
|
Serial.print("Keywords: ");
|
||||||
|
Serial.print(scanner->keyword1(), HEX);
|
||||||
|
Serial.print(" ");
|
||||||
|
Serial.println(scanner->keyword2(), HEX);
|
||||||
|
} else {
|
||||||
|
Serial.println("failed");
|
||||||
|
Serial.println("No ECU response. Check wiring and key position.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println();
|
||||||
|
if (scanner->isInitialized()) {
|
||||||
|
Serial.println("Polling PIDs...");
|
||||||
|
Serial.println("RPM | Speed | Coolant | Throttle | Voltage");
|
||||||
|
Serial.println("---------+---------+---------+----------+--------");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (!scanner || !scanner->isInitialized()) {
|
||||||
|
delay(1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis() - lastPollMs < POLL_INTERVAL_MS) return;
|
||||||
|
lastPollMs = millis();
|
||||||
|
|
||||||
|
uint8_t data[4];
|
||||||
|
|
||||||
|
// RPM (2 data bytes)
|
||||||
|
uint8_t len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_RPM, data, sizeof(data));
|
||||||
|
if (len >= 2) {
|
||||||
|
float rpm = obd2::decodeRpm(data[0], data[1]);
|
||||||
|
Serial.print(rpm, 0);
|
||||||
|
Serial.print(" rpm");
|
||||||
|
} else {
|
||||||
|
Serial.print("--- ");
|
||||||
|
}
|
||||||
|
Serial.print(" | ");
|
||||||
|
|
||||||
|
// Speed (1 data byte)
|
||||||
|
len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_SPEED, data, sizeof(data));
|
||||||
|
if (len >= 1) {
|
||||||
|
Serial.print(obd2::decodeSpeed(data[0]));
|
||||||
|
Serial.print(" km/h");
|
||||||
|
} else {
|
||||||
|
Serial.print("--- ");
|
||||||
|
}
|
||||||
|
Serial.print(" | ");
|
||||||
|
|
||||||
|
// Coolant temp (1 data byte)
|
||||||
|
len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_COOLANT_TEMP, data, sizeof(data));
|
||||||
|
if (len >= 1) {
|
||||||
|
Serial.print(obd2::decodeCoolantTemp(data[0]));
|
||||||
|
Serial.print(" C");
|
||||||
|
} else {
|
||||||
|
Serial.print("--- ");
|
||||||
|
}
|
||||||
|
Serial.print(" | ");
|
||||||
|
|
||||||
|
// Throttle (1 data byte)
|
||||||
|
len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_THROTTLE, data, sizeof(data));
|
||||||
|
if (len >= 1) {
|
||||||
|
Serial.print(obd2::decodeThrottle(data[0]), 1);
|
||||||
|
Serial.print(" %");
|
||||||
|
} else {
|
||||||
|
Serial.print("--- ");
|
||||||
|
}
|
||||||
|
Serial.print(" | ");
|
||||||
|
|
||||||
|
// Control module voltage (2 data bytes)
|
||||||
|
len = scanner->requestPid(obd2::MODE_CURRENT, obd2::PID_CONTROL_VOLTAGE, data, sizeof(data));
|
||||||
|
if (len >= 2) {
|
||||||
|
Serial.print(obd2::decodeControlVoltage(data[0], data[1]), 1);
|
||||||
|
Serial.print(" V");
|
||||||
|
} else {
|
||||||
|
Serial.print("--- ");
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user