uint32_t colourTest; /* * Initialise variables and SI4432 for the low frequency sweep */ void initBandscope() { // set up checkerboard sizes waterfallHeight = WATERFALL_HEIGHT; gridHeight = SCREEN_HEIGHT - waterfallHeight - 10; gridWidth = SCREEN_WIDTH; yGrid = Y_GRID; // no of grid divisions yDelta = gridHeight / yGrid; // no of points/division xGrid = X_GRID; xOrigin = 0; yOrigin = 0; displayPoints = setting.BandscopePoints; xDelta = SCREEN_WIDTH / xGrid; 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 - we don't use it in this mode * The "sSprite" is for displaying the sidebar stuff, but reused here for the waterfall */ tSprite.deleteSprite(); sSprite.deleteSprite(); sSprite.setColorDepth (16); // we don't need 16bit but its faster sSprite.setAttribute ( PSRAM_ENABLE, false ); // Don't use the PSRAM on the WROVERs sSprite.createSprite ( gridWidth, waterfallHeight ); // Full width sSprite.setScrollRect(0, 0, gridWidth, waterfallHeight, TFT_BLACK); /* * Create and draw the sprite for the gain scale */ CreateGridScale (); // 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.SetDrive ( setting.Drive ); // Set transmitter power level rcvr.SetPreampGain ( setting.PreampGain ); sweepStartDone = false; // Make sure this initialize is only done once per sweep initSweep = true; tinySA_mode = BANDSCOPE; setting.Mode = tinySA_mode; ResetBandscopeMenuStack(); // Put menu stack back to root level } /* * This function section handles the fast bandscope sweep * The display is split and shows a waterfall. * The number of points is reduced, and frequency change is done using an offset to allow the * delay time between changing frequency and taking a reading to be reduced. * * Frequency scheme: * When the LO frequency is < 480MHz the LO can be adjusted +- 80kHz from the * nominal frequency in 156.25Hz steps, ie +- 512 steps. * Actually this is not possible! +-511 steps is * * * If the LO is above 480MHz the the adjustment is +-160kHz in 312.5Hz steps. * If the IF is 434MHz then 480MHz -> 46Mhz for the signal being analysed, so fine * for most of the HF bands. * * In bandscope mode the RBW is fixed at the minimum 2.6kHz, span at 200kHz and * there are 80 data points * * 200kHz -> 2.5kHz steps between each reading or 16 * 156.25Hz if in low band * * Start by setting the LO to the frequency for the start of the sweep plus 80kHz * and set the offset value at -80kHz. * At each reading increment the offset value by 16 (8 in high band). * In this mode the delay time between reading is set at a shorter value than * normally used by the RBW as the LO does not turn off at each change in offset, unlike * a normal frequency change. * When the offset value reaches +80kHz then we need to reset the LO (using normal delaytime) * and continue until we get to the end of the sweep. * * Due to the limitation of not being able to do +-512, or +-16 stesp, we will use +-31 steps per frequency * jump instead * */ void doBandscope() { static uint32_t autoSweepStep = 0; static uint32_t autoSweepFreq = 0; static uint32_t autoSweepFreqStep = 0; static unsigned long setFreqMicros; static unsigned long nowMicros; static unsigned long bandscopeDelay; static int16_t pointMinGain; // to record minimum gain for the current display point static int16_t pointMaxRSSI; // to record max RSSI of the samples in the current display point static uint32_t pointMaxFreq; // record frequency where maximum occurred static int16_t lastMode; // Record last operating mode (sig gen, normal) static uint16_t minRSSI = 255; // Minimum level for the sweep static uint16_t lastMinRSSI; // Minimum level for the previous sweep static bool resetAverage; // Flag to indicate a setting has changed and average valuesneeds to be reset static bool jsonDocInitialised = false; static uint16_t chunkIndex; /* * If paused and at the start of a sweep then do nothing */ if (!sweepStartDone && paused) return; /* * If the "sweepStartDone" flag is false or if the "initSweep" flag is true, we need * to set things up for the sweep. */ if (( !sweepStartDone || initSweep || changedSetting ) ) { if ( initSweep || changedSetting ) // Something has changed, or a first start, so need to owrk out some basic things { sweepPoints = setting.BandscopePoints; autoSweepFreqStep = ( setting.BandscopeSpan ) / sweepPoints; offsetFreqIncrement = autoSweepFreqStep; // 2500 Hz for 200kHz span, 80 points per sweep bandwidth = rcvr.SetRBW ( setting.BandscopeRBW10, &delaytime, &bpfIndex ); // Set it in the receiver Si4432 //Serial.printf("set rcvr Freq get:%u, tempIF:%u\n", rcvr.GetFrequency(), tempIF); rcvr.SetFrequency ( setting.IF_Freq + 1300 ); // Set the RX Si4432 to the IF frequency, offset a bit // sweepFreqStep = autoSweepFreqStep; // Step for each reading if ( setting.Attenuate != old_settingAttenuate ) { if ( !att.SetAtten ( setting.Attenuate )) // Set the internal attenuator setting.Attenuate = att.GetAtten (); // Read back if limited (setting.Attenuate was outside range) old_settingAttenuate = setting.Attenuate; } // pre-calculate adjustment for RSSI values dBadjust = (double)setting.Attenuate - 120.0 + setting.LevelOffset - setting.ExternalGain + bpfCalibrations[bpfIndex]; Serial.printf("Bandscope dBadjust = %f; leveloffset = %f; attenuate = %i, ext gain = %f\n", dBadjust, setting.LevelOffset, setting.Attenuate, setting.ExternalGain); resetAverage = changedSetting; maxGrid = setting.BandscopeMaxGrid; minGrid = setting.BandscopeMinGrid; #ifdef USE_WIFI // Vary number of points to send in each chunk depending on delaytime // A chunk is sent at the end of each sweep regardless wiFiPoints = wiFiTargetTime / delaytime; if (wiFiPoints > MAX_WIFI_POINTS) wiFiPoints = MAX_WIFI_POINTS; if (wiFiPoints > setting.BandscopePoints) wiFiPoints = setting.BandscopePoints; // Serial.printf("No of wifiPoints set to %i\n", wiFiPoints); if ( numberOfWebsocketClients > 0 ) pushBandscopeSettings (); #endif // #ifdef USE_WIFI } // initSweep || changedSetting autoSweepStep = 0; // Set the step counter to zero autoSweepFreq = setting.BandscopeStart; // Set the start frequency. while (( micros() - setFreqMicros ) < delaytime ) // Make sure enough time has elasped since previous frequency write { } resetOffsets(); // set the offset value in the SI4432 xmit.SetOffset(offsetValue); setFreqMicros = micros(); // Store the time the frequency was changed // set the LO frequency (offsetFreq is -ve at start!) uint32_t xmitFreq = setting.IF_Freq + autoSweepFreq - offsetFreq; // Serial.printf("XmitFreq %i\n", xmitFreq); xmit.SetFrequency ( xmitFreq ); // delay will vary depending on whether or not the nominal frequency is changed bandscopeDelay = delaytime; // long delay #ifdef USE_WIFI if ( numberOfWebsocketClients > 0 ) // Start off the json document for the scan { chunkIndex = 0; initChunkSweepDoc (autoSweepStep); Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array jsonDocInitialised = true; } else jsonDocInitialised = false; #endif // #ifdef USE_WIFI // sweepStep = 0; startFreq = setting.BandscopeStart + setting.IF_Freq; // Start freq for the LO stopFreq = setting.BandscopeSpan + startFreq; // Stop freq for the LO // Serial.printf(" start %i; stop %i; points %i \n", startFreq, stopFreq, sweepPoints ); lastMinRSSI = minRSSI; minRSSI = 255; // real value should always be less DisplayBandscopeInfo (); // Display axis and other info sweepStartDone = true; // Make sure this initialize is only done once per sweep initSweep = false; changedSetting = false; lastSweepStartMicros = sweepStartMicros; // Set last time we got here sweepStartMicros = micros(); // Current time sweepMicros = sweepStartMicros - lastSweepStartMicros; // Calculate sweep time (no rollover handling) } // End of "if ( !sweepStartDone ) || initSweep || changedSetting )" /* * Here we do the actual sweep. Save the current step and frequencies for the next time * through, then wait the required amount of time based on the RBW before taking the * signal strength reading and changing the transmitter (LO) frequency. */ uint16_t oldSweepStep = autoSweepStep; uint32_t oldSweepFreq = autoSweepFreq; /* * Wait until time to take the next reading. If a long wait then check the touchscreen * and Websockets while we are waiting to improve response */ nowMicros = micros(); while (( nowMicros - setFreqMicros ) < bandscopeDelay ) { if ( ( nowMicros - setFreqMicros + delaytime > MIN_DELAY_WEBSOCKETS ) && ( (nowMicros - lastWebsocketMicros > websocketInterval) || (numberOfWebsocketClients > 0) ) ) { webSocket.loop (); // Check websockets - includes Yield() to allow other tasks to run lastWebsocketMicros = nowMicros; } if ( nowMicros - setFreqMicros > 100 ) // Wait some time to allow DMA sprite write to finish! UiProcessTouch (); // Check the touch screen nowMicros = micros(); } int rxRSSI = rcvr.GetRSSI (); // Read the RSSI from the RX SI4432 /* * Note that there are two different versions of the print statement to send the * RSSI readings to the serial output. You can change which one is commented out. * * The first one produces a tab separated list of just the frequency and RSSI * reading. That format can be easily read inte something like Excel. * * The second one produces a listing more fit for human consumption! */ if ( showRSSI ) // Displaying RSSI? { // Serial.printf ( "%s\t%03d\n", // FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output Serial.printf ( "Freq: %s - RSSI: %03d\n", FormatFrequency ( autoSweepFreq) , rxRSSI ); // Send it to the serial output } if ( (numberOfWebsocketClients > 0) || (setting.ShowGain) ) gainReading = GetPreampGain ( &AGC_On, &AGC_Reg ); // Record the preamp/lna gains autoSweepFreq += autoSweepFreqStep; // Increment the frequency autoSweepStep++; // and increment the step count offsetFreq += offsetFreqIncrement; offsetValue += offsetIncrement; /* * Change the local oscillator frequency for the next reading and record the time for * the RBW required settling delay. */ setFreqMicros = micros(); // Store the time the LO frequency was changed if (offsetValue >= 512) // reached offset limits { resetOffsets(); xmit.SetOffset(offsetValue); uint32_t f = setting.IF_Freq + autoSweepFreq - offsetFreq; // Serial.printf("Sweep setFreq %i\n", f); xmit.SetFrequency ( f ); // Set the new LO frequency as soon as RSSI read bandscopeDelay = delaytime; } else { xmit.SetOffset(offsetValue); bandscopeDelay = offsetDelayTime; } #ifdef USE_WIFI if ( numberOfWebsocketClients > 0 ) { if ( jsonDocInitialised ) { JsonObject dataPoint = Points.createNestedObject (); // Add an object to the Json array to be pushed to the client dataPoint["x"] = oldSweepFreq/1000000.0; // Set the x(frequency) value dataPoint["y"] = rxRSSI; // Set the y (RSSI) value chunkIndex++; // increment no of data points in current WiFi chunk if ( chunkIndex >= wiFiPoints ) // Send the chunk of data and start new jSon document { String wsBuffer; if ( wsBuffer ) { // Serial.print("D"); serializeJson ( jsonDocument, wsBuffer ); // Serial.printf("J%u", wsBuffer.length() ); unsigned long s = millis(); webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients if (millis() - s > 1000) { Serial.println("webSocketTimeout"); Serial.println(wsBuffer); numberOfWebsocketClients = 0; } // Serial.print("j"); } else Serial.println("No buffer :("); } } if ( ( chunkIndex >= wiFiPoints ) || !jsonDocInitialised ) // Start new jSon document { chunkIndex = 0; initChunkSweepDoc (autoSweepStep); Points = jsonDocument.createNestedArray ( "Points" ); // Add Points array jsonDocInitialised = true; } } #endif // #ifdef USE_WIFI if (rxRSSI < minRSSI) // Detect minimum for sweep minRSSI = rxRSSI; uint16_t pixelsPerPoint = SCREEN_WIDTH / displayPoints; for (uint16_t i = 0; i< pixelsPerPoint; i++) { uint16_t tmp = oldSweepStep * pixelsPerPoint + i; myActual[tmp] = rxRSSI; myGain[tmp] = gainReading; DrawCheckerBoard ( tmp ); // Draw the grid if ( resetAverage || setting.Average == AV_OFF ) // Store data, either as read or as rolling average myData[tmp] = myActual[oldSweepStep]; else { switch ( setting.Average ) { case AV_MIN: if ( myData[tmp] > myActual[oldSweepStep] ) myData[tmp] = myActual[oldSweepStep]; break; case AV_MAX: if ( myData[tmp] < myActual[oldSweepStep] ) myData[tmp] = myActual[oldSweepStep]; break; case AV_2: myData[tmp] = ( myData[tmp] + myActual[oldSweepStep] ) / 2; break; case AV_4: myData[tmp] = ( myData[tmp]*3 + myActual[oldSweepStep] ) / 4; break; case AV_8: myData[tmp] = ( myData[tmp]*7 + myActual[oldSweepStep] ) / 8; break; } DisplayPoint ( myData, tmp, AVG_COLOR ); } if ( setting.ShowSweep ) DisplayPoint ( myActual, tmp, DB_COLOR ); if ( setting.ShowGain ) displayGainPoint ( myGain, tmp, GAIN_COLOR ); if ( setting.ShowStorage ) DisplayPoint ( myStorage, tmp, STORAGE_COLOR ); // If in the first few points show the scale if ( ( tmp < 4 * CHAR_WIDTH ) && (tmp > 0) ) { int16_t scaleX = -tmp + 1; // relative to the img sprite img.setPivot( scaleX, 0); gainScaleSprite.pushRotated ( &img, 0, TFT_BLACK ); // Send the sprite to the target sprite, with transparent colour } if ( tmp > 0 ) // Only push if not first point (two pixel wide img) img.pushSprite ( xOrigin + tmp - 1 , yOrigin ); /* * put data into the top row of the waterfall * 16 bit colours have 5 bits for Red, 6 bits for Green, 5 bits for Blue * We will just change the green level here for first test */ uint32_t level = (uint32_t)( (float)(rxRSSI - setting.WaterfallMin) * setting.WaterfallGain) ; // testing colours if (rxRSSI < setting.WaterfallMin) level = 0; uint32_t green = level; uint32_t red = 0; uint32_t blue = 0; if (green > 63) { green = 63; red = level - 63; if ( red > 31 ) { red = 31; blue = level - 63 - 31; if ( blue > 31 ) blue = 31; } } uint32_t pixelColour = (red << 11) + (green << 5) + blue; if (colourTest > 0) // delete at some stage pixelColour = colourTest; // Serial.printf("rxRSSI %i; red %i; green %i; blue %i; colour %i \n", rxRSSI, red, green, blue, pixelColour); sSprite.drawPixel ( tmp, 0, pixelColour ); } // myFreq[oldSweepStep] = oldSweepFreq; // Store the frequency for XML file creation if ( autoSweepStep >= sweepPoints ) // If we have got to the end of the sweep { // autoSweepStep = 0; sweepStartDone = false; resetAverage = false; if ( sweepCount < 2 ) sweepCount++; // Used to disable wifi at start if ( myActual[setting.BandscopePoints-1] == 0 ) // Ensure a value in last data point { myActual[setting.BandscopePoints-1] = rxRSSI; // Yes, save it myGain[setting.BandscopePoints-1] = gainReading; // myFreq[setting.BandscopePoints-1] = oldSweepFreq; } if ( showRSSI == 1 ) // Only show it once? showRSSI = 0; // Then turn it off #ifdef USE_WIFI if (( numberOfWebsocketClients > 0) && jsonDocInitialised && (chunkIndex > 0) ) { String wsBuffer; if (wsBuffer) { serializeJson ( jsonDocument, wsBuffer ); webSocket.broadcastTXT ( wsBuffer ); // Send to all connected websocket clients } else Serial.println ( "No buffer :("); } #endif // #ifdef USE_WIFI // scroll the waterfall down one pixel sSprite.scroll( 0, 1 ); sSprite.pushSprite( 0, gridHeight + 1 ); } // End of "if ( autoSweepStep >= sweepPoints )" } // End of "doBandscope" /* * "DisplayBandscopeInfo" - Draws the frequency info below the checkerboard. Called * when a setting is changed to set axis labels */ void DisplayBandscopeInfo () { 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 }; tSprite.fillSprite ( BLACK ); tSprite.setTextColor ( WHITE ); /* * Update frequency labels at bottom if changed */ tft.setTextColor ( WHITE,BLACK ); tft.setTextSize ( 1 ); fStart = (double)( setting.BandscopeStart / 1000000.0 ); // Start freq fCenter = (double)( ( setting.BandscopeStart + setting.BandscopeSpan/2.0 ) / 1000000.0 ); fStop = (double)( (setting.BandscopeStart + setting.BandscopeSpan ) / 1000000.0 ) ; // Stop freq if ( old_startFreq != fStart || old_stopFreq != fStop ) { // Serial.printf("DisplayBandscopeInfo fStart %f; old_startFreq %f \n", fStart, old_startFreq); // Serial.printf("DisplayBandscopeInfo fStop %f; old_stopFreq %f \n", fStop, old_stopFreq); 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.setCursor ( SCREEN_WIDTH - 25, SCREEN_HEIGHT - CHAR_HEIGHT ); tft.print ( fStop ); /* * Show the center frequency: */ tft.setCursor ( SCREEN_WIDTH / 2 - 20 + 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 ); /* * We use the "tSprite" to paint the data at the top of the screen to avoid * flicker. */ /* * 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 "DisplayBandscopeInfo" void resetOffsets () { if (setting.BandscopeStart < 480000000) // low range. Assume never change range mid sweep! { offsetStep = -31; offsetIncrement = 16; // 16 * 156.25 = 2500 } else // high range { offsetStep = -63; offsetIncrement = 8; // 8 * 312.5 = 2500 } if (setting.BandscopeSpan == 400000) // wider span, same no of points offsetIncrement = offsetIncrement * 2; offsetFreq = offsetStep * offsetFreqIncrement; offsetValue = offsetStep * offsetIncrement; }