// // Scratch name saveFile() builds the new protocol file under before swapping it // over the real one. Mirrors USB_UPLOAD_TMP_PATH in USB_Comm.ino. static const char* SAVE_TMP_PATH = "/save.tmp"; // // ********************************************************************* // // NOTE: "setFrequency(int value)" int = 0 turn off the frequencies // int = 1 turn on the frequencies // void setFrequency(int value){ // setGens() loads all four at once on purpose. Calling setGen1..setGen4 one at // a time started Channel 1 before Channel 2's frequency was known, and a timer // that is already running will not accept a new prescaler -- see NOTE 30 and // the note above haltTimer() in PWM.ino. if(value==0){ stopAll(); // stops all the PWM clocks from running to insure setGens() works setGens(F1, 100, // 100 actually sets 0 for Duty Cycle which forces output low F2, 100, F3, 100, F4, 100); } else{ stopAll(); setGens(F1, 100-(D1*100), // NOTE: for this block of code the (Dn*100) is where "D" F2, 100-(D2*100), // stands for Duty Cycle and "n" is the associated F3, 100-(D3*100), // Channel or Generator. Subtracting (Dn*100) from F4, 100-(D4*100)); // 100 inverts the input Duty Cycle which is needed } // for the way PWMs are generated with this program. } // ********************************************************************* void blinker(int loopTime, int delayTime){ for(int b=0; b0){ display.clearDisplay(); // Clear the OLED buffer display.setTextSize(2); display.setCursor(0,0); // set to origin display line display.print("File "); // message line 1 display.print(fileNo); // state what file was in error display.setCursor(0,22); // set to next display line if(fileRtnCode == 2) // say WHICH way it was bad, so the card can be display.print("NO # MARK"); // fixed without guesswork else if(fileRtnCode == 3) display.print("TOO MANY"); else if(fileRtnCode == 4) display.print("NO END REC"); else if(fileRtnCode == 5) display.print("NO RECORDS"); else display.print("FAILURE"); display.setTextSize(1); // the explanation goes on in the small font display.setCursor(0,48); if(fileRtnCode == 2) display.print("no # header line"); else if(fileRtnCode == 3){ display.print("over "); display.print(MAX_FREQ_RECORDS); display.print(" records"); } else if(fileRtnCode == 4) display.print("no 0,... end record"); else if(fileRtnCode == 5) display.print("no frequency records"); else display.print("cannot open file"); display.display(); // DISPLAY TO THE SCREEN blinker(50, 125); // show the card is not plugged in while(1); // infinit loop } singleShot = 0; // readFile uses singleShot so restore it to zero (0) fetchMem(); // Everything is now stored in flash memory fetchProtoID(0); // Get the 'memGrp' number from parameter line 0 fetchProtoID(memGrp); // Now get the starting parameters for the program fileNoHold = fileNo; // this is the file number you are working with, save it display.setTextSize(1); // restore the font size to small display.display(); // UPDATE Display Parameters } // ********************************************************************* void fetchMem(){ if(DEBUG) Serial.println("Entering 'fetchFmMem()'"); intCnt = 0; protoID = 1; // Start with line 1 eeAddress = FLASH_START; // point to memory start address EPromFreqObject customVar1; // Variables to store custom objects in EEPROM while(protoID != 0){ // Search a line at a time if(intCnt > MAX_FREQ_RECORDS){ // Backstop. The terminator sits one slot past Serial.println("fetchMem: no end-of-file record found, stopping"); break; // the last frequency record, so intCnt can } // legitimately reach MAX_FREQ_RECORDS but no // further. Without this, a store whose // terminator is missing walks off the end -- // and EEPROM.get() then returns without // touching customVar1, so protoID keeps its // last non-zero value and this loop never ends. eeAddress = FLASH_START+(sizeofVar1*intCnt);// point to next line in memory ++intCnt; // increment the corresponding line counter EEPROM.get(eeAddress,customVar1); // Fetch data from EPROM protoID=customVar1.field0; // fetch line number indentifier and check if at line 0 yet if(protoID==0) // well is it because you are looking for last line in file break; F1=customVar1.field1; // Fetch and load Frequency 1 D1=customVar1.field2; // Fetch and load Duty Cycle 1 F2=customVar1.field3; // Fetch and load Frequency 2 D2=customVar1.field4; // Fetch and load Duty Cycle 2 F3=customVar1.field5; // Fetch and load Frequency 3 D3=customVar1.field6; // Fetch and load Duty Cycle 3 F4=customVar1.field7; // Fetch and load Frequency 4 D4=customVar1.field8; // Fetch and load Duty Cycle 4 if(DEBUG){ // Show the complete line information Serial.print(protoID); Serial.print(", "); Serial.print(F1); Serial.print(", "); Serial.print(D1); Serial.print(", "); Serial.print(F2); Serial.print(", "); Serial.print(D2); Serial.print(", "); Serial.print(F3); Serial.print(", "); Serial.print(D3); Serial.print(", "); Serial.print(F4); Serial.print(", "); Serial.print(D4); Serial.print("\n"); } } EPromControlObject customVar2; // Variables to store custom objects in EEPROM EEPROM.get(eeAddress,customVar2); // Fetch data from EPROM protoID=customVar2.field0; // fetch the data 'set' related to this protoID memGrp=customVar2.field1; // Fetch and load Frequency 1 modeID=customVar2.field2; // Fetch and load Duty Cycle 1 endTime1=customVar2.field3; // Fetch and load Frequency 2 endTime2=customVar2.field4; // Fetch and load Duty Cycle 2 startFreqGrpID=customVar2.field5; // Fetch and load Frequency 3 stopFreqGrpID=customVar2.field6; // Fetch and load Duty Cycle 3 startSweepGrpID=customVar2.field7; // Fetch and load Frequency 4 stopSweepGrpID=customVar2.field8; // Fetch and load Duty Cycle 4 sweepFreqInc=customVar2.field9; // sweep Frequency Increment value fileNo=customVar2.field10; // file number ranging from 0 to 9 if(DEBUG){ // Show the last line in the file which should be line labelled 0 Serial.print(protoID); Serial.print(", "); Serial.print(memGrp); Serial.print(", "); Serial.print(modeID); Serial.print(", "); Serial.print(endTime1); Serial.print(", "); Serial.print(endTime2); Serial.print(", "); Serial.print(startFreqGrpID); Serial.print(", "); Serial.print(stopFreqGrpID); Serial.print(", "); Serial.print(startSweepGrpID); Serial.print(", "); Serial.print(stopSweepGrpID); Serial.print(", "); Serial.print(sweepFreqInc); Serial.print(", "); Serial.print(fileNo); Serial.print("\n"); Serial.println("Exiting 'fetchFmMem()'"); } } // ********************************************************************* // The record numbers in a protocol file's control record arrive unchecked. // memGrp and the four group IDs go straight out of the file and into // fetchProtoID(), and rotaryLimits() -- which does clamp them -- only runs when // the knob has actually been turned, so a file loaded and started without // touching the knob had never been validated at all. A number past the end of // the record set read leftover EEPROM belonging to some larger file loaded // earlier; past record 300 it fell off the end of the image entirely. Called // from fetchProtoID() every time the control record is re-read, so there is no // path that can reload these and skip the check. void clampGroupIDs(){ if(protoIDlast < 1) return; // nothing loaded yet to measure against if(memGrp < 1) memGrp = 1; if(memGrp > protoIDlast) memGrp = protoIDlast; if(startFreqGrpID < 1) startFreqGrpID = 1; if(startFreqGrpID > protoIDlast) startFreqGrpID = protoIDlast; if(stopFreqGrpID < 1) stopFreqGrpID = 1; if(stopFreqGrpID > protoIDlast) stopFreqGrpID = protoIDlast; if(startSweepGrpID < 1) startSweepGrpID = 1; if(startSweepGrpID > protoIDlast) startSweepGrpID = protoIDlast; if(stopSweepGrpID < 1) stopSweepGrpID = 1; if(stopSweepGrpID > protoIDlast) stopSweepGrpID = protoIDlast; } // ********************************************************************* void fetchProtoID(int value){ // Never hand an out-of-range record number to EEPROM.get(). The library // bounds-checks and returns WITHOUT WRITING to the variable it was given, so // an out-of-range read left F1..F4 holding whatever happened to be on the // stack -- and put it straight on the outputs. Clamping here covers every // caller at once. The structs below are value-initialised for the same // reason: a refused read can then only ever yield zeros, never stack contents. if(value != 0){ if(value < 1) value = 1; if(protoIDlast > 0 && value > protoIDlast) value = protoIDlast; } protoID=value; // fetch protoID number if(DEBUG){ Serial.print("--- protoID = "); Serial.print(protoID); Serial.println(" ---"); } if(protoID != 0){ EPromFreqObject customVar1 = {}; // Variables to store custom objects in EEPROM eeAddress=(protoID-1)*sizeofVar1; // point to proper "Frequency Set" eeprom area EEPROM.get(eeAddress,customVar1); // Fetch data from EPROM protoID=customVar1.field0; // fetch the the 'set' related to this protoID F1=customVar1.field1; // Fetch and load Frequency 1 D1=customVar1.field2; // Fetch and load Duty Cycle 1 F2=customVar1.field3; // Fetch and load Frequency 2 D2=customVar1.field4; // Fetch and load Duty Cycle 2 F3=customVar1.field5; // Fetch and load Frequency 3 D3=customVar1.field6; // Fetch and load Duty Cycle 3 F4=customVar1.field7; // Fetch and load Frequency 4 D4=customVar1.field8; // Fetch and load Duty Cycle 4 } else { EPromControlObject customVar2 = {};// Variables to store custom objects in EEPROM eeAddress=eeAddress2; // define starting eeprom address EEPROM.get(eeAddress,customVar2); // Fetch data from the EProm protoID=customVar2.field0; // should be a zero / changed to a number sign '#' memGrp=customVar2.field1; // protoID to start with modeID=customVar2.field2; // fetch the mode from EEPROM endTime1=customVar2.field3; // fetch the frequency run time from the EEPROM endTime2=customVar2.field4; // fetch the Program run time from the EEPROM startFreqGrpID=customVar2.field5; // fetch the starting Frequency Group ID stopFreqGrpID=customVar2.field6; // fetch the stopping Frequency Group ID startSweepGrpID=customVar2.field7;// fetch the starting Sweep Group ID stopSweepGrpID=customVar2.field8; // fetch the stopping Sweep Group ID sweepFreqInc=customVar2.field9; // fetch the sweep Frequency increment value fileNo=customVar2.field10; // file number ranging from 0 to 9 clampGroupIDs(); // the file's own numbers are not trustworthy } } // ********************************************************************* void delayTime12(){ microTime12 = micros() + microDelay12; // microTime12 used for 180 degree phase shift while(micros() <= microTime12); } // ********************************************************************* void delayTime34(){ microTime34 = micros() + microDelay34; // microTimee34 used for 180 degree phase shift while(micros() <= microTime34); } // ********************************************************************* void saveCurrent(){ eeAddress=(protoID-1)*sizeofVar1; // point to proper "Frequency Set" eeprom area EPromFreqObject customVar1; // Variables to store custom objects in EEPROM customVar1.field0=protoID; // protoID customVar1.field1=F1; // F1 customVar1.field2=D1; // D1 customVar1.field3=F2; // F2 customVar1.field4=D2; // D2 customVar1.field5=F3; // F3 customVar1.field6=D3; // D3 customVar1.field7=F4; // F4 customVar1.field8=D4; // D4 EEPROM.put(eeAddress,customVar1); // Update Flash memory0 eeAddress = eeAddress2; // save this FLash Memory address EPromControlObject customVar2; // Variables to store custom objects in EEPROM customVar2.field0=protoID; // Always zero (0) to indicate ENDING GROUP customVar2.field1=memGrp; // This will contain last Memory Group used by program customVar2.field2=modeID; // This will contain last mode used/set customVar2.field3=endTime1; // This will contain Frequency run time customVar2.field4=endTime2; // This will contain Program run time customVar2.field5=startFreqGrpID; // Contains Start Memory Group for Protocol Mode 2 customVar2.field6=stopFreqGrpID; // Contains Stop Memory Group for Protocol Mode 2 customVar2.field7=startSweepGrpID; // Contains Start Memory Group for Sweep Mode 3 customVar2.field8=stopSweepGrpID; // Comtains Stop Memory Group for Sweep Mode 3 customVar2.field9=sweepFreqInc; // sweep Frequency Increment value customVar2.field10=(fileNo); // file number a number from 0 to 9 converted to char EEPROM.put(eeAddress,customVar2); // Update Flash memory0 } // ********************************************************************* void setClocks(byte function){ // check what you want to do if(function == false){ // on condition zero out the RunTimes(s) runTime1=runTime2=0; // zero the Runtimes } if(function == true){ // if true then set the RunTime(s) runTime1=endTime1+millis(); // set the RUN Frequency Set time runTime2=endTime2+millis(); // set the RUN Protocol time } } // ********************************************************************* void checkTime(){ runTime=millis(); // fetch the present millisecond time if(runFlag == 1 && !digitalRead(cursorPin)){ displaySuspended(); savePgmTime = runTime2 - runTime; // this id to restore time left for program time saveRunTime = runTime1 - runTime; // to restore time left for frequency runtime setFrequency(0); // turn off the four channel frequencies delay(1000); // used for debouncing the switch connection while(digitalRead(cursorPin)); // loop forever or until unsuspended displayRunning(); setFrequency(1); // turn on the four channel frequencies runTime=millis(); // fetch the present millisecond time runTime2 = runTime+savePgmTime; // restore Program Run time runTime1 = runTime+saveRunTime; // restore frequency Run Time delay(1000); // used for debouncing the swithc connection runFlag=1; // restore the runFlag value incase if flipped } if((long)(runTime - runTime2) >= 0){ // check if program runtime ran out (wrap-safe, runFlag=0; // if here, STOP the program fetchProtoID(protoID); // restore to what ever protoID is Display(); // The Display will need to be activated singleShot=0; } if((long)(runTime - runTime1) >= 0){ // check if a frequency time ran out (wrap-safe) runTime1=endTime1+runTime; // reset the frequency runTime1 time out if(setupOpt==0){ // Must be in setupOpt==0 to perform mode check if(modeID==2) // mode 2 equals protocol mode protocol(); // update to next reguired frequency paramters else if(modeID==3) // mode 3 equals sweep mode sweep(); // update to next reguired frequency paramters } displayRunning(); } } // ********************************************************************* void protocol(){ ++protoID; // update to next frequency set if(protoID>stopFreqGrpID) protoID=startFreqGrpID; // on condition cycle back to start Frequency Group fetchProtoID(protoID); // fetch the frequency group setFrequency(1); // LIGHT UP THE FREQUENCIES in other words START them runTime1=endTime1+millis(); // set the RUN Frequency Set time // Display(); // The Display will need to be activated } // ********************************************************************* void sweep(){ // Advance FIRST, then put the new frequency out. It used to emit sweepAccum and // advance afterwards, which meant the very first call re-emitted the frequency the // run had ALREADY started on -- the start frequency ran for two Freq Times, and // every one after it for one. That is where "the first step lasts 10 seconds and // the rest last 5" came from. if(sweepNumCnt <= 0){ // the sweep is finished: begin it again, which calcSweepParams(); // puts sweepAccum back on the start frequency } // and that start frequency is what goes out below else { sweepAccum += sweepAccumInc; // the next step if(sweepAccumInc > 0){ // never step past the limit: the final step is if(sweepAccum > sweepF2) // allowed to be shorter than Freq. Inc. so that sweepAccum = sweepF2; // the sweep finishes ON the limit rather than } // overshooting it or stopping short else { if(sweepAccum < sweepF2) sweepAccum = sweepF2; } --sweepNumCnt; // one fewer still owed } fetchProtoID(startSweepGrpID); // get all 4 frequencies and duty cycle values F1=sweepAccum; // replace F1 with the accumulator value if (DEBUG) { Serial.print("Both SweepAccum & F1 = "); Serial.println(F1); } setFrequency(1); // LIGHT UP THE FREQUENCIES in other words START them // The limit indication is driven off the frequency actually being put out, not off // a counter, so it cannot be thrown out by the sweep restarting on the same pass. sweepShowID = (sweepAccum == sweepF2) ? stopSweepGrpID : startSweepGrpID; } // ********************************************************************* void calcSweepParams(){ fetchProtoID(startSweepGrpID); // fetch the all the start frequency sweepF1=F1; // fetch beginning frequency & put into sweepF1 Save Area fetchProtoID(stopSweepGrpID); // fetch frequency limi or stop frequency sweepF2=F1; // determint which is going to be the restart sweep sweepRestart=sweepF1; // initialize sweepRestart sweepAccum=sweepRestart; // intialize the Sweep Accummulater sweepShowID=startSweepGrpID; // a sweep always begins on the start Sweep Group if(DEBUG) { Serial.print("sweepFreqInc = "); Serial.println(sweepFreqInc); Serial.print("endTime1 = "); Serial.println(endTime1); Serial.print("endTime2 = "); Serial.println(endTime2); } // sweepNumCnt is how many MORE frequencies the sweep still owes AFTER the one it // is sitting on. The start frequency is put out by the run itself, so this counts // the steps sweep() has to make from there. if(sweepFreqInc<=0.0){ // no Freq. Inc. set: work one out from the times if(endTime1>0) // endTime1 is the divisor here -- guard it sweepNumCnt=(endTime2-endTime1)/endTime1; // caculate how many frequency inc/dec possible else sweepNumCnt=2; // no Freq Time set at all, fall back to a single step --sweepNumCnt; if(sweepNumCnt<1) // Progm Time too short to hold more than one step sweepNumCnt=1; // -- clamp so the division below stays valid sweepAccumInc = (sweepF2-sweepF1)/sweepNumCnt; // will be either positive or negative } // -- an even division, so it lands on the limit else { // Freq. Inc. is the STEP, exactly as dialled. It used to be treated as nothing // more than a way of guessing how many steps would fit, after which the whole // span was divided evenly by that count -- so 2 Hz to 1000 Hz "by 50" actually // stepped by 998/19 = 52.53, and 2 Hz to 3000 Hz "by 50" stepped by 50.81. // The number dialled in was never the number used unless it happened to divide // the span exactly. sweepAccumInc = (sweepF2 >= sweepF1) ? sweepFreqInc : -sweepFreqInc; // direction // Full-size steps that fit inside the span, and one short one to finish on the // limit if they do not land on it. 2 -> 1000 by 50 gives 19 full steps to 952, // then a 48 Hz step onto 1000 exactly. The limit is never overshot. float span = sweepF2 - sweepF1; if(span < 0) span = -span; sweepNumCnt = (int)(span / sweepFreqInc); // whole steps that fit if(sweepNumCnt < 0) sweepNumCnt = 0; float landsOn = sweepF1 + (sweepNumCnt * sweepAccumInc); float missBy = landsOn - sweepF2; if(missBy < 0) missBy = -missBy; if(missBy > 0.001) // did not come out on the limit, so one more ++sweepNumCnt; // step is owed -- a short one, onto the limit if(sweepNumCnt < 1) // Freq. Inc. covers the whole span in one go, sweepNumCnt = 1; // or the two groups hold the same frequency } // ***** LEFT IN FOR DEBUGGING, works fine so presently can be removed desired ******** if(DEBUG) { Serial.print("sweepF1 = "); Serial.println(sweepF1); Serial.print("sweepF2 = "); Serial.println(sweepF2); Serial.print("sweepAccum = "); Serial.println(sweepAccum); Serial.print("sweepAcummInc = "); Serial.println(sweepAccumInc); Serial.print("sweepNumCnt = "); Serial.println(sweepNumCnt); } } // ********************************************************************* // Mode 4 -- Adjust Mode. Self-contained real-time knob control of Channel 1. // // This routine takes over completely for as long as the generator is running. // It does not return to the main loop between knob clicks, and it deliberately // does not call checkTime(), saveCurrent() or Display() -- none of which are // wanted here and all of which are slow. The red button's interrupt clears // runFlag, which is what ends the while loop below, so Stop works exactly as it // does everywhere else without this routine having to watch for it. // // Three rules keep it responsive: // 1. The encoder runs on an interrupt (adjustEncoderStart), so no amount of // work in this loop can cause a missed click. // 2. The PWM hardware is touched only when a click actually arrives -- not on // every pass of the loop, which is what crippled the original Mode 4. // 3. The screen repaints on a timer, at most ten times a second, and the knob // never waits on it. // // Retuning goes through setFrequency(1), the same call Protocol and Sweep modes // use. Three attempts at a cheaper Channel-1-only register write all failed on // the hardware -- see the note in PWM.ino where retuneGen1() used to be. Because // adjustEncoderTake() hands back every click banked since the last look, the // clicks that arrive during a retune are applied together on the next pass as // one larger step, so a fast spin coalesces rather than falling behind. // // Adjustments are deliberately temporary. F1 and D1 themselves are dialled here, // because setFrequency() reads those globals, but the record's original values // are saved on entry and put back on exit, so EEPROM is never modified. void adjustModeRun(){ float stepF = (sweepFreqInc > 0.0) ? sweepFreqInc : 1.00; // Setup=4's Freq. Inc. float saveF1 = F1; // the record's own values. F1 and D1 themselves float saveD1 = D1; // are what get dialled below, because // setFrequency() reads those globals -- they are // put back on the way out, so the protocol record // is still never modified. byte editDuty = 0; // 0 = knob adjusts F1, 1 = knob adjusts D1 byte lastBtn = HIGH; // black button previous state, for edge detect unsigned long btnTime = 0; // last accepted press, for debouncing unsigned long nextPaint = 0; // when the screen may next be repainted byte dirty = 1; // something changed, screen needs repainting adjustEncoderStart(); // switch the encoder over to interrupt driven while (runFlag == 1) { // the red button's interrupt ends this loop int clicks = adjustEncoderTake(); // every click since we last looked, none lost if (clicks != 0) { if (editDuty) { D1 += clicks * 0.05; // 0.05 per click for duty cycle if (D1 < 0.00) D1 = 0.00; if (D1 > 1.00) D1 = 1.00; } else { F1 += clicks * stepF; if (F1 < 0.04) F1 = 0.04; // 0.0373 Hz is the MCPWM hardware floor if (F1 > 50000.0) F1 = 50000.0; } // The whole proven path, exactly as Protocol and Sweep modes call it. // Calling setGen1() on its own does not work: setFrequency() begins with // stopAll(), and without that stop only some of the registers latch -- // which is why the previous attempt moved the output but landed nowhere // near the requested frequency. Channels 2-4 are momentarily restarted // too, which is the price of using the path that actually works. setFrequency(1); dirty = 1; } byte btn = digitalRead(cursorPin); // black button flips F1 <-> D1 if (btn == LOW && lastBtn == HIGH && (millis() - btnTime) > 250) { editDuty ^= 1; btnTime = millis(); dirty = 1; } lastBtn = btn; if (dirty && (long)(millis() - nextPaint) >= 0) { // repaint 10 times a second displayAdjust(F1, D1, editDuty); nextPaint = millis() + 100; dirty = 0; } if ((long)(millis() - runTime2) >= 0) // Progm Time ran out (wrap-safe). Clearing runFlag = 0; // runFlag ends this loop exactly as the red // button's interrupt does, so the // main loop's stop path turns the outputs off and // repaints with no special case needed here. // checkTime() cannot be reused for this: it treats the // black button as Pause, which is the F/D toggle in this // mode, and it repaints with displayRunning() over the // Adjust screen. runTime2 was armed by setClocks(1) // before this function was entered. // Freq Time (runTime1) is deliberately NOT checked -- // it decides when to advance to the next record, and // this mode never leaves the record it started on. delay(1); // keeps the ESP32 task watchdog fed. Safe to do } // here only because the encoder is on an // interrupt and cannot miss a click during it. adjustEncoderStop(); // hand the encoder back to polled rotary() F1 = saveF1; // put the record's own values back, so what was D1 = saveD1; // dialled here stays temporary. Without this the } // next Run would saveCurrent() the dialled value // into the protocol record permanently. // ********************************************************************* void checkSaving(){ if(!digitalRead(cursorPin)){ display.clearDisplay(); // Clear the OLED buffer display.setCursor(0,0); // Set the OLED cursor postion display.setTextSize(2); // Set the OLED text size display.print("Update the"); // Display the quoted text display.setCursor(0,20); // Set the OLED cursor postion display.print("SD Card ??"); // Display the quoted text curX = line0XC; // restore the CurX to origin curY = line0YC; // resotore the CurY to origin display.setCursor(curX,curY); // set user cursor position display.print("_"); // using the underscore to show display cursor position display.display(); // This command actually displays text to the OLED blinker(8, 250); } if(!digitalRead(cursorPin)){ display.clearDisplay(); // Clear the OLED buffer display.setCursor(0,0); // Set the OLED cursor postion display.print("SDram Card"); // Display the quoted text display.setCursor(0,24); // Set the OLED cursor postion display.print(" UPDATING"); // Display the quoted text display.display(); // This command actually displays text to the OLED saveFile(); } } // ********************************************************************* // Shows why a save could not be completed. The protocol file on the card is // still the old one and is untouched, so this reports and returns rather than // stopping the generator -- the operator can fix the card and try again. void saveFailed(const char* headline, const char* detail){ display.clearDisplay(); display.setTextSize(2); display.setCursor(0,0); display.print("SAVE"); display.setCursor(0,22); display.print(headline); display.setTextSize(1); display.setCursor(0,48); display.print(detail); display.setCursor(0,56); display.print("old file kept"); display.display(); Serial.print("saveFile FAILED: "); Serial.println(detail); blinker(30, 60); // ~4 seconds, long enough to be read } // ********************************************************************* // Writes the whole protocol set back to the SD card. // // This builds the replacement under a temporary name and only swaps it in once // every record is safely written. It used to delete the target FIRST and then // rebuild it a record at a time -- 276 separate open/append/close cycles for a // full file, several seconds of them -- so a reset, a power cut or a card pulled // anywhere in that window left a half-written protocols file and NO original to // fall back on. Recovering from that needs someone who knows the file format. // The destructive part is now two metadata operations at the very end, after // every byte is already on the card, and it is skipped entirely if anything // went wrong on the way. This is the same write-then-rename the USB upload has // always used (see USB_UPLOAD_TMP_PATH in USB_Comm.ino). // // The single open handle is the other half of it: one open and one close for the // whole file instead of 277 of them is both quicker and that many fewer chances // for the card to fail mid-save. void saveFile(){ protoIDhold=protoID; // save entry protoID // fileNoHold = fileNo; // save back fileNoHold to see if another file read needed sprintf(fileName, "/protocols-%d.txt", fileNoHold); // build the filename for any digit count bool ok = false; const char* why = ""; SD.remove(SAVE_TMP_PATH); // clear any leftover from an attempt that failed File out = SD.open(SAVE_TMP_PATH, FILE_WRITE); if(!out){ why = "cannot open work file"; } else { const char* hdr = "Protocols-x #\n"; ok = (out.print(hdr) == strlen(hdr)); for(protoID=1; protoID<=protoIDlast && ok; ++protoID){ fetchProtoID(protoID); workStr = ""; workStr+=String(protoID); workStr+=(","); workStr+=String(F1); workStr+=(","); workStr+=String(D1); workStr+=(","); workStr+=String(F2); workStr+=(","); workStr+=String(D2); workStr+=(","); workStr+=String(F3); workStr+=(","); workStr+=String(D3); workStr+=(","); workStr+=String(F4); workStr+=(","); workStr+=String(D4); workStr+=("\n"); ok = (out.print(workStr) == workStr.length()); // a short write means the if(DEBUG) Serial.print(workStr); // card is full or failing } if(ok){ fetchProtoID(0); workStr = ""; workStr+=String(0); workStr+=(","); workStr+=String(protoIDhold); workStr+=(","); workStr+=String(modeID); workStr+=(","); workStr+=String(endTime1/1000); workStr+=(","); // write as whole seconds (endTime1 is ms // internally) -- matches screen 5's "Freq // Time" display so the CSV is spreadsheet- // friendly instead of showing raw ms workStr+=String(endTime2/60000); workStr+=(","); // write as whole minutes (endTime2 is ms // internally) -- matches screen 5's "Progm // Time" display, same reasoning as above workStr+=String(startFreqGrpID); workStr+=(","); workStr+=String(stopFreqGrpID); workStr+=(","); workStr+=String(startSweepGrpID); workStr+=(","); workStr+=String(stopSweepGrpID); workStr+=(", "); workStr+=String(sweepFreqInc); workStr+=(", "); // fileNoHold, not fileNo: the fetchProtoID(0) call above re-reads "fileNo" from // EEPROM's control record, which for a preset-load-then-save is the preset's own // stored value, not the file actually being written here. fileNoHold always // reflects the real target of this save (see the sprintf(fileName...) above) and is left // untouched by fetchProtoID(), so it's the correct value for this file's own record. workStr+=String(fileNoHold); workStr+=("\n"); ok = (out.print(workStr) == workStr.length()); if(DEBUG) Serial.print(workStr); } out.close(); // flush to the card before anything is judged if(!ok) why = "card full or failing"; } if(ok){ // one last look at what actually landed, so a File chk = SD.open(SAVE_TMP_PATH); // silent failure cannot get past ok = (chk && chk.size() > strlen("Protocols-x #\n")); if(chk) chk.close(); if(!ok) why = "work file unreadable"; } if(ok){ SD.remove(fileName); // the only destructive moment left, and the if(!SD.rename(SAVE_TMP_PATH, fileName)){ // replacement is already complete ok = false; // and sitting on the card why = "rename failed"; } } else { SD.remove(SAVE_TMP_PATH); // tidy up; the original is untouched } fetchProtoID(protoIDhold); // restore the dialled record either way -- the // loop above walked protoID across every group if(ok){ blinker(20, 100); displayRunning(); } else { saveFailed("FAILED", why); displayRunning(); } }