// Huge thanks to https://github.com/f4goh/FST4W for the original FST4W work! // // Huge thanks to IK1HGI (Antonio) for introducing me to FST4W and the top band! // // Reference: https://physics.princeton.edu/pulsar/k1jt/FST4_Quick_Start.pdf #include #include #include #include #include // GPS stuff #include TinyGPSPlus gps; #include AltSoftSerial altSerial; // Uses pin 8 (D8) for RX on Arduino Nano #define ENABLE_LCD_SUPPORT 1 #ifdef ENABLE_LCD_SUPPORT #include // https://randomnerdtutorials.com/esp32-esp8266-i2c-lcd-arduino-ide/ LiquidCrystal_I2C lcd(0x27, 16, 2); #endif #ifdef ENABLE_OLED_SUPPORT #include #endif #include "MyFont.h" // Global defines #define PTT_PIN 5 // D5 #define IS_GPS_SYNCED_PIN 6 // D6 // Digital mode properties // Use https://github.com/etherkit/Si5351Arduino/blob/master/examples/si5351_calibration/si5351_calibration.ino // to derive calibration value below! int32_t si5351CalibrationFactor = 16999; // si5351 calibration factor // #define FST4W_DEFAULT_FREQ 473450UL // 472 KHz band (630m band)! Tune receiver to 472.000 KHz USB! #define FST4W_DEFAULT_FREQ 137450UL // Antonio's frequency #define FST4W_SYMBOL_COUNT 160 // #define FST4W_DEFAULT_FREQ 14097050UL // 20 meter band for testing // fst4sim "IK1HGI JN45 20" 120 1500 0.0 0.1 1.0 10 -15 F const uint8_t FST4Wsymbols[FST4W_SYMBOL_COUNT] = { 0, 1, 3, 2, 1, 0, 2, 3, 3, 0, 3, 3, 0, 3, 1, 0, 0, 2, 2, 1, 3, 1, 3, 0, 3, 1, 0, 1, 0, 3, 1, 3, 0, 0, 3, 0, 1, 1, 2, 3, 1, 0, 3, 2, 0, 1, 0, 3, 1, 2, 1, 2, 3, 2, 1, 1, 0, 0, 1, 3, 3, 1, 3, 0, 0, 2, 3, 1, 2, 0, 3, 0, 2, 2, 1, 2, 0, 1, 3, 2, 1, 0, 2, 3, 0, 3, 0, 3, 2, 2, 0, 1, 0, 2, 0, 1, 0, 0, 2, 2, 3, 1, 3, 0, 2, 1, 1, 3, 0, 1, 0, 3, 2, 2, 2, 3, 1, 0, 3, 2, 0, 1, 0, 0, 3, 2, 1, 2, 0, 2, 2, 0, 3, 0, 0, 1, 3, 3, 3, 0, 0, 0, 1, 3, 2, 1, 1, 0, 0, 2, 0, 2, 0, 1, 3, 2, 1, 0, 2, 3 }; // Note - Change me for using a different FST4W mode! uint8_t FST4W_MODE = 0; // Use 'Serial Port Commands' to change mode // 0 = FST4W 120 // 1 = FST4W 300 // 2 = FST4W 900 // 3 = FST4W 1800 float toneSpacing[4] = { 1.464, // 120 0.558, // 300 0.1803, // 900 0.08929 // 1800 }; // Load WSPR/FST4W symbol length unsigned int symbolLength[4] = { 683, // 120 1792, // 300 5547, // 900 11200 // 1800 }; enum OperatingModes { MODE_FST4W, }; // Class instantiations boolean txEnabled = false; #ifdef ENABLE_OLED_SUPPORT SSD1306Wire display(0x3c, SDA, SCL); #endif RTC_DS3231 rtc; Si5351 si5351; DateTime dt; unsigned long freq; int rtc_lost_power = 0; int vfo_ok = 1; const int ledPin = LED_BUILTIN; enum modes { FST4W, }; enum modes mode = FST4W; // Note OperatingModes operatingMode = MODE_FST4W; uint64_t frequency = FST4W_DEFAULT_FREQ * 100ULL; uint8_t symbolCount; void updateDisplay() { char sub_mode[2]; #ifdef ENABLE_OLED_SUPPORT display.clear(); display.setFont(Roboto_Mono_Thin_16); display.drawString(0, 0, String((double)frequency / 100000000, 6U) + "MHz"); display.setFont(ArialMT_Plain_10); display.drawString(0, 20, F("Mode: Standalone Beaon")); display.drawString(0, 30, F("OpMode: FST4W")); display.drawString(0, 50, "TxEnabled: " + String(txEnabled ? "true" : "false")); display.display(); #endif #ifdef ENABLE_LCD_SUPPORT lcd.clear(); lcd.setCursor(0, 0); lcd.print(String((double)frequency / 100000000, 6U) + "MHz"); lcd.setCursor(0, 1); sprintf(sub_mode, "%d", FST4W_MODE); lcd.print("FST4W (" + String(sub_mode) + ") TX: " + String(txEnabled ? "T" : "F")); #endif } // Loop through the string, transmitting one character at a time. void jtTransmitMessage() { uint8_t i; // Serial.printf("TX! FST4W Mode is (%d)\n", FST4W_MODE); // Reset the tone to the base frequency and turn on the output si5351.set_clock_pwr(SI5351_CLK0, 1); si5351.output_enable(SI5351_CLK0, 1); si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); txEnabled = true; updateDisplay(); digitalWrite(LED_BUILTIN, HIGH); digitalWrite(PTT_PIN, HIGH); for (i = 0; i < symbolCount; i++) { // Thanks to https://github.com/W3PM/Auto-Calibrated-GPS-RTC-Si5351A-FST4W-and-WSPR-MEPT/blob/main/w3pm_GPS_FST4W_WSPR_V1_1a.ino unsigned long timer = millis(); si5351.set_freq(frequency + (FST4Wsymbols[i] * (toneSpacing[FST4W_MODE] * 100)), SI5351_CLK0); // delay(symbolLength[FST4W_MODE]); while ((millis() - timer) <= symbolLength[FST4W_MODE]) { __asm__("nop\n\t"); }; } // Turn off the output si5351.set_clock_pwr(SI5351_CLK0, 0); si5351.output_enable(SI5351_CLK0, 0); digitalWrite(PTT_PIN, LOW); digitalWrite(LED_BUILTIN, LOW); txEnabled = false; updateDisplay(); } String getTemperature() { return String(rtc.getTemperature()); } String getTime() { char date[10] = "hh:mm:ss"; rtc.now().toString(date); return date; } // debug helper void led_flash() { digitalWrite(LED_BUILTIN, HIGH); delay(250); digitalWrite(LED_BUILTIN, LOW); delay(250); digitalWrite(LED_BUILTIN, HIGH); delay(250); digitalWrite(LED_BUILTIN, LOW); delay(250); digitalWrite(LED_BUILTIN, HIGH); delay(250); digitalWrite(LED_BUILTIN, LOW); delay(250); digitalWrite(LED_BUILTIN, HIGH); } void set_mode() { frequency = FST4W_DEFAULT_FREQ * 100ULL;; symbolCount = FST4W_SYMBOL_COUNT; operatingMode = MODE_FST4W; // Serial.println("Getting ready for FST4W..."); } void sync_time_with_gps() { bool updated = 0; Serial.println(F("Finding GPS data...")); delay(500); digitalWrite(IS_GPS_SYNCED_PIN, LOW); altSerial.begin(9600); // Do the GPS thing until success. do { while (altSerial.available() > 0) gps.encode(altSerial.read()); if (gps.time.isUpdated() && gps.date.isUpdated()) { byte Year = gps.date.year(); byte Month = gps.date.month(); byte Day = gps.date.day(); byte Hour = gps.time.hour(); byte Minute = gps.time.minute(); byte Second = gps.time.second(); rtc.adjust(DateTime(Year, Month, Day, Hour, Minute, Second)); // Serial.println(("[+] Time updated from GPS, w00t!")); updated = 1; led_flash(); digitalWrite(IS_GPS_SYNCED_PIN, HIGH); } else { Serial.println(F("[!] No GPS fix yet. Can't set RTC yet. Please wait...")); delay(500); } } while (!updated); } void sync_time_with_gps_with_timeout() { int tries = 0; digitalWrite(IS_GPS_SYNCED_PIN, LOW); bool updated = 0; altSerial.begin(9600); do { while (altSerial.available() > 0) gps.encode(altSerial.read()); if (gps.time.isUpdated() && gps.date.isUpdated() && (gps.location.isValid() && gps.location.age() < 2000)) { byte Year = gps.date.year(); byte Month = gps.date.month(); byte Day = gps.date.day(); byte Hour = gps.time.hour(); byte Minute = gps.time.minute(); byte Second = gps.time.second(); rtc.adjust(DateTime(Year, Month, Day, Hour, Minute, Second)); digitalWrite(IS_GPS_SYNCED_PIN, HIGH); led_flash(); updated = 1; } else { tries = tries + 1; #ifdef ENABLE_LCD_SUPPORT lcd.clear(); lcd.setCursor(0, 0); lcd.print(F("GPS wait ")); lcd.print(tries); #endif delay(500); } if (tries > 120) // keep trying gps sync for a ~minute only break; } while (!updated); } void setup() { int ret = 0; char date[10] = "hh:mm:ss"; mode = FST4W; freq = FST4W_DEFAULT_FREQ; // Safety first! pinMode(PTT_PIN, OUTPUT); digitalWrite(PTT_PIN, LOW); pinMode(IS_GPS_SYNCED_PIN, OUTPUT); digitalWrite(IS_GPS_SYNCED_PIN, LOW); // Setup serial and IO pins Serial.begin(9600); pinMode(ledPin, OUTPUT); digitalWrite(ledPin, HIGH); // OFF delay(2000); Serial.println("\n..."); // Initialize the Si5351 ret = si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, si5351CalibrationFactor); if (ret != true) { Serial.flush(); vfo_ok = 0; } si5351.set_clock_pwr(SI5351_CLK0, 0); // safety first if (ret != 1) { Serial.print(F("Si5351 init status (should be 1 always) = ")); Serial.println(ret); } // Initialize the rtc if (!rtc.begin()) { Serial.println(F("Couldn't find RTC!")); Serial.flush(); abort(); } if (rtc.lostPower()) { Serial.println(F("RTC lost power, continue!?")); Serial.flush(); rtc_lost_power = 1; // rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // hack! // abort(); // NOTE } rtc.disable32K(); rtc.now().toString(date); // Print status /* Serial.print("Current UTC time is = "); Serial.println(date); Serial.print("Temperature is: "); Serial.print(rtc.getTemperature()); Serial.println(" C"); */ // Set CLK0 output si5351.set_freq(freq * 100, SI5351_CLK0); si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); // Set for maximum power // delay(10000); // Keep TX on for 5 seconds for tunining purposes. si5351.set_clock_pwr(SI5351_CLK0, 0); // Disable the clock initially // Note set_mode(); // Sanity checks if (!vfo_ok) { Serial.println(F("Check VFO connections!")); led_flash(); delay(50); } if (rtc_lost_power) { Serial.println(F("Check and set RTC time!")); led_flash(); delay(50); } // Initialising the UI #ifdef ENABLE_OLED_SUPPORT display.init(); display.flipScreenVertically(); display.setFont(ArialMT_Plain_10); #endif #ifdef ENABLE_LCD_SUPPORT lcd.begin(16, 2); lcd.init(); lcd.backlight(); #endif updateDisplay(); digitalWrite(ledPin, HIGH); // OFF // do automatic gps sync with timeout at startup Serial.println("Waiting for GPS, check LCD for status..."); sync_time_with_gps_with_timeout(); updateDisplay(); } // main loop void loop() { char c; if (Serial.available() > 0) { c = Serial.read(); Serial.println(c); if (c == 't') { jtTransmitMessage(); } if (c == 'w') { char date[10] = "hh:mm:ss"; rtc.now().toString(date); Serial.print(F("Current UTC time is = ")); Serial.println(date); } if (c == 'g') { sync_time_with_gps(); char date[10] = "hh:mm:ss"; rtc.now().toString(date); Serial.print(F("Current UTC time is = ")); Serial.println(date); } else if (c == '0') { Serial.println(F("Settings FST4W_MODE to 0 (FST4W-120)...")); FST4W_MODE = 0; updateDisplay(); } else if (c == '1') { Serial.println(F("Settings FST4W_MODE to 1 (FST4W-300)...")); FST4W_MODE = 1; updateDisplay(); } else if (c == '2') { Serial.println(F("Settings FST4W_MODE to 2 (FST4W-900)...")); FST4W_MODE = 2; updateDisplay(); } else if (c == '3') { Serial.println(F("Settings FST4W_MODE to 3 (FST4W-1800)...")); FST4W_MODE = 3; updateDisplay(); } } dt = rtc.now(); // FST4W-120 if (dt.second() == 0 && (dt.minute() % 6 == 0) && mode == FST4W) { // production // if (dt.second() == 0 && (dt.minute() % 2 == 0) && mode == FST4W && FST4W_MODE == 0) { // testing jtTransmitMessage(); } // FST4W-300 if (dt.second() == 0 && (dt.minute() % 5 == 0) && mode == FST4W && FST4W_MODE == 1) { jtTransmitMessage(); } // FST4W-900 if (dt.second() == 0 && (dt.minute() % 15 == 0) && mode == FST4W && FST4W_MODE == 2) { jtTransmitMessage(); } // FST4W-1800 if (dt.second() == 0 && (dt.minute() % 30 == 0) && mode == FST4W && FST4W_MODE == 3) { jtTransmitMessage(); } delay(10); }