J2534 specification and function reference ›
ELM327 specification and command reference ›
The ELM327 interface is available on the Nano ET. The other ScanDoc adapters use the J2534 PassThru protocol.
Changes in the J2534 DLL, ELM327 and ScanDoc adapter firmware that affect integration: new functions, protocols and parameters - with usage examples.
The libraries ship as a single archive. Platforms: Windows x86/x64/ARM64 (separate builds for Windows 7), macOS (universal), Linux (x64, x86, ARM, ARM64), Android (arm64-v8a, armeabi-v7a, x86, x86_64), iOS (XCFramework). The docs/ folder holds the SDK documentation: getting started, API reference, configuration, error handling, DoIP, firmware update, log format, Android, iOS.
Download J2534 libraries 2.0.0.241 - Windows x86/x64/ARM64 (separate builds for Windows 7), macOS (universal), Linux (x64, x86, ARM, ARM64), Android (arm64-v8a, armeabi-v7a, x86, x86_64), iOS (XCFramework), Chrome and Edge 137+ browsers (WebAssembly); the docs/ folder holds the SDK documentation (getting started, API reference, configuration, error handling, DoIP, DevHub, firmware update, log format, Android, iOS, browser). Static .a archives ship only for iOS and the Linux x64 server build; all other platforms load the library dynamically.
New
ConfigWrite now silently skips the ble_name key: it returns no error, so that applications which send the whole set of settings keep working. ConfigRead returns the name as before.PassThruReadVersion over USB now takes about 6.5 ms instead of 16. The adapter sends a reply longer than 248 bytes in 248-byte chunks with a 4 ms pause, so transfer from the adapter to the computer runs at about 59 KB/s: a 4 KB reply takes about 70 ms. Transfer from the computer to the adapter still runs at 3 Mbit/s.TCP_NODELAY is now enabled on the connection, and the whole reply goes out at once.Fixed
PassThruConnect CAN. The adapter occasionally restarted by itself about 300 ms after a CAN channel was opened. Also, OtaUpdate after a CAN session within the same PassThruOpen did not start the update: the adapter did not respond to its start for 30 s.PassThruClose. In addition, during a CAN or DoIP session and after it, the adapter could hang when the port was opened or closed. With firmware build 86 and library 2.0.0.241 the port can be opened and closed safely after PassThruClose. A hang is possible only if the port is closed during a session, before PassThruClose. On macOS and Linux the port is free for another program after PassThruClose; on Windows it stays held until the library is unloaded, as before. Do not open the port yourself and do not change DTR and RTS while the library is using it.PassThruWriteMsgs still got an "all accepted" reply, and the messages that did not fit were lost. In addition, the adapter lost memory on each such message and, under heavy traffic, within a few dozen seconds started rejecting requests for lack of memory until it was rebooted. The adapter now waits for queue space within the call timeout and reports how many messages it accepted. The library returns a shortfall as ERR_BUFFER_FULL with a zero timeout and as ERR_TIMEOUT with a non-zero one, and writes the number of accepted messages to pNumMsgs.BUFFER_OVERFLOW bit set in RxStatus.ETHERNET_NDIS. PassThruDisconnect and GET_NDIS_ADAPTER_INFO with the channel ID returned by PassThruConnect answered ERR_INVALID_CHANNEL_ID, and PassThruClose did not close this channel. A failed disconnect now returns ERR_FAILED; previously 0xFFFB came back instead of a J2534 code.P4_MIN = 0 frames broke off with TX_FAILED.New
Promise of an object in which ret holds the J2534 code and the other fields carry the output parameters. Information about discovered vehicles (ISO13400_GET_VEHICLE_INFO, HSFZ_GET_VEHICLE_INFO) arrives as an array of objects with the fields vin, logicalAddress, ip and others rather than as structure bytes. The user picks a BLE adapter by name in the browser dialog, and on the first connection the system asks for the PIN. A wrong or cancelled PIN gives ERR_FAILED and the text BLE pairing rejected, as in the native library. Besides the PassThru functions, ConfigRead, ConfigWrite, ConfigReset, ConfigReboot and firmware update are available: OtaUpdate takes the firmware file as an ArrayBuffer and reports progress, and OtaAbort stops an update in progress, in which case OtaUpdate returns the new code OTA_RESULT_ERR_ABORTED (-510). Two files go onto your web site, j2534.js and j2534_web.mjs, and the page must be served over HTTPS. The demo page j2534_demo.html opens straight from disk. Firmware build 86 is required. Closing the USB port (releasePort() or closing the tab) reboots the Nano ET: when the port is closed, the USB driver toggles the DTR line, and on the Nano ET this line controls the adapter reset. The native library releases the lines in a safe order, but the browser does not allow that. Details are in docs/WEB.md.
Example
import { loadJ2534 } from './j2534.js';
const ISO15765 = 6;
connectButton.onclick = async () => {
// the browser shows the device picker only in response to a user action
const port = await navigator.serial.requestPort({ filters: [{ usbVendorId: 0x0403 }] });
const j2534 = await loadJ2534(port);
// over BLE, pass a BLE device instead of the port:
// const device = await navigator.bluetooth.requestDevice({ filters: [{ services: [0xfff0] }] });
// const j2534 = await loadJ2534(device);
const { ret, deviceId } = await j2534.PassThruOpen();
if (ret !== 0) {
const { errorDescription } = await j2534.PassThruGetLastError();
console.error(errorDescription);
return;
}
const { channelId } = await j2534.PassThruConnect(deviceId, ISO15765, 0, 500000);
const { msgs } = await j2534.PassThruReadMsgs(channelId, 10, 100);
// firmware update from a file chosen by the user
const fw = await fileInput.files[0].arrayBuffer();
const r = await j2534.OtaUpdate(deviceId, fw, (current, total) => {
progressBar.max = total;
progressBar.value = current;
});
// r.ret: 0 on success, -510 after OtaAbort, other codes in FIRMWARE_UPDATE.md
};
Fixed
HSFZ_GET_VEHICLE_INFO returned only the first 8 bytes of information about each discovered vehicle. The application received a fragment of the VIN without the ECU address and IP address. Each hsfz_vehicle_info_t record now arrives in full, 32 bytes: VIN, ECU address, MAC and IP. Fixed in all native libraries.PassThruWriteMsgs with more than 40 messages or more than 5115 bytes hung the adapter. The library sent such a call as a single command, and the adapter does not accept such a command. The adapter did not respond to it and the call waited for the full timeout, while over LAN/WLAN the adapter parsed the data bytes of this command as subsequent commands. The library now splits the call into several commands, and each of them gets the remainder of the call timeout. All messages are checked before the first one is sent. Firmware build 86 answers such a command from older library versions with ERR_INVALID_MSG.PassThruReadMsgs with a timeout and a request for more than 128 messages was rejected entirely, because the adapter returns at most 128 messages at a time. The call now returns up to 128 messages, and the rest are read by the next call.CLEAR_RX_BUFFER did not clear messages the library had already fetched from the adapter, and the next PassThruReadMsgs returned old frames. The library buffer is now cleared too.ERR_DEVICE_NOT_CONNECTED. Descriptor 0 is free when the process has closed standard input, as services usually do.PassThruReadMsgs with the adapter closed. If the adapter was not open, the function returned an error but left in *pNumMsgs the number the application had requested. An application that checks the number of messages read rather than the return code processed that many empty messages. In this case *pNumMsgs is now 0.PassThruGetLastError. After the connection was lost, the function returned the error text of one of the previous calls, for example ERR_BUFFER_EMPTY, although the return code was correct. The text now names the cause: LAN socket closed, BLE not connected or serial port not open. For ERR_BUFFER_OVERFLOW (0x12) Unknown error: 18 was shown instead of the name.Download J2534 libraries 2.0.0.225
New
ConfigRead, and ConfigWrite drops a pairing. No new library calls were added for this.
Example
char json[2048];
if (ConfigRead(dev, json, sizeof(json)) == STATUS_NOERROR) {
/* in the reply, among the other settings:
"ble_bonds":[{"name":"WS-07","mac":"A4:C1:38:11:22:33"}] */
}
ConfigWrite(dev, "{\"ble_bond_del\":\"A4:C1:38:11:22:33\"}");
/* takes effect at once, no ConfigReboot needed */
Fixed
reciv ack end single msg timeout SG, then reciv ack counter error SG.TX_FAILED, and only reconnecting the channel brought it back.New
ConfigRead, ConfigWrite, ConfigReboot and ConfigReset (ordinals @52-@55 on Windows) were exported before, but they were absent from the documentation, so there was no way to use them from outside. docs/API_REFERENCE.md now describes the prototypes and the behaviour, and docs/CONFIGURATION.md carries the table of keys with the ranges the firmware validates. The device handles these commands ahead of J2534 routing, so they work over every transport: LAN/WLAN, BLE and USB.
Example
char json[2048];
ConfigRead(dev, json, sizeof(json)); /* the whole configuration as one JSON object */
ConfigWrite(dev, "{\"ble_name\":\"WS-07\"}"); /* only the keys you pass are changed */
ConfigReboot(dev); /* the settings take effect on reboot;
device_id becomes invalid, reopen after ~10 s */
ptConfigRead, ptConfigWrite, ptConfigReboot and ptConfigReset.
Example
external fun ptConfigRead(devId: Int): String? // null on error
external fun ptConfigWrite(devId: Int, json: String): Int
external fun ptConfigReboot(devId: Int): Int
external fun ptConfigReset(devId: Int): Int
val json = j2534.ptConfigRead(devId) ?: return // reason: ptGetLastError()
j2534.ptConfigWrite(devId, """{"ble_name":"WS-07"}""")
j2534.ptConfigReboot(devId)
PassThruOpen deliberately does not check the version, to avoid an extra exchange with the device on every open: the installed build comes from PassThruReadVersion, and a mismatch shows up in the .qlog as the line firmware build N is older than build 85 required by DLL ….Fixed
ERR_FAILED, and PassThruGetLastError gives the text BLE pairing rejected (wrong PIN or not paired). ERR_FAILED was chosen deliberately, because applications rarely ask for the error text on ERR_DEVICE_NOT_CONNECTED. This works on every platform: Android, macOS, Windows and Linux. Other BLE failures still return ERR_DEVICE_NOT_CONNECTED, but now with the transport code: it used to be printed as a POSIX error number, and a refused write surfaced as the string 0x7 - Argument list too long.ptClose over BLE. When closing a BLE connection, the library closed file descriptor 0, although there is no socket in BLE mode. Usually this silently closed stdin, but if fd 0 was held by a JVM object at that moment, fdsan aborted the process: attempted to close file descriptor 0 … owned by native object. That is why the crash was intermittent.ptClose over BLE took an extra second. The library discarded the device's reply to the close command, waited out the 1 s receive timeout and logged 0x6E - Connection timed out, as if the device had not answered. Closing now completes on the device's reply. macOS, Windows and Linux were not affected.ptOpen over BLE crashed when the device did not answer the open command. The app terminated with SIGSEGV. On other open failures (rejected pairing, connection timeout) the library did not release its global JNI reference to BLEManager, so references piled up with repeated attempts.FAST_INIT. The device reply was copied into the application pt_msg_t without a length check: a short reply led to a read of uninitialised memory, and an inflated length in the reply led to a write past the structure the application had passed. On Android the same call sent a malformed frame onto the bus, and on failure it aborted the application. The input size check was fixed as well: it accepted NumOfBytes values around 0x20000000.FIVE_BAUD_INIT. The input is limited to a single address byte, as §11.3.3.3 requires; a block of any length used to be accepted, and an application passing more received a success code instead of a refusal..qlog file was not created. Only PassThruOpen opened the log, while the ptOpen call bypasses it. So for a third-party Android application the sdlogs folder stayed empty at any logging level, records went to logcat only, and a session log could not be collected from a customer.FIVE_BAUD_INIT and FAST_INIT lines printed only > ok: neither the init address nor the ECU answer reached the log, so a failed init could not be analysed from it. The line now carries both the request and the answer: io 1 FIVE_BAUD_INIT 33 > 8F6F 2850ms.Download J2534 libraries 2.0.0.213 - Windows x86/x64/ARM64 (separate builds for Windows 7), macOS (universal), Linux (x64, x86, ARM, ARM64), Android (arm64-v8a, armeabi-v7a, x86, x86_64), iOS (XCFramework); the docs/ folder holds the SDK documentation (getting started, API reference, configuration, error handling, DoIP, firmware update, log format, Android, iOS). Static .a archives ship only for iOS and the Linux x64 server build; all other platforms load the library dynamically.
Fixed
CAN_PS, ISO15765_PS, J1939_PS and pin setting on _PS channels - connecting these three protocols was rejected by the device; they now work the same way as TP2_0_PS and ISO9141_PS. Pin setting has been brought in line with J2534-2 on three points:
PassThruConnect on the default pins, although a _PS channel must stay silent until SET_CONFIG(J1962_PINS). The channel now goes on the bus only after the pins are set.SET_CONFIG(J1962_PINS) switched a live channel to other contacts in the middle of a session. Per the standard, pins are set once per channel: a repeated call returns ERR_CHANNEL_IN_USE; other pins are possible only after PassThruDisconnect.ERR_PIN_NOT_SUPPORTED.uint32_t ch;
pt_config_t pins = { J1962_PINS, 0x0000060EU }; /* pins 6 and 14 */
pt_config_list_t cfg = { 1, &pins };
PassThruConnect(dev, ISO15765_PS, 0, 500000, &ch);
/* the channel is not on the bus yet */
if (PassThruIoctl(ch, SET_CONFIG, &cfg, NULL) != STATUS_NOERROR) {
/* ERR_PIN_NOT_SUPPORTED - this combination is not in the device wiring */
}
/* only now PassThruWriteMsgs / PassThruReadMsgs;
a repeated SET_CONFIG(J1962_PINS) - ERR_CHANNEL_IN_USE */
PassThruWriteMsgs returned success, PassThruReadMsgs returned nothing and the CONNECTION_LOST flag was never raised. ECU programming over TP2.0 verified on the bench.REQUEST_CONNECTION: a dozen attempts against a silent ECU used up all filter slots and muted the channel; TEARDOWN_CONNECTION on TP1_6_PS was rejected - the connection could not be closed from the application side; TP2.0 did not accept a connection established by the ECU and did not pass frames received outside a connection to the application (§19.3.1 J2534-2).PassThruReadMsgs. The connection here is point-to-point, the tester address is registered at routing activation, there is nothing to filter out: the channel passes every message to the application and PassThruStartMsgFilter answers ERR_NOT_SUPPORTED. A failure in the CAN driver rebooted the device in the middle of a DoIP session. The receive task watchdog is raised from 5 to 30 s - establishing a DoIP connection legitimately takes up to 20 s.PassThruConnect got a queue overflow on its very first message. Base CAN and ISO15765 went to the other CAN controller and never reached the bus. Reading of the receive queue did not recover after a corrupted entry - the flag was checked incorrectly in all CAN-based protocols.GET_NDIS_ADAPTER_INFO - uninitialised data was returned under STATUS_NOERROR. The reply now carries the adapter identifier, MAC, the IPv4 address at which the ECU sees the device, and the activation line state; on a device without Ethernet - ERR_NOT_SUPPORTED.GET_PROTOCOL_INFO - answered for only some protocols and in the wrong format. It now works on any open channel: timestamp resolution (1 µs), supported parity, UART data bits. A parameter the device cannot answer is flagged in the supported field while the call itself returns STATUS_NOERROR.PassThruDisconnect - accessing an already terminated channel task corrupted device memory.New
libj2534.xcframework contains slices for the device (arm64) and the simulator (arm64/x86_64), minimum iOS 12.0. Each slice carries the j2534.h and j2534_ota.h headers, a module map (Swift import J2534, CoreBluetooth autolinked) and a privacy manifest. The Pass-Thru API prototypes are declared in j2534.h itself - on all platforms. mbedTLS is compiled in, there are no external dependencies. Xcode setup: Embed = Do Not Embed (the library is static), -lc++ in Other Linker Flags, the NSBluetoothAlwaysUsageDescription (BLE) and NSLocalNetworkUsageDescription (WLAN) keys in Info.plist - without them iOS terminates the app on first use of the transport.
import J2534
var deviceId: UInt32 = 0
// PassThruOpen takes a mutable char* - pass a copy of the string
var cstr = Array("ScanDoc;b:N4999".utf8CString) // BLE by name prefix
let ret = cstr.withUnsafeMutableBufferPointer { PassThruOpen($0.baseAddress, &deviceId) }
if ret == 0 {
var fw = [CChar](repeating: 0, count: 80)
var dll = [CChar](repeating: 0, count: 80)
var api = [CChar](repeating: 0, count: 80)
PassThruReadVersion(deviceId, &fw, &dll, &api)
PassThruClose(deviceId)
}
/* protocol and IOCTL IDs are macros with a cast and do not import into Swift:
use the numbers, let CAN: UInt32 = 5, let ISO15765: UInt32 = 6 */
ptOtaUpdate(devId, firmwarePath, callback) and ptOtaAbort(devId) are added to the JNI layer; previously OtaUpdate/OtaAbort were available only through the C API. Progress is reported in onProgress(current, total) - blocks, 1-based.
// update.bin has been copied into app storage beforehand.
// The call blocks - run it off the main thread.
val res = j2534.ptOtaUpdate(devId, file.absolutePath,
object : OtaProgressListener {
override fun onProgress(current: Int, total: Int) { /* progress bar */ }
})
if (res.status == 0) {
// firmware written, the device reboots: devId is invalid,
// reconnect with a new ptOpen (over BLE - allow ~10 s)
}
// res.status < 0 - an ota_result_t code (see j2534_ota.h)
// ptOtaAbort(devId) cancels the update without rebooting the device
log_level key in j2534.json: -1 off, 0 errors, 1 +warnings, 2 +info, 3 +debug, 4 +verbose; the default is 3, i.e. without configuration the log is written in full. At -1 neither the sdlogs folder nor the .qlog file is created and nothing is written to disk. On Android the level can also be set from code - ptSetLogLevel(int); a level set this way takes priority over the configuration file, so in a release build the log cannot be enabled from outside.
// Android: call before ptOpen
j2534.ptSetLogLevel(-1) // release build - log off
j2534.ptSetLogLevel(3) // support case - full log
// Other platforms: j2534.json in the configuration folder
// macOS ~/Library/Application Support/Quantex/
// Linux ~/.config/quantex/
// Windows %APPDATA%\Quantex\
{ "log_level": -1, "devices": [] }
Fixed
PassThruReadVersion and the .qlog header returned 2.0.0.0: the build number was not passed into the Android build. The version is now derived by the same rule as on the other platforms; this is the number to quote when contacting support.serial_* symbols: the USB transport is excluded from the iOS build, but calls to it remained. The transport is replaced by a stub - connecting with a c: string returns a regular port-open error..qlog log - message data was written up to 125 bytes only, and the record of a group of messages passed in a single PassThruReadMsgs or PassThruWriteMsgs call was limited by a fixed buffer. The message and the whole group are now written in full - important for long responses such as a DTC list..qlog log - the library and the device produced the log text with two independent implementations, and the decoding of the same values differed. The established-connection flag of TP2.0 and TP1.6 in the RxStatus field was printed as CONNECTION_ESTABLISHED by the library and as CONN_OK by the device; IOCTL names differed in 22 places. The names of protocols, TxFlags and RxStatus flags, IOCTLs and their parameters are now produced by a single implementation, so the application log and the device log of the same exchange read side by side.Download J2534 libraries 2.0.0.200 - Windows x86/x64/ARM64 (separate builds for Windows 7), macOS (universal), Linux (x64, x86, ARM, ARM64), Android (arm64-v8a, armeabi-v7a, x86, x86_64), iOS.
New
ISO13400_PS (0x8FFD) and HSFZ_PS (0x8FFC). They are not part of the SAE J2534 standard - this is a proprietary ScanDoc extension: diagnostics over Ethernet - vehicle discovery on the network (VIN, logical address), TCP connection, routing activation, UDS exchange. The default tester address is 0 - set ISO13400_SOURCE_ADDR before routing activation, otherwise the gateway refuses; the ECU address is passed in every message ([TA][SA][UDS]), ISO13400_TARGET_ADDR is not set via Set/GetConfig. Transmission is serialized by P2: one outstanding UDS request at a time, NRC 7F xx 78 extends the wait to P2*max (6 s). New channel parameter ISO13400_P3_DOIP (0x8108) - pause between messages.
uint32_t ch, code = 0;
pt_config_t sa = { ISO13400_SOURCE_ADDR, 0x0E80 };
pt_config_list_t cfg = { 1, &sa };
PassThruConnect(dev, ISO13400_PS, 0, 0, &ch);
PassThruIoctl(ch, SET_CONFIG, &cfg, NULL); /* SA - before routing activation */
PassThruIoctl(ch, ISO13400_DISCOVER_VEHICLES, NULL, NULL); /* the ECU IP is remembered automatically */
PassThruIoctl(ch, ISO13400_CONNECT_TCP, NULL, NULL);
PassThruIoctl(ch, ISO13400_ACTIVATE_ROUTING, NULL, &code); /* 0x10 = success */
/* then PassThruWriteMsgs / PassThruReadMsgs - regular UDS */
0x55 (J2534 frame marker) - J2534, anything else (text AT command) - ELM327.Fixed
PassThruStartMsgFilter compared only the 4 CAN ID bytes, ignoring the given filter length. Frames are now matched over the full length as the standard requires: PASS/BLOCK by frame content works.
/* Suppress TesterPresent responses (07E8 02 7E ...) in the receive queue */
pt_msg_t mask = {0}, pattern = {0};
mask.protocol_id = pattern.protocol_id = CAN;
mask.data_size = pattern.data_size = 6; /* 4 CAN ID bytes + 2 data bytes */
memcpy(mask.data, "\xFF\xFF\xFF\xFF\xFF\xFF", 6);
memcpy(pattern.data, "\x00\x00\x07\xE8\x02\x7E", 6);
uint32_t fid;
PassThruStartMsgFilter(ch, BLOCK_FILTER, &mask, &pattern, NULL, &fid);
AT SH command on an active CAN channel broke Flow Control (the FC went out without padding, DLC=3 - the gateway sent no Consecutive Frames) and overwrote the receive filter with its own TX ID (reception without AT CRA broke). Per the datasheet, AT SH sets only the transmit header - the receive filter is now controlled only by AT CRA/CF/CM.PassThruStopPeriodicMsg could send one extra frame after stopping._PS - pin selection via SET_CONFIG(J1962_PINS) was not applied, frames never reached the bus.Fixed