import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:testing/model/json_element.dart'; import 'package:testing/src/esp_log.dart'; import 'package:testing/src/hexjson.dart'; import 'package:testing/src/hexvar.dart'; import 'package:testing/src/instr_desc.dart'; import 'package:testing/src/utility.dart'; enum FlashStatus { idle, start, completed, aborted } enum DataSt { DS_DT, DS_VAR } const int CC_VARIABLE_NOT_FOUND = -53; const int CC_OK = 0; const int CC_ERROR = -1; const int CC_LINE_COMMENT = 1; const int CC_RPTEND = -50; const int CC_INVALID_FUNCTION_NAME = -55; const int CC_UNIMPLEMENTED_FUNCTION = -52; const int CC_ECU_NO_RESPONSE = -1; bool tpModeActive = true; class RptHandle { String hdl; int strtPos; int endPos; String varName; RptHandle({ required this.hdl, required this.strtPos, required this.endPos, required this.varName, }); @override String toString() => 'RptHandle(hdl: $hdl, strtPos: $strtPos, endPos: $endPos, varName: $varName)'; } const int MAX_REPEATS = 20; final List rphdls = List.generate( MAX_REPEATS, (_) => RptHandle(hdl: "", strtPos: 0, endPos: 0, varName: ""), ); FlashStatus flashStatus = FlashStatus.idle; JsonElement? jsroot; Future openFsqFile(String fsqFileName) async { final file = File(fsqFileName); if (!await file.exists()) { Logger.log("Failed to open fsq file for reading"); return null; } else { Logger.log("File opened"); return file; } } Future> waitForSimulatedECUResponse() async { // Simulate ECU response arriving in < 2 seconds Logger.log('sending ecu response....'); await Future.delayed(const Duration(milliseconds: 200)); return ['0x7F', 'XX', '0x78']; } Future sendTesterPresent() async { try { // If ECU does NOT respond within 2 seconds → throw timeout final resp = await waitForSimulatedECUResponse().timeout( const Duration(seconds: 2), ); Logger.log("ECU Response (Tester Present): $resp"); if (resp.length >= 2 && resp[0] == '0x7E' && resp[1] == '0x00') { return true; // VALID } return false; // Wrong response } catch (e) { Logger.log("TesterPresent TIMEOUT — ECU did not respond in 2 seconds"); return false; // TIMEOUT = FAILURE } } class TesterPresentController { bool active = false; bool abortFSQ = false; Future start({ required Future Function() sendTesterPresent, Duration timeout = const Duration(seconds: 2), }) async { if (active) return; active = true; abortFSQ = false; tpModeActive = true; while (active) { Logger.log("TesterPresent: Sending 3E 00"); // Send Tester Present (3E 00) final ok = await sendTesterPresent(); if (!ok) { Logger.log("TesterPresent: NO VALID RESPONSE → STOP FOTA"); abortFSQ = true; active = false; break; } Logger.log("TesterPresent: Valid 7E 00 received, waiting before next TP"); // Wait exactly 2 seconds AFTER receiving OK await Future.delayed(timeout); } } void stop() { Logger.log("TesterPresent STOPPED"); tpModeActive = false; active = false; } } final tpController = TesterPresentController(); Future waitFor2709Response() async { Logger.log("Waiting for ECU response for 2709..."); final start = DateTime.now(); while (true) { // --------- GLOBAL TIMEOUT (3 sec) --------- if (DateTime.now().difference(start) > const Duration(seconds: 3)) { Logger.log("2709 TIMEOUT → FOTA CANCEL"); return false; } // ======== WAIT FOR ECU RESPONSE ======== final resp = await waitForSimulatedECUResponse(); Logger.log("ECU Response: $resp"); // ====================================================== // CASE 1: ECU SILENT (resp empty) // ====================================================== if (resp.isEmpty) { Logger.log("ECU silent → sending 3E 80"); await send3E80(); await Future.delayed(Duration(milliseconds: 300)); continue; } // ====================================================== // CASE 2: ECU → 7F XX 78 (RESPONSE PENDING) // After sending 3E 80, keep checking response. // ====================================================== if (resp.length >= 3 && resp[0] == '0x7F' && resp[2] == '0x78') { Logger.log("ECU pending → sending 3E 80"); await Future.delayed(Duration(milliseconds: 300)); await send3E80(); // keep looping to wait for next ECU message continue; } // ====================================================== // CASE 3: POSITIVE RESPONSE // ====================================================== if (resp.length >= 2 && resp[0] == '0x67' && resp[1] == '0x09') { Logger.log("ECU accepted 2709"); return true; } // ====================================================== // ANY OTHER RESPONSE = FAILURE // ====================================================== Logger.log("Unexpected ECU response → $resp"); return false; } } Future send3E80() async { Uint8List data = convertHexStringToBytes("3E80"); Logger.log("Sending: $data"); // CAN send function // await handleSendToECU(data); } Future readLine(RandomAccessFile fp, Uint8List buffer) async { int bytesRead = 0; while (true) { int byte; try { byte = await fp.readByte(); } catch (_) { break; // unexpected error } if (byte == -1) { break; // EOF reached } buffer[bytesRead++] = byte; if (byte == '\n'.codeUnitAt(0)) { return bytesRead; } if (bytesRead >= buffer.length - 1) { break; } } return bytesRead; // returns 0 if true EOF } int trimCRLF(Uint8List buffer, int length) { while (length > 0) { final ch = buffer[length - 1]; if (ch == '\r'.codeUnitAt(0) || ch == '\n'.codeUnitAt(0)) { buffer[length - 1] = 0; length--; } else { break; } } return length; } class CmdOpResult { final Uint8List fn; final int fnLen; final Uint8List? data; final int dataLen; CmdOpResult({ required this.fn, required this.fnLen, required this.data, required this.dataLen, }); } CmdOpResult cmdopsep(Uint8List buffer, int length) { final colonIndex = buffer.indexOf(':'.codeUnitAt(0)); if (colonIndex == -1) { // No colon found return CmdOpResult( fn: buffer.sublist(0, length), fnLen: length, data: null, dataLen: 0, ); } // Split function and data final fn = buffer.sublist(0, colonIndex); final data = buffer.sublist(colonIndex + 1, length); return CmdOpResult( fn: fn, fnLen: fn.length, data: data, dataLen: data.length, ); } class CmdOpSep { final List fn; final List data; CmdOpSep(this.fn, this.data); } CmdOpSep cmdOpSep(Uint8List buff, int len) { int colonIndex = -1; for (int i = 0; i < len; i++) { if (buff[i] == ':'.codeUnitAt(0)) { colonIndex = i; break; } } if (colonIndex == -1) { // no colon return CmdOpSep(buff.sublist(0, len), []); } return CmdOpSep( buff.sublist(0, colonIndex), buff.sublist(colonIndex + 1, len), ); } final Map variableValues = {}; void setVariableValue(String varName, int v) { variableValues[varName] = v; } void freeLoopHandle(RptHandle h) { h.hdl = ""; h.strtPos = 0; h.endPos = 0; h.varName = ""; } Future commandRepeatStart( RandomAccessFile fp, String jsonFileName, Uint8List fdata, int fdataLen, ) async { Logger.log("Repeat Start Command"); String content = String.fromCharCodes(fdata.sublist(0, fdataLen)); List parts = content.split(","); if (parts.length < 4) { Logger.log("repeatstart parse error: not enough args"); return CC_ERROR; } String rptHdl = parts[0].trim(); // handle name String rptVar = parts[1].trim(); // loop variable name String varInitVal = parts[2].trim(); String varEndVal = parts[3].trim(); int initVal = int.tryParse(varInitVal) ?? 0; int? fetched = await getVariableValue(jsonFileName, varEndVal); int endVal; if (fetched != null) { endVal = fetched; } else { endVal = convertHexStringToInt(varEndVal); } RptHandle? h = getHandle(rptHdl); if (h == null) { Logger.log("Failed to get repeat handle"); return CC_ERROR; } // Save file start position (ftell) h.strtPos = await fp.position(); h.hdl = rptHdl; h.varName = rptVar; Logger.log("Handle startPos = ${h.strtPos}"); Var? nv = allocateNewVal(rptVar); if (nv == null) { Logger.log("RepeatStart: failed to allocate loop variable"); return CC_ERROR; } Logger.log("RepeatStart: loop variable '$rptVar' allocated"); Logger.log("INNER LOOP WILL RUN FOR $endVal times"); for (int i = initVal; i <= endVal; i++) { nv.value = i; // Reset file position await fp.setPosition(h.strtPos); Logger.log("Running iteration $i"); int crawlerRet = await crawler(fp, jsonFileName, h); if (crawlerRet != CC_OK && crawlerRet != CC_RPTEND) { removeVariable(rptVar); return crawlerRet; } } // --------------------------------------------------------- // Cleanup removeVariable(rptVar); await fp.setPosition(h.endPos); // freeLoopHandle(h) h.hdl = ""; h.varName = ""; h.strtPos = 0; h.endPos = 0; return CC_OK; } Future getSectorData( String jsonFileName, int index, int loopCount, int sizePerPacket, int upto, Uint8List buff, ValueSetter setLen, ) async { int transmittedSoFar = loopCount * sizePerPacket; if (transmittedSoFar >= upto) { return -1; } int fetchableSize = 0; if (transmittedSoFar + sizePerPacket <= upto) { fetchableSize = sizePerPacket; } else { fetchableSize = upto - transmittedSoFar; } // JSON Path `SectorData[index].JsonData` String actJsonSrch = "SectorData[$index].JsonData"; // Read JSON file completely final file = File(jsonFileName); if (!file.existsSync()) { print("Failed to open file"); return -1; } final jsonText = await file.readAsString(); final json = jsonDecode(jsonText); if (!json.containsKey("SectorData")) { print("SectorData not found!"); return -1; } String hexString; try { hexString = json["SectorData"][index]["JsonData"]; } catch (e) { print("JsonData not found!"); return -1; } List chars = hexString.codeUnits; int seekIncr = 1 + (transmittedSoFar * 2); if (seekIncr >= chars.length) { return -1; } int totalChars = fetchableSize * 2; for (int i = 0; i < totalChars; i++) { if (seekIncr + i >= chars.length) break; buff[i] = chars[seekIncr + i]; } setLen(totalChars); return 0; } Future sendCANFrame(Uint8List data) async { // This function must: // 1. Transmit the CAN frame via UART/Wifi // 2. Wait for TX ACK from your firmware (Indication) // 3. Set 'Indication' to 0 or negative Logger.log("Sending CAN Frame..."); await Future.delayed(const Duration(milliseconds: 50)); // simulate tx // Simulated indication: //Indication = 0; // success //Indication = -1; // fail if (indication == 0) { Logger.log("ECU TX SUCCESS (Indication=0)"); return 0; } else { Logger.log("ECU TX FAIL (Indication=$indication)"); return -1; } } int indication = 0; // Dummy handleSend - REPLACE with your actual send function. Future handleSend(Uint8List data) async { Logger.log('handleSend (stub) sending ${data.length} bytes: $data'); // return 0 for success, non-zero for failure // Send to communica, wait for indication. // if indication = 0 -> restart tpController (reason -> succesfull TX CAN msg) // if indication < 0 -> couldnt TX CAN MSG, stop TP controller. retry CAN msg // indication = 0 after retry -> start tpController. // indication < 0 after retry -> stop FOTA altogether -> returns here with negative 1. indication = await sendCANFrame(data); if (indication == 0) { Logger.log("CAN TX SUCCESS → restarting TP"); tpController.start(sendTesterPresent: sendTesterPresent); return 0; } Logger.log("CAN TX FAILED → stopping TP & retrying once."); tpController.stop(); // disable TesterPresent so it doesn't interfere // ------------ STEP 2: Retry -------------- int retryResult = await sendCANFrame(data); if (retryResult == 0) { Logger.log("RETRY SUCCESS → restarting TP"); tpController.start(sendTesterPresent: sendTesterPresent); return 0; // success } // ------------ STEP 3: Both attempts failed → FOTA FAIL -------------- Logger.log("RETRY FAILED → FOTA MUST STOP"); tpController.stop(); return -1; // wait for ECU response from communica within given timeout -> 1 second. // pass the msg from communica via the modal, and find out ECU response. // if ECU's response [0] == data[0] + 0x40 -> positive response -> return here with 0. // if ECU's response [0] == 0x7f && response [2] == 0x78 -> wait response, there is no timeout for ECU response anymore. we'll wait indefinately. // wait response = keep on sending 0x3e 0x80 (tp without response), as long as we dont get a +ve response or NRC, we keep on sending it, with given timeout of 1.5 seconds. // if ECU's response is anything else, then, consider it a random response, it is not what we need, ignore the other response // if ECU's response [0] == 0x7f && response [2] != 0x78 || response [2] != 0x00, then it is an NRC, stop FOTA altogether // return -ve in case of NRC, or timeout. // after a successful CAN TX and a +ve response, if there is no CAN TX to or no CAN RX from ECU i.e. no msg from ECU as incoming, or we havent sent any msg, under given timeout (1.5 seconds for now) // send a tester present -> 0x3e, 0x00. wait for response (1 second) -> 0x7e, 0x00 // if on sending tester present with ack, we get no 0x7e 0x00, it means ECU isnt responding, stop FOTA altogether, and send 0x11 0x02 for ECU soft restart. intentional. // implement this entire logic using tpcontroller. return 0; } Future commandSendBulkData( String jsonFileName, RptHandle? hin, Uint8List fdata, // bytes carrying "bsc,1,36+bsc+json_sectordata[...]" etc. ) async { Logger.log("Sending bulk data"); // ----------------------------- // Step 1: Parse fdata arguments // ----------------------------- final raw = utf8.decode(fdata); // convert bytes to String Logger.log("RAW sendbulkdata: $raw"); // Split on first two commas; the rest stays as sendcmd (commas may appear inside bracket) final firstComma = raw.indexOf(','); if (firstComma == -1) { Logger.log("sendbulkdata: invalid args (no comma)"); return CC_ERROR; } final secondComma = raw.indexOf(',', firstComma + 1); if (secondComma == -1) { Logger.log("sendbulkdata: invalid args (only one comma)"); return CC_ERROR; } final seqvar = raw.substring(0, firstComma).trim(); final initvalstr = raw.substring(firstComma + 1, secondComma).trim(); String sendcmdstr = raw.substring(secondComma + 1).trim(); // Normalize spaces around brackets and commas (C's strtok is forgiving) sendcmdstr = sendcmdstr .replaceAll(" [", "[") .replaceAll("[ ", "[") .replaceAll(" ]", "]") .replaceAll("] ", "]") .replaceAll(" ,", ",") .replaceAll(", ", ",") .trim(); Logger.log( "Parsed args: seqvar=$seqvar initval=$initvalstr sendcmd=$sendcmdstr", ); // ----------------------------- // Initialize sequence variable // ----------------------------- final int init = int.tryParse(initvalstr) ?? 0; ValuePointer? seqPtr = getValuePointer(seqvar); if (seqPtr == null) { // allocate and re-fetch pointer await Future.microtask(() => allocateNewVal(seqvar)); seqPtr = getValuePointer(seqvar); if (seqPtr == null) { Logger.log("sendbulkdata: failed to allocate seq variable '$seqvar'"); return CC_ERROR; } } seqPtr.value = init; // --------------------------------------------------- // Step 2: Extract packet size and base command string // --------------------------------------------------- final open = sendcmdstr.indexOf('['); final close = sendcmdstr.lastIndexOf(']'); if (open == -1 || close == -1 || close <= open) { Logger.log("sendbulkdata: malformed command string, missing brackets"); return CC_ERROR; } final baseCmdStr = sendcmdstr .substring(0, open) .trim(); // "36+bsc+json_sectordata" final bracketContentRaw = sendcmdstr .substring(open + 1, close) .trim(); // "json_sector_len[i],0FC" // Clean bracket content and split on first comma only (like strtok) var inside = bracketContentRaw .replaceAll(", ", ",") .replaceAll(" ,", ",") .trim(); final commaIndex = inside.indexOf(','); if (commaIndex == -1) { Logger.log("sendbulkdata: invalid bracket content (no comma)"); return CC_ERROR; } final totalLenStr = inside.substring(0, commaIndex).trim(); final pktSizeStr = inside.substring(commaIndex + 1).trim(); final pktSize = convertHexStringToInt(pktSizeStr); if (pktSize <= 0) { Logger.log("sendbulkdata: invalid packet size hex '$pktSizeStr'"); return CC_ERROR; } Logger.log("Packet size parsed = $pktSize"); // Resolve total buffer length variable final int? totalBuffLenNullable = await getVariableValue( jsonFileName, totalLenStr, ); if (totalBuffLenNullable == null) { Logger.log( "sendbulkdata: Could not resolve total buffer length for $totalLenStr", ); return CC_ERROR; } final int totalBuffLen = totalBuffLenNullable; Logger.log("Total buffer length = $totalBuffLen"); // --------------------------------------------------- // Step 3: Allocate buffers once (Dart typed arrays) // --------------------------------------------------- final Uint8List cdata = Uint8List(pktSize + 40); final Uint8List sectorHexChars = Uint8List( pktSize * 2 + 8, ); // ascii hex chars int transmittedBuffLen = 0; int iIter = 0; int status = CC_OK; // Pre-split tokens from baseCmdStr by '+' final tokens = baseCmdStr .split('+') .map((s) => s.trim()) .where((s) => s.isNotEmpty) .toList(); if (tokens.isEmpty) { Logger.log("sendbulkdata: baseCmdStr tokens empty"); return CC_ERROR; } // --------------------------------------------------- // Step 4: Transmission loop - iterate until all bytes sent // --------------------------------------------------- while (transmittedBuffLen < totalBuffLen) { // Clear cdata cdata.fillRange(0, cdata.length, 0); int cdatai = 0; bool foundSectorData = false; int sizeOfHexTransmitetdData = 0; // Parse tokens (like inner strtok loop in C) for (final tok in tokens) { if (tok == seqvar) { // write one byte from sequence variable final int seqVal = seqPtr.value; cdata[cdatai++] = seqVal & 0xFF; } else if (tok.toLowerCase() == 'json_sectordata') { // Resolve index from hin->varName if (hin == null || hin.varName.isEmpty) { Logger.log("sendbulkdata: missing repeat handle or handle.varName"); status = CC_ERROR; break; } final int? idxVal = await getVariableValue(jsonFileName, hin.varName); if (idxVal == null) { Logger.log( "sendbulkdata: Failed to resolve index from ${hin.varName}", ); status = CC_ERROR; break; } final int sectorIndexForJson = idxVal > 0 ? idxVal - 1 : 0; // Ask getSectorData to fill sectorHexChars with ASCII hex chars int lenChars = 0; final gsdRet = await getSectorData( jsonFileName, sectorIndexForJson, iIter, pktSize, totalBuffLen, sectorHexChars, (v) => lenChars = v, ); if (gsdRet != 0) { Logger.log("sendbulkdata: getSectorData failed"); status = CC_ERROR; break; } // convert ascii hex chars to bytes final hexString = utf8.decode(sectorHexChars.sublist(0, lenChars)); final Uint8List bytes = convertHexStringToBytes(hexString); cdata.setRange(cdatai, cdatai + bytes.length, bytes); cdatai += bytes.length; sizeOfHexTransmitetdData = bytes.length; foundSectorData = true; } else { // If token is even length and hex-like → treat as hex string final maybeHex = tok.replaceAll(' ', ''); final isHexCandidate = RegExp(r'^[0-9A-Fa-f]+$').hasMatch(maybeHex); if ((maybeHex.length & 1) == 0 && isHexCandidate) { final bytes = convertHexStringToBytes(maybeHex); cdata.setRange(cdatai, cdatai + bytes.length, bytes); cdatai += bytes.length; } else { Logger.log("sendbulkdata: Unhandled token: '$tok'"); status = CC_ERROR; break; } } } // end tokens loop if (status != CC_OK || !foundSectorData) { Logger.log("sendbulkdata: status error or no sector data found"); status = CC_ERROR; break; } // --------------------------------------- // Send buffer (call your transport) // --------------------------------------- final packet = Uint8List.fromList(cdata.sublist(0, cdatai)); final sendRet = await handleSend(packet); if (sendRet != 0) { Logger.log("sendbulkdata: handleSend failed: $sendRet"); status = CC_ERROR; break; } transmittedBuffLen += sizeOfHexTransmitetdData; Logger.log( "Sent packet ($cdatai bytes), progress $transmittedBuffLen/$totalBuffLen", ); // increment sequence & loop counter seqPtr.value = seqPtr.value + 1; iIter++; } // end while // --------------------------------------------------- // Step 5: Cleanup (Dart GC will reclaim arrays) // --------------------------------------------------- if (status != CC_OK) { Logger.log("sendbulkdata: finished with status $status"); return CC_ERROR; } return CC_OK; } // store values like your getValuePointer + allocateNewVal final Map _vars = {}; void setValue(String name, int value) { _vars[name] = value; } int? getValue(String name) => _vars[name]; Future performFn( RandomAccessFile fp, Uint8List fn, int fnLen, Uint8List fdata, int fdataLen, RptHandle? hin, String jsonFileName, ) async { // Convert function name final fnStr = String.fromCharCodes(fn.sublist(0, fnLen)).trim(); Logger.log('[performFn] FN = "$fnStr" (len=$fnLen)'); // Print incoming data Logger.log( '[performFn] RAW DATA ($fdataLen bytes): "${String.fromCharCodes(fdata.sublist(0, fdataLen))}"', ); Logger.log( '[performFn] HEX BYTES: ${fdata.sublist(0, fdataLen).map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}', ); if (fdataLen == 0) { Logger.log("[performFn] WARNING: fdata is EMPTY → nothing to send."); } // ------------------------------- // SEND // ------------------------------- if (fnStr == "send") { return await commandSend(jsonFileName, fdata.sublist(0, fdataLen)); } // ------------------------------- // REPEAT START // ------------------------------- if (fnStr == "repeatstart") { return await commandRepeatStart(fp, jsonFileName, fdata, fdataLen); } // ------------------------------- // REPEAT END // ------------------------------- if (fnStr == "repeatend") { Logger.log("Repeat END Command"); if (hin != null) hin.endPos = await fp.position(); return CC_RPTEND; } // ------------------------------- // SEND BULK DATA // ------------------------------- if (fnStr == "sendbulkdata") { return commandSendBulkData(jsonFileName, hin, fdata.sublist(0, fdataLen)); } // ------------------------------- // FUNCTION() // ------------------------------- if (fnStr == "function") { final raw = String.fromCharCodes(fdata); final funcName = raw.split("[").first; final argsStr = raw.contains("[") ? raw.split("[")[1].split("]")[0] : ""; final args = argsStr.split(","); final algo = args.isNotEmpty ? args[0] : ""; final keyStr = args.length > 1 ? args[1] : ""; int keyLen = int.tryParse(keyStr) ?? 0; if (funcName == "CalculateKeyFromSeed") { int key = 0; int nrev = 0; for (int i = 0; i < keyLen; i++) { final originalByte = ((key >> ((keyLen - 1 - i) * 8)) & 0xFF); nrev |= (originalByte << (i * 8)); } setValue("key", nrev); return CC_OK; } return CC_INVALID_FUNCTION_NAME; } // ------------------------------- // PROTOCOL // ------------------------------- if (fnStr == "protocol") { final protocolStr = String.fromCharCodes(fdata, 0, fdataLen).trim(); Logger.log("protocol: $protocolStr"); const protocolMap = { "ISO15765-250KB-11BIT-CAN": PROTOCOL_ISO15765_250KB_11BIT_CAN, "ISO15765-250Kb-29BIT-CAN": PROTOCOL_ISO15765_250KB_29BIT_CAN, "ISO15765-500KB-11BIT-CAN": PROTOCOL_ISO15765_500KB_11BIT_CAN, "ISO15765-500KB-29BIT-CAN": PROTOCOL_ISO15765_500KB_29BIT_CAN, "ISO15765-1MB-11BIT-CAN": PROTOCOL_ISO15765_1MB_11BIT_CAN, "ISO15765-1MB-29BIT-CAN": PROTOCOL_ISO15765_1MB_29BIT_CAN, "250KB-11BIT-CAN": PROTOCOL_250KB_11BIT_CAN, "250Kb-29BIT-CAN": PROTOCOL_250KB_29BIT_CAN, "500KB-11BIT-CAN": PROTOCOL_500KB_11BIT_CAN, "500KB-29BIT-CAN": PROTOCOL_500KB_29BIT_CAN, "1MB-11BIT-CAN": PROTOCOL_1MB_11BIT_CAN, "1MB-29BIT-CAN": PROTOCOL_1MB_29BIT_CAN, "OE-IVN-250KBPS-11BIT-CAN": PROTOCOL_OE_IVN_250KB_11BIT_CAN, "OE-IVN-250KBPS-29BIT-CAN": PROTOCOL_OE_IVN_250KB_29BIT_CAN, "OE-IVN-500KBPS-11BIT-CAN": PROTOCOL_OE_IVN_500KB_11BIT_CAN, "OE-IVN-500KBPS-29BIT-CAN": PROTOCOL_OE_IVN_500KB_29BIT_CAN, "OE-IVN-1MBPS-11BIT-CAN": PROTOCOL_OE_IVN_1MB_11BIT_CAN, "OE-IVN-1MBPS-29BIT-CAN": PROTOCOL_OE_IVN_1MB_29BIT_CAN, }; final protocolToSet = protocolMap[protocolStr]; if (protocolToSet == null) { Logger.log("Unknown Protocol"); return CC_UNIMPLEMENTED_FUNCTION; } Logger.log("Protocol set: $protocolToSet"); return CC_OK; } // ------------------------------- // TXID // ------------------------------- if (fnStr == "txid") { final valueStr = String.fromCharCodes(fdata, 0, fdataLen).trim(); final int hexValue = int.parse(valueStr, radix: 16); Logger.log("Txid set: ${hexValue.toRadixString(16).toUpperCase()}"); return CC_OK; } // ------------------------------- // RXID // ------------------------------- if (fnStr == "rxid") { final valueStr = String.fromCharCodes(fdata, 0, fdataLen).trim(); final int hexValue = int.parse(valueStr, radix: 16); Logger.log("Rxid set: ${hexValue.toRadixString(16).toUpperCase()}"); return CC_OK; } // ------------------------------- // START PADDING // ------------------------------- if (fnStr == "strtpadding") { final valueStr = String.fromCharCodes(fdata, 0, fdataLen).trim(); final int hexValue = int.parse(valueStr, radix: 16) & 0xFF; Logger.log("Padding byte: ${hexValue.toRadixString(16).toUpperCase()}"); return CC_OK; } // ------------------------------- // STOP PADDING // ------------------------------- if (fnStr == "Stoppadding") { Logger.log("Stop padding"); return CC_OK; } // ------------------------------- // TP START // ------------------------------- if (fnStr == "strtTP") { Logger.log("Tester Present: START"); tpController.start(sendTesterPresent: sendTesterPresent); return CC_OK; } // ------------------------------- // TP STOP // ------------------------------- if (fnStr == "stopTP") { tpController.stop(); Logger.log("Tester Present: STOP"); return CC_OK; } // ------------------------------- // SET T2MAX // ------------------------------- if (fnStr == "setT2MAX") { final valueStr = String.fromCharCodes(fdata, 0, fdataLen).trim(); final int timeMs = int.parse(valueStr, radix: 16) & 0xFFFF; Logger.log("T2MAX set: ${timeMs.toRadixString(16).toUpperCase()} ms"); return CC_OK; } // ------------------------------- // SET STMIN // ------------------------------- if (fnStr == "setstmin") { final valueStr = String.fromCharCodes(fdata, 0, fdataLen).trim(); final int timeMs = int.parse(valueStr, radix: 16) & 0xFFFF; Logger.log("STmin set: ${timeMs.toRadixString(16).toUpperCase()} ms"); return CC_OK; } // ------------------------------- // ECUMAPFILE / CHKSUM // ------------------------------- if (fnStr == "EcuMapFile" || fnStr == "chksum") { return CC_OK; } // ------------------------------- // UNKNOWN COMMAND // ------------------------------- Logger.log("Unrecognized command: $fnStr"); return CC_UNIMPLEMENTED_FUNCTION; } Future lineOp( RandomAccessFile fp, Uint8List buff, int len, RptHandle? h, String jsonFileName, ) async { Logger.log("FN($len): ${String.fromCharCodes(buff)}"); // Comment check if (len >= 2 && buff[0] == '/'.codeUnitAt(0) && buff[1] == '/'.codeUnitAt(0)) { Logger.log("Comment Only"); return CC_LINE_COMMENT; } // Split fn:data final sep = cmdOpSep(buff, len); final fn = sep.fn; // List final data = sep.data; // List Logger.log("FN(${fn.length}): ${String.fromCharCodes(fn)}"); if (data.isNotEmpty) { Logger.log("DATA(${data.length}): ${String.fromCharCodes(data)}"); } // Convert List --> Uint8List final Uint8List fnBytes = Uint8List.fromList(fn); final Uint8List dataBytes = Uint8List.fromList(data); Logger.log('fnBytes: $fnBytes'); Logger.log('dataBytes: $dataBytes'); return await performFn( fp, fnBytes, fnBytes.length, dataBytes, dataBytes.length, h, jsonFileName, ); // return 0; } Future getVariableValue(String jsonFileName, String nameIn) async { // Step 1: Copy & clean name String name = nameIn.trim(); // Step 2: Check for array-like syntax final regex = RegExp(r'(\w+)\[(.+)\]'); final match = regex.firstMatch(name); if (match != null) { final tok = match.group(1)!; final indexStr = match.group(2)!; final vindex = await getVariableValue(jsonFileName, indexStr); if (vindex == null || vindex < 0) return null; final indexingForJson = vindex > 0 ? vindex - 1 : 0; // Handle JSON variable types if (tok == "json_strt_addr") { final val = await getJsonAddressVal( jsonFileName, "JsonStartAddress", indexingForJson, ); return val; } else if (tok == "json_end_addr") { final val = await getJsonAddressVal( jsonFileName, "JsonEndAddress", indexingForJson, ); return val; } else if (tok == "json_sector_len") { final start = await getJsonAddressVal( jsonFileName, "JsonStartAddress", indexingForJson, ); final end = await getJsonAddressVal( jsonFileName, "JsonEndAddress", indexingForJson, ); return end - start + 1; // inclusive } else if (tok == "json_checksum") { final val = await getJsonAddressVal( jsonFileName, "JsonCheckSum", indexingForJson, ); return val; } else { Logger.log("Unknown JSON variable requested: $tok"); return null; } } // Step 3: Handle no_of_sectors or noofsectors else if (name == "no_of_sectors" || name == "noofsectors") { final val = await getNumberOfSectors(jsonFileName); Logger.log("Got no of sector value: $val"); return val; } // Step 4: Otherwise, try to find normal variable final actVal = getValuePointer(name); if (actVal == null) { Logger.log("Unexpected variable not found: $name"); return null; } else { Logger.log("Found variable $name = ${actVal.value}"); } return actVal.value; } RptHandle? getHandle(String hdl) { RptHandle? nextFree; for (var h in rphdls) { // Empty entry (hdl == "") if (h.hdl.isEmpty) { nextFree ??= h; } // hdl matches existing else if (h.hdl == hdl) { return h; } } // No free slot found → return null if (nextFree == null) { return null; } // Assign name (C strcpy) nextFree.hdl = hdl; return nextFree; } Future commandSend(String jsonFileName, Uint8List fdata) async { Logger.log("Send Command"); // Output buffer (same as hdata[1024]) final Uint8List hdata = Uint8List(1024); int hlen = 0; DataSt ds = DataSt.DS_DT; // DS_DT = hex-data, DS_VAR = section int usedLen = 0; // Process entire fdata buffer while (usedLen < fdata.length) { int ch = fdata[usedLen]; // ================================ // (1) DATA MODE (hex text) // ================================ if (ds == DataSt.DS_DT) { // If we see '+', switch to variable mode if (ch == '+'.codeUnitAt(0)) { ds = DataSt.DS_VAR; usedLen++; continue; } // Find next '+' or end int plusIndex = fdata.indexOf('+'.codeUnitAt(0), usedLen); if (plusIndex == -1) plusIndex = fdata.length; // Extract hex token as string String tok = String.fromCharCodes(fdata.sublist(usedLen, plusIndex)); int toklen = tok.length; if (toklen % 2 != 0) { Logger.log("Hexstring not even: $tok"); } else { Uint8List bytes = convertHexStringToBytes(tok); hdata.setRange(hlen, hlen + bytes.length, bytes); hlen += bytes.length; } // Jump past this token and + null-like separator ds = DataSt.DS_VAR; usedLen = plusIndex + 1; continue; } // ================================ // (2) VARIABLE MODE () // ================================ if (ds == DataSt.DS_VAR) { if (ch != '<'.codeUnitAt(0)) { Logger.log("Unexpected non-var start: '${String.fromCharCode(ch)}'"); return CC_VARIABLE_NOT_FOUND; } // Find closing '>' int closing = fdata.indexOf('>'.codeUnitAt(0), usedLen); if (closing == -1) { Logger.log("Variable enclosure missing"); return CC_VARIABLE_NOT_FOUND; } // Extract full <...> contents String inside = String.fromCharCodes( fdata.sublist(usedLen + 1, closing), ); // skip '<' and '>' List parts = inside.split(','); String varName = parts[0].trim(); int varByteSize = (parts.length > 1) ? int.tryParse(parts[1]) ?? 1 : 1; Logger.log("Fetching variable $varName (size: $varByteSize)"); // ---- Get variable value final variableValue = await getVariableValue(jsonFileName, varName); if (variableValue == null) { Logger.log("Could not get value for variable: $varName"); return CC_VARIABLE_NOT_FOUND; } // ---- Write variable bytes (BIG-ENDIAN) for (int i = 0; i < varByteSize; i++) { int shift = (8 * (varByteSize - 1 - i)); hdata[hlen++] = (variableValue >> shift) & 0xFF; } // Move past <...> usedLen = closing + 1; ds = DataSt.DS_DT; continue; } } // LOG FINAL PACKET Logger.log("SEND CMD FINAL LEN = $hlen"); Logger.log("FINAL BYTES = ${hdata.sublist(0, hlen)}"); // ---- SEND THROUGH CAN return handleSend(hdata.sublist(0, hlen)); // return CC_OK; } /// Correct version of convertHexStringToBytes (from previous chat) Uint8List convertHexStringToBytes(String hexString) { hexString = hexString.replaceAll(RegExp(r'[^0-9A-Fa-f]'), ''); int length = hexString.length; Uint8List bytes = Uint8List(length ~/ 2); for (int i = 0; i < length; i += 2) { bytes[i ~/ 2] = int.parse(hexString.substring(i, i + 2), radix: 16); } return bytes; } int convertHexStringToInt(String hexStr) { // Clean and normalize hex hexStr = hexStr.replaceAll(RegExp(r'[^0-9A-Fa-f]'), ''); if (hexStr.length.isOdd) hexStr = "0$hexStr"; final bytes = convertHexStringToBytes(hexStr); // Step 1: Place into END of 8-byte buffer (rev) final rev = List.filled(8, 0); int hblen = bytes.length; int offset = 8 - hblen; for (int i = 0; i < hblen; i++) { rev[offset + i] = bytes[i]; } // Step 2: Reverse to get nrev final nrev = List.filled(8, 0); for (int i = 0; i < 8; i++) { nrev[i] = rev[7 - i]; } // Step 3: Interpret nrev AS LITTLE-ENDIAN INT64 (this matches C behavior) final bd = ByteData(8); for (int i = 0; i < 8; i++) { bd.setUint8(i, nrev[i]); } // LITTLE ENDIAN !! int result = bd.getInt64(0, Endian.little); return result; } String normalizeHex(String raw) { raw = raw.replaceAll(RegExp(r'[^0-9A-Fa-f]'), ''); if (raw.isEmpty) return "00"; if (raw.length.isOdd) raw = "0$raw"; return raw; } Future getJsonHexValueOfKey(String jsonFilePath, String searchKey) async { final buffer = Uint8List(200); int arlen = 0; int jr = await getJsonValue( jsonFilePath, searchKey, buffer, (newLen) => arlen = newLen, ); if (jr != 0) { Logger.log("Json value failed to obtain"); return 0; } // Extract raw string String raw = utf8.decode(buffer.sublist(0, arlen), allowMalformed: true); raw = raw.replaceAll('\u0000', '').trim(); // Logger.log("RAW = $raw"); // Logger.log("CODE UNITS = ${raw.codeUnits}"); // FINAL FIX – DO NOT SUBSTRING final extracted = raw; // Logger.log("EXTRACTED = $extracted"); return int.parse(extracted, radix: 16); } Future getJsonAddressVal( String jsonFilePath, String vname, int vindex, ) async { // Equivalent to: sprintf(actJsonSrch, "SectorData[%d].%s", vindex, vname); final actJsonSrch = "SectorData[$vindex].$vname"; // print("Actual Json search str = $actJsonSrch"); // Call your Dart version of getJsonHexValueOfKey() return await getJsonHexValueOfKey(jsonFilePath, actJsonSrch); } Future getElementStringFromFile( String filePath, int start, int length, ) async { if (length > 100) { return ""; // same as arr[0] = '\0' } final file = File(filePath); final raf = await file.open(); // Save current pointer final originalPos = await raf.position(); // Move pointer like fseek(fp, s) await raf.setPosition(start); // Read l bytes final data = await raf.read(length); // Null terminate equivalent: Dart strings do NOT need it final str = utf8.decode(data, allowMalformed: true); // Restore pointer (like fseek(fp, n)) await raf.setPosition(originalPos); await raf.close(); return str; } dynamic searchKey(dynamic root, String key) { if (root == null) return null; // Convert key to a mutable buffer like C final keycp = key.split('').toList(); keycp.add('\x00'); // null-terminator like C int keyOpIndex = 0; dynamic t = root; while (true) { if (keyOpIndex >= keycp.length) return t; final char = keycp[keyOpIndex]; // --------------------------- // Case 1: array indexing "[n]" // --------------------------- if (char == '[') { int start = keyOpIndex + 1; int end = start; // find closing bracket ']' while (end < keycp.length && keycp[end] != ']') end++; if (end >= keycp.length) return null; final indexStr = keycp.sublist(start, end).join(); final index = int.tryParse(indexStr); if (index == null) return null; if (t is! List || index >= t.length) return null; t = t[index]; keyOpIndex = end + 1; continue; } // --------------------------- // Case 2: dot separator "." // --------------------------- if (char == '.') { keyOpIndex++; continue; } // --------------------------- // Case 3: End of key // --------------------------- if (char == '\x00') { return t; } // --------------------------- // Case 4: Normal object key // read until '[' or '.' or '\0' // --------------------------- int start = keyOpIndex; while (keyOpIndex < keycp.length && keycp[keyOpIndex] != '[' && keycp[keyOpIndex] != '.' && keycp[keyOpIndex] != '\x00') { keyOpIndex++; } final keyName = keycp.sublist(start, keyOpIndex).join(); // Access Map key if (t is Map) { if (!t.containsKey(keyName)) { return null; } t = t[keyName]; } else { return null; } // loop continues } } String getElementStringFromBytes(Uint8List bytes, int s, int l) { if (l > 100) { return ""; // same as C arr[0] = '\0'; } if (s < 0 || l <= 0 || s + l > bytes.length) { return ""; } // Slice bytes [s .. s+l) final Uint8List sub = bytes.sublist(s, s + l); // Decode UTF-8 and return return String.fromCharCodes(sub); } Future getJsonValue( String jsonFilePath, String key, Uint8List outBuffer, void Function(int) updateLength, ) async { // Load JSON file final file = File(jsonFilePath); if (!await file.exists()) { Logger.log("JSON file not found: $jsonFilePath"); return -1; } final content = await file.readAsString(); dynamic parsedJson; try { parsedJson = json.decode(content); } catch (e) { Logger.log("Failed to parse JSON: $e"); return -1; } // Extract the value using nested key search final value = searchKey(parsedJson, key); if (value == null) { return -3; // Key not found } final raw = value.toString(); final length = raw.length; // Set length (like writing to pointer in C) updateLength(length); // Ensure buffer is large enough if (length > outBuffer.length) { return -2; } // Write into output buffer for (int i = 0; i < length; i++) { outBuffer[i] = raw.codeUnitAt(i); } return 0; } Future getNumberOfSectors(String jsonFilePath) async { // Must be Uint8List because native code receives raw bytes final buffer = Uint8List(200); int len = 0; // We will set this using callback int result = await getJsonValue( jsonFilePath, "NoOfSectors", buffer, (int v) => len = v, // callback sets length ); if (result != 0 || len == 0) { Logger.log("Failed to obtain NoOfSectors"); return 0; } // Extract string from raw bytes final value = utf8.decode(buffer.sublist(0, len)); return int.tryParse(value.trim()) ?? 0; } Future scrollJson(String jsonFilePath) async { try { final file = File(jsonFilePath); if (!await file.exists()) { Logger.log("Failed to open file"); return -1; } // Reset pool index njsoi = 0; for (int i = 0; i < MAX_JSON_ITEMS; i++) { // optional: reset fields of each node if you reuse pool final node = jsonElementStorageArray[i]; node.start = 0; node.end = 0; node.len = 0; node.parent = null; node.child = null; node.next = null; node.prev = null; node.waitingForColon = false; node.elemIndex = -1; node.type = JsonElementType.object; } final bytes = await file.readAsBytes(); int n = 0; JsonElement? current; final root = getNewRootElement(); // iterate through bytes and feed parser with a single-byte buffer each time while (n < bytes.length && n < 0xFFFFFF) { final Uint8List buf = Uint8List(1); buf[0] = bytes[n]; final int ret = parseBytes(buf, 1, n, root, current, (cur) { current = cur; }); if (ret != 0) { Logger.log("parse error at index $n (ret=$ret)"); return -2; } n++; } Logger.log("Read Bytes count: $n"); jsroot = root; return 0; } catch (e) { Logger.log("Exception reading JSON: $e"); return -2; } } Future fetchTotalBytesToFlash(String jsonFilePath) async { final noOfSectors = await getNumberOfSectors(jsonFilePath); Logger.log("Got no of sector value: $noOfSectors"); int total = 0; for (int i = 0; i < noOfSectors; i++) { final startKey = "SectorData[$i].JsonStartAddress"; final endKey = "SectorData[$i].JsonEndAddress"; Logger.log("Searching for $startKey"); final startAddr = await getJsonHexValueOfKey(jsonFilePath, startKey); Logger.log("Searching for $endKey"); final endAddr = await getJsonHexValueOfKey(jsonFilePath, endKey); final size = (endAddr - startAddr) + 1; Logger.log( "Sector $i → Start: ${startAddr.toRadixString(16)} End: ${endAddr.toRadixString(16)} Size: $size", ); total += size; } Logger.log("Total bytes to flash = $total"); return total; } Future crawler( RandomAccessFile fp, String jsonFilePath, RptHandle? h, ) async { final Uint8List buff = Uint8List(1024); int rl; do { rl = await readLine(fp, buff); Logger.log('readline: $rl ${String.fromCharCodes(buff.sublist(0, rl))}'); if (rl > 0) { final rx = trimCRLF(buff, rl); Logger.log('trimCRLF: $rx ${String.fromCharCodes(buff.sublist(0, rx))}'); if (rx > 0) { if (tpController.abortFSQ) { Logger.log("TP ABORT → FSQ TERMINATED"); return CC_ECU_NO_RESPONSE; } final clean = Uint8List.fromList(buff.sublist(0, rx)); final loRet = await lineOp(fp, clean, clean.length, h, jsonFilePath); Logger.log('loRet: $loRet'); if (loRet < CC_OK) { return loRet; } } } } while (rl > 0); return CC_OK; } Future start(String jsonFilePath, String fsqFilePath) async { try { final jsonFile = File(jsonFilePath); final fsqFile = File(fsqFilePath); if (!await jsonFile.exists()) { Logger.log("JSON file not found: $jsonFilePath"); return; } if (!await fsqFile.exists()) { Logger.log("FSQ file not found: $fsqFilePath"); return; } Logger.log("JSON file path: $jsonFilePath"); Logger.log("FSQ file path: $fsqFilePath"); final stopwatch = Stopwatch()..start(); final scrollResult = await scrollJson(jsonFilePath); if (scrollResult != 0) { Logger.log("Error: Failed to scroll JSON file (ERR: $scrollResult)"); return; } else { Logger.log("Sucess: scroll JSON file: $scrollResult"); } Logger.log("Time spent parsing JSON: ${stopwatch.elapsedMicroseconds} µs"); fetchTotalBytesToFlash(jsonFilePath); flashStatus = FlashStatus.start; final checkFsqFile = await openFsqFile(fsqFilePath); if (checkFsqFile == null) { return; } initVars(); final fp = await fsqFile.open(); int crRet = await crawler(fp, jsonFilePath, null); await fp.close(); flashStatus = FlashStatus.completed; if (crRet != 0) { flashStatus = FlashStatus.aborted; Logger.log('Crawler Returned with error: $crRet'); } Logger.log('crawler result: $crRet'); } catch (e) { Logger.log('Exception opening directory, $e'); return; } }