このサイトはMacを使って processing の動作や活用法を学びます。

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