const expectedProductName = "CNLohr lolra ch32v203 goertzel test";
const filter = { vendorId : 0x1209, productId : 0xd035 };
let dev = null;
let loopAbort = false;
const IQHistoryLen = 4096;
var IQHistoryArray = new Uint32Array(IQHistoryLen);
var MPHistoryArray = new Float32Array(IQHistoryLen*2);
var IQHistoryHead = 0|0;
var lastIntensity = 1.0;
var lastNumQ = 0;
var lastTotalTime = 1;
var lastTimeUsed = 1;
var graphIsClicked = false;
function graphClick( e )
{
if( e.type == "mousedown" ) graphIsClicked = true;
if( e.type == "mouseup" ) graphIsClicked = false;
return true;
}
function setStatus( msg )
{
document.getElementById( "STATUS" ).innerHTML = msg;
console.log( msg );
}
function setStatusError( msg )
{
setStatus( "" + msg + "" );
console.log( msg );
console.trace();
}
function tryConnect()
{
if( !navigator.hid )
{
return;
}
if( !dev )
{
navigator.hid.getDevices().then( (devices) =>
{
if( devices.length == 0 )
setStatusError( "No devices found. Open a device." );
else
devices.forEach( tryOpen );
});
}
}
async function closeDeviceTool()
{
loopAbort = false;
setStatusError( "Disconnected" );
dev = null;
}
var playingAudioProcessor = null;
var audioContext = null;
var targetModulation = 0;
var targetGain = 0.0;
function UpdateButtonNames()
{
document.getElementById( "ToggleAudioButton" ).value = ( targetGain > 0.0 ) ? "Stop Audio" : "Start Audio";
document.getElementById( "ToggleAudioModulationButton" ).value = ( targetModulation > 0.5 ) ? "FM" : "AM";
}
async function toggleAudioModulation()
{
if( playingAudioProcessor != null )
{
var newVal = 1 - targetModulation;
targetModulation = newVal;
}
UpdateButtonNames();
}
async function toggleAudio()
{
if( playingAudioProcessor == null )
{
var bypass = '\
class PlayingAudioProcessor extends AudioWorkletProcessor {\
static get parameterDescriptors() {\
return [\
{ name: "gain", defaultValue: 0, },\
{ name: "sampleAdvance", defaultValue: 0.5, },\
]\
};\
constructor() {\
super();\
this.rbuffer = new Float32Array(8192); \
this.rbufferhead = 0|0; \
this.rbuffertail = 0|0; \
this.sampleplace = 0.0; \
this.dcoffset = 0.0; \
this.totalsampcount = 0|0; \
\
this.port.onmessage = (e) => { \
for( var i = 0|0; i < e.data.length|0; i++ ) \
{ \
let n = (this.rbufferhead + (1|0))%(8192|0); \
if( n == this.rbuffertail ) \
{ \
this.rbuffertail = (this.rbuffertail + (1|0))%(8192|0); \
/*console.log( `Overflow` ); */ \
} \
var vv = e.data[i]; \
this.dcoffset = this.dcoffset * 0.995 + vv * 0.005; \
this.rbuffer[this.rbufferhead] = vv - this.dcoffset; \
this.rbufferhead = n; \
} \
}; \
}\
\
process(inputs, outputs, parameters) {\
/*console.log( parameters.gain[0] );*/ \
/*console.log( this.ingestData );*/ \
let len = outputs[0][0].length; \
const sa = Math.fround( parameters.sampleAdvance[0] ); /*float*/ \
var s = Math.fround( this.sampleplace ); /*float*/ \
var tail = this.rbuffertail | 0; /* int*/ \
var tailnext = this.rbuffertail | 0; /* int*/ \
if( tail == this.rbufferhead ) { /*console.log( "Underflow " );*/ return true; }\
var tsamp = Math.fround( this.rbuffer[tail] ); \
var nsamp = Math.fround( this.rbuffer[tailnext] ); \
this.totalsampcount += len|0; \
for (let b = 0|0; b < len|0; b++) { \
s += sa; \
var excess = Math.floor( s ) | 0; \
if( excess > 0 ) \
{ \
s -= excess; \
tail = ( tail + (excess|0) ) % (8192|0); \
tailnext = ( tail + (1|0) ) % (8192|0); \
if( tail == this.rbufferhead ) { /* console.log( "Underflow" ); */ break; } \
tsamp = Math.fround( this.rbuffer[tail] ); \
nsamp = Math.fround( this.rbuffer[tailnext] ); \
} \
var valv = tsamp * (1.0-s) + nsamp * s; \
outputs[0][0][b] = valv*parameters.gain[0]; \
} \
/*console.log( tail + " " + this.rbuffertail + " " + tsamp + " " + nsamp );*/ \
this.rbuffertail = tail; \
this.sampleplace = s; \
return true; \
} \
} \
\
registerProcessor("playing-audio-processor", PlayingAudioProcessor);';
// The following mechanism does not work on Chrome.
// const dataURI = URL.createObjectURL( new Blob([bypass], { type: 'text/javascript', } ) );
// Extremely tricky trick to side-step local file:// CORS issues.
// https://stackoverflow.com/a/67125196/2926815
// https://stackoverflow.com/a/72180421/2926815
let blob = new Blob([bypass], {type: 'application/javascript'});
let reader = new FileReader();
await reader.readAsDataURL(blob);
let dataURI = await new Promise((res) => {
reader.onloadend = function () {
res(reader.result);
}
});
audioContext = new AudioContext();
await audioContext.audioWorklet.addModule(dataURI);
playingAudioProcessor = new AudioWorkletNode(
audioContext,
"playing-audio-processor"
);
playingAudioProcessor.connect(audioContext.destination);
audioContext.resume();
let gainParam = playingAudioProcessor.parameters.get( "gain" );
gainParam.setValueAtTime( 0, audioContext.currentTime );
}
var newVal = 1.0 - targetGain;
console.log( "Setting gain to: " + newVal );
let gainParam = playingAudioProcessor.parameters.get("gain");
gainParam.setValueAtTime( newVal, audioContext.currentTime);
targetGain = newVal;
document.getElementById( "ToggleAudioButton" ).value = ( newVal > 0.5 ) ? "Stop Audio" : "Start Audio";
UpdateButtonNames();
}
function onLoadWebHidControl()
{
liveGraph = document.getElementById( "LiveGraph" );
liveGraph.addEventListener( "mousedown", graphClick );
liveGraph.addEventListener( "mouseup", graphClick );
UpdateButtonNames();
setTimeout( sendLoop, 1 );
if( !navigator.hid )
{
setStatusError( "Browser does not support HID." );
document.getElementById( "connectButton" ).hidden = true;
}
else
{
navigator.hid.addEventListener("disconnect", (event) => { if( event.device.productName == expectedProductName ) closeDeviceTool(); } );
}
setTimeout( () => { elapsedOK = true; }, 3000 );
}
function reqConnect()
{
loopAbort = true;
const initialization = navigator.hid.requestDevice( { filters: [ filter ] } );
initialization.then( gotUSBDevice );
initialization.catch( setStatusError );
}
function gotUSBDevice(result)
{
if( result.length < 1 )
{
setStatusError( "Error: No devices found" );
return;
}
if( result[0].productName != expectedProductName )
{
setStatusError( "Error: Wrong device name. Got " + result[0].productName + " Expected " + expectedProductName );
return;
}
const thisDev = result[0];
tryOpen( thisDev );
}
function tryOpen( thisDev )
{
thisDev.open().then( ( result ) => {
if( result === undefined )
{
if( dev ) dev.close();
loopAbort = false;
dev = thisDev;
setStatus( "Connected." );
}
else
{
setStatusError( "Error: Could not open; " + result );
}
} ).catch( (e) => setStatusError( "Error: Could not open; " + e ) );
}
let sendReport = null;
let receiveReport = null;
async function sendLoopError( e )
{
console.log( "SEND LOOP ERROR" );
sendReport = null;
receiveReport = null;
if( dev ) await dev.close();
dev = null;
setStatusError( e );
}
function updateWebHidDeviceWithParameters( paramlist )
{
var i = 0|0;
var arraySend = new Uint8Array(63);
for( var i = 0|0; i < paramlist.length|0; i++ )
{
var vv = paramlist[i] | 0;
arraySend[i*4+7] = (vv>>0)&0xff;
arraySend[i*4+8] = (vv>>8)&0xff;
arraySend[i*4+9] = (vv>>16)&0xff;
arraySend[i*4+10] = (vv>>24)&0xff;
}
arraySend[3] = paramlist.length | 0;
sendReport = dev.sendFeatureReport( 0xAC, arraySend ).catch( sendLoopError );
if( !sendReport ) sendLoopError( "error creating sendFeatureReport" );
}
FMiirphase = 0.0; /* for FM */
FMlastphase = 0.0; /* for FM */
FMphaseout = 0.0; /* for FM */
lastadc = 0|0;
remote_clock_mhz = 0;
remote_clock_last_timestamp = 0|0;
remote_clock_last_timems = 0.0;
remote_clock_initted = false;
remote_clock_refinement = 1.0;
remote_clock_total_s = 0;
remote_clock_total_ticks = 0.0;
function ComputeRemoteClock( remote_time_ticks, now_ms )
{
if( !remote_clock_initted )
{
remote_clock_last_timestamp = remote_time_ticks;
remote_clock_last_timems = now_ms;
remote_clock_refinement = 1.0;
remote_clock_initted = true;
remote_clock_total_ms = 0;
remote_clock_total_ticks = 0|0;
return;
}
var delta_s = (now_ms - remote_clock_last_timems)/1000.0;
var delta_clock = ((remote_time_ticks - remote_clock_last_timestamp)|0)*2; // convert 144MHz to 288MHz
var this_mhz = delta_clock / delta_s;
remote_clock_total_s += delta_s;
remote_clock_total_ticks += delta_clock * 1.0;
remote_clock_mhz = remote_clock_total_ticks / remote_clock_total_s;
remote_clock_total_s *= 0.99999;
remote_clock_total_ticks *= 0.99999;
remote_clock_last_timestamp = remote_time_ticks;
remote_clock_last_timems = now_ms;
}
async function sendLoop()
{
const sleep = ms => new Promise(r => setTimeout(r, ms));
var frameNo = 0|0;
var lastTime = performance.now();
let goodCount = 0;
let badCount = 0;
let kBsecAvg = 0;
let xActionSecAvg = 0;
while( true )
{
if( dev && dev !== null && !loopAbort )
{
receiveReport = dev.receiveFeatureReport( 0xAD ).catch( sendLoopError );
if( !receiveReport ) sendLoopError( "error creating receiveReport" );
frameNo++
const updateStatsPerfPer = 4;
if( frameNo % updateStatsPerfPer == 0 )
{
let thisTime = performance.now();
let deltaTime = thisTime - lastTime;
let kBsec = (255*1000/1024*updateStatsPerfPer)/(deltaTime);
let xActionSec = (2*updateStatsPerfPer*1000)/(deltaTime);
kBsecAvg = kBsecAvg * 0.9 + kBsec * 0.1;
xActionSecAvg = xActionSecAvg * 0.9 + xActionSec * 0.1;
document.getElementById( "StatusPerf" ).innerHTML =
(kBsecAvg).toFixed(2) + " kB/s
" +
(xActionSecAvg).toFixed(2) + "x/s
";
document.getElementById( "GeneralData" ).innerHTML =
"
| Count: " + goodCount + " / " + badCount + " | " + "Inten: " + ((Math.log( lastIntensity * lastIntensity )/Math.log(10)) * 10-120).toFixed(2) + "db (" + lastIntensity + ") | " + "ADCs: " + (lastadc>>16).toFixed(0) + " / " + (lastadc&0xffff).toFixed(0) + " | " + "Remote clock: MHz ` + " | " + "" + ((remote_clock_mhz-288000000)/288).toFixed(3) + " PPM | " + "