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.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