/* * "simpleSA.ino" * * This is the main program file for the "TinySA for ESP32" (spectrum analyzer) * * * Copyright (C) 2020 David Wilde (M0WID), John Price (WA2FZW) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * * * The starting point for this version is the "tinySA_touch05" software developed * by David (M0WID). * That software was based on the original version for STM "Blue Pill" by Erik Kaashoek PD0EK. * For more information on that version visit https://groups.io/g/HBTE/topics * * Glen VK3PE has designed a set of PCB - see http://www.carnut.info/tinySA/tinySA.html * * Erik has since gone on to produce a commecial version, google tinySA, and this software * has been renamed simpleSA to avoid confusion. * * Modified by John Price (WA2FZW): * * Version 1.0: * Just add comments, try to figure out how it all works and move some stuff * around for clarity! * * * Version 1.1: * Added the file "Si4432.h" which contains symbols for all the Si4432 * registers and some other things related to it's operation. * * Added the files "PE4302.h" and "PE4302.cpp" which provide a class/object * implementation of the attenuator handling. One can now use either the * serial or parallel type modules connected directly to the processor * or the parallel module interfaced via a PCF8574 GPIO expander chip as * Glenn (VK3PE) did on his hardware implementation. * * The value stored in "setting.attenuate" is now a positive number. Previously * it was stored as a negative value and displayed as "Atten: -nn", which would * imply a gain! The PE4302 library also range limits the attenuator setting * to a value between 0dB and 31dB, which is the range of the PE4302. * * Incorporated changes from David's Version 05 release. Those changes include * eliminating the support for the Arduino and the addition of WiFi capability. * * Added the file "Si4432.h" which contains symbols for all the Si4432 * * * Version 1.6: * Added the file "Si4432.cpp" and moved all of the functions to handle the * interface to the hardware out of here. * * * Version 1.7: * Moved all the global variables to the top of the file and moved the "setup" * and "loop" functions to the top where they are customarily found. * * Stripped the serial interface command handler out of the "loop" function and * created the "CheckCommand" function; made a lot of changes to how that all * works also. * * * Version 1.8: * Replaced the original code to handle the Si4432 modules with a class/object * implementation using real SPI protocol. * * * Version 2.0: * Overhauled the serial command handler and help menu. * * * Version 2.1: * A major overhaul of the entire architecture! I moved all the functions that * handle reading and processing commands from the serial input into "Cmd.cpp". * This is the first step into being able to use common command processing for * the serial interface, web page and touch screen. * * * Version 2.7: * More restructuring. Added markers . Added more * commands to the serial command handler. Re-organized the touch screen menus * in a more logical hierarchy and added some new capabilities there. * * * Version 2.8 Changes by M0WID: * Data now pushed to the web clients * Grid y changed to have 10 divisions * Various bug fixes * Layout of display changed * New Signal Generator mode * Preparation for other modes eg High Frequency Range * Scale for preamp gain trace added * Spur Reduction now does something! * * Version 3.0a Changes by M0WID * Variable no of points to push to WiFi clients to give more frequent updates at narrow RBW * and fewer chart updates at wide RBW to reduce load on slow tablet or smartphone clients * Only check for websockets at intervals or if client connected as websockets impose a significant time penalty * Signal Generator menu and keypad frequency entry * Calibrate of signal generator output added to menu * Hot spots on touch to get to specific parts of the menu faster * * Version 3.0e Changes by M0WID * IF Sweep implemented to enable characterisation of the SAW filters, with its own menu * LCD display no longer uses pin2 for reset - it is wired to the ESP32 reset instead, freeing up pin 2 for the tracking generator * LCD display backlight no longer uses PWM to control the brightness, freeing up Pin 25 for the tracking generator. * You can uncomment the #define in my_SA.h if you still want the PWM backlight. * Tracking generator options added, with either one or two additional SI4432. If one SI4432 * then the track generator LO is tapped off the existing LO and the track gen IF runs * at the same frequency as the receiver. This can give reduced sensitivity. * If two SI4432 are implemented then the tracking generator IF can be offset to improve rejection * of the tracking generator IF. * New console commands for IF Sweep and tracking generator * New menu for tracking generator accessed from Output Menu * SPI now runs at 16MHz (was at 1MHz - oops!) * Additional functions in ui.cmd to allow signed frequency entry. * * Version 3.0f Changes by M0WID * * Version 0.03 * renamed simpleSA. Doesn't seem that simple to me! * First go at bandscope mode * VSPI reduced to 10Mhz as some boards will not run at 16MHz * Put onto github - lets see how it works! Version numbering changed - still in beta! * * Version 0.1 * Tracking generator now can be used as a signal generator * Web page for signal generator * RX sweep for testing the SI4432 FIR filters * Output range of signal generator increased by also reducing SI4432 output power as well as uing attenuator * Storage feature - temporary only not to disk - added to web page traces * X grid lines added to web chart * * Version 0.11 (pending) * Fix scaling for track gen frequency on web * Axis Y2 on web chart is hidden if relevant traces not enabled * Add self-calibrate * */ #include "simpleSA.h" // Definitions needed by the whole program #include // Basic Arduino definitions #include // Serial Peripheral Interface library #include // I2C library #include "si4432.h" // Si4432 tranceiver class definitions and prototypes #include "pE4302.h" // PE4302 attenuator class #include "cmd.h" // Command processing functions #include "marker.h" // Marker class #if USE_WIFI // M0WID - We need the following if using WiFi #include "simpleSA_wifi.h" // Our WiFI definitions #include // ESP32 WiFi support library #include // Asynchronous TCP library for Espressif MCUs #include #include #include #endif #include "SPIFFS.h" // ESP32 built-in "file system" #include // TFT_eSPI library #include // Library to save and restore configuration information #include "preferences.h" // Functions to write/read settings and config #include "ui.h" // Touch screen interface definitions /* * Note the actual definitions for display and touch screen pins used are defined * in the file "M0WID_Setup_ILI9341_TinySA.h" in the "User_Setups" directory of * the "TFT_eSPI" library. * * These are based on using HSPI (spi2) for display and VSPI (spi3) for the SI4432s * and the attenuator module. * * They are listed here just for reference; those not defined in the "TFT_eSPI" * library are defined in the various other files comprising the program: * * GPIO-0 SD_CS Reserved for SD card chip select * GPIO-1 RX0 Used by USB * GPIO-2 TFT_RST TFT Reset; Could be set to -1 if using processor reset * GPIO-2 tinySA_led The on-board LED flashes on data transfer * GPIO-3 TX0 Used by USB * GPIO-4 SI_RX_CS RX Si4432 Transceiver chip select * GPIO-5 SI_TX_CS TX Si4432 Transceiver chip select * GPIO-6 to 11 Reserved for the Flash memory * GPIO-12/MISO2 TFT_MISO Display data input (to processor) * GPIO-13/MOSI2 TFT_MOSI Display data output (from processor) * GPIO-14/CLK2 TFT_SCLK Display SPI clock * GPIO-15 TFT_CS Display chip select * GPIO-16 * GPIO-17 * GPIO-18/CLK VSPI_SCLK Transceiver & Attenuator SPI clock * GPIO-19/MISO VSPI_SDO Transceiver & Attenuator SPI data in (to processor) * GPIO-21/SDA SDA PCF8575 Data line (VK3PE Implementation only) * GPIO-21/PE4302_LE PE4302 attenuator Latch Enable (serial attenuator option) * GPIO-22/SCL SCL PCF8575 Clock line (VK3PE Implementation only) * GPIO-23/MOSI VSPI_SDI Transceiver & Attenuator SPI data out (from processor) * GPIO-25 TFT_LED Display backlight intensity (PWM) * GPIO-26 TOUCH_CS Touch screen chip select (in TFT_eSPI library setup) * GPIO-27 TFT_DC Display data/command select line * GPIO-32 ENC_PB Reserved for encoder pushbutton switch * GPIO-33 TS_INT Reserved for touch screen interrupt request * GPIO-33 ENC_BB or encoder backup button * GPIO-34 ENC_B Reserved for encoder pin "B" (input only pin) * GPIO-35 ENC_A Reserved for encoder pin "A" (input only pin) * GPIO-36 Sensor_VP (input only pin) * GPIO-39 Sensor_VN (input only pin) * RX0 USB Receive * TX0 USB Transmit */ /* * The "sprites" are a method supported by the "TFT_eSPI" library that allow faster * updates of the display and reduces flicker. We create three; one for the scan image * proper, one for the data at the top of the screen and a separate one for the data * at the top of the screen. We don't use one for the touch screen menus as there is * no flicker problem there. */ TFT_eSPI tft = TFT_eSPI(); // The TFT display object proper TFT_eSprite img = TFT_eSprite ( &tft ); // Sprite for the chart drawing TFT_eSprite tSprite = TFT_eSprite ( &tft ); // Sprite for the top of chart display TFT_eSprite sSprite = TFT_eSprite ( &tft ); // Sprite for the side of chart display TFT_eSprite gainScaleSprite = TFT_eSprite ( &tft ); // Sprite for the gain scale TFT_eSPI_Button key[SIG_KEY_COUNT]; // Used for signal generator modes for simplicity /* * Definition for signal generator keys */ sig_key_t sig_keys[SIG_KEY_COUNT] = { // x, y, width, height, normal colour, active colour, text // x and y are mid points { 18, 88, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 0 100MHz+ { 18+ NUM_W, 88, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 1 10MHz+ { 18+ 2*NUM_W, 88, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 2 1MHz+ {130, 88, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 3 100kHz+ {130+ NUM_W, 88, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 4 10kHz+ {130+ 2*NUM_W, 88, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 5 1kHz+ {237, 88, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 6 100Hz+ // {237+ NUM_W, 88, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 10Hz+ Pointless - SI4432 has insufficient resolution and huge drift! // {237+ 2*NUM_W, 88, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 1Hz+ { 18, 126, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 7 100MHz+ { 18+ NUM_W, 126, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 8 10MHz+ { 18+ 2*NUM_W, 126, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 9 1MHz+ {130, 126, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 10 100kHz+ {130+ NUM_W, 126, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 11 10kHz+ {130+ 2*NUM_W, 126, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 12 1kHz+ {237 , 126, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 13 100Hz+ // {237+ NUM_W, 126, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 10Hz+ Pointless - SI4432 has insufficient resolution and huge drift! // {237+ 2*NUM_W, 126, NUM_W, NUM_H, TFT_WHITE, TFT_CYAN, "", ""}, // 1Hz+ { 45, 25, KEY_W, KEY_H, TFT_WHITE, TFT_CYAN, "SALo", "SALo"}, // 14 SALo // { 45, 165, KEY_W, KEY_H, TFT_WHITE, TFT_GREEN, "FM", "FM on"}, // xx FM (frequency Modulation) on/off // {105, 165, KEY_W, KEY_H, TFT_WHITE, TFT_GREEN, "AM", "AM on"}, // xx AM (amplitude modulation) on/off {230, 175, KEY_W, KEY_H, TFT_WHITE, TFT_GREEN, "ON", "OFF"}, // 15 ON/OFF (Sig gen output on/off) {285, 25, KEY_W, KEY_H, TFT_WHITE, TFT_CYAN, "Menu", "Menu"}, // 16 When pressed launch menu {230, 25, KEY_W, KEY_H, TFT_WHITE, TFT_CYAN, "123", "123"} // 17 When pressed keypad to enter frequency }; /* * We create four sprites for the markers and an array of their addresses. The addresses * will be passed to the marker objects when we initialize them in the "setup" function. * * It would make more sense to actually create these in the marker itself, but any attempts * to do that caused newer versions of the "TFT_eSPI" library to crash or do goofy things. */ TFT_eSprite mkr1 = TFT_eSprite ( &tft ); // Sprites for the markers TFT_eSprite mkr2 = TFT_eSprite ( &tft ); TFT_eSprite mkr3 = TFT_eSprite ( &tft ); TFT_eSprite mkr4 = TFT_eSprite ( &tft ); TFT_eSprite* mSprites[MARKER_COUNT] = { &mkr1, &mkr2, &mkr3, &mkr4 }; /* * Create an array of un-initialized markers. We'll initialize them in the "setup" * function. */ Marker marker[MARKER_COUNT]; // Array of marker objects /* * These are used for positioning the markers: */ peak_t peaks[MARKER_COUNT]; // Peaks in the current scan peak_t oldPeaks[MARKER_COUNT]; // Peaks in the previous scan /* * The "vspi" object handles communications between the processor and the serial * version of the PE4302 attenuator and the Si4432 modules. If we ever implement * use of the SD card memory on the display, it will also use the VSPI bus. * * It's created as a pointer so it can be passed to the objects that use it. */ SPIClass* vspi = new SPIClass ( VSPI ); // Create VSPI object and a pointer to it /* * Constructors for PE4302 object - which one is used depends on the type set in My_SA.h */ #if ( PE4302_TYPE == PE4302_PCF) // Create the PCF8574 attenuator object PE4302 att ( PCF8574_ADDRESS ); #endif #if ( PE4302_TYPE == PE4302_GPIO ) // The parallel mode one PE4302 att ( DATA_16, DATA_8, DATA_4, DATA_2, DATA_1, DATA_0 ); #endif #if ( PE4302_TYPE == PE4302_SERIAL ) // The serial mode one PE4302 att (vspi, PE4302_LE ); #endif /* * Create the transceiver objects: */ Si4432 rcvr ( vspi, SI_RX_CS, RX_4432 ); // Create object for the receiver Si4432 xmit ( vspi, SI_TX_CS, TX_4432 ); // And the transmitter (local oscillator) #ifdef TG_IF_INSTALLED Si4432 tg_if ( vspi, SI_TG_IF_CS, TGIF_4432 ); // Create object for the tracking generator IF SI4432 #endif #ifdef TG_LO_INSTALLED Si4432 tg_lo ( vspi, SI_TG_LO_CS, TGLO_4432 ); // Create object for the tracking generator LO SI4432 #endif /* * Global definitions: */ bool tgIF_OK; // true if the tracking generator IF SI4432 is present and initialised OK bool tgLO_OK; // true if the tracking generator LO SI4432 is present and initialised OK uint8_t numberOfWebsocketClients; // How many connections uint16_t wiFiPoints; // Push data to the wifi clinets when this many points collected unsigned long wiFiTargetTime = WIFI_UPDATE_TARGET_TIME; unsigned long websocketInterval = WEBSOCKET_INTERVAL; uint16_t websocketFailCount; #ifdef USE_WIFI // Json document buffers //size_t capacity = JSON_ARRAY_SIZE ( MAX_WIFI_POINTS + 1 ) // + ( MAX_WIFI_POINTS + 1 ) * JSON_OBJECT_SIZE ( 3 ) + JSON_OBJECT_SIZE( 5 ); static DynamicJsonDocument jsonDocument ( 5000 ); // Buffer for json data to be pushed to the web clients static JsonArray Points = jsonDocument.createNestedArray ( "Points" ); // add Points array #endif /* * Variables to determine size of grid and waterfall * In Bandscope mode the grid is reduced. In future it may be possible to add * a waterfall to the main sweep, but not yet */ uint16_t gridHeight = GRID_HEIGHT; uint16_t gridWidth = DISPLAY_POINTS; uint16_t yGrid = Y_GRID; // no of grid divisions uint16_t yDelta = gridHeight / yGrid; // no of points/division uint16_t xGrid = X_GRID; uint16_t xOrigin = X_ORIGIN; uint16_t yOrigin = Y_ORIGIN; uint16_t displayPoints = DISPLAY_POINTS; uint16_t xDelta = displayPoints / xGrid; uint16_t waterfallHeight = WATERFALL_HEIGHT; int16_t maxGrid; int16_t minGrid; /* * Some varibales for the various operating modes */ uint16_t tinySA_mode = SA_LOW_RANGE; // Low frequency range const char *modeText[] = { "SALo", "SAHi", "SGLo", "SGHi", "IFSw", "0SpL", "0SpH", "BScp", "RXSw" }; // For mode display float bandwidth; // The current bandwidth (not * 10) unsigned long delaytime = 2000; // delay time to allow SI4432 filters to settle uint32_t steps = displayPoints; // Number of frequency steps in the sweep uint32_t sweepPoints; // Number of points in the sweep. Can be more than DISPLAY_POINTS if RBW is set less than video resolution uint32_t startFreq = 0; // Default start frequency is 0MHz uint32_t stopFreq = 100000000; // Default stop frequency is 100MHz uint32_t tempIF; // IF used for this sweep. Changes if Spur reduction is on double dBadjust; // Sum of attenuation, external gain, calibration offset and RBW correction uint16_t bpfCalibrate; // set true if a SI4432 bandpass filter calibration run is taking place uint16_t bpfCount; // no of bandpass filters available double bpfCalibrations[MAX_SI4432_FILTERS]; // temporary storage for calibration values uint16_t bpfIndex; // Index for current rbw filter /* * Variables for offset frequency tuning (used in Bandscope mode) */ int32_t offsetFreq; // Frequency offset from nominal setting int16_t offsetStep; // increments by one at each reading int16_t offsetValue; // Offset value to be written to Si4432 int16_t offsetIncrement; // Increment of offsetValue per reading int32_t offsetFreqIncrement; // Increment offsetFreq per reading unsigned long offsetDelayTime = 5000; // Delay time when using frequency offset to change frequency int VFO = RX_4432; // Set current VFO for command parser to the receiver Si4432 int gainReading; // Current preamp gain (will vary during a scan if AGC enabled) bool AGC_On; // Flag indicates if Preamp AGC is enabled uint8_t AGC_Reg; // Fixed value for preampGain if not auto uint8_t showRSSI = 0; // When zero don't show it (serial output only) int changedSetting = true; // Something in the "setting" structure changed int updateSidebar = false; // Flag to indicate update of sidebar is needed // UI functions extern uint8_t ui_mode; // What mode we are in (in "ui.cpp") extern void StartSigGenMenu ( void ); // Function to launch sig gen menu extern void StartSigGenFreq ( void ); // Function to set frequency extern void ResetSAMenuStack ( void ); // Reinitialise stack for SA mode extern void ResetIFsweepMenuStack ( void ); // Reinitialise stack for IF Sweep mode extern void ResetRXsweepMenuStack ( void ); // Reinitialise stack for IF Sweep mode extern void ResetBandscopeMenuStack ( void ); // Reinitialise stack for Bandscope mode extern void ShowSplash ( void ); // Displays the splash screen extern void pushSettings (); extern void pushIFSweepSettings (); extern void pushRXSweepSettings (); extern void pushBandscopeSettings (); /* * Variables for IFSweep Mode */ uint32_t startFreq_IF = IF_SWEEP_START; uint32_t stopFreq_IF = IF_SWEEP_STOP; uint32_t sigFreq_IF = 15000000; // 15 Mhz reference /* * Variables for RXSweep Mode */ uint32_t startFreq_RX = RX_SWEEP_START; uint32_t stopFreq_RX = RX_SWEEP_STOP; uint32_t sigFreq_RX = 15000000; // 15 Mhz reference /* * Variables for timing websocket event checks: */ unsigned long lastWebsocketMicros; // For timing between websocket events unsigned long loopStartMicros; // For timing the scan //unsigned long loopMicros; // To report the scan time /* * Variables for measuring sweep time: */ unsigned long sweepStartMicros; // For timing the scan unsigned long lastSweepStartMicros; // For timing the scan unsigned long sweepMicros; // To report the scan time uint32_t sweepCount; // Used to inhibit handling Wifi until // two sweeps have been done and WiFi // has stabilized /* * These arrays contain the data for the various scan points; they are used in * here and in the "TinySA_wifi" modules: */ uint8_t myData[SCREEN_WIDTH+1]; uint8_t myStorage[SCREEN_WIDTH+1]; uint8_t myActual[SCREEN_WIDTH+1]; uint8_t myGain[SCREEN_WIDTH+1]; // Preamp gain //uint32_t myFreq[SCREEN_WIDTH+1]; // Frequency for XML file uint16_t peakLevel; // Current maximum signal level uint16_t oldPeakLevel; // Old maximum signal level uint16_t peakIndex; uint16_t peakGain; // Actual gain at the peak (ie minimum gain) static int old_settingAttenuate = -1000; static int old_settingPowerGrid = -1000; static int old_settingMax = -1; static int old_settingMin = -1; static double old_startFreq = -1; static double old_stopFreq = -1; static int requiredRBW10 = 0; static int old_requiredRBW10 = -1; static int vbw = 0; static int old_vbw = -1; static int old_settingAverage = -1; static int old_settingSpur = -100; static int old_bandwidth = 0; int16_t standalone = true; uint16_t spacing = 10000; bool paused = false; uint16_t sigGenOutputOn = false; uint32_t oldFreq; // to store the current Signal Generator frequency /* * "setActualPowerRequested" is set by menu or WiFi. * When request is set the peak value during the sweep is used to calculate * the offset needed to make the power reading match a user input level * A calibration tool */ bool setActualPowerRequested = false; float actualPower = CAL_POWER; // Defined in "My_SA.h" int initSweep = true; // Flag to indicate sweep needs to restart from the beginning // Set when sweep settings change int16_t sweepStartDone = false; // Ensure initialize of sweep is only done once /* * All the following deal with the optional rotary encoder which could be used * to handle the menu options. * * Note - None of the encoder stuff has been implemented yet, but we'll leave the * definitions here for the time being: * * Symbols for button events and states (change to UC if we ever use them): */ #ifdef USE_ROTARY // Not defined anywhere! enum buttont_event { shortClickRelease=1, longClick=2, longClickRelease=3, shortBackClickRelease=4, longBackClick=5, longBackClickRelease=6, buttonRotateUp=7, buttonRotateDown=8 }; enum button_state { buttonUp, buttonDown, buttonLongDown }; int buttonState = buttonUp; // The current reading from the encoder switch int backButtonState = buttonUp; // The current reading from the backup button int buttonEvent = 0; // Short click release? int lastButtonRead = HIGH; // The previous reading from the encoder switch int lastBackButtonRead = HIGH; // The previous reading from the backup button uint32_t lastDebounceTime = 0; // The last time the encoder switch was toggled uint32_t debounceDelay = 50; // The debounce time; increase if the output flickers uint32_t longPressDelay = 350; // The long press time; increase if the output flickers int32_t incr; int32_t incrBase = 1000000; int incrBaseDigit = 7; #endif // "#ifdef USE_ROTARY" settings_t setting; // Structure to track & save settings sigGenSettings_t sigGenSetting; // settings for signal generator mode trackGenSettings_t trackGenSetting; // parameters for tracking gen mode Preferences preferences; // "preferences" is an object to enable // saving data to Flash /* * "setup" initializes everything: */ void setup () { bool fsStatus = false; // True if SPIFFS loads ok Serial.begin ( 115200 ); // Start up the USB connection tft.begin (); // Start the display object tft.setRotation ( 3 ); // If upside down, change to '1' /* * Disabling the use of the PSRAM on the WROVER boards for the TFT_eSPI library * should speed things up slightly. We do the same thing for the sprites. */ tft.setAttribute ( PSRAM_ENABLE, false ); // Forces all future Sprites to be created in the on-board RAM area preferences.begin ( "tinySA", false ); // We retreive stuff from flash memory ReadConfig (); // Read menu and touch settings ReadSettings(); // Read attenuation, level adjustment etc ReadSigGenSettings(); // Values for signal generator mode ReadTrackGenSettings(); // Values for tracking generator mode setting.ShowStorage = false; // Display stored scan (on or off) setting.SubtractStorage = false; // Subtract stored scan (on or off) /* * The touch screen needs to be calibrated. In previous versions, the instructions * were to un-comment the call to the "TouchCalibrate" here the first time you * ran the software and to insert the numbers it gave you into the code. * * This is no longer necessary as the configuration can now be done from the touch * acreen menu system, but we leave it here as some displays are nowhere near correct * and if they are way off, you won't be able to access the touch menus at all. * * The "config.touch_cal" fed into the "tft.setTouch" function is also reacalled * from the flash memory upon startup, so one should only need to calibrate the * touch screen one time. */ // TouchCalibrate (); // Comment out after first run tft.setTouch ( config.touch_cal ); // Send calibration data to the display #ifdef BACKLIGHT_LEVEL setUpLEDC(); // Set up the backlight control #endif /* * Display the splash screen: */ ShowSplash (); // Display the splash screen /* * Set up the pins used for the VSPI bus used by both SI4432 modules and the serial * mode PE4302 attenuator module and initialize the bus object: */ pinMode ( V_SCLK, OUTPUT ); // SPI Clock is an output pinMode ( V_SDO, INPUT ); // SDO (MISO) is an input pinMode ( V_SDI, OUTPUT ); // SDI (MOSI) is an output digitalWrite ( V_SCLK, LOW ); // Make SPI clock LOW digitalWrite ( V_SDI, LOW ); // Along with MOSI vspi->begin ( V_SCLK, V_SDO, V_SDI ); // Start the VSPI: SCLK, MISO, MOSI // SI4432 SPI rated for 10MHz according to data sheet. Seems OK at 16MHz vspi->setFrequency(BUS_SPEED); // run at 10MHz // tft.println("VSPI started"); /* * Set the CS pins normally used for the SI4432 to high, so if present they do not interfere * with the bus signals * */ pinMode (SI_TG_LO_CS, OUTPUT); digitalWrite(SI_TG_LO_CS, HIGH); // make sure the module doesn't get selected pinMode (SI_TG_IF_CS, OUTPUT); digitalWrite(SI_TG_IF_CS, HIGH); // make sure the module doesn't get selected /* * Initialize the SI4432 for RX and LO(TX) */ tft.setCursor(0,120); // tft.println ( "Initializing TX SI4432" ); if ( ! xmit.Init ( config.TX_capacitance ) ) // Initialize the transmitter module DisplayError ( ERR_FATAL, "LO SI4432 failed", "to initialise", NULL, NULL ); // tft.println( "Initializing RX SI4432" ); if ( ! rcvr.Init ( config.RX_capacitance ) ) // Initialize the receiver module DisplayError ( ERR_FATAL, "RX SI4432 failed", "to initialise", NULL, NULL ); bandwidth = rcvr.SetRBW ( setting.Bandwidth10, &delaytime, &bpfIndex ); // Set initial bandwidth, delaytime and filter SetRX ( 0 ); // RX in receive, LO in transmit rcvr.SetPreampGain ( setting.PreampGain ); bpfCount = rcvr.GetBandpassFilterCount(); // no of RBW filters available ReadBpfCalibration(); // Get bandpass filter (RBW) calibrations printBpfCal(); // and display the values // If the tracking generator IF SI4432 is defined, then check to see if it is connected tgIF_OK = false; #ifdef TG_IF_INSTALLED tft.setCursor(0,150); tft.println ( "Initializing track gen IF SI4432" ); tgIF_OK = tg_if.Init ( config.tgIF_capacitance ); if ( tgIF_OK ) { tft.println ("Tracking IF initialised"); } else { DisplayError ( ERR_WARN, "Track gen IF SI4432 failed", "to initialise", NULL, NULL ); } #endif // If the tracking generator LO SI4432 is defined, then check to see if it is connected tgLO_OK = false; #ifdef TG_LO_INSTALLED tft.println ( "Initializing track gen LO SI4432" ); tgLO_OK = tg_lo.Init ( config.tgLO_capacitance ); if ( tgLO_OK ) tft.println ("Tracking LO initialised"); else { DisplayError ( ERR_WARN, "Track gen LO SI4432 failed", "to initialise", NULL, NULL ); } #endif /* * Initialize the SPIFFS system */ fsStatus = SPIFFS.begin (); // Mount the file system if ( !fsStatus ) // If we can't access the file system { Serial.println ( "SPIFFS Mount Failed" ); // Error message to serial output DisplayError ( ERR_WARN, "Failed to mount SPIFFS", "Use ESP sketch Data Upload", "In the Arduino IDE", "WiFi Disabled" ); } // printSPIFFS (); // Print out the contents of SPIFFS to the serial port #if ( USE_WIFI ) // see My_SA.h if ( fsStatus ) // If we can access the file system { #ifdef USE_ACCESS_POINT // If using a WiFi access point startAP (); // Start it up #else // Connect to the router using SSID // and password defined in TinySA.h Serial.println ( "Connecting..." ); // Indicate trying to connect if ( connectWiFi () ) // Connection established? Serial.println( "Connected" ); // We are connected! else Serial.println( "Connection Failed!" ); #endif // "#ifdef USE_ACCESS_POINT" /* * Start the websockets server and event handler */ // tft.println("Starting Websockets"); webSocket.begin (); webSocket.onEvent ( webSocketEvent ); Serial.println ( "WebSockets started" ); websocketInterval = WEBSOCKET_INTERVAL; // tft.println("Building Server"); buildServer (); /* * OTA programming * * Port defaults to 3232 * ArduinoOTA.setPort(3232) * Hostname defaults to esp3232-[MAC] * ArduinoOTA.setHostname("myesp32"); * No authentication by default * ArduinoOTA.setPassword("admin"); * Password can be set with it's md5 value as well * MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 * ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); * #define WIFI_SSID ":)" // SSID of your WiFi if not using access point * #define WIFI_PASSWORD "S0ftR0ckRXTX" // Password for your WiFi */ ArduinoOTA .onStart([]() { String type; if (ArduinoOTA.getCommand() == U_FLASH) type = "sketch"; else // U_SPIFFS type = "filesystem"; // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() SPIFFS.end(); Serial.println("Start updating " + type); }) .onEnd([]() { Serial.println("\nEnd"); }) .onProgress([](unsigned int progress, unsigned int total) { Serial.printf("Progress: %u%%\r", (progress / (total / 100))); tft.setCursor ( 20, 200); tft.setTextColor(WHITE); tft.setFreeFont ( &FreeSansBold9pt7b ); // Select Free Serif 9 point font tft.print( "Progress:"); tft.fillRect(120, 182, 100, 20, SIG_BACKGROUND_COLOR); // x, y, w, h, color tft.setCursor (120, 200); tft.printf("%u%%", (progress / (total / 100))); }) .onError([](ota_error_t error) { Serial.printf("Error[%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); else if (error == OTA_END_ERROR) Serial.println("End Failed"); }); ArduinoOTA.begin(); IPAddress ipAddress = WiFi.localIP (); // Get our IP address Serial.print ( "Setup - WiFi access point started - browse to http://" ); Serial.println ( ipAddress.toString().c_str() ); } #endif // "#if ( USE_WIFI )" if ( !USE_WIFI || !fsStatus ) // If the WiFI option is not enabled Serial.println ( "\nWiFi not enabled!\n" ); // Serial.println ( "Initializing PE4302" ); // tft.println("Initialising PE4302"); att.Init (); // Initialize the PE4302 attenuator att.SetAtten ( 0 ); // Set the attenuation to zero // tft.println("Setup Complete"); Serial.printf ( "\nsimpleSA %s Initialization Complete\n", PROGRAM_VERSION ); ShowMenu (); // Display the menu of commands on serial monitor /* * Initialize the markers. The objects need the address of the display object and * the individual marker objects previously created. */ for ( int i = 0; i < MARKER_COUNT; i++ ) { marker[i].Init ( mSprites[i], i + 1 ); // Pass in the sprite object marker[i].Status ( setting.MkrStatus[i] ); // Set color and enabled status } delay ( 3000 ); // Time to read splash and initialize messages - Read fast! ClearDisplay (); setMode ( setting.Mode ); // setMode initializes stuff for the selected mode lastWebsocketMicros = micros(); } // End of "setup" /* * ######################################################################################## * * "loop" runs forever and handles all the things needed to make it work. * * ####################################################################################### */ void loop () { loopStartMicros = micros(); #if USE_WIFI //Serial.println("l"); if ( ( numberOfWebsocketClients > 0) || ( loopStartMicros - lastWebsocketMicros > websocketInterval ) || ( loopStartMicros < lastWebsocketMicros ) ) // handles rollover of micros() { // Serial.print("L"); webSocket.loop (); // Check websockets for events, but not at first // Serial.println("l"); lastWebsocketMicros = loopStartMicros; } #endif CheckCommand (); // Anything from the serial input? if ( ( ( tinySA_mode != SIG_GEN_LOW_RANGE ) && ( tinySA_mode != OTA_UPDATE ) ) || ( ui_mode != UI_NORMAL ) ) { UiProcessTouch (); // Handle the touch screen if ( ui_mode != UI_NORMAL ) { initSweep = true; return; // If in menu don't do anything else } } /* * Not fully implemented yet. */ switch ( tinySA_mode ) { case SA_LOW_RANGE: doSweepLow(); // Spectrum Analyser Low Frequency range break; case SA_HIGH_RANGE: doSweepHigh(); // Spectrum Analyser High Frequency range break; case SIG_GEN_LOW_RANGE: doSigGenLow(); // Signal Generator Low Frequency range break; case SIG_GEN_HIGH_RANGE: doSigGenHigh(); // Signal Generator High Frequency range break; case IF_SWEEP: doIF_Sweep(); // IF Sweep break; case RX_SWEEP: doRX_Sweep(); // RX Sweep break; case BANDSCOPE: doBandscope(); // Bandscope Sweep break; case OTA_UPDATE: // Over the air (wifi) update doOTA(); break; default: DisplayError ( ERR_WARN, "Invalid Mode!", "Mode changed to", "Analayze Low range", NULL ); initSweepLow (); } // end of switch } // end of loop /* * SetMode initializes various things for the selected mode then changes mode. It is called * by the web interface, command line or menu. */ void setMode ( uint16_t newMode ) { switch ( newMode ) { case SA_LOW_RANGE: initSweepLow(); break; // case SA_HIGH_RANGE: // initSweepHigh(); // break; case SIG_GEN_LOW_RANGE: initSigLow(); break; // case SIG_GEN_HIGH_RANGE: // initSigHigh(); // break; case BANDSCOPE: initBandscope(); break; case IF_SWEEP: initIF_Sweep(); break; case RX_SWEEP: initRX_Sweep(); break; case OTA_UPDATE: initOTA(); break; default: DisplayError ( ERR_WARN, "Invalid Mode!", "Mode not changed", NULL, NULL ); } } /* * "menuExit" is called when the user leaves the menu * The required action depends on the selected mode, and whether or not the * device is already in the selected mode */ void menuExit() { switch ( tinySA_mode ) { case SA_LOW_RANGE: if ( setting.Mode == SA_LOW_RANGE ) RedrawHisto(); else initSweepLow(); break; case SA_HIGH_RANGE: initSweepHigh(); break; case SIG_GEN_LOW_RANGE: initSigLow(); break; case SIG_GEN_HIGH_RANGE: initSigHigh(); break; case IF_SWEEP: if ( setting.Mode == IF_SWEEP ) RedrawHisto(); else initIF_Sweep(); break; case RX_SWEEP: if ( setting.Mode == RX_SWEEP ) RedrawHisto(); else initRX_Sweep(); break; case BANDSCOPE: if ( setting.Mode == BANDSCOPE ) RedrawHisto(); else initBandscope(); break; case OTA_UPDATE: initOTA(); break; default: // add handler here break; } // end of switch } /* * Initialise common to low, high, RX and IF_Sweeps */ void init_sweep() { ClearDisplay (); /* * Set up the "img" Sprite. This is the image for the graph. It makes for faster display * updates and less flicker. * * 16 bit colour depth is faster than 8 and much faster than 4 bit! BUT - sprites * pushed to it do not have correct colour - 8 bit and it is fine. * * All marker sprites are WHITE for now. */ tft.unloadFont(); img.unloadFont(); img.deleteSprite(); img.setTextSize ( 1 ); img.setColorDepth ( 16 ); img.setAttribute ( PSRAM_ENABLE, false ); // Don't use the PSRAM on the WROVERs img.createSprite ( 2, gridHeight + 1 ); // Only 2 columns wide /* * The "tSprite" is used for displaying the data above the scan grid. */ tSprite.deleteSprite(); tSprite.setRotation ( 0 ); tSprite.setTextSize ( 1 ); tSprite.setColorDepth ( 16 ); tSprite.setAttribute ( PSRAM_ENABLE, false ); // Don't use the PSRAM on the WROVERs tSprite.createSprite ( tft.width() - X_ORIGIN, Y_ORIGIN ); /* * The "sSprite" is used for displaying the data to the side of the scan grid. */ sSprite.unloadFont(); sSprite.deleteSprite(); sSprite.setTextSize ( 1 ); sSprite.setColorDepth ( 16 ); sSprite.setAttribute ( PSRAM_ENABLE, false ); // Don't use the PSRAM on the WROVERs /* * Create and draw the sprite for the gain scale */ CreateGainScale (); // Make sure everything will be reset old_settingAttenuate = -1000; old_settingPowerGrid = -1000; old_settingMax = -1; old_settingMin = -1; old_startFreq = -1; old_stopFreq = -1; old_requiredRBW10 = -1; old_vbw = -1; old_settingAverage = -1; old_settingSpur = -100; old_bandwidth = 0; SetRX ( 0 ); // LO to transmit, RX to receive xmit.SetOffset ( 0 ); // make sure frequency offset registers are zero xmit.SetDrive ( setting.Drive ); // Set transmitter power level rcvr.SetPreampGain ( setting.PreampGain ); #ifdef SI_TG_IF_CS if (tgIF_OK) { tg_if.TxMode ( trackGenSetting.IF_Drive ); // turn on the IF oscillator in tracking generator } #endif #ifdef SI_TG_LO_CS if (tgLO_OK) { tg_lo.TxMode ( trackGenSetting.LO_Drive ); // turn on the Local Oscillator in tracking generator } #endif sweepStartDone = false; // Make sure this initialize is only done once per sweep initSweep = true; } /* * Initialise the JSON document that is used to push the sweep data to the web clients * Used in all the sweep modes */ void initChunkSweepDoc (uint32_t startIndex) { jsonDocument.clear (); jsonDocument["PreAmp"] = setting.PreampGain; jsonDocument["mType"] = "chunkSweep"; jsonDocument["StartIndex"] = startIndex; jsonDocument["sweepPoints"] = sweepPoints; jsonDocument["sweepTime"] = (uint32_t)(sweepMicros/1000); } /* * Initialise high frequency mode sweep. */ void initSweepHigh () { // To be done! DisplayError ( ERR_WARN, "Sweep not done!", "Mode changed to", "Analayze Low range", NULL ); tinySA_mode = SA_LOW_RANGE; setting.Mode = tinySA_mode; } /* * High freq range sweep - not yet implemented * * Will use signals direct into LO SI4432, which be in receive * no mixer, attenuator or low pass filter */ void doSweepHigh () { DisplayError ( ERR_WARN, "Sweep High Range", "not implemented.", "Low range mode", "selected for you!" ); initSweepLow(); } /* * Initialise sig gen high frequency mode. * May turn out to not be needed! */ void initSigHigh () { // To be done! DisplayError ( ERR_WARN, "IF Sweep not done!", "Mode changed to", "Analayze Low range", NULL ); tinySA_mode = SA_LOW_RANGE; setting.Mode = tinySA_mode; } /* * High freq range signal generator - not yet implemented * May turn out to be included in sig gen mode and auto * transition depending on set frequency */ void doSigGenHigh () { DisplayError ( ERR_WARN, "Signal Generator High Range", "not implemented.", "Low range SA mode", "selected for you!" ); initSweepLow(); } /* * Over The Air (OTA) update allows the ESP32 to be programmed over wifi * This is handled as a separte mode as it has a hit on scan times * The Init function just draws a simple page indicating it is in OTA Mode */ void initOTA() { boolean pressed; uint16_t t_x = 0, t_y = 0; // To store the touch coordinates tft.fillScreen(SIG_BACKGROUND_COLOR); tft.setTextColor(WHITE); tft.setFreeFont ( &FreeSansBold9pt7b ); // Select Free Serif 9 point font tft.setTextDatum ( TC_DATUM ); // Top center text position datum tft.drawString ( "simpleSA", 160, 20 ); tft.drawString ( "OTA Mode", 160, 60 ); tft.drawString ( "Press to Exit", 160, 100 ); tft.setTextDatum ( TL_DATUM ); // Back to default top left tft.setFreeFont ( NULL ); // Select default font // Make sure touch is not pressed when leaving do { pressed = tft.getTouch(&t_x, &t_y); // Just uses standard TFT_eSPI function as not bothered about speed } while ( pressed ); tinySA_mode = OTA_UPDATE; // don't set the setting.Mode as we may want to return to previous mode } /* * Here we just call the OTA monitor and check if the user has pessed the screen to exit * This is called once every main loop scan */ void doOTA() { uint16_t t_x = 0, t_y = 0; // To store the touch coordinates static boolean lastPress = true; ArduinoOTA.handle(); // check if OTA programming requested // Get current touch state and coordinates boolean pressed = tft.getTouch(&t_x, &t_y); // Just uses standard TFT_eSPI function as not bothered about speed if ( pressed && !lastPress ) { // go back to previous mode // wait for press to be released do { pressed = tft.getTouch(&t_x, &t_y); // Just uses standard TFT_eSPI function as not bothered about speed } while ( pressed ); setMode(setting.Mode); } lastPress = pressed; } /* * "SetRX" - Mode 3 is sig gen, 0 is normal (RX as receive, LO on), 1 is both * receiving 2 not used */ void SetRX ( int p ) { int saRX = p; if ( saRX == ( 3 )) // Both on TX - sig gen mode { rcvr.TxMode ( sigGenSetting.RX_Drive ); // Put receive module in TX mode xmit.TxMode ( sigGenSetting.LO_Drive ); // Put transmit module in TX mode } else { if ( saRX == 0 ) // Normal configuration { rcvr.RxMode (); // Put receive module in RX mode xmit.TxMode ( setting.Drive ); // Put transmit (LO) module in TX mode } else if ( saRX == 1 ) // Both in receive mode { rcvr.RxMode (); // Put receive module in RX mode xmit.RxMode (); // Put transmit module in RX mode } else if ( saRX == 2 ) // Normal configuration { rcvr.RxMode (); // Put receive module in RX mode xmit.TxMode ( setting.Drive ); // Put transmit module in TX mode } } } /* * "ClearDisplay" - Function to clear the display. */ void ClearDisplay () { tft.fillScreen ( BLACK ); // Fade to black } /* * "textWhite" sets the text size to '1' and the text color to "WHITE". */ void textWhite() { tft.setTextSize ( 1 ); tft.setTextColor ( WHITE ); // Draw white text (transparent background) } /* * Find out if the touch was in the bounds of the slider */ bool sliderPressed( bool pressed, uint16_t t_x, uint16_t t_y ) { uint16_t minX = SLIDER_X - 5; uint16_t maxX = SLIDER_X + SLIDER_WIDTH + 2 * SLIDER_KNOB_RADIUS + 5; uint16_t minY = SLIDER_Y; uint16_t maxY = SLIDER_Y + 2 * SLIDER_KNOB_RADIUS; return( ( (t_x >= minX) && (t_x <= maxX) && (t_y >= minY) && (t_y <= maxY) ) && pressed ); } float sliderPercent( uint16_t t_x ) { float p = (float)(t_x - SLIDER_X - SLIDER_KNOB_RADIUS) * 100.0 / (float)SLIDER_WIDTH; //Serial.printf("t_x: %i p: %f SLIDER_X: %i SLIDER_KNOB_RADIUS: %i \n", t_x, p, SLIDER_X, SLIDER_KNOB_RADIUS); if ( p > 100.0 ) p = 100.0; if ( p < 0.0 ) p = 0.0; return( p ); } /* * Draw a slider control. Uses the sSprite. */ void drawSlider (uint16_t x, uint16_t y, float pos, float value, const char *unit) { // range check float p = pos; if (p < 0) p=0.0; if (p > 100.0) p=100.0; // draw a rectangle first, then bar, then position the "knob" on top sSprite.fillSprite(SIG_BACKGROUND_COLOR); sSprite.fillRoundRect( SLIDER_KNOB_RADIUS, ( sSprite.height() - SLIDER_BOX_HEIGHT ) / 2, SLIDER_WIDTH, SLIDER_BOX_HEIGHT, SLIDER_BOX_HEIGHT/4, SLIDER_BOX_COLOR); uint16_t knob_x = pos * SLIDER_WIDTH / 100 + SLIDER_KNOB_RADIUS; sSprite.fillRoundRect( SLIDER_KNOB_RADIUS, ( sSprite.height() - SLIDER_BOX_HEIGHT ) / 2, knob_x - SLIDER_KNOB_RADIUS, SLIDER_BOX_HEIGHT, SLIDER_BOX_HEIGHT/4, SLIDER_FILL_COLOR); sSprite.fillCircle(knob_x, sSprite.height()/2, SLIDER_KNOB_RADIUS, SLIDER_KNOB_COLOR); // Draw the value sSprite.setCursor(SLIDER_WIDTH + 2 * SLIDER_KNOB_RADIUS + 1, SLIDER_KNOB_RADIUS - 3); sSprite.printf("%3.0f %s", value, unit); // Serial.println("drawSlider"); sSprite.pushSprite(SLIDER_X, SLIDER_Y); } /* * "DisplayError" - Added by M0WID to display an error message of up to three lines * on the display. * * Modified in Version 2.7 by WA2FZW: * * Added another line and error levels. Also use a much better looking font * to display the error message! Instead of painting the entire screen in * the appropriate color for the error level, we use a rounded filled rectangle * around the message; looks really nice! */ void DisplayError ( uint8_t severity, const char *line1, const char *line2, const char *line3, const char *line4 ) { char line0[20]; // Line 0 of the message const char *lines[] = { line0, line1, line2, line3, line4 }; // Pointers to the lines ClearDisplay (); // Fade to black if ( severity == ERR_INFO ) // Informational message? { strcpy ( line0, "Information:" ); // Set line 0 tft.fillRoundRect ( 0, 40, 320, 150, 10, WHITE ); // White background } else if ( severity == ERR_WARN ) // Warning message? { strcpy ( line0, "Warning:" ); // Set line 0 tft.fillRoundRect ( 0, 40, 320, 150, 10, YELLOW ); // Yellow background } else if ( severity == ERR_FATAL ) // Fatal error? { strcpy ( line0, "Fatal Error:" ); // Set line 0 tft.fillRoundRect ( 0, 40, 320, 150, 10, RED ); // Red background } tft.setTextColor ( BLACK ); // Black text tft.setFreeFont ( &FreeSansBold9pt7b ); // Select nice font tft.setCursor ( 20, 70 ); // Cursor for the first line for ( int i = 0; i < 5; i++ ) // Display the lines { if ( lines[i] != NULL ) // Ignore any NULL lines { tft.print ( lines[i] ); tft.setCursor ( 20, 100 + ( i * 20 )); // Cursor on the next line } } delay ( DELAY_ERROR * 1000 ); // Allow time to read it if ( severity == ERR_FATAL ) // If fatal error while ( true ) {} // Halt execution ClearDisplay (); tft.setFreeFont ( NULL ); // Set everything back to the default font tft.setTextColor ( WHITE ); // and default text color tft.setTextDatum ( TL_DATUM ); // and default position datum } /* * "RedrawHisto" - Reset all old values to force redraw of labels * Called when exiting menu and should be called when changed from web */ void RedrawHisto () { delayMicroseconds(200); // short delay to make sure any SI4432 frequency updates are completed initSweep = true; memset ( peaks, 0, sizeof ( peak_t )); // peaks are copied to oldPeaks at start of sweep old_settingAttenuate = 1000; old_settingPowerGrid = -1000; old_settingMax = -1; old_settingMin = -1; old_startFreq = -1; old_stopFreq = -1; old_settingAverage = -1; old_settingSpur = -100; ClearDisplay(); switch (tinySA_mode) { case BANDSCOPE: DisplayBandscopeInfo(); break; default: DisplayInfo(); break; } DrawFullCheckerBoard(); } /* * "DisplayInfo" - Draws the background text around the checkerboard. Called * when a setting is changed to set axis labels and top info bar */ void DisplayInfo () { const char *averageText[] = { " OFF", " MIN", " MAX", " 2", " 4", " 8" }; const char *referenceOutText[] = { " 30", " 15", " 10", " 4", " 3", " 2", " 1" }; double fStart; double fCenter; double fStop; // enum { SA_LOW_RANGE, SA_HIGH_RANGE, SIG_GEN_LOW_RANGE, SIG_GEN_HIGH_RANGE, IF_SWEEP, ZERO_SPAN_LOW_RANGE, ZERO_SPAN_HIGH_RANGE, TRACKING_GENERATOR, RX_SWEEP }; tSprite.fillSprite ( BLACK ); tSprite.setTextColor ( WHITE ); /* * Update side bar info */ if ( initSweep || changedSetting || updateSidebar ) { // Serial.println ( "DisplayInfo - InitSweep True" ); sSprite.createSprite ( X_ORIGIN, SCREEN_HEIGHT ); sSprite.fillSprite( BLACK ); sSprite.setCursor ( 0, 0 ); sSprite.setTextColor ( WHITE ); sSprite.print ( setting.PowerGrid ); sSprite.println ( "dB" ); sSprite.setCursor ( 0, CHAR_HEIGHT * 2 ); sSprite.setTextColor ( DB_COLOR ); sSprite.printf ( "%4i", maxGrid ); sSprite.setCursor ( 0, gridHeight + yOrigin ); sSprite.printf ( "%4i", minGrid ); sSprite.setTextColor ( WHITE ); sSprite.setCursor ( 0, HALF_CHAR_H * 8 ); sSprite.setTextColor ( GREEN ); if ( setting.Bandwidth10 !=0 ) // 0 = auto sSprite.setTextColor ( ORANGE ); sSprite.println ( "RBW" ); if ( bandwidth >= 100 ) sSprite.printf ( "%4.0f\n", bandwidth ); else sSprite.printf ( "%4.1f\n", bandwidth ); sSprite.setTextColor ( WHITE ); sSprite.setCursor ( 0, HALF_CHAR_H * 13 ); sSprite.printf ( "VRe\n%4i", vbw ); // Video resolution (not VBW) sSprite.setCursor ( 0, HALF_CHAR_H * 18 ); if ( setting.PreampGain == 0x60 ) // AGC on { sSprite.setTextColor ( GREEN ); sSprite.println ( "Gain" ); sSprite.print ( "auto" ); } else { sSprite.setTextColor ( ORANGE ); // Show value in red if not auto sSprite.println ( "Gain" ); int lnaGain = 5; // Assume low gain if ( setting.PreampGain & LNAGAIN ) // If LNA is turned on lnaGain = 25; // High range int pgaGain = ( setting.PreampGain & PGAGAIN ) * 3; // 3dB per bit sSprite.printf ( "%4i", lnaGain + pgaGain ); } sSprite.setTextColor ( WHITE ); sSprite.setCursor ( 0, HALF_CHAR_H * 23 ); sSprite.printf ( "Attn\n%4i", setting.Attenuate ); sSprite.setCursor ( 0, HALF_CHAR_H * 28 ); // Start at top-left corner sSprite.printf ( "Ext\n%4.1f", setting.ExternalGain ); // Place holder for external gain/attenuation value sSprite.setTextColor ( WHITE ); if ( setting.Average ) sSprite.setTextColor ( AVG_COLOR ); sSprite.setCursor ( 0, HALF_CHAR_H * 33 ); sSprite.println ( "Avg" ); sSprite.print ( averageText[setting.Average] ); sSprite.setTextColor ( WHITE ); sSprite.setCursor ( 0, HALF_CHAR_H * 38 ); sSprite.println ( "Spur" ); if ( setting.Spur ) sSprite.print ( " ON" ); else sSprite.print ( " OFF" ); sSprite.setCursor ( 0, HALF_CHAR_H * 43 ); sSprite.println ( "Ref" ); if ( setting.ReferenceOut >= 0 && setting.ReferenceOut < 7 ) sSprite.print ( referenceOutText[setting.ReferenceOut] ); else sSprite.print ( " OFF" ); if (tgIF_OK) { sSprite.setCursor ( 0, HALF_CHAR_H * 48 ); sSprite.println ( "Trk" ); if ( trackGenSetting.Mode == 1 ) sSprite.print ( " ON" ); else if (trackGenSetting.Mode == 2 ) sSprite.print ( " SIG" ); else sSprite.print ( " OFF" ); } if ( USE_WIFI ) { sSprite.setCursor ( 0, HALF_CHAR_H * 53 ); // Show number of connected clients sSprite.printf ( "Web\n%4u", numberOfWebsocketClients ); } sSprite.pushSprite ( 0, 0 ); sSprite.deleteSprite(); // Save memory } // End of "if ( initSweep || changedSetting )" /* * Update frequency labels at bottom if changed */ tft.setTextColor ( WHITE,BLACK ); tft.setTextSize ( 1 ); if (tinySA_mode == IF_SWEEP) { fStart = startFreq_IF/1000000.0; fCenter = (double) ((( startFreq_IF + stopFreq_IF ) / 2.0 ) / 1000000.0 ); fStop = stopFreq_IF/1000000.0; } else if (tinySA_mode == RX_SWEEP) { fStart = startFreq_RX/1000000.0; fCenter = (double) ((( startFreq_RX + stopFreq_RX ) / 2.0 ) / 1000000.0 ); fStop = stopFreq_RX/1000000.0; } else { fStart = (( (double)setting.ScanStart )/ 1000000.0); // Start freq fCenter = (double) ((( setting.ScanStart + setting.ScanStop ) / 2.0 ) / 1000000.0 ); fStop = (( (double)setting.ScanStop )/ 1000000.0 ); // Stop freq } if ( old_startFreq != fStart || old_stopFreq != fStop ) { tft.fillRect ( xOrigin, SCREEN_HEIGHT - CHAR_HEIGHT, SCREEN_WIDTH - xOrigin - 1, SCREEN_HEIGHT - 1, BLACK ); // Show operating mode tft.setCursor ( xOrigin + 50, SCREEN_HEIGHT - CHAR_HEIGHT ); tft.setTextColor ( DB_COLOR ); tft.printf ( "Mode:%s", modeText[setting.Mode] ); tft.setTextColor ( WHITE ); tft.setCursor ( xOrigin + 2, SCREEN_HEIGHT - CHAR_HEIGHT ); tft.print ( fStart ); // tft.print( "MHz" ); tft.setCursor ( SCREEN_WIDTH - 37, SCREEN_HEIGHT - CHAR_HEIGHT ); tft.print ( fStop ); // tft.print ( "MHz"); /* * Show the center frequency: */ tft.setCursor ( SCREEN_WIDTH / 2 - 40 + xOrigin, SCREEN_HEIGHT - CHAR_HEIGHT ); tft.print ( fCenter ); tft.print ( "(MHz)" ); old_startFreq = fStart; // Save current frequency range old_stopFreq = fStop; // For next time } tft.setCursor ( 220, SCREEN_HEIGHT - CHAR_HEIGHT ); // Show sweep time tft.printf ( "%6ums", sweepMicros / 1000 ); // tft.setCursor ( 100, SCREEN_HEIGHT - CHAR_HEIGHT ); // Show number of connected clients // tft.print( numberOfWebsocketClients ); /* * We use the "tSprite" to paint the data at the top of the screen to avoid * flicker. */ tSprite.setCursor ( 0, 0 ); tSprite.print ( "/" ); // Nasty! /* * Show marker values: * * The "xPos" and "yPos" arrays are the coordinates of where to place the marker data. * * The "posIndex" variable keeps track of the next available position for the marker * data. If we want fixed positions for each marker, then change the "xPos" and "yPos" * indicies to use "m". */ int xPos[MARKER_COUNT] = { 20, 20, 160, 160 }; int yPos[MARKER_COUNT] = { 0, CHAR_HEIGHT, 0, CHAR_HEIGHT }; int posIndex = 0; for ( int m = 0; m < MARKER_COUNT; m++ ) { tSprite.setCursor ( xPos[m], yPos[m] ); if (( marker[m].isEnabled()) && ( setting.ShowSweep || setting.Average != AV_OFF )) { tSprite.setTextColor ( WHITE ); tSprite.printf ( "%u:%5.1fdBm %8.4fMHz", marker[m].Index()+1, rssiTodBm ( oldPeaks[m].Level ), oldPeaks[m].Freq / 1000000.0 ); } else { tSprite.setTextColor ( DARKGREY ); tSprite.printf ( "%u:", marker[m].Index()+1 ); } posIndex++; } int x = tSprite.width () - 45; tSprite.setTextColor ( WHITE ); tSprite.pushSprite ( xOrigin, 0 ); // Write sprite to the display updateSidebar = false; } // End of "DisplayInfo" /* * Draw the complete checkerboard * This can be optimized if needed */ void DrawFullCheckerBoard() { for ( int p=0; p 0 ) img.pushSprite( xOrigin+p-1, yOrigin ); } // Serial.println ( "DrawFullCheckerBoard" ); } /* * "DrawCheckerBoard" - Paints the grid. It now uses a sprite so no need to * erase the old grid, just draw a new one. * * The img sprite is two pixels wide, and the image from the previous data point * is scrolled and then new data point drawn. The data from last is scrolled * so any line from before is retained. */ void DrawCheckerBoard ( int x ) { if ( x == 0 ) // "x" is the sweepStep, if zero return; // then just return. if ( x == 1 ) { img.fillSprite ( BLACK ); // Clear the sprite img.drawFastVLine ( 0, 0, gridHeight, DARKGREY ); // Draw vertical line at edge for ( int y=0; y <= yGrid; y++ ) img.drawPixel ( 1, y*yDelta, DARKGREY ); // Draw the horizontal grid lines } else { img.setScrollRect ( 0, 0, 2,gridHeight, BLACK ); // Scroll the whole sprite img.scroll ( -1, 0 ); int lastStep = x - 1; if (( x % xDelta ) == 0 ) // Need a vertical line here img.drawFastVLine ( 1, 0, gridHeight, DARKGREY ); else for ( int y = 0; y <= yGrid; y++ ) img.drawPixel ( 1, y * yDelta, DARKGREY ); // Draw the horizontal grid lines } } /* * Function to work out the y coordinate on img sprite for a given RSSI value * Takes into account the display scaling, attenuation and level offset */ uint16_t rssiToImgY ( uint8_t rSSI ) { int delta = maxGrid - minGrid; double y = rssiTodBm ( rSSI ); y = ( y - minGrid ) * gridHeight / delta; if ( y >= gridHeight ) y = gridHeight-1; if ( y < 0 ) y = 0; return gridHeight - 1 - (int) y; } /* * Function to convert rSSi to dBm */ double rssiTodBm ( uint8_t rSSI ) { return ( rSSI / 2.0 + dBadjust ); } /* * Function to convert dBm to RSSI */ uint8_t dBmToRSSI ( double dBm ) { return ( 2 * ( dBm - dBadjust ) ); } /* * "DisplayPoint" - Display a point on the chart. * * The "img" sprite is 2 pixels wide to enable the last points to be also * plotted to make lines look good. The img sprite has been scrolled in * "DrawCheckerBoard". */ void DisplayPoint ( uint8_t* data, int i, int color ) { if ( i < 1 ) return; int lastPoint = i - 1; int delta = maxGrid - minGrid; double f0 = data[i] / 2.0 + dBadjust; // Current point f0 = ( f0 - minGrid ) * gridHeight / delta; if ( f0 >= gridHeight ) f0 = gridHeight-1; if ( f0 < 0 ) f0 = 0; double f1 = data[lastPoint] / 2.0 + dBadjust; // Previous point f1 = ( f1 - minGrid ) * gridHeight / delta; if ( f1 >= gridHeight ) f1 = gridHeight-1; if ( f1 < 0 ) f1 = 0; int Y0 = gridHeight - 1 - (int) f0; int Y1 = gridHeight - 1 - (int) f1; img.drawLine ( 0, Y1, 1, Y0, color ); } /* * "DisplayGainPoint" - Added by M0WID to display a scan of the gain setting. */ void displayGainPoint ( unsigned char *data, int i, int color ) { if ( i == 0 ) return; int lastPoint = i - 1; int delta = 50; // Scale of y axis double f0 = ( data[i] ) ; // Serial.printf ( "gain %f \n", f ); f0 = ( f0 ) * gridHeight / delta; if ( f0 >= gridHeight ) f0 = gridHeight - 1; if ( f0 < 0 ) f0 = 0; double f1 = ( data[lastPoint] ); f1 = ( f1 ) * gridHeight / delta; if ( f1 >= gridHeight ) f1 = gridHeight - 1; if ( f1 < 0 ) f1 = 0; int Y0 = gridHeight - 1 - (int) f0; int Y1 = gridHeight - 1 - (int) f1; img.drawLine ( 0, Y1, 1, Y0, color ); } /* * "CreateGainScale" puts the decibel values for the gain trace into a sprite for * display at the right side of the grid. * * NOTE Due to temporary error in TFT_eSPI library the inverted colour is used * so TFT_BLUE actually results in TFT_GREEN! */ void CreateGainScale () { gainScaleSprite.deleteSprite(); gainScaleSprite.setAttribute ( PSRAM_ENABLE, false ); gainScaleSprite.setColorDepth ( 16 ); // Using 16 bit (RGB565) colors gainScaleSprite.createSprite(CHAR_WIDTH * 2, gridHeight); gainScaleSprite.fillSprite(BLACK); gainScaleSprite.setPivot( 0, 0 ); gainScaleSprite.setTextSize ( 1 ); gainScaleSprite.setTextColor ( TFT_BLUE ); // Draw text (transparent background) GAIN_COLOR int w = gainScaleSprite.width(); // Serial.printf("Create gain scale - get sprite width %i \n", w); if ( w != CHAR_WIDTH * 2 ) { Serial.println ( "Error - no gain scale sprite" ); return; } for ( int i = 0; i < 5; i++ ) { gainScaleSprite.setCursor ( 0, 2 + ( yDelta * i * 2 )); gainScaleSprite.print ( 50 - ( i * 10 )); } } /* * "CreateGridScale" puts the decibel values for the main trace into a sprite for * display at the left side of the Bandscope grid. * The gainScaleSprite is reused as there in no need for a gain trace on the bandscope * * NOTE Due to temporary error in TFT_eSPI library the inverted colour is used * so TFT_BLUE actually results in TFT_GREEN! */ void CreateGridScale () { gainScaleSprite.deleteSprite(); gainScaleSprite.setAttribute ( PSRAM_ENABLE, false ); gainScaleSprite.setColorDepth ( 16 ); // Using 16 bit (RGB565) colors gainScaleSprite.createSprite(CHAR_WIDTH * 4, gridHeight); gainScaleSprite.fillSprite(BLACK); gainScaleSprite.setPivot( 0, 0 ); gainScaleSprite.setTextSize ( 1 ); gainScaleSprite.setTextColor ( TFT_WHITE ); int w = gainScaleSprite.width(); // Serial.printf("Create gain scale - get sprite width %i \n", w); if ( w != CHAR_WIDTH * 4 ) { Serial.println ( "Error - no gain scale sprite" ); return; } // Markers at every other line, there are 10 lines int16_t valueInterval = (setting.BandscopeMaxGrid - setting.BandscopeMinGrid) / yGrid * 2 ; int16_t v = setting.BandscopeMaxGrid; int16_t yoffset; for ( int i = 0; i < 5; i++ ) { if (i > 0) yoffset = -3; else yoffset = 0; gainScaleSprite.setCursor ( 0, yoffset + ( yDelta * i * 2 )); gainScaleSprite.printf ( "%4i", v ); v = v - valueInterval; } } /* * "ledcAnalogWrite" - Arduino like analogWrite used for PWM control of backlight. * * "value" has to be between 0 and valueMax */ void ledcAnalogWrite ( uint8_t channel, uint32_t value, uint32_t valueMax = 255 ) { if ( value > valueMax ) value = valueMax; uint32_t duty = ( 8191 / valueMax ) * value; // Calculate duty, 8191 from 2 ^ 13 - 1 ledcWrite ( channel, duty ); // write duty to LEDC } /* * "setUpLEDC" - Sets up the LEDC PWM for the backlight. * * "TFT_BL" is defined in the User_setup.h file in the "TFT_eSPI" library * If BACKLIGHT_LEVEL is not defined then your board uses a resistor instead */ #ifdef BACKLIGHT_LEVEL void setUpLEDC () { ledcSetup ( LEDC_CHANNEL_0, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT ); ledcAttachPin ( TFT_LED, LEDC_CHANNEL_0 ); ledcAnalogWrite ( LEDC_CHANNEL_0, BACKLIGHT_LEVEL ); } #endif /* * "TouchCalibrate" - Runs the touchscreen calibration sequence using the * procedure in the "TFT_eSPI" library. * * This used to be called from "setup" by removing the comment from the * function call. That can still be done, however, it is now also invoked * from the "CONFIG" menu. * * The calibration constants are saved to the flash memory and recalled at * startup, so it need only be done once. */ void TouchCalibrate () { uint8_t calDataOK = 0; tft.fillScreen ( BLACK ); tft.setCursor ( 20, 0 ); tft.setTextFont ( 2 ); tft.setTextSize ( 1 ); tft.setTextColor ( WHITE, BLACK ); tft.println ( "Touch corners as indicated" ); tft.setTextFont ( 1 ); tft.println (); tft.calibrateTouch ( config.touch_cal, MAGENTA, BLACK, 15 ); Serial.print ( "\n\n// New calibration data: { " ); for ( uint8_t i = 0; i < 5; i++ ) { Serial.print ( config.touch_cal[i] ); if ( i < 4 ) Serial.print ( ", " ); } Serial.println ( " };\n\n" ); WriteConfig(); // Save the new calibration data to flash tft.fillScreen ( BLACK ); tft.setTextColor ( GREEN, BLACK); tft.println ( "Calibration complete!" ); tft.println ( "Calibration values sent to Serialport\nand saved to flash." ); delay ( 3000 ); } /* * Print out the files in the SPIFFS (SPI Fllat File System) * This requires some files to be loaded into the ESP32's SPIFFS file system * before the capability can be used. * * Conditionalized on whether or not WiFi is in use: */ void printSPIFFS() { char fileName[25]; char fileSize[10]; Serial.println ( "Contents of SPIFFS:" ); fs::File root = SPIFFS.open ( "/" ); // Open the root object fs::File file = root.openNextFile (); // Open the 1st file while ( file ) // Loop through the files { if ( file.isDirectory () ) // If the file is a directory { strcpy ( fileName, file.name () ); Serial.printf ( " DIR : %-s\n", fileName ); } else // File is a regular file { strcpy ( fileName, file.name() ); itoa ( file.size(), fileSize, 10 ); Serial.printf ( " FILE: %-25s%10s\n", fileName, fileSize ); } file = root.openNextFile(); // Move to the next file } } /* * Print out the bpf calibration values */ void printBpfCal() { Serial.println("Bandpass Filter Calibration values:\n"); for (int i = 0; i< bpfCount; i++) { Serial.printf("filter: %i, rbw10=%i , cal=%f \n", i, rcvr.GetBandpassFilter10(i), bpfCalibrations[i] ); } }