Visit my GitHub repository, https://github.com/jh1ood/WebSDR, for the source codes.
% wc * 6 5 177 README.md 314 1285 10123 index.html 177 683 5216 index.js 572 2016 19648 sprig_audio.c 1069 3989 35164 total
Ham Radio Blog
Visit my GitHub repository, https://github.com/jh1ood/WebSDR, for the source codes.
% wc * 6 5 177 README.md 314 1285 10123 index.html 177 683 5216 index.js 572 2016 19648 sprig_audio.c 1069 3989 35164 total
The program index.js works as an HTTP server, and first send a file index.html to your Web browser when a connection is requested.
index.js
16 var http = require('http'); 17 var fs = require('fs'); 18 var index = fs.readFileSync(__dirname + '/index.html'); 19 var app = http.createServer(function(req, res) { 20 res.writeHead(200, {'Content-Type' : 'text/html'}); 21 res.end(index); 22 }) 23 .listen(3000);
The file index.html contains a JavaScript code for displaying the waterfall image, and for receiving and playing the audio signals.
index.html
<!doctype html> <html> <head> <style> * { margin: 0; padding: 0; box-sizing : border-box; } body { font: 13px Helvetica, Arial; } form { background: # 000; padding: 3px; position: fixed; bottom: 0; width: 100% ; } form input { border: 0; padding: 10px; width: 80% ; margin-right : .5% ; } form button { width: 15% ; background: rgb(143, 188, 143); border: none; padding: 10px; } #messages{list-style-type : none; margin : 0; padding : 0; } #messages li{padding : 5px 10px; } #messages li : nth-child(odd){background : #eee; } #messages {margin-bottom : 40px } #messages {font-size : 2em } #btn1 {position : fixed; top : 10px; left : 10px; font-size : 2em; background-color: darkseagreen } #btn2 {position : fixed; top : 10px; left : 60px; font-size : 2em; background-color: darkseagreen } #btn3 {position : fixed; top : 10px; left : 150px; font-size : 2em; background-color: cornsilk } #btn4 {position : fixed; top : 10px; left : 230px; font-size : 2em; background-color: cornsilk } #btn5 {position : fixed; top : 10px; left : 310px; font-size : 2em; background-color: paleturquoise } #btn6 {position : fixed; top : 10px; left : 390px; font-size : 2em; background-color: paleturquoise } #btn7 {position : fixed; top : 10px; left : 470px; font-size : 2em; background-color: thistle } #btn8 {position : fixed; top : 10px; left : 550px; font-size : 2em; background-color: thistle } #mycanvas0 {position : fixed; top : 70px; left : 10px; font-size : 2em } #mycanvas1 {position : fixed; top : 90px; left : 10px; font-size : 2em } #mycanvas2 {position : fixed; top : 620px; left : 10px; font-size : 2em } </style> </head> <body> <script src="https://code.jquery.com/jquery-1.11.1.js"></script> <script src="https://code.createjs.com/createjs-2015.11.26.min.js"></script> <script src='/socket.io/socket.io.js'></script> <button id="btn1"><font size="5">CQ</font></button> <button id="btn2"><font size="5">QRZ?</font></button> <button id="btn3"><font size="5">VFO+</font></button> <button id="btn4"><font size="5">VFO-</font></button> <button id="btn5"><font size="5">BFO+</font></button> <button id="btn6"><font size="5">BFO-</font></button> <button id="btn7"><font size="5">BFO+</font></button> <button id="btn8"><font size="5">BFO-</font></button> <div id="messages"></div> <form action=""> <input id="m" autocomplete="off"/><button>Send</button> </form> <canvas id="mycanvas0" width="512" height= "20" style="background:white;"></canvas> <canvas id="mycanvas1" width="512" height="512" style="background:white;"></canvas> <canvas id="mycanvas2" width="280" height= "60" style="background:cornsilk;"></canvas> <script> function colormap(charcode) { // 0x00~0xff var tmp = charcode / 255.0; // 0.0~1.0 var val; var r, g, b; if (tmp < 0.50) { r = 0.0; } else if (tmp > 0.75) { r = 1.0; } else { r = 4.0 * tmp - 2.0; } if (tmp < 0.25) { g = 4.0 * tmp; } else if (tmp > 0.75) { g = -4.0 * tmp + 4.0; } else { g = 1.0; } if (tmp < 0.25) { b = 1.0; } else if (tmp > 0.50) { b = 0.0; } else { b = -4.0 * tmp + 2.0; } rgb[1] = 255.0 * g; rgb[0] = 255.0 * r; rgb[2] = 255.0 * b; } function waterFall(myarray) { ctx1.putImageData(imgData1, 0, 1); imgData1 = ctx1.getImageData(0, 0, 512, 512); for (j = 0; j < 512; j++) { colormap(myarray[j]); imgData1.data[0 + j * 4] = rgb[0]; imgData1.data[1 + j * 4] = rgb[1]; imgData1.data[2 + j * 4] = rgb[2]; imgData1.data[3 + j * 4] = 255; } } function showBpf(ctx0, imgData0, bfo) { var cwmarker0 = Math.floor( bfo[0] / (16000 / 2048) ); var cwmarker1 = Math.floor( bfo[1] / (16000 / 2048) ); console.log('bfo = ', bfo); for (i = 0; i < 20; i++) { for (j = 0; j < 512; j++) { var r, g, b, a; if(j == cwmarker0 || j == cwmarker1) { r = 0; g = 0; b = 0; } else if ( j >= cwmarker0-20 && j <= cwmarker0+20 && i > 10) { r = 135; g = 206; b= 235; } else if ( j >= cwmarker1-20 && j <= cwmarker1+20 && i > 10) { r = 216; g = 191; b= 216; } else { r = 255; g = 255; b= 255; } imgData0.data[0 + j * 4 + i * imgData0.width * 4] = r; imgData0.data[1 + j * 4 + i * imgData0.width * 4] = g; imgData0.data[2 + j * 4 + i * imgData0.width * 4] = b; imgData0.data[3 + j * 4 + i * imgData0.width * 4] = 255; } } ctx0.putImageData(imgData0, 0, 0); } function onClick(e) { var rect = e.target.getBoundingClientRect(); var mx = e.clientX - rect.left; var my = e.clientY - rect.top; console.log('mouse clicked: ', mx, my); socket.emit('message77', mx); } function playAudioStream(ctx, audio_f32) { var audio_buf = ctx.createBuffer(1, audio_f32.length, 16000); var audio_src = ctx.createBufferSource(); var current_time = ctx.currentTime; audio_buf.getChannelData(0).set(audio_f32); audio_src.buffer = audio_buf; audio_src.connect(ctx.destination); if (current_time < scheduled_time) { audio_src.start(scheduled_time); scheduled_time += audio_buf.duration; } else { audio_src.start(current_time); scheduled_time = current_time + audio_buf.duration; } } // main var socket = io(); var myarray = new Array(); var canvas0, canvas1, canvas2; var ctx0, ctx1; var imgData0, imgData1; var rgb = new Array(3); canvas0 = document.getElementById('mycanvas0'); canvas1 = document.getElementById('mycanvas1'); ctx0 = canvas0.getContext('2d'); ctx1 = canvas1.getContext('2d'); canvas1.addEventListener('click', onClick, false); imgData0 = ctx0.createImageData(512, 20); imgData1 = ctx1.createImageData(512, 512); var stage = new createjs.Stage("mycanvas2"); var t = new createjs.Text("IC-7410", "26px serif", "DarkRed"); t.x = 10; t.y = 10; stage.addChild(t); stage.update(); var bfo = [1000.0, 2000.0]; showBpf(ctx0, imgData0, bfo); for (i = 0; i < 512; i++) { for (j = 0; j < 512; j++) { imgData1.data[0 + j * 4 + i * imgData1.width * 4] = j % 256; imgData1.data[1 + j * 4 + i * imgData1.width * 4] = i % 256; imgData1.data[2 + j * 4 + i * imgData1.width * 4] = 128; imgData1.data[3 + j * 4 + i * imgData1.width * 4] = 255; } } ctx1.putImageData(imgData1, 0, 0); var audioCtx = new AudioContext; var scheduled_time = 0; socket.on('your message', function(msg) { $('#messages').append($('<div>').text(msg)); window.scrollTo(0, document.body.scrollHeight); }); socket.on('waterfall', function(data) { waterFall(data); }); socket.on('sound' , function(data) { playAudioStream(audioCtx, new Float32Array(data)); }); socket.on('freqmsg', function(msg) { t.text = msg; stage.update(); }); $('#btn1').click(function() { socket.emit('message1'); console.log('message1'); }); $('#btn2').click(function() { socket.emit('message2'); console.log('message2'); }); $('#btn3').click(function() { socket.emit('message3'); console.log('message3'); }); $('#btn4').click(function() { socket.emit('message4'); console.log('message4'); }); $('#btn5').click(function() { socket.emit('message5'); bfo[0]+=100; showBpf(ctx0, imgData0, bfo);}); $('#btn6').click(function() { socket.emit('message6'); bfo[0]-=100; showBpf(ctx0, imgData0, bfo);}); $('#btn7').click(function() { socket.emit('message7'); bfo[1]+=100; showBpf(ctx0, imgData0, bfo);}); $('#btn8').click(function() { socket.emit('message8'); bfo[1]-=100; showBpf(ctx0, imgData0, bfo);}); $('form').submit(function() { socket.emit('your message', $('#m').val()); $('#m').val(''); return false; }); </script> </body> </html>
Node.js is a server-side run-time environment for executing JavaScript code.
We have two programs running on a Raspberry PI, and one of the two is index.js, a JavasSript program that communicates with your IC-7410 and your Web browser.
// file name = index.js // (c) 2017 JH1OOD/Mike const bufa = new Buffer('fefe80e017' , 'hex'); // preamble const bufz = new Buffer('fd' , 'hex'); // postamble const buf1 = new Buffer('fefe80e0174351fd' , 'hex'); // CQ const buf2 = new Buffer('fefe80e01751525a3ffd' , 'hex'); // QRZ? const buf3 = new Buffer('fefe80e003fd' , 'hex'); // read freq var buf4 = new Buffer('fefe80e0050000000000fd', 'hex'); // send freq var SerialPort = require('serialport'); var serial = new SerialPort( '/dev/ttyUSB0', {baudrate : 19200, parser : SerialPort.parsers.byteDelimiter([ 0xfd ])}); var http = require('http'); var fs = require('fs'); var index = fs.readFileSync(__dirname + '/index.html'); var app = http.createServer(function(req, res) { res.writeHead(200, {'Content-Type' : 'text/html'}); res.end(index); }) .listen(3000); var io = require('socket.io').listen(app); var freqHz = 0; // VFO-A frequency var ndata = 512 + 2048; // waterfall_data[512] + sound_data[2048] var pos = 0; var myarray = new Array(); var mywater = new Array(); var mysound = new Array(); // -- water fall and Sound -- process.stdin.on('readable', function() { // from spinor_audio var buf = process.stdin.read(); if (buf !== null) { for(var i=0;i<buf.length;i++) { myarray[pos++] = buf[i]; if (pos == ndata) { for(var j=0;j<512;j++) { // waterfall_data mywater[j] = myarray[j]; } for(var j=0;j<2048;j++) { // sound_data var tmp = myarray[j+512]; if (tmp>128) tmp -=256; mysound [j] = tmp / 128.0; // signed char to -1.0~+1.0 } io.emit('waterfall', mywater); io.emit('sound' , mysound); pos = 0; } } } }); // -- serial for IC-7410 -- serial.on('open', function() { console.error('serial port /dev/ttyUSB0 is opened.'); }); serial.on('data', function(data) { if (!(data[0] == 0xfe & data[1] == 0xfe)) { console.error('-- received serial data format error'); } if (data[2] == 0xe0 & data[3] == 0x80 & data[4] == 0x03) { var f10 = data[5] >> 4 & 0x0f; var f1 = data[5] & 0x0f; var f1k = data[6] >> 4 & 0x0f; var f100 = data[6] & 0x0f; var f100k = data[7] >> 4 & 0x0f; var f10k = data[7] & 0x0f; var f10m = data[8] >> 4 & 0x0f; var f1m = data[8] & 0x0f; var freq = f10m.toString() + f1m.toString() + "," + f100k.toString() + f10k.toString() + f1k.toString() + "." + f100.toString() + f10.toString() + f1 + " kHz"; freqHz = f10m * 10000000 + f1m * 1000000 + f100k * 100000 + f10k * 10000 + f1k * 1000 + f100 * 100 + f10 * 10 + f1; io.emit('freqmsg', 'VFO A: ' + freq); } }); serial.on('error', function(err) { console.error('Error: ', err.message); }) // -- set frequency -- function setfreq(f) { var f10m = Math.floor(f / 10000000); f -= f10m * 10000000; var f1m = Math.floor(f / 1000000); f -= f1m * 1000000; var f100k = Math.floor(f / 100000); f -= f100k * 100000; var f10k = Math.floor(f / 10000); f -= f10k * 10000; var f1k = Math.floor(f / 1000); f -= f1k * 1000; var f100 = Math.floor(f / 100); f -= f100 * 100; var f10 = Math.floor(f / 10); f -= f10 * 10; var f1 = Math.floor(f / 1); var data = new Array( [ 0xfe, 0xfe, 0x80, 0xe0, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd ]); buf4[5] = f10 << 4 | f1; buf4[6] = f1k << 4 | f100; buf4[7] = f100k << 4 | f10k; buf4[8] = f10m << 4 | f1m; buf4[9] = 0; serial.write(buf4); } // -- WebSocket -- io.on('connection', function(socket) { socket.on('message1', function() { serial.write(buf1); }); socket.on('message2', function() { serial.write(buf2); }); socket.on('message3', function() { var newfreq = freqHz + 2000; setfreq(newfreq); }); socket.on('message4', function() { var newfreq = freqHz - 2000; setfreq(newfreq); }); socket.on('message5', function() { console.log('0'); // should be stdout to reach sprig_audio }); socket.on('message6', function() { console.log('1'); // should be stdout to reach sprig_audio }); socket.on('message7', function() { console.log('2'); // should be stdout to reach sprig_audio }); socket.on('message8', function() { console.log('3'); // should be stdout to reach sprig_audio }); var cwpitch = 650.0; socket.on('message77', function(mx) { var newfreq = freqHz - (16000/2048 * mx - cwpitch); setfreq(newfreq); }); socket.on('your message', function(msg) { serial.write(bufa); serial.write(msg ); serial.write(bufz); io.emit('your message', msg); }); }); // -- request freq -- function sendTime() { serial.write(buf3); // console.error('Freq asked..'); } setInterval(sendTime, 100); // -- EOF --
You can select the signal by changing the BFO frequency and listen to it with your favourite pitch. In other words, the envelop and the phase of a pure tone at CW pitch is modulated with a desired signal down converted close to DC.
An artificial signal, shown as a sinusoid in the waterfall image, is generated in a program, and can be heard on your left and right audio channels alternatively.