21 モールス信号解析アプリ (Processing)
モールス信号ON/OFFのパルス幅をPICで測定し、USB経由でPCに送信するPIC回路や Ardiuinoプログラムを以前作成ました。
これらボードからの出力信号を、より直感的に全体像を把握し、細部の検討も可能なアプリケーションを以前Visual C# 2019 を使い作成しました。しかし、Visual C#ゆえWindous専用でMac環境では使用できませんでした。今回、このプログラムと同等の機能を持つプログラムを Precessingで作成します。Precessingを使用すれば、WindousでもMacでも使用できるアプリケーションになります。
図のようにで作成済みのCW練習機を接続してから、PCでモールス信号解析アプリプログラムを実行します。電鍵を叩くと、ウインドにデータが現れます。
データー集計表[3]で全体的な傾向を掴むことができます。信号の継続時間がグラフィカルに表示されている画面[5]からは、「長点の直後のスペース時間が長い傾向にある」とか「文字の最終マークの時間が短い傾向にある」など、普段は気が付きづらい癖が発見できます。
1 プログラム概要
このアプリケーションの操作概要は以下の通りです。
- 1 シリアルポート:
- このアプリと接続するシリアルポートを指定するプルダウンメニューです。実行開始時に接続されているシリアルポート一覧を取得して、その最終行のデバイスが選択されています。希望のデバイスでない場合には、プルダウンメニューを展開し選び直します。もし、プルダウンメニューに希望のデバイスがない場合は、実行開始時に、接続デバイスの取得エラーの可能性があるので、デバイスの接続を再確認してから、Processingスケッチを再実行してください。
- 2 通信速度 :
- スライドバーで信号解析の基準となる通信速度を指定します。JARLモールス電信技能認定制度に準拠した9段階の設定が可能です。段位名の他に字/分と基準となる短点の時間が ms 単位で表示されます。
- 3 データー集計表:
- モールス通信が終了したことを示す GAP(2秒以上の無信号状態)が検出された後に、データを分類し属性毎に集計され結果が表示されます。
- 4 集計単位:
- データー集計表で使用される単位を指定します。
短点を選んだ場合、基準となる短点時間を1単位として集計を行います。 - 5 図形表示画面:
- 受信データを図形として表示します。
マーク(電鍵を押している間)は0点より上に、スペース(電鍵接点が離れている間)は下に、その継続時間分の長さで表します。スペース時間の表示倍率は短点3倍の時間を超えると1/2の倍率にして描かれます。
2 接続可能な練習機
このアプリと接続するモールス信号練習機からは、定められたルールでデータが送られてくる必要があります。現在、このルールに従っている PICモールス信号練習機は以下の3種類があります。
-
PIC16F1619 使用練習機
- 作成記事 モールス信号のパルス幅を測定3
- Cliosity開発ボードとUSBシリアル変換アダプターを使っています。サイドトーンは正弦波で出力さえれます。Cliosity開発ボードを使用するためPICkitプログラマーは必要ありません。
- PIC18F14K50 使用練習機
- 作成記事 モールス信号のパルス幅を測定4
- 秋月電気で販売されている「PIC18F14K50使用USB対応超小型マイコンボード」を使います。必要となるハードウエアは、圧電スピーカー、電鍵だけです。ただし、PICkitプログラマーは必要です。
- PIC16F1549 使用練習機
- 作成記事 USB - モールス練習機
- 最新のUSB搭載PICです。PIC16F1549はクリスタル発振子が不必要なため構成部品数は最小です。ブレッドボードやユニバーサルボードで作成するのに最適です。MCCでUSBライブラリが使用できるのも大きな魅力です。PICkitプログラマーは必要です。
- Arduino UNO 使用練習機
- 作成記事 Arduino モールス通信練習機 5
- Arduino UNO を使用した練習機です。必要となるハードウエアは、圧電スピーカー、電鍵だけです。
3 スケッチ内容
この長くなりますが、スケッチを以下に掲載します。
/* CW アナライザー */
import processing.serial.*;
import controlP5.*;
import java.util.*;
// main state names
final int waitting = 0;
final int keyStart = 1;
final int keyDraw = 2;
final int keyIdle = 3;
final int dataDsp = 4;
final int gapIdle = 5;
final int reDraw = 6;
final int mkReDraw = 7;
final int spReDraw = 8;
// Key時間画描ウインドのサイズ
final int gwWidth = 350;
final int gwHight = 250;
final int gwTop = 100;
final int gwLeft = 40;
final int gwRight = gwLeft + gwWidth; // 390
final int xStart = 50; // 棒グラフの開始点
// データ解析ウインドのサイズ
final int dwTop = 110;
final int dwLeft = 410;
final int daTop = 130;
final int daLeft = 450;
final String noData = " --- --- ---\n";
final int[] vPos = {110,130,149,168,187,206};
boolean isUnitDot = false; // データ単位
// 速度スライダー
final String[] dan = {"名人","5段","4段","3段",
"2段","初段","1級","2級","3級"};
final int[] cpm = {180,160,140,120,110,90,60,45,25};
String strDan;
// 共通変数
List<String> portList; // ポートリスト
int dotMs = 100; // 基準短点時間
int state = waitting; // 開始ステート
int yRef = 200; // ゼロ基準線
int xPos = xStart; // 棒グラフ位置
int xInc = 0; // 棒グラフ移動量
int yVal = 0; // 棒グラフ長さ
int numKey = 0; // List index
Serial myPort;
ControlP5 cp5;
ScrollableList d1;
Slider s1;
IntList mkList,spList;
IntList list_1,list_2,list_3;
void setup() {
size(600, 400);
cp5 = new ControlP5(this);
PFont myFont = createFont("Monospaced",12);
ControlFont cf1 = new ControlFont(myFont);
textFont(myFont);
cp5.setFont(cf1);
portList = Arrays.asList(Serial.list());
// ScrollableList name, x, y, w, h
d1 = cp5.addScrollableList("dropdown",20,25,200,100)
.setBarHeight(20)
.setItemHeight(20)
.addItems(portList);
d1.setValue(portList.size()-1);
d1.getCaptionLabel()
.toUpperCase(false)
.getStyle().marginTop = 4;
d1.getValueLabel()
.toUpperCase(false)
.getStyle().marginTop = 4;
// Slider : name,min,max,初期値(float),x,y,w,h
s1 = cp5.addSlider("slider1",0,8,6,260,25,140,20);
s1.getValueLabel().setVisible(false);
s1.getCaptionLabel().setVisible(false);
s1.setNumberOfTickMarks(9)
.setSliderMode(Slider.FLEXIBLE);
// Toggle name, 初期値 (boolean), x, y, w, h
cp5.addToggle("toggle1",false,480,235,50,20)
.setMode(ControlP5.SWITCH)
.setCaptionLabel("");
mkList = new IntList(); //
spList = new IntList(); //
list_1 = new IntList(); //
list_2 = new IntList(); //
list_3 = new IntList(); //
}
void draw() {
switch(state){
case waitting:
drawWindow();
textSize(32);
text("Waitting", 150, 160);
break;
case keyStart:
drawWindow();
xPos = xStart;
state = keyIdle;
break;
case keyDraw:
if(xPos > gwRight)xPos = gwRight;
noStroke();
fill(0,200,0);
rect(xPos,yRef,3,yVal);
xPos += xInc;
state = keyIdle;
break;
case keyIdle:
break;
case dataDsp:
dspData();
state = gapIdle;
break;
case gapIdle:
break;
case reDraw:
drawWindow();
xPos = xStart;
numKey = 0;
noStroke();
fill(0,200,0);
state = mkReDraw;
break;
case mkReDraw:
yVal = mkList.get(numKey);
yVal = yVal*25/dotMs;
if(yVal>100) yVal = -100; // 枠から出ないよう
else yVal = -yVal; // 100で制限
if(xPos > gwRight)xPos = gwRight;
rect(xPos,yRef,3,yVal);
xPos += 2;
if(numKey < mkList.size()-1)
state = spReDraw;
else
state = dataDsp;
break;
case spReDraw:
yVal = spList.get(numKey);
yVal = yVal*25/dotMs;
if(yVal>225) // 9単位超は
yVal = 150; // 150で制限
else if(yVal>75) // 3単位超は
yVal = yVal/2+38; // 倍率1/2
// ウインド終端なら、その位置に留まる
if(xPos > gwRight)xPos = gwRight;
rect(xPos,yRef,3,yVal); // グラフを描く
if(yVal<=50) xPos += 2; // 2単位超えたら
else xPos += 4; // 文字間として広め
numKey++; // 次のデータ
state = mkReDraw;
break;
}
}
// ++++++++++++++++++++++++++++++++
// データ解析して表示
// ++++++++++++++++++++++++++++++++
void dspData(){
fill(0); // データ一覧表を消去
rect(440,118,120,100);
// mkListを処理
list_1.clear();
list_2.clear();
for(int i = 0;i<mkList.size();i++){
int val = mkList.get(i);
if(val < dotMs*2) list_1.append(val);
else list_2.append(val);
}
dspVals(list_1,vPos[1]);
dspVals(list_2,vPos[2]);
// spListを処理
list_1.clear();
list_2.clear();
list_3.clear();
for(int i = 0;i<spList.size();i++){
int val = spList.get(i);
if(val < dotMs*2) list_1.append(val);
else if(val < dotMs*5) list_2.append(val);
else list_3.append(val);
}
dspVals(list_1,vPos[3]);
dspVals(list_2,vPos[4]);
dspVals(list_3,vPos[5]);
}
// ++++++++++++++++++++++++++++++++
// データ一覧表にテータ記入
// ++++++++++++++++++++++++++++++++
void dspVals(IntList al,int vDsp){
String str;
if(al.size() > 0){
float max = al.max();
float min = al.min();
float ave = al.sum() / al.size();
if(isUnitDot){
max /= dotMs;
min /= dotMs;
ave /= dotMs;
str = String.format("%5.1f%5.1f%5.1f",ave,max,min);
}else{
str = String.format("%5d%5d%5d",(int)ave,(int)max,(int)min);
}
}else{
str = noData;
}
fill(255);
text(str, daLeft, vDsp);
}
// ++++++++++++++++++++++++++++++++
// 単位が変更された時
// ++++++++++++++++++++++++++++++++
void toggle1(boolean theFlag) {
isUnitDot = theFlag;
state = dataDsp;
}
// ++++++++++++++++++++++++++++++++
// 速度スライダーが変更された時
// ++++++++++++++++++++++++++++++++
void slider1(int n) {
dotMs = 6000/cpm[n];
strDan = String.format( "%s%6d%6d", dan[n],cpm[n], dotMs);
fill(0);
rect(430,25,130,20); // 古い記述を消し
fill(255);
text(strDan,430,40); // 新しい記述を描く
// 受信データがるなら再描画
if(state == gapIdle) state = reDraw;
}
// ++++++++++++++++++++++++++++++++
// シリアルポートdropdownが変更された時
// ++++++++++++++++++++++++++++++++
void dropdown(int n) {
// 既に接続されていれば、切断して
// 新しい選択項目は、index n で接続する
if ( myPort != null ) myPort.stop();
myPort = new Serial(this,portList.get(n),115200);
}
// ++++++++++++++++++++++++++++++++
// シリアル通信を受信した時
// ++++++++++++++++++++++++++++++++void drawWindow(){
void serialEvent(Serial port) {
// 受信データがあれば、IDE コンソールに出力
if ( port.available() > 0 ) {
String strCheck = "Combined";
String inString = port.readStringUntil('\n');
inString = trim(inString);
if ( inString != null ) {
// 長さ7なら有効なデータなのでdrawKeyingで処理
if(inString.length() == 7){
drawKeying(inString);
}
// Combinedを含んでいればモード変更
else if(inString.contains(strCheck)){
//println("Ready");
port.write("4"); // 分離表示モード'4' 送信
state = keyStart;
}
}
}
}
// +++++++++++++++++++++++++++++++++++++++++
// 長さ7の文字列を受けとり
// 先頭文字と値の文字列に分けて処理
// +++++++++++++++++++++++++++++++++++++++++
void drawKeying(String inString){
String headChar = inString.substring(0,1);
int inVal = int(trim(inString.substring(1)));
int val = inVal*25/dotMs;
// 先頭文字により処理が異なる
switch(headChar){
case "M": // 'M'の場合 ------------
if(state == gapIdle){ // Gap後最初の
mkList.clear(); // データなら
spList.clear(); // List.clear
}
mkList.append(inVal); // データ保管
if(val>100) yVal = -100; // 枠から出ないよう
else yVal = -val; // 100で制限
xInc = 2; // 横軸増分 +2
if(state == gapIdle){ // Gap後のデータなら
state = keyStart; // keyStartへ
}else{ // 通常は
state = keyDraw; // keyStartへ
}
break;
case "S": // 'S'の場合 ---------------
spList.append(inVal);
if(val<=75){ // 3単位までは
yVal = val; // そのまま
}else if(val<=225){ // 以降は
yVal = val/2+38; // 倍率1/2
}else{ // 枠から出ないよう
yVal = 150; // 150で制限
}
if(val<=50) xInc = 2; // 2単位超えたら
else xInc = 4; // 文字間として広め
state = keyDraw;
break;
case "G": // 'G'の場合 -----------
state = dataDsp; // データ解析へ
break;
}
}
// ++++++++++++++++++++++++++++++++
// 棒グラフ用ウインドを描く
// ++++++++++++++++++++++++++++++++void drawWindow(){
void drawWindow(){
background(0);
stroke(127);
fill(50);
rect(gwLeft,gwTop,gwWidth,gwHight);
line(gwLeft,gwTop+25,gwRight,gwTop+25);
line(gwLeft,gwTop+75,gwRight,gwTop+75);
line(gwLeft,gwTop+100,gwRight,gwTop+100);
line(gwLeft,gwTop+125,gwRight,gwTop+125);
line(gwLeft,gwTop+175,gwRight,gwTop+175);
line(gwLeft,gwTop+225,gwRight,gwTop+225);
fill(255);
textSize(12);
text("3", 25, 130);
text("1", 25, 180);
text("1", 25, 230);
text("3", 25, 280);
text("7", 25, 330);
text("シリアルポート",20,20);
text("段位ー速度",260,20);
text(strDan,430,40);
text("段位 字/分 短点(mS)",430,20);
text("単位:短点 mS",dwLeft,250);
String str = " Ave Max Min";
text(str, daLeft, dwTop);
line(dwLeft,dwTop+2,dwLeft+160, dwTop+2);
str = "Dot\nDash\nMark\nChar\nWord";
text(str, dwLeft, vPos[1]);
str = noData+noData+noData+noData+noData;
text(str,daLeft, vPos[1]);
}