simpleSA/simpleSA.ino
M0WID 1746058f31 Fixed TG CS pin define compile error
Corrected problem with CS pins floating if module installed but not initialised.
Also corrected compiler error if neither TG SI4432 defined
2020-10-09 21:14:58 +01:00

2080 lines
62 KiB
C++

/*
* "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 <http://www.gnu.org/licenses/>.
*
*
*
* 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 <Arduino.h> // Basic Arduino definitions
#include <SPI.h> // Serial Peripheral Interface library
#include <Wire.h> // 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 <WiFi.h> // ESP32 WiFi support library
#include <AsyncTCP.h> // Asynchronous TCP library for Espressif MCUs
#include "SPIFFS.h" // ESP32 built-in "file system"
#endif
#include <TFT_eSPI.h> // TFT_eSPI library
#include <Preferences.h> // 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 ( 2 ) + JSON_OBJECT_SIZE( 5 );
static DynamicJsonDocument jsonDocument ( 4000 ); // 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
/*
* 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 ); // Set initial bandwidth and delaytime
SetRX ( 0 ); // RX in receive, LO in transmit
rcvr.SetPreampGain ( setting.PreampGain );
bpfCount = rcvr.GetBandpassFilterCount(); // no of RBW filters available
// 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 ();
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 ( ( setting.Mode != SIG_GEN_LOW_RANGE ) || (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 ( setting.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;
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;
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;
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 TG_IF_INSTALLED
if (tgIF_OK) {
tg_if.TxMode ( trackGenSetting.IF_Drive ); // turn on the IF oscillator in tracking generator
}
#endif
#ifdef TG_LO_INSTALLED
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; // Fixed gain
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();
}
/*
* "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<displayPoints; p++ )
{
DrawCheckerBoard ( p );
if ( 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 rSSi to dBm
*/
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
}
}