Raspberry PiでWebSDR(4)

index.jsというプログラムはHTTPサーバとして動作し、 接続が要求された時にあなたのWebブラウザにindex.htmlファイルを送ります。

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);

index.htmlファイルは、ウォーターフォール画面を表示したりオーディオ信号を受信して再生するためのJavaScriptのコードを含んでいます。

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>

Raspberry PiでWebSDR(3)

Node.jsはJavaScriptプログラムのサーバーサイドでの実行環境です。

私たちはRaspberry PIの上で2つのプログラムを走らせますが、そのうちの1つがindex.jsです。これは、あなたのIC-7410とあなたのウェブブラウザの両方と通信を行います。

// 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 --

Raspberry PiでWebSDR(2)

デュアルウォッチ機能を、より単純で対称的な方法で実装してみました。

あなたは、2つのBFO周波数を独立して制御することができます。CWピッチは、左右のチャンネルに対してそれぞれ400Hzと700Hzとに設定されているので、モノラルなオーディオシステムでも信号を区別することができます。

Raspberry PiでSDR

あなたはBFOの周波数を変えることにより信号を選択することができ、また、それをあなたの好きなCWピッチで聞くことができます。言い換えれば、CWピッチのピュアトーンが、直流付近にダウンコンバートされた希望信号で、振幅と位相とが変調されています。

ウォーターフォール画面上では正弦波上に見えている人工的な信号が加えられています。そして、それはあなたの左及び右のオーディオチャンネルから交互に聞こえてきます。