diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7f693e9..8405ae7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,6 @@ V2.1.0 ##New Features +* Added support for RF Explorer Plus models * Added logger. App logs can be downloaded via the "Help" menu. * Frequency range can now be entered manually (via "Band" menu or f-hotkey) * Added hotkeys 1-9 for switching bands. Use "Band" menu or SHIFT- to save current band diff --git a/renderer.js b/renderer.js index 2930d9b..348e901 100644 --- a/renderer.js +++ b/renderer.js @@ -56,7 +56,8 @@ let formValid = false let curKeyInputTarget = ''; let keyInputTargets = { - MANUAL_BAND_SETTINGS: 'MANUAL_BAND_SETTINGS' + MANUAL_BAND_SETTINGS: 'MANUAL_BAND_SETTINGS', + SWEEP_POINT_SETTINGS: 'SWEEP_POINT_SETTINGS' } document.getElementById('donate-button').addEventListener ('click', () => openDonateWindow() ) @@ -1414,7 +1415,7 @@ ipcRenderer.on ( 'SET_SCAN_DEVICE', async (event, message) => { }) ipcRenderer.on ( 'DEVICE_SETTINGS', async (event, message) => { - if ( scanDevice instanceof RFExplorer ) { + if ( scanDevice instanceof RFExplorer && scanDevice.constructor.MODEL === 'BASIC' ) { showPopup ( 'warning', 'POPUP_CAT_SETTINGS', @@ -1422,18 +1423,28 @@ ipcRenderer.on ( 'DEVICE_SETTINGS', async (event, message) => { "This device does not have configurable settings!", ['Ok'] ) - } else if ( scanDevice instanceof TinySA ) { + } else if ( + (scanDevice instanceof RFExplorer && scanDevice.constructor.MODEL === 'PLUS') || + scanDevice instanceof TinySA + ) { Swal.fire({ title: "Enter number of sweep points", - input: "text", - width: '200px', + html:'

Please enter a value between ' + scanDevice.getMinSweepPoints() + ' and ' + scanDevice.getMaxSweepPoints() + '

' + + '

Sweep points

', + width: '600px', showCancelButton: true, confirmButtonText: "Ok", confirmButtonColor: "#0099ff", + stopKeydownPropagation: false, customClass: { title: 'sweetalert2-title', validationMessage: 'sweetalert2-validation-message' }, + preConfirm: function () { + return new Promise(function (resolve) { + resolve(document.getElementById('swal-input').value) + }) + }, inputValidator: value => { return new Promise( resolve => { if ( !isNaN(value) ) { @@ -1444,13 +1455,31 @@ ipcRenderer.on ( 'DEVICE_SETTINGS', async (event, message) => { }); } }).then ( result => { + curKeyInputTarget = ''; + if (result.isConfirmed) { - global.SWEEP_POINTS = result.value - log.info ( "Number of sweep points was set to: " + global.SWEEP_POINTS ) - configStore.set ( 'sweep_points', global.SWEEP_POINTS ) - scanDevice.setConfiguration ( global.START_FREQ, global.STOP_FREQ, global.SWEEP_POINTS ); + if ( scanDevice instanceof RFExplorer && scanDevice.constructor.MODEL === 'PLUS' ) { + global.SWEEP_POINTS = parseInt(result.value) + configStore.set ( 'sweep_points', global.SWEEP_POINTS ) + scanDevice.setSweepPoints ( parseInt(global.SWEEP_POINTS) ) + } else if ( scanDevice instanceof TinySA ) { + global.SWEEP_POINTS = result.value + log.info ( "Setting number of sweep points to: " + global.SWEEP_POINTS ) + configStore.set ( 'sweep_points', global.SWEEP_POINTS ) + scanDevice.setConfiguration ( global.START_FREQ, global.STOP_FREQ, global.SWEEP_POINTS ); + } else { + log.error ( `Unable to set sweep points! Unknown device: ${scanDevice.constructor.NAME} ${scanDevice.constructor.HW_TYPE} ${scanDevice.constructor.MODEL}` ) + } } }) + + curKeyInputTarget = keyInputTargets.SWEEP_POINT_SETTINGS; + setTimeout(()=>{ + document.getElementsByClassName('swal2-confirm')[0].disabled = true + document.getElementById('swal-input').focus() + }, 200); + } else { + log.error ( `Unknown device type: ${scanDevice.constructor.NAME}, ${scanDevice.constructor.HW_TYPE}, ${scanDevice.constructor.MODEL}`) } }) @@ -1742,6 +1771,7 @@ document.addEventListener ( "keydown", async e => { switch ( curKeyInputTarget ) { // Manual frequency band settings modal case keyInputTargets.MANUAL_BAND_SETTINGS: + case keyInputTargets.SWEEP_POINT_SETTINGS: switch ( e.key ) { case 'Enter': if ( formValid ) { @@ -1923,6 +1953,19 @@ document.addEventListener ( "keyup", async e => { } } break; + case keyInputTargets.SWEEP_POINT_SETTINGS: { + formValid = false + + if ( !scanDevice.isValidSweepPointRange(document.getElementById('swal-input').value) ) { + document.getElementById('swal-input').style.backgroundColor = "#ffb6b6" + document.getElementsByClassName('swal2-confirm')[0].disabled = true + } else { + formValid = true + document.getElementById('swal-input').style.backgroundColor = "unset" + document.getElementsByClassName('swal2-confirm')[0].disabled = false + } + } break; + default: } }) diff --git a/scan_devices/rf_explorer.js b/scan_devices/rf_explorer.js index 1c91053..680a2ed 100644 --- a/scan_devices/rf_explorer.js +++ b/scan_devices/rf_explorer.js @@ -3,20 +3,35 @@ const { DelimiterParser } = require ( '@serialport/parser-delimiter'); class RFExplorer { - static NAME = 'RF Explorer'; - static HW_TYPE = 'RF_EXPLORER'; - static BAUD_RATE = 500000; - static MIN_SPAN = 112000 + static NAME = 'RF Explorer'; // Basic device name + static MODEL = '' // This divides devices with the same base NAME and HW_TYPE into specific models + // The device type shares the same API with similar devices. This is e.g. needed for sw to know how a + // device can be contacted via the serial port. + static HW_TYPE = 'RF_EXPLORER'; + static BAUD_RATE = 500000; + + static MIN_SPAN_BASIC = 112000; // 112 kHz + static MAX_SPAN_BASIC = 600000000; // 600 MHz + static MIN_SWEEP_POINTS_BASIC = 112 + static MAX_SWEEP_POINTS_BASIC = 112 + + static MIN_SPAN_PLUS = 112000; // 112 kHz + static MAX_SPAN_PLUS = 192500000; // 192.5 MHz + static MIN_SWEEP_POINTS_PLUS = 112 + static MAX_SWEEP_POINTS_PLUS = 65535 + static deviceCommands = { GET_CONFIG: '#0C0', // Get current configuration SET_CONFIG: '#0C2-F:', // Set configuration - HOLD: '#0CH' // Tell the device to stop sending scan data + HOLD: '#0CH', // Tell the device to stop sending scan data + SET_SWEEP_POINTS_LARGE :'#0Cj' // Set number of sweep points up to 65536 }; static deviceEvents = { NAME: 'RF Explorer', DEVICE_DATA: '#C2-M:', // '#C2-M:' = Main model code, expansion model code and firmware version CONFIG_DATA: '#C2-F:', // '#C2-F:' = config data from scan device - SCAN_DATA: '$S', // '$S' = sweep data, 'p' = ASCII code 112 ( 112 sweep points will be received) 'à' = ASCII code 224 ( 224 sweep points will be received) + SCAN_DATA: '$S', // '$S' = sweep data, 'p' = ASCII code 112 ( 112 sweep points will be received) 'à' = ASCII code 224 ( 224 sweep points will be received) + SCAN_DATA_LARGE: '$z', // '$s' = sweep data, 'p' = ASCII code 112 ( 112 sweep points will be received) 'à' = ASCII code 224 ( 224 sweep points will be received) CALIBRATION_DATA: '#CAL:', // Calibration data ?? (nothing in the docs) QUALITY_DATA: '#QA:', // Quality data ?? (nothing in the docs) SERIAL_NUMBER: '#Sn' // Serial number @@ -31,6 +46,40 @@ class RFExplorer { this.port = port } + getMinSweepPoints () { + switch ( RFExplorer.MODEL ) { + case 'BASIC': return RFExplorer.MIN_SWEEP_POINTS_BASIC; + case 'PLUS' : return RFExplorer.MIN_SWEEP_POINTS_PLUS; + } + } + + getMaxSweepPoints () { + switch ( RFExplorer.MODEL ) { + case 'BASIC': return RFExplorer.MAX_SWEEP_POINTS_BASIC; + case 'PLUS' : return RFExplorer.MAX_SWEEP_POINTS_PLUS; + } + } + + isValidSweepPointRange ( numOfSweepPoints ) { + switch ( RFExplorer.MODEL ) { + case 'BASIC': + if ( numOfSweepPoints >= RFExplorer.MIN_SWEEP_POINTS_BASIC && + numOfSweepPoints <= RFExplorer.MAX_SWEEP_POINTS_BASIC ) { + return true + } else { + return false + } + + case 'PLUS': + if ( numOfSweepPoints >= RFExplorer.MIN_SWEEP_POINTS_PLUS && + numOfSweepPoints <= RFExplorer.MAX_SWEEP_POINTS_PLUS ) { + return true + } else { + return false + } + } + } + getConfiguration () { // IMPORTANT: After requesting the configuration data, the device immediately starts sending scan data. // No additional command is required! @@ -72,6 +121,17 @@ class RFExplorer { } } + async setSweepPoints ( numberOfSweepPoints ) { + log.info ( `Setting number of sweep points to ${numberOfSweepPoints} ...` ) + // Second character will be replaced by a binary lenght value + // '00' is just a placeholder for two bytes in the buffer which will be replaced by MSB/LSB + let sendBuf = Buffer.from ( RFExplorer.deviceCommands.SET_SWEEP_POINTS_LARGE + '00', 'ascii' ); + sendBuf.writeUInt8 ( 0x6, 1 ); + sendBuf.writeUInt8 ( (numberOfSweepPoints & 0xFF00) >> 8, 4 ); // MSB + sendBuf.writeUInt8 ( numberOfSweepPoints & 0x00FF , 5 ); // LSB + await this.port.writePromise ( sendBuf, 'ascii' ) + } + setHandler (data$) { log.info ( `Setting handler for ${RFExplorer.NAME} data receiption ... ` ) const parser = this.port.pipe(new DelimiterParser({ delimiter: '\r\n' })) @@ -79,6 +139,25 @@ class RFExplorer { parser.on ( 'data', (res) => { let buf = String.fromCharCode.apply ( null, res ) + // When a command is sent to the device, on newer models of RF Explorer (PLUS variant), + // the device is transmitting a so called EOS sequence ('End Of Sweep') to acknowledge + // the end of an ongoing sweep. This sequence is FF FE FF FE 00 (as string: 'ÿþÿþ '). + // We need to strip these characters before processing the string. + let eosSequences = [ + { sequence: 'ÿþÿþ\x00#', offset: 5 }, + { sequence: 'þ\x00#' , offset: 2 }, + { sequence: 'ÿþÿþ\x00$', offset: 5 }, + { sequence: 'þ\x00$' , offset: 2 } + ] + + for ( const eosSequence of eosSequences) { + let eosIdx = buf.indexOf ( eosSequence.sequence ) + + if ( eosIdx !== -1 ) { + buf = buf.substring ( eosIdx + eosSequence.offset ) + } + } + for ( let deviceEventType in this.constructor.deviceEvents ) { let deviceEvent = this.constructor.deviceEvents[deviceEventType] @@ -94,8 +173,26 @@ class RFExplorer { return } - const dataLength = parseInt ( deviceEvent === RFExplorer.deviceEvents.SCAN_DATA ? buf[deviceEvent.length] : (buf.length - deviceEvent.length) ) - const data = buf.substring ( deviceEvent === RFExplorer.deviceEvents.SCAN_DATA ? (deviceEvent.length + 1) : deviceEvent.length ) // +1 to exclude the byte following the message ID which contains the number of sweep points + let dataLength = 0; + let data = '' + + switch (deviceEvent) { + case RFExplorer.deviceEvents.SCAN_DATA: + dataLength = buf.charCodeAt(deviceEvent.length) + data = buf.substring ( deviceEvent.length + 1 ) // +1 to exclude the byte following the message ID which contains the number of sweep points + break + + case RFExplorer.deviceEvents.SCAN_DATA_LARGE: + const MSB = buf.charCodeAt(deviceEvent.length) + const LSB = buf.charCodeAt(deviceEvent.length + 1) + dataLength = (MSB << 8) | LSB; + data = buf.substring ( deviceEvent.length + 2 ) // +2 to exclude the two bytes following the message ID which contain the number of sweep points + break + + default: + dataLength = buf.length - deviceEvent.length + data = buf.substring ( deviceEvent.length ) + } switch ( deviceEvent ) { case this.constructor.deviceEvents.CONFIG_DATA: // Received config data from scan device @@ -153,16 +250,19 @@ class RFExplorer { case 4: mainModelString = '2.4G' ; break; case 5: mainModelString = 'WSUB3G' ; break; case 6: mainModelString = '6G' ; break; - case 10: mainModelString = 'WSUB1G_PLUS'; break; + case 10: mainModelString = 'WSUB1G_PLUS'; + RFExplorer.MODEL = 'PLUS' + break; case 60: mainModelString = 'RFEGEN' ; break; default: + RFExplorer.MODEL = 'BASIC' log.error( `Unknown 'RF Explorer' model code: ${mainModelCode}` ) } switch (expansionModelCode) { case 4: expansionModelString = '2.4G' ; break; case 5: expansionModelString = 'WSUB3G' ; break; - case 12: expansionModelString = '2.4G Gen 2' ; break; + case 12: expansionModelString = 'WSUB3G PLUS' ; break; case 255: expansionModelString = 'No expansion model'; break; default: log.error( `Unknown expansion model code: ${expansionModelCode}` ) @@ -181,6 +281,15 @@ class RFExplorer { }]) } break + + case this.constructor.deviceEvents.SCAN_DATA_LARGE: + if ( this.received_config_data ) { + data$.next([{ + type: "SCAN_DATA", + values: data + }]) + } + break } } } diff --git a/scan_devices/tiny_sa.js b/scan_devices/tiny_sa.js index bc0c107..e315085 100644 --- a/scan_devices/tiny_sa.js +++ b/scan_devices/tiny_sa.js @@ -1,28 +1,31 @@ -// API information taken from : https://github.com/RFExplorer/RFExplorer-for-.NET/wiki/RF-Explorer-UART-API-interface-specification +// API information taken from : https://tinysa.org/wiki/pmwiki.php?n=Main.USBInterface const { DelimiterParser } = require ( '@serialport/parser-delimiter'); const { Subject, firstValueFrom } = require('rxjs'); class TinySA { - // Device type which shares the same API with similar devices. This is e.g. needed for sw to know how a + static NAME = 'tinySA'; // Basic device name + static MODEL = '' // This divides devices with the same base NAME and HW_TYPE into specific models + // The device type shares the same API with similar devices. This is e.g. needed for sw to know how a // device can be contacted via the serial port. static HW_TYPE = 'TINY_SA'; static HW_VERSION= 'HW Version'; - // Basic device name - static NAME = 'tinySA'; - // This devides devices with the same base NAME and HW_TYPE into specific models - static MODEL = '' + static BAUD_RATE = 115200; // For TinySA the connection baudrate doesn't seem to matter static MIN_FREQ_BASIC = 0 static MAX_FREQ_BASIC = 960000000 static MIN_SPAN_BASIC = 1 // couldn't find any specification if and what the minimum span is static MAX_SPAN_BASIC = 959900000 // couldn't find any specification if and what the maximum span is + static MIN_SWEEP_POINTS_BASIC = 51 + static MAX_SWEEP_POINTS_BASIC = 290 + static MIN_FREQ_ULTRA = 0 static MAX_FREQ_ULTRA = 6000000000 static MIN_SPAN_ULTRA = 1 // couldn't find any specification if and what the minimum span is static MAX_SPAN_ULTRA = 5999900000 // couldn't find any specification if and what the maximum span is + static MIN_SWEEP_POINTS_ULTRA = 25 + static MAX_SWEEP_POINTS_ULTRA = 450 - static BAUD_RATE = 115200; // For TinySA the connection baudrate doesn't seem to matter static deviceCommands = { GET_VERSION: 'version', GET_FREQ_CONFIG: 'sweep', // Get current configuration @@ -85,6 +88,40 @@ class TinySA { } } + getMinSweepPoints () { + switch ( TinySA.MODEL ) { + case 'BASIC': return TinySA.MIN_SWEEP_POINTS_BASIC; + case 'ULTRA': return TinySA.MIN_SWEEP_POINTS_ULTRA; + } + } + + getMaxSweepPoints () { + switch ( TinySA.MODEL ) { + case 'BASIC': return TinySA.MAX_SWEEP_POINTS_BASIC; + case 'ULTRA': return TinySA.MAX_SWEEP_POINTS_ULTRA; + } + } + + isValidSweepPointRange ( numOfSweepPoints ) { + switch ( TinySA.MODEL ) { + case 'BASIC': + if ( numOfSweepPoints >= TinySA.MIN_SWEEP_POINTS_BASIC && + numOfSweepPoints <= TinySA.MAX_SWEEP_POINTS_BASIC ) { + return true + } else { + return false + } + + case 'ULTRA': + if ( numOfSweepPoints >= TinySA.MIN_SWEEP_POINTS_ULTRA && + numOfSweepPoints <= TinySA.MAX_SWEEP_POINTS_ULTRA ) { + return true + } else { + return false + } + } + } + static isValidFreqConfig ( startFreq, stopFreq ) { switch (TinySA.MODEL) { case 'BASIC': @@ -328,7 +365,6 @@ class TinySA { } } - setHandler () { log.info ( `Setting handler for ${TinySA.NAME} data receiption ... ` ) const delimiterParser = this.port.pipe ( new DelimiterParser({ delimiter: 'ch> ' }) )