// // *** This code is for communicating with the USB port and uploading and download protocal-x files *** // // ************* ****** Check if updating the file ********************************** // This part of the code development was started on 06-25-26. It addes direct * // connectiveity to a computer through the ESP32 USB port to modified whatever * // protocol-x.txt that is loaded into the EProm. It was developed as an alternative * // to removing the micro-SD card to make changes to the Generator run parameters * // ********************************************************************************** // // +++ VERY FIRST THING TO DO IS DECIDE IF SENDING, RECEIVING, OR QUITTING *** // bool passFail; // pass or fail flag byte srqFlag; // SEND RECEIVE QUIT flag // // Note: "curY" defined in 1st - main module void usbcomm(){ // 1st decide if SEND. RECEIVE, or QUIT curY = 25; // use curY as a position marker srqFlag = 0; // SEND RECEIVE QUIT flag while(1 == 1) { // loop until the break if(!digitalRead(interruptPin)) // until you break decide what to do break; // Now do SEND, RECEIVE, or QUIT while(!digitalRead(cursorPin)){ // loop until cursor is pressed display.clearDisplay(); // Clear the OLED buffer display.invertDisplay(true); // Invert the display as a flag display.setCursor(0,0); // Set the OLED cursor postion display.setTextSize(2); // Set the OLED text size display.print(" USB Comm."); // Display the quoted text display.setTextSize(1); // Set the OLED text size if(curY == 25) { // if here send to CPU on condition display.setCursor(0,curY); // Set the OLED cursor postion display.print("-> Send To CPU"); // Display the active quoted text display.setCursor(0,40); // Set the OLED cursor postion display.print(" Receive Fm CPU"); // Display the quoted text display.setCursor(0,55); // Set the OLED cursor postion display.print(" Return to Main"); // Display the quoted text srqFlag = 1; // set 1 means SEND } if(curY == 40) { // if here then Receive from CPU display.setCursor(0,25); // Set the OLED cursor postion display.print(" Send To CPU"); // Display the active quoted text display.setCursor(0,curY); // Set the OLED cursor postion display.print("-> Receive Fm CPU"); // Display the active quoted text display.setCursor(0,55); // Set the OLED cursor postion display.print(" Return to Main"); // Display the quoted text srqFlag = 2; // set 2 means RECEIVE } if(curY == 55) { // if here then exit on condition display.setCursor(0,25); // Set the OLED cursor postion display.print(" Send To CPU"); // Display the active quoted text display.setCursor(0,40); // Set the OLED cursor postion display.print(" Receive Fm CPU"); // Display the active quoted text display.setCursor(0,curY); // Set the OLED cursor postion display.print("-> Return to Main"); // Display the active quoted text srqFlag= 0; // set 0 means QUIT - JUST EXIT } display.display(); // actually displays text to the OLED delay(250); // allow a little debounce time curY += 15; // update to create a step if(curY > 55) curY = 25; // force to start at top of step } } if(srqFlag == 1) { // on condition -- SEND to PC display.clearDisplay(); // Clear the OLED buffer display.setTextSize(2); // Set the OLED text size display.setCursor(0,5); // Set the OLED cursor postion display.print(" Turn off"); // Display the quoted text display.setCursor(0,25); // Set the OLED cursor postion display.print(" MiniCom"); // Display the quoted text display.setCursor(0,45); // Set the OLED cursor postion display.print(" Start Py"); // Display the quoted text display.display(); // actually display the info display.setTextSize(1); // Set the OLED text size delay(2000); // give some display viewing time passFail = downloadProgramOverUsb(); // Send the data if (!passFail) sendError(); else sendGood(); } if(srqFlag == 2) { // on condition you will be RECEIVING display.clearDisplay(); // Clear the OLED buffer display.setTextSize(2); // Set the OLED text size display.setCursor(0,5); // Set the OLED cursor postion display.print(" Turn off"); // Display the quoted text display.setCursor(0,25); // Set the OLED cursor postion display.print(" MiniCom"); // Display the quoted text display.setCursor(0,45); // Set the OLED cursor postion display.print(" Start Py"); // Display the quoted text display.display(); // actually display the info display.setTextSize(1); // Set the OLED text size delay(2000); // give some display viewing time passFail = uploadProgramOverUsb(); // load from PC to SD card, then reload from SD if (!passFail) sendError(); else sendGood(); } runFlag = 0; // insure RunFlag is off display.invertDisplay(false); // restore the display to normal } void sendGood(){ display.clearDisplay(); // Clear the OLED buffer display.invertDisplay(true); // Invert the display as a flag display.setCursor(0,25); // Set the OLED cursor postion display.setTextSize(2); // Set the OLED text size display.print(" SEND GOOD"); // Display the quoted text display.display(); // actually display the info display.setTextSize(1); // Set the OLED text size delay(5000); // give some display viewing time Serial.print("Pass Fail = "); Serial.println(passFail); } void sendError(){ display.clearDisplay(); // Clear the OLED buffer display.invertDisplay(true); // Invert the display as a flag display.setCursor(0,25); // Set the OLED cursor postion display.setTextSize(2); // Set the OLED text size display.print(" SEND FAIL"); // Display the quoted text display.display(); // actually display the info display.setTextSize(1); // Set the OLED text size delay(10000); // give some display viewing time Serial.print("Pass Fail = "); Serial.println(passFail); } //**************************************************************************** // The following code is the SEND code to send the EEProm data to the computer // *************************************************************************** // USB_Comm.ino // ------------------------------------------------------------ // EEPROM-to-USB download support for ESP32 Four Channel Generator // Sends the CURRENT EEPROM image out over Serial as ASCII text. // // Assumptions: // 1. EPromFreqObject and EPromControlObject are already declared globally. // 2. FLASH_START is already defined. // 3. EEPROM.begin(...) has already been called in setup(). // 4. The EEPROM has already been loaded from microSD before this routine runs. // ------------------------------------------------------------ // Included in the main module #include // Optional: adjust if your Serial monitor / Python utility uses another timeout const unsigned long COMM_TIMEOUT_MS = 15000; // ------------------------------------------------------------ String readSerialLine(unsigned long timeoutMs) { String s = ""; unsigned long startMs = millis(); while ((millis() - startMs) < timeoutMs) { while (Serial.available()) { char c = (char)Serial.read(); if (c == '\r') { continue; // ignore CR } if (c == '\n') { s.trim(); return s; } s += c; } delay(1); } s.trim(); return s; // may be blank on timeout } // ------------------------------------------------------------ void serialSendLine(const String &s) { Serial.print(s); Serial.print("\r\n"); } // ------------------------------------------------------------ // Parses a trailing ":N" (N = 0-99) off the PC's "READY:N" handshake line, // so the device knows which /protocols-N.txt to send -- lets the PC // download ANY preset file, not just whatever's currently active in EEPROM. // ------------------------------------------------------------ static bool parseFileNoAfterColon(const String &line, byte &outFileNo) { int colonPos = line.indexOf(':'); if (colonPos < 0) return false; String numPart = line.substring(colonPos + 1); numPart.trim(); if (numPart.length() == 0) return false; for (unsigned int i = 0; i < numPart.length(); i++) { if (!isDigit(numPart[i])) return false; } int v = numPart.toInt(); if (v < 0 || v > 99) return false; outFileNo = (byte)v; return true; } // ------------------------------------------------------------ // ******** Entry point for Downloading to Computer *********** // Reads the requested /protocols-N.txt straight off the SD card and // streams its lines to the PC -- EEPROM is not involved, so this can // download ANY preset file, not just whatever's currently active/loaded. // Returns true if the file was sent successfully. bool downloadProgramOverUsb() { //*********************************************************** // Added code to bypass wait and time out It is presumed ready if(srqFlag != 2) { //This is a RECEIVE FUNCTION TEST Serial.println(); serialSendLine("DOWNLOAD_READY"); serialSendLine("TYPE READY: TO BEGIN OR QUIT TO ABORT"); String cmd = readSerialLine(COMM_TIMEOUT_MS); if (cmd.equalsIgnoreCase("QUIT")) { serialSendLine("DOWNLOAD_ABORTED"); return false; } byte requestedFileNo; if (!cmd.startsWith("READY") || !parseFileNoAfterColon(cmd, requestedFileNo)) { serialSendLine("ERROR:NO_READY_RECEIVED"); return false; } sprintf(fileName, "/protocols-%d.txt", requestedFileNo); // shared global filename buffer } // *** END OF PRESUMED READY *** File srcFile = SD.open(fileName); if (!srcFile) { serialSendLine("ERROR:FILE_NOT_FOUND"); return false; } // Begin transfer framing serialSendLine("BEGIN_FILE"); serialSendLine("#"); // minimal header, matches what readFileModified() expects bool sawHeader = false; while (srcFile.available()) { String line = srcFile.readStringUntil('\n'); line.trim(); if (line.length() == 0) continue; if (!sawHeader) { // skip the file's own header line (e.g. "Protocols-0 #") -- sawHeader = true; // the "#" line above already re-establishes it for the PC continue; } serialSendLine(line.c_str()); } srcFile.close(); serialSendLine("END_FILE"); serialSendLine("DOWNLOAD_OK"); return true; } // //**************************************************************************** // The following code is to RECEIVE CVS data store in the protocol-x file // found in a default directory on the USERS PC. // *************************************************************************** // // ============================================================ // USB upload / receive support for Protocol-X // Assumptions: // 1) Frequency record lines contain 9 CSV fields // 2) Control record lines contain 11 CSV fields // Writes validated lines to the SD card (protocols-N.txt), not EEPROM -- // see uploadProgramOverUsb() below for why. // ============================================================ static const char* USB_UPLOAD_READY = "UPLOAD_READY"; static const char* USB_BEGIN_FILE = "BEGIN_FILE"; static const char* USB_END_FILE = "END_FILE"; static const char* USB_UPLOAD_OK = "UPLOAD_OK"; static const char* USB_UPLOAD_ERROR = "UPLOAD_ERROR"; static const unsigned long USB_UPLOAD_START_TIMEOUT_MS = 30000UL; static const unsigned long USB_UPLOAD_LINE_TIMEOUT_MS = 10000UL; static const size_t USB_UPLOAD_MAX_TOKENS = 11; // ------------------------------------------------------------ // Local serial helpers // ------------------------------------------------------------ static void usbUploadSendLine(const char* s) { Serial.print(s); Serial.print("\r\n"); } static bool usbUploadReadLine(String &lineOut, unsigned long timeoutMs) { unsigned long startMs = millis(); lineOut = ""; while ((millis() - startMs) < timeoutMs) { while (Serial.available() > 0) { char c = (char)Serial.read(); if (c == '\r') { continue; } if (c == '\n') { lineOut.trim(); return true; } lineOut += c; // simple guard against runaway lines if (lineOut.length() > 220) { return false; } } delay(2); } return false; } static void usbUploadFlushInput() { while (Serial.available() > 0) { Serial.read(); } } // ------------------------------------------------------------ // Strict numeric parsing // ------------------------------------------------------------ static bool parseIntStrict(const String &s, int &outVal) { char *endPtr = nullptr; long v = strtol(s.c_str(), &endPtr, 10); if ((endPtr == s.c_str()) || (*endPtr != '\0')) return false; outVal = (int)v; return true; } static bool parseULongStrict(const String &s, unsigned long &outVal) { char *endPtr = nullptr; unsigned long v = strtoul(s.c_str(), &endPtr, 10); if ((endPtr == s.c_str()) || (*endPtr != '\0')) return false; outVal = v; return true; } static bool parseFloatStrict(const String &s, float &outVal) { char *endPtr = nullptr; float v = strtof(s.c_str(), &endPtr); if ((endPtr == s.c_str()) || (*endPtr != '\0')) return false; outVal = v; return true; } static bool parseByteStrict(const String &s, byte &outVal) { int tmp = 0; if (!parseIntStrict(s, tmp)) return false; if (tmp < 0 || tmp > 255) return false; outVal = (byte)tmp; return true; } // ------------------------------------------------------------ // CSV splitting // ------------------------------------------------------------ static int splitCsvUpload(const String &line, String tokens[], int maxTokens) { int count = 0; int start = 0; while (true) { if (count >= maxTokens) { return maxTokens + 1; // too many fields } int commaPos = line.indexOf(',', start); if (commaPos < 0) { tokens[count] = line.substring(start); tokens[count].trim(); count++; break; } tokens[count] = line.substring(start, commaPos); tokens[count].trim(); count++; start = commaPos + 1; } return count; } // ------------------------------------------------------------ // Record parsers // ------------------------------------------------------------ static bool parseFreqCsvLineUpload(const String &line, EPromFreqObject &obj) { String t[9]; int n = splitCsvUpload(line, t, 9); if (n != 9) return false; if (!parseIntStrict (t[0], obj.field0)) return false; if (!parseFloatStrict(t[1], obj.field1)) return false; if (!parseFloatStrict(t[2], obj.field2)) return false; if (!parseFloatStrict(t[3], obj.field3)) return false; if (!parseFloatStrict(t[4], obj.field4)) return false; if (!parseFloatStrict(t[5], obj.field5)) return false; if (!parseFloatStrict(t[6], obj.field6)) return false; if (!parseFloatStrict(t[7], obj.field7)) return false; if (!parseFloatStrict(t[8], obj.field8)) return false; return true; } static bool parseControlCsvLineUpload(const String &line, EPromControlObject &obj) { String t[11]; int n = splitCsvUpload(line, t, 11); if (n != 11) return false; if (!parseIntStrict (t[0], obj.field0)) return false; if (!parseIntStrict (t[1], obj.field1)) return false; if (!parseByteStrict (t[2], obj.field2)) return false; // field3/field4 here are whole seconds / whole minutes (the CSV convention -- see // readFileModified()/saveFile()), not ms. This function only validates the incoming line // before it's written verbatim to SD; the 0-9999 bound matches the device's own on-screen // rotary-encoder limits for these values (see rotary_Decode.ino). if (!parseULongStrict(t[3], obj.field3)) return false; if (obj.field3 > 9999) return false; if (!parseULongStrict(t[4], obj.field4)) return false; if (obj.field4 > 9999) return false; if (!parseIntStrict (t[5], obj.field5)) return false; if (!parseIntStrict (t[6], obj.field6)) return false; if (!parseIntStrict (t[7], obj.field7)) return false; if (!parseIntStrict (t[8], obj.field8)) return false; if (!parseFloatStrict(t[9], obj.field9)) return false; if (!parseByteStrict (t[10], obj.field10)) return false; return true; } // ------------------------------------------------------------ // Temp file used while receiving, so a dropped/bad transfer never // leaves the live protocols-N.txt file (read on every boot) truncated. // ------------------------------------------------------------ static const char* USB_UPLOAD_TMP_PATH = "/upload.tmp"; // ************************************************************ // Main receive routine * // Writes validated CSV lines straight to the SD card file * // this device currently has loaded (protocols-N.txt), then * // reloads it via the normal boot-time SD load path. This * // avoids the ESP32's NVS-backed EEPROM emulation entirely -- * // its ~20KB "nvs" partition doesn't have room to safely * // rewrite a ~10800 byte blob (see project notes). * // Returns true on success, false on failure * // ************************************************************ bool uploadProgramOverUsb() { bool sawBegin = false; bool sawEnd = false; bool sawControlRecord = false; int freqCount = 0; int controlCount = 0; String line; usbUploadFlushInput(); usbUploadSendLine(USB_UPLOAD_READY); if (!usbUploadReadLine(line, USB_UPLOAD_START_TIMEOUT_MS)) { usbUploadSendLine("ERROR:TIMEOUT_WAITING_BEGIN"); return false; } line.trim(); byte requestedFileNo; if (!line.startsWith(USB_BEGIN_FILE) || !parseFileNoAfterColon(line, requestedFileNo)) { usbUploadSendLine(("ERROR:EXPECTED_BEGIN_GOT:" + line).c_str()); return false; } sawBegin = true; SD.remove(USB_UPLOAD_TMP_PATH); // clear any stale temp file from a prior failed attempt File outFile = SD.open(USB_UPLOAD_TMP_PATH, FILE_WRITE); if (!outFile) { usbUploadSendLine("ERROR:SD_OPEN_FAILED"); return false; } outFile.print("#\n"); // minimal header readFileModified() requires while (true) { if (!usbUploadReadLine(line, USB_UPLOAD_LINE_TIMEOUT_MS)) { outFile.close(); SD.remove(USB_UPLOAD_TMP_PATH); usbUploadSendLine(("ERROR:LINE_TIMEOUT_AFTER_FREQ:" + String(freqCount)).c_str()); return false; } line.trim(); if (line.length() == 0) { continue; } if (line == "#") { continue; } if (line == USB_END_FILE) { sawEnd = true; break; } String tokens[USB_UPLOAD_MAX_TOKENS]; int tokenCount = splitCsvUpload(line, tokens, USB_UPLOAD_MAX_TOKENS); if (tokenCount == 9) { EPromFreqObject freqObj; if (!parseFreqCsvLineUpload(line, freqObj)) { outFile.close(); SD.remove(USB_UPLOAD_TMP_PATH); usbUploadSendLine(("ERROR:BAD_FREQ_LINE:" + line).c_str()); return false; } freqCount++; if (freqCount > MAX_FREQ_RECORDS) { // stop here rather than write a file the outFile.close(); // generator cannot load: past this the SD.remove(USB_UPLOAD_TMP_PATH); // control record has no room left and usbUploadSendLine(("ERROR:TOO_MANY_FREQ_RECORDS:MAX=" + // every setting in it String(MAX_FREQ_RECORDS)).c_str()); // would be lost return false; } outFile.print(line); outFile.print("\n"); usbUploadSendLine(("ACK:FREQ:" + String(freqCount)).c_str()); } else if (tokenCount == 11) { EPromControlObject ctrlObj; if (!parseControlCsvLineUpload(line, ctrlObj)) { outFile.close(); SD.remove(USB_UPLOAD_TMP_PATH); usbUploadSendLine(("ERROR:BAD_CTRL_LINE:" + line).c_str()); return false; } outFile.print(line); outFile.print("\n"); controlCount++; sawControlRecord = true; usbUploadSendLine("ACK:CTRL"); } else { outFile.close(); SD.remove(USB_UPLOAD_TMP_PATH); usbUploadSendLine(("ERROR:BAD_TOKEN_COUNT:" + String(tokenCount) + ":" + line).c_str()); return false; } } outFile.close(); if (!sawBegin || !sawEnd || !sawControlRecord || controlCount != 1) { SD.remove(USB_UPLOAD_TMP_PATH); usbUploadSendLine(("ERROR:INCOMPLETE:BEGIN=" + String(sawBegin) + ":END=" + String(sawEnd) + ":CTRL=" + String(sawControlRecord) + ":CTRLCOUNT=" + String(controlCount)).c_str()); return false; } // Everything validated -- atomically replace the requested protocols-N.txt // (N came from the PC's "BEGIN_FILE:N" line, NOT the device's own currently- // active fileNo -- this lets the PC target any preset slot directly). sprintf(fileName, "/protocols-%d.txt", requestedFileNo); SD.remove(fileName); if (!SD.rename(USB_UPLOAD_TMP_PATH, fileName)) { usbUploadSendLine("ERROR:SD_RENAME_FAILED"); return false; } if (requestedFileNo == 0) { fileFetch(); // protocols-0.txt is always the live/default file -- reload it into // EEPROM immediately, same as at boot, so the change takes effect // without needing a reset } // else: leave EEPROM untouched -- the uploaded preset stays on the SD card until // the user deliberately activates it via screen 5's File= dial + confirm. usbUploadSendLine(USB_UPLOAD_OK); return true; }