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