/* Updated 2024 June 1 This sketch produces two timed LED flashes, one before and one after a given time. The flasher operates independently but can also be programmed and managed from the capture computer in real time via a USB-connected terminal. Parts: Arduino Mega or clone, e.g.: https://www.amazon.com/KEYESTUDIO-Arduino-Type-C-Powerful-Contoller/dp/B08V4RCRS2 The ATmega2560 processor datasheet is: https://ww1.microchip.com/downloads/en/devicedoc/atmel-2549-8-bit-avr-microcontroller-atmega640-1280-1281-2560-2561_datasheet.pdf Adafruit Ultimate GPS Logging Shield: https://www.adafruit.com/product/1272 Note: this sketch assumes you'll use Adafruit's newest GPS shield, identified by a 6-pin rectangular ICSP header at its end. If you have an older GPS shield that lacks the ICSP connector you'll need to make changes in two places in the sketch; see the comments LED, via a current limiting resistance, usually a 100-500 ohms fixed resistor plus 10-20kohm trimpot, all in series. This LED has an integral fixed resistor: https://www.amazon.com/EDGELEC-LED-Emitting-Diffused-Colored/dp/B07WNQC7QF Buy a selection of trimpots to see what value works: https://www.amazon.com/HUAREW-Multiturn-Potentiometer-Adjustment-Assortment/dp/B08779QH55 The Adafruit GPS shield has a prototype area for mounting parts. Wiring: Mega USB to computer; when used standalone also may be powered via the 5.5 X 2.1 power jack GPS shield mounted on the Mega connect GPS PPS pin to Mega pin 20 connect GPS TX pin to Mega pin 19 connect GPS RX pin to Mega pin 18 connect LED, with series resistor and trimpot, to pin 5 and one of several ground pins Data input: Prediction parameters are read from a file named input.txt in the GPS shield's micro SD drive. It has 9 numerical values like: yyyy mm dd hh mm ss n.n s s prediction date & time star+asteroid combined magn flash duration flash interval flash duration and interval are in seconds; interval is the number of seconds between flashes. It's best not to use any punctuation between entered values. Multiple predictions may be entered, one per line. Entries need not be in date & time order but will be flashed in order. There is error checking to see if values are in a given range, but be careful to enter exactly 9 numbers for each prediction because the search routine looks at the predictions in 9-value chunks. Predictions are usually entered via the USB-connected terminal -- see the instructions when the sketch is running -- but you can write input.txt directly onto the SD card as well. USB-Serial management: The SD card provides the prediction input data file and logs the output, for which a computer connection isn't needed, but it is very useful to manage real time operation via a USB-connected serial terminal program. Termite is perfect: https://www.compuphase.com/software_termite.htm Use 115200 bits/sec; end each input line with a Carriage Return OR New Line (not both); select monospace font. The Arduino IDE (Integrated Development Environment) is the software used to edit and upload C++ code to the Arduino microprocessor board. You can get it here: https://www.arduino.cc/en/software Coding the Arduino is straightforward and there is a huge amount of help on the internet -- just start your search with "arduino". End of comments; functional code starts below. */ // Tools > Manage Libraries usually stores to directory Documents/Arduino/libraries. // Note: if you have an older Adafruit GPS logger shield that lacks the rectangular 6 pin ICSP header at the end of the shield, // instead of the standard SD library below fetch this Adafruit "SD card on any pin" library: // https://github.com/adafruit/SD/archive/master.zip // Extract the zip to Documents/Arduino/libraries and change the directory name from SD-Master to SD. // Also you'll need to change the SD.begin(sd_cs) command; look for it lower in the sketch. #include // use SD drive; install with Tools > Manage Libraries; search for SD #include // parse NMEA data; in Tools > Manage Libraries; search for TinyGPSPlus #define gpsSerial Serial1 // Mega pins 18 (TX) and 19 (RX) talk with the GPS #define ppsPin 20 // this pin listens for PPS #define flashPin 5 // the external flasher LED; pin 5 is Timer 3 OC3A output #define sd_cs 10 // SPI chip select pin for the SD drive int yrs, pyrs, interval, halfinterval; // current and prediction year, flash interval byte mos, days, utchour, utcmin, utcsec, pmos, pdays, phrs, pmins, psecs, nextsec, nextmin, nexthour; // current and pred date/time units byte flashdurn; // flash length long JD, pJD, etime, minetime; // current and prediction Julian Day, realtime seconds before/after event, minimum etime when searching int saveposition; // start location of next future prediction in input.txt file float combmag, ledmag, calmag; // star + asteroid combined, current flash and calibration star magnitudes char logname[15]; // name of output file; yyyymmdd.log or output.log; the SD library requires this to be DOS-type 8.3 format char writename[100]; // name of file or text to write char outputline[100]; // character string to output data char inputline[100]; // and for input data char notes[50]; // status information char ledstring[10]; // OCR3A Timer 3 PWM setpoint expressed as a magnitude string char combstring[10]; // combined magnitude as string char calstring[10]; // calibration magn as string byte flashleft = 0; // flash seconds remaining counter volatile bool ppsokay; // becomes true when pps interrupt happens bool active = false, showtime = false, goodlog = false; // input good/active prediction, show realtime status, log write good bool blinky, continuous, dozero, logzero, makelog; // flash blink/continuous/zero-sec, log zero secs File inputFile, logFile; // used for SD read, write TinyGPSPlus nmea; // name the TinyGPS++ instance/object TinyGPSCustom leapSeconds(nmea, "PMTKLSC", 1); // custom method parses Leap Seconds Correction from PMTKLSC sentence, first csv position void setup() { // pins pinMode(sd_cs, OUTPUT); // SD chip select pinMode(ppsPin, INPUT_PULLUP); // from GPS; listens for the PPS pinMode(flashPin, OUTPUT); // to the flash LED // Serial Serial.begin(115200); // Serial Monitor gpsSerial.begin(9600); // GPS while (!gpsSerial) {}; // wait until open delay(100); // gps attachInterrupt(digitalPinToInterrupt(ppsPin), ppsIsr, RISING); // PPS interrupt service routine // these commands are specific for the PA1616D GNSS (GPS, GLONASS) receiver used in the Adafruit shield // gpsSerial.print(F("$PMTK103*30\r\n")); // cold restart; don't use time, position, almanacs or ephemeris data at restart // gpsSerial.print(F("$PMTK104*37\r\n")); // full cold restart; also clear system/user configurations; i.e. reset to factory status // gpsSerial.print(F("$PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n")); // turn off all nmea sentences // gpsSerial.print(F("$PMTK353,1,0,0,0,0*2A\r\n")); // use GPS satellites only; nmea sentences will show a $GP prefix // gpsSerial.print(F("$PMTK353,0,1,0,0,0*2A\r\n")); // use GLONASS satellites only; $GL prefix gpsSerial.print(F("$PMTK353,1,1,0,0,0*2B\r\n")); // use GPS and GLONASS constellations; $GN prefix gpsSerial.print(F("$PMTK220,1000*1F\r\n")); // send nmea sentences every 1000 milliseconds gpsSerial.print(F("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n")); // send sentences RMC and GGA gpsSerial.print(F("$PMTK875,1,1*38\r\n")); // send PMTKLSC Leap Seconds Correction Serial.print(F("\r\n\nWaiting for GPS...")); while (!(nmea.date.isValid() && nmea.time.isValid() && nmea.location.isValid() && nmea.altitude.isValid() && leapSeconds.isValid())) { if (gpsSerial.available()) nmea.encode(gpsSerial.read()); // loop until all above values are valid } yrs = nmea.date.year(); mos = nmea.date.month(); days = nmea.date.day(); utcsec = nextsec = nmea.time.second(); utcmin = nextmin = nmea.time.minute(); utchour = nexthour = nmea.time.hour(); Serial.write("good."); // the flash is made by timer 3 in mode 6, 9-bit fast PWM with TOP = 0x01FF, prescaler = 1, using compare register // OCR3A. PWM frequency is 16MHz/1/512 = 31.250kHz with duty cycle OCR3A/512. TCCR3A bit COM3A1 turns PWM on/off. // User sets OCR3A to control the brightness. See the ATmega2560 datasheet for details. TCCR3A = bit(WGM31); // WGM31 and WGM32 set 9-bit fast PWM TCCR3B = bit(WGM32) | bit(CS30); // CS30 selects prescale divider 1 active = dozero = logzero = false; // assume no active prediction ledmag = combmag = calmag = 12.0; // default until overwritten SD.begin(sd_cs); // talk to the SD drive // SD.begin(sd_cs, 11, 12, 13); // use this if you have an older Adafruit GPS logger that lacks a 6-pin ICSP header char savefile[] = "saved.txt"; if (!SD.exists(savefile)) // then create it { inputFile = SD.open("saved.txt", FILE_WRITE); inputFile.write("0 0 0 0 0 12.0 12.0"); inputFile.close(); } if (inputFile = SD.open("input.txt")) // look for prediction { Serial.print(F("\r\ninput.txt found;")); inputFile.setTimeout(10); minetime = 0x0FFFFFFF; inputFile.seek(0); // initialize the saved minimum etime; maximum value for a long while (inputFile.available()) readInput(); // scan whole file to find next future occultation and its 'saveposition' inputFile.seek(saveposition); active = readInput(); // return to saveposition and read one prediction inputFile.close(); inputFile.setTimeout(1000); } else Serial.print(F("\r\ninput.txt file NOT FOUND.")); if (active) // input data look good and we have an active prediction { Serial.print(F(" data successfully read.")); // recover saved params; if not from the current prediction ignore all except calmag and use defaults instead if (!readsave()) {active = dozero = logzero = true; ledmag = combmag;} // so...combmag always comes from input.txt and calmag always comes from saved.txt; ledmag comes from saved.txt if the // old prediction is still active, otherwise it is a new prediction and ledmag is set to the combmag from input.txt magstring(); // make magns into strings for easier printing sprintf(logname, "%4d%02d%02d.log", pyrs, pmos, pdays); // prediction date becomes log file name logFile = SD.open(logname, FILE_WRITE); sprintf(outputline, "\r\n\nprediction: %4d %02d %02d %02d %02d %02d comb magn: %s flash length: %d interval: %d", pyrs, pmos, pdays, phrs, pmins, psecs, combstring, flashdurn, interval); // line will be written below halfinterval = interval / 2; // now halved for the pre- and post-event sides } else // prediction bad { Serial.print(F(" PREDICTION IS OLD OR DATA BAD.")); readsave(); // look for saved calmag value active = dozero = logzero = false; ledmag = combmag = calmag; magstring(); resetInput(); // set some parameters to default values sprintf(logname, "output.log"); // when a prediction date isn't available, write to this generic output file (if SD card works) logFile = SD.open(logname, FILE_WRITE); // open log file sprintf(outputline, "\r\n\nNO PREDICTION. countdown from start of session. cal magn %s flash duration %d", calstring, flashdurn); } logFile.write(outputline); if (logFile.print(F("\r\ndate UTC secs to pred latitude longitude alt(MSL) flash/cal sats LSC"))) { Serial.print(F("\r\nHeader written to log file ")); Serial.write(logname); } else {Serial.print(F("\r\nNO LOG FILE.")); sprintf(logname, "NONE");} logFile.close(); // the difference between calibration magnitude, where the LED PWM level is 32, and the expected star + asteroid combined // magnitude sets a recommended flash flux so that, if the user adjusts the camera exposure to the combined magnitude, then // the flash level should also be approximately correct. OCR3A/512 is the PWM duty cycle. // BTW the inverse of the power function below is like m2 - m1 = -2.50 log(B2/B2) // e.g. 10.495 = -2.5 * log10(128 / 32) + 12.0 is 4X brighter than 12.0, when 12.0 is the 32 count calibration magnitude OCR3A = constrain(round(pow(2.512, calmag - ledmag) * 32.0), 1, 511); // set flash flux using ledmag relative to calmag showprediction(); sprintf(notes, "start"); if (dozero) { sprintf(notes, "%s; zero-sec flash on", notes); if (logzero) sprintf(notes, "%s, logged", notes); else sprintf(notes, "%s, not logged", notes); } else sprintf(notes, "%s; zero-sec flash off", notes); } // end of setup() void (*resetFunc)() = 0; // reset function at address 0; used to restart void loop() { while (gpsSerial.available()) nmea.encode(gpsSerial.read()); // feed nmea data into tinygpsplus if (ppsokay) // ppsisr() has announced the next second { ppsokay = false; utcsec = nextsec; utcmin = nextmin; utchour = nexthour; // update utc if (flashleft == flashdurn && makelog) // flash has already been started in ppsisr(); write to log { makelog = false; sprintf(outputline, "\r\n%4d %02d %02d %02d %02d %02d %9ld ", yrs, mos, days, utchour, utcmin, utcsec, etime); logFile = SD.open(logname, FILE_WRITE); if (logFile && logFile.write(outputline)) // open and write are good { goodlog = true; // time data successfully written logFile.print(nmea.location.lat(), 5); logFile.write(" "); // sprintf won't do float variables so print these individually logFile.print(nmea.location.lng(), 5); logFile.write(" "); logFile.print(nmea.altitude.meters(), 0); sprintf(outputline, "m %s/%s %2d ", ledstring, calstring, int(nmea.satellites.value())); logFile.write(outputline); logFile.write(leapSeconds.value()); logFile.close(); } showtime = true; // show realtime information } if (showtime) // display countdown and status; also for unlogged things { showtime = false; sprintf(outputline, "\r\n%02d %02d %02d", utchour, utcmin, utcsec); Serial.write(outputline); if (active) // when working with an active prediction { sprintf(outputline, " %9ld %9ld ", etime - halfinterval - flashdurn, etime + halfinterval); Serial.write(outputline); } else Serial.write(" "); // when no active prediction char OCR3Astring[4]; dtostrf(float(OCR3A), -4, 0, OCR3Astring); // right-pad with spaces to keep columns straight sprintf(outputline, "%s/%s/%s %3d ", ledstring, calstring, OCR3Astring, int(nmea.satellites.value())); Serial.write(outputline); if (goodlog) {sprintf(notes, "flash logged"); goodlog = false;} // show flash and log behavior else if (blinky) sprintf(notes, "blink"); else if (continuous) sprintf(notes, "LED on"); else if (flashleft == flashdurn) sprintf(notes, "flash not logged"); // unlogged zero-sec else if (strlen(notes) < 2) sprintf(notes, "not logged"); // enter key tapped if (OCR3A < 5 || OCR3A > 500) sprintf(notes, "%s (range caution)", notes); Serial.write(notes); sprintf(notes, " "); } if (flashleft > 0) flashleft -= 1; // midflash // pps reliably advances the time but here nmea data, if good, overwrite the time variables if (nmea.time.isValid() && nmea.time.isUpdated()) // may fail when gps reception is poor, but no matter { nextsec = nmea.time.second(); nextmin = nmea.time.minute(); nexthour = nmea.time.hour(); if (nmea.date.isValid()) {yrs = nmea.date.year(); mos = nmea.date.month(); days = nmea.date.day();} secsplus(); // at the pps the nmea values are still in the previous second, so increment to current second } secsplus(); // advance time to next second and decide whether to flash at the upcoming pps JD = calcJD(yrs, mos, days); // current Julian Day etime = calcEtime(pJD, JD, phrs, nexthour, pmins, nextmin, psecs, nextsec); // seconds to prediction, + before, - after if (active) // decide whether to make the goalpost flashes at the next pps { if (etime == halfinterval + flashdurn) {flashleft = flashdurn; makelog = true;} // first flash; flashdurn makes symmetrical else if (etime == -halfinterval) {flashleft = flashdurn; makelog = true;} // second flash else if (etime == -(halfinterval + flashdurn + 1)) { Serial.write("\r\nFinished - search for next prediction in 10 seconds"); inputFile = SD.open("saved.txt", FILE_WRITE | O_TRUNC); inputFile.write("\r\n0 0 0 1 1 "); inputFile.print(calmag, 1); inputFile.print(calmag, 1); inputFile.close(); } else if (etime == -(halfinterval + flashdurn + 10)) resetFunc(); else if ((dozero || blinky || continuous) && etime <= halfinterval + flashdurn + 60) // turn these off near prediction { dozero = logzero = blinky = continuous = false; sprintf(notes, "zero-sec flash off"); showtime = true; } } if (blinky) flashleft = nextsec % 2; // blink at the pps else if (continuous) bitSet(TCCR3A, COM3A1); // always on else if (dozero && nextsec == 0) {flashleft = flashdurn; showtime = true; makelog = logzero;} // zero-sec flash } // ppsokay if (Serial.available()) SerialRead(); // look for keyboard input } // loop() void ppsIsr() // interrupt service routine for the pps { // TCNT3 contains an unknown value before PWM starts but setting TCNT3 = 65535 makes it roll over to 0 at the start // of the pulse train; this corrects width of the first pulse and makes PPS-to-flash delay a constant 4.3 microseconds if (flashleft) {bitSet(TCCR3A, COM3A1); TCNT3 = 0xFFFF;} // start or continue flash else bitClear(TCCR3A, COM3A1); // stop flash ppsokay = true; // tell loop that pps just happened } void SerialRead() // process input from keyboard { static byte ndx; char s; static bool econfirm = false, dconfirm = false, setcal = false; // for confirming removals and changes s = Serial.read(); if (isPrintable(s)) {inputline[ndx] = s; if (ndx < sizeof(inputline)) ndx++; return;} // not CR or NL; return after every char inputline[ndx] = '\0'; // inputline is complete; add a string end ndx = 0; // reset for next input line // Serial.write("\r\n"); for (byte x=0; x= 0 && number < yrs) // set calibration or LED magnitude { if (setcal) // calibration: set the magnitude where OCR3A = 32 (of 0-511 range) { setcal = false; calmag = number; dtostrf(calmag, 1, 1, calstring); // make a string version for easy printing // ledmag -= calmag; calmag = number; ledmag += calmag; dtostrf(calmag, 1, 1, calstring); // keep OCR3A value but change magn sprintf(notes, "cal magn change"); } else {ledmag = number; sprintf(notes, "LED magn change");} // led magn change dtostrf(ledmag, 4, 1, ledstring); OCR3A = constrain(round(pow(2.512, calmag - ledmag) * 32.0), 1, 511); // set flash flux using ledmag relative to calmag showtime = true; writesave(); // also store the values } else if (number >= yrs && number <= yrs + 4) // valid year; write a prediction line to input.txt { inputFile = SD.open("input.txt", FILE_WRITE); if (inputFile) {inputFile.write(inputline); inputFile.write("\r\n");} // write new data inputFile.close(); sprintf(writename, "input.txt"); writeFile(); // show what the file looks like Serial.print(F("\r\n\nBE SURE TO [r]ESTART WHEN YOU HAVE FINISHED ENTERING PREDICTIONS !!!!\r\n")); } else if (inputline[8] == '.') // try to print file named like sxxxxxxx.xxx { snprintf(writename, 20, inputline); writeFile(); } } else if (strlen(inputline) < 2) // a CR or single letter { setcal = false; if ((econfirm || dconfirm) && s != 'y') s = 'n'; // for input.txt empty or log file delete, anything but 'y' means no if (s == '\0') {blinky = continuous = false; showtime = true;} // naked CR turns these off else if (s == 'a') {continuous = !continuous; blinky = false; showtime = true;} else if (s == 'b') {blinky = !blinky; continuous = false; showtime = true;} else if (s == 'c') { Serial.write("\r\nEnter new calibration star magnitude; currently "); Serial.print(calmag, 1); setcal = true; } else if (s == 'd') // show SD directory { Serial.write("\r\n\n"); snprintf(writename, 20, "Log files(unsorted)"); dashline(); Serial.write("\r\n"); File dir = SD.open("/"); dir.rewindDirectory(); File file = dir.openNextFile(); // "system" is first and doesn't need to be listed byte ctr; while (file = dir.openNextFile()) // look for .log files { if (file.name()[0] == '2') // the leading 2 in "yyyymmdd.log" {Serial.write(file.name()); Serial.write(" "); ctr++; if (ctr > 6) {ctr = 0; Serial.write("\r\n");}} } file.close(); dir.close(); Serial.write("\r\n"); dashline(); Serial.write("\r\n"); } else if (dconfirm) // confirm file delete { if (s == 'y') { if (SD.remove(&writename[7])) sprintf(notes, "%s removed", &writename[7]); else sprintf(notes, "%s file not found", &writename[7]); } else sprintf(notes, "%s NOT removed", &writename[7]); writename[0] = '\0'; dconfirm = false; showtime = true; } else if (s == 'e') // empty input.txt { econfirm = true; Serial.print(F("\r\nConfirm you want to empty input.txt? y/n")); } else if (econfirm) // confirm input.txt empty { if (s == 'y') { inputFile = SD.open("input.txt", FILE_WRITE | O_TRUNC); inputFile.close(); sprintf(notes, "input.txt emptied"); } else sprintf(notes, "input.txt NOT emptied"); econfirm = false; showtime = true; } else if (s == 'f') {blinky = continuous = false; flashleft = flashdurn; makelog = true;} // make a manual flash else if (s == 'i') {sprintf(writename, "input.txt"); writeFile();} // show input.txt else if (s == 'l') {snprintf(writename, 20, logname); writeFile();} // show current log file else if (s == 'p') showprediction(); // display prediction info else if (s == 'r') {resetInput(); resetFunc();} // sets some default values, reboot else if (s == '?') // show input help { sprintf (writename, "Input instructions"); Serial.write("\r\n\n"); dashline(); showhelp(); dashline(); waiting(); } else if (s == 'z') // switch zero-sec between three states { if (!dozero) {logzero = dozero = true; sprintf(notes, "zero-sec flash on, logged");} // with log else if (logzero) {logzero = false; sprintf(notes, "zero-sec flash on, not logged");} // no log else {dozero = false; sprintf(notes, "zero-sec flash off");} // no zero-sec showtime = true; writesave(); // show and save state } } else // not starting with a number, not one of the above single letter choices { if (strcmp(inputline, "input.txt") == 0) {sprintf(writename, "input.txt"); writeFile();} // look for specific input strings else if (strcmp(inputline, "output.log") == 0) {sprintf(writename, "output.log"); writeFile();} else if (strcmp(inputline, "saved.txt") == 0) {sprintf(writename, "saved.txt"); writeFile();} else if (strncmp(inputline, "remove ", 7) == 0) // name a file to remove { strcpy(writename, inputline); // copy the input line, including "remove " Serial.print(F("\r\nConfirm you want to ")); Serial.write(writename); Serial.write(" from the SD card? y/n"); dconfirm = true; } else // assume it is a comment and write it to the log file { logFile = SD.open(logname, FILE_WRITE); if (logFile){logFile.write("\r\n"); logFile.write(inputline, strlen(inputline)); logFile.close();} Serial.write("\r\nlog: "); Serial.write(inputline, strlen(inputline)); // confirm to screen } } } // SerialRead() void waiting() { Serial.print(F("\r\n\nWaiting for flashes. Type ? for instructions. Logging to ")); Serial.write(logname); Serial.print(F("\r\n\n secs to: first second flash sat")); Serial.print(F("\r\nUTC flash flash magn/cal/flux count notes")); showtime = true; } void secsplus() // add a second { nextsec++; // below are faster than moduloes // day can become longer than a month but the JD calculator will handle this properly, e.g. July "32" is treated as Aug 1 if (nextsec > 59) {nextsec = 0; nextmin++; if (nextmin > 59) {nextmin = 0; nexthour++; if (nexthour > 23) {nexthour = 0; days++;}}} } long calcJD (int y, byte m, byte d) // Modified Julian Day { // after van Flandern and Pulkkinen (1979) http://adsabs.harvard.edu/abs/1979ApJS...41..391V long mJD = 367L * y - 7 * (y + (m + 9) / 12) / 4 + 275 * m / 9 + d - 678987L; // integer division, (L)ong notation; return mJD; } long calcEtime(long pjd, long jd, byte ph, byte h, byte pm, byte m, byte ps, byte s) // secs between two JDs & times { long deltasecs = (pjd - jd) * 86400L + (ph - h) * 3600L + (pm - m) * 60L + ps - s; // positive when first date > second date return deltasecs; } bool readsave() // read saved data from SD and parse to the user-editable variables { inputFile = SD.open("saved.txt"); inputFile.setTimeout(10); inputFile.readBytesUntil('\0', outputline, sizeof(outputline)); inputFile.setTimeout(1000); inputFile.close(); char *tok; int i[5]; float f[2]; byte x = 0; tok = strtok (outputline," "); // parse space-delimited tokens while (tok != NULL) { if (x<5) i[x] = atoi(tok); // saved prediction time, dozero and logzero else f[x-5] = atof(tok); // calmag and ledmag tok = strtok (NULL, " "); x++; } dozero = i[3]; logzero = i[4]; calmag = f[0]; ledmag = f[1]; return i[0] == phrs && i[1] == pmins && i[2] == psecs; // true when saved and current prediction times match } void writesave() // write the user-editable variables to SD { inputFile = SD.open("saved.txt", FILE_WRITE | O_TRUNC); sprintf(outputline,"\r\n%d %d %d %d %d ", phrs, pmins, psecs, dozero, logzero); inputFile.write(outputline); inputFile.print(calmag); inputFile.write(" "); inputFile.print(ledmag); inputFile.close(); } void magstring() // turn magnitude floats into strings for easier printing { dtostrf(combmag, 1, 1, combstring); dtostrf(calmag, 1, 1, calstring); dtostrf(ledmag, 4, 1, ledstring); } bool readInput() // read prediction parameters from input.txt file, initially in a while loop to search multiple input lines to find { // the earliest future event. Using 'saveposition' this function is called again to locate the working prediction int startposition = inputFile.position(); // used when searching for the prediction we'll be flashing pyrs = inputFile.parseInt(); pmos = inputFile.parseInt(); pdays = inputFile.parseInt(); phrs = inputFile.parseInt(); pmins = inputFile.parseInt(); psecs = inputFile.parseFloat(); // decimal point input breaks parseInt so use parseFloat where the user might use one combmag = inputFile.parseFloat(); combmag = constrain(combmag, -5, 25); // combined star + asteroid; used to autocalc flash level float c = inputFile.parseFloat(); flashdurn = round(c); // flash duration, seconds; c = inputFile.parseFloat(); interval = round(c); // whole flash interval in seconds pJD = calcJD(pyrs, pmos, pdays); // prediction Julian Day JD = calcJD(yrs, mos, days); // current Julian Day etime = calcEtime(pJD, JD, phrs, utchour, pmins, utcmin, psecs, utcsec); // seconds to event if (pyrs >= yrs && pmos > 0 && pmos < 13 && pdays > 0 && pdays < 32 // see if values are reasonable && phrs < 24 && pmins < 60 && psecs < 60 && flashdurn < 100 && interval > flashdurn && interval < 1800 && etime > -interval/2 && etime <= minetime ) {minetime = etime; saveposition = startposition; return true;} // if earliest future etime, save its location else return false; } // readInput() void resetInput() // set values when input.txt isn't used { pJD = JD = calcJD(yrs, mos, days); // current date phrs = nmea.time.hour(); pmins = nmea.time.minute(); psecs = 0; etime = 0; flashdurn = 5; halfinterval = 0; dozero = logzero = blinky = continuous = false; } void writeFile() // print contents of selected file { File wfile = SD.open(writename); if (wfile) { Serial.write("\r\n\n"); dashline(); if (writename[0] == 'i') Serial.write("\r\n"); // input.txt needs a CR here while (wfile.available()) Serial.write(wfile.read()); wfile.close(); if (writename[0] != 'i') Serial.write("\r\n"); // log files need a CR here dashline(); waiting(); } } void dashline() // make a separator line { Serial.write(writename); Serial.write(" "); // writename has content name/information for (unsigned int x=0; x<92-strlen(writename); x++) Serial.write('-'); } void showprediction() // display the prediction information { Serial.write("\r\n\n"); sprintf (writename, "Prediction"); dashline(); sprintf (outputline, "\r\nCurrent: %4d %02d %02d %2d %02d %02d", yrs, mos, days, utchour, utcmin, utcsec); Serial.write(outputline); Serial.write(" "); Serial.print(nmea.location.lat(), 5), Serial.write(" "); Serial.print(nmea.location.lng(),5); Serial.write(" "); Serial.print(nmea.altitude.meters(),0); sprintf (outputline, "m MSL Sats: %d LSC: ", int(nmea.satellites.value())); Serial.write(outputline); Serial.write(leapSeconds.value()); if (active) { sprintf (outputline, "\r\nPredict: %4d %02d %02d %2d %02d %02d in ", pyrs, pmos, pdays, phrs, pmins, psecs); Serial.write(outputline); int d, h, m, s; d = etime / 86400; h = etime / 3600 % 24; m = etime / 60 % 60; s = etime % 60; if (d > 0) {sprintf(outputline, "%d days, %d hours, %d minutes, ", d, h, m); Serial.write(outputline);} // interval to prediction else if (h > 0) {sprintf(outputline, "%d hours, %d minutes, ", h, m); Serial.write(outputline);} else if (m > 0) {sprintf(outputline, "%d minutes, ", m); Serial.write(outputline);} Serial.print(s); Serial.write(" seconds"); s = psecs - halfinterval - flashdurn, m = pmins, h = phrs; // actual time of first flash; adjust seconds if (s < 0) {s += 60; m -= 1; if (m < 0) {m += 60; h -= 1; if (h < 0) h = 23;}} // adjust other units if needed sprintf(outputline, "\r\n First timed flash: %2d %02d %02d Comb. magnitude: %s", h, m, s, combstring); Serial.write(outputline); sprintf(outputline, " Flash length: %d Interval: %d", flashdurn, interval); Serial.write(outputline); s = psecs + halfinterval; m = pmins; h = phrs; // same for second flash if (s > 59) {s -= 60; m += 1; if (m > 59) {m -= 60; h += 1; if (h > 23) h = 0;}} sprintf(outputline, "\r\n Second flash: %2d %02d %02d Calibration magn: %s", h, m, s, calstring); } else sprintf(outputline, "\r\nNo prediction. Flash length: %d Calibration magn: %s", flashdurn, calstring); Serial.write(outputline); Serial.write(" Flash magn range: "); Serial.print(calmag - 3.0, 1); Serial.write('~'); Serial.print(calmag + 3.2, 1); Serial.write("\r\n"); dashline(); waiting(); } void showhelp() { Serial.print(F("\r\n - press Enter for countdown times ? for these instructions")); Serial.print(F("\r\n - see [p]rediction information [nn.n] to set flash magnitude")); Serial.print(F("\r\n - make a [f]lash toggle: [b]link [a]lways on [z]ero-sec on+log / on-log / off")); Serial.print(F("\r\n - [write a comment to the log file] ")); Serial.print(sizeof(inputline)); Serial.print(F(" characters maximum per line")); Serial.print(F("\r\n - show [i]nput.txt file [e]mpty input.txt file show log [d]irectory")); Serial.print(F("\r\n - show current [l]og file or older [yyyymmdd.log] log file [remove yyyymmdd.log]")); Serial.print(F("\r\n - add prediction [yyyy mm dd hh mm ss nn.n s s] to input.txt")); Serial.print(F("\r\n UTC date & time, combined magnitude, flash duration, flash interval")); Serial.print(F("\r\n - [r]estart flasher set [c]alibration star magnitude (currently ")); Serial.print(calmag, 1); Serial.print(')'); Serial.print(F("\r\n - AFTER PREDICTIONS ARE ADDED, [r]ESTART TO MAKE USE OF THE NEW DATA\r\n")); } // end of sketch