chibimameのアイコン画像
chibimame 2025年01月28日作成 © MIT
製作品 製作品 閲覧数 66
chibimame 2025年01月28日作成 © MIT 製作品 製作品 閲覧数 66

WiFiを使用したトランシーバ遠隔操作(仮称:TRXterm)

概要

本製作品(以降、TRXtermと呼びます)は、アマチュア無線のトランシーバに接続し、WiFiで離れた場所のPCからトランシーバの操作、音声の送受を行えるようにするものです。

キャプションを入力できます

以下の図のようにアマチュア無線のトランシーバに接続されたTRXtermは、WiFiでPCと接続され、PCから遠隔でトランシーバを操作します。これにより、以下の2通りの運用が可能になります。

運用形態1

シャック(無線機のある部屋)にある無線機を別の部屋にあるPCからアクセスすることができます。

運用形態2

トランシーバを屋外にあるアンテナの直下に置き、WiFiで接続された室内のPCから運用を行えるようになります。

キャプションを入力できます

1の運用形態では、リビングなどから暖かいこたつに入りながら、無線を行うことができます。
2の運用形態では、暖かい部屋からのアクセスに加えて、屋外のアンテナから室内のトランシーバへの同軸ケーブルの引き込みが不要となります。同軸ケーブルを使用しないことから電波の損失を軽減することに加え、同軸ケーブルの引き回しがなくなります。
通常、同軸ケーブルは、電波の損失を軽減させるため、より太いケーブルが使用されますが、その分、ケーブルが固く、加えて重くなります。結果として室内への引き込みが難しくなります。また、室内への同軸ケーブルの引回しに壁の穴あけが必要になるなど課題があります。このシステムは、トランシーバの近くにWiFi操作のできるSPRESENSEを主体としたTRXtermを置き、WiFiでPCと接続を可能にすることにより、屋外でアンテナとトランシーバを直結でき、同軸ケーブルが不要となります。

技術的には、WiFiを使用して以下の機能が必要になります。

  1. 音声信号のやり取り
    受信の際は、トランシーバからの音声をPCで発生させます。また、送信の際は、PCのマイクの音をトランシーバ側へ送信します。
  2. トランシーバ操作のコマンドの送受
    通常、トランシーバは、バンドや周波数を変えるため、RS232Cなどのコマンドインターフェースを持っています。(このインターフェースは、八重洲無線のFT-817の場合、CATと呼ばれます。)このCATコマンドをPCから送り、TRXtermで受けてトランシーバに対してコマンドを中継します。

部品

名称 機能 型番 備考
本体 SPRESENSE™ メインボード CXD5602PWBMAIN1 モニター提供品
拡張基板 SPRESENSE™ 拡張ボード CXD5602PWBEXT1 モニター提供品
WiFiモジュール SPRESENSE用Wi-Fi add-onボード THOUSANDIY-005
ケース 透明ケース
コネクタ CAT接続用 MINIDIN8
トランーバマイク、スピーカ接続コネクタ 音声接続用 3極オーディオジャック
トランシーバ接続ケーブル CAT接続用 自作
トランーバマイク、スピーカ接続ケーブル 自作

回路図

SPRESENSEメインボードと拡張ボードの構成にWiFi基板を実装し、音声ジャックとCAT用端子を追加してケースに入れました。電子工作としては、おこがましいのですが、ジャックとコネクタへのはんだ付けとケース収容が中心です。

  • 音声は、3極オーディオ端子から拡張基板のHP(ヘッドフォーン)とMIC A端子に接続されます。
  • トランシーバのCAT操作は、MINI DIN8極コネクタのRX D、TX D端子が拡張基板のJP13 D02、D03端子へピンヘッダーを介して接続され、MINI DIN8極コネクタのGND端子がJP3のグランド端子に接続されています。
  • WiFi基板は、メインボードの端子にコネクタ接続します。

キャプションを入力できます

蓋をあけた時の写真を以下に示します。拡張ボードにメインボードが接続され、メインボードにWiFi add-onボードが載っています。拡張ボードのピンソケットに外部から接続するオーディオジャックとMINI DINコネクタが接続されています。

キャプションを入力できます

プログラム構成

大きく音声機能とWiFi機能に分かれます。ArdiunoライブラリAudio voice_effectorを参考にして、音声機能にWiFi機能を追加し、メインコアだけのシングルコア構成にしました。
しかし、この構成では、WiFiの音声パケット送信にばらつきや転送に時間がかかるため、ArdiunoライブラリSignal ProcessingのVoiceChangerを参考にメインコアに音声機能、サブコア1にWiFi機能を実装して、マルチコア構成にしました。
ここでは、マルチコア構成でTRXtermのプログラム構成を説明します。プログラム構成図を以下に示します。
ArdiunoライブラリVoiceChangerでは、アナログマイク入力から音声をPCM変換して、このPCMデータをVoiceChanger変換して、ヘッドフォン出力に出しています。このマイク入力の音声データをWiFiでPCに送り、WiFiを介してPCから送られる音声データをヘッドフォン出力に出すようにしました。VoiceChangerでは、これをマルチコアで構成しており、サブコアで行っているVoiceChanger処理(FFT処理)をWiFiの送受信処理に置き換えました。メインコアとサブコア間のデータのやり取りに負荷がかかりますが、アマチュア無線の交信は、相手が送信の時は、こちらは、受信、相手が受信の時は、送信と片方向の送受になりますので、同時に双方向にデータを送ることはありませんので負荷は片方向のやり取りだけになります。
キャプションを入力できます

1. 音声部分

アマチュア無線の帯域制限で、音声は3KHzと定められています。サンプリング周波数は、扱う周波数の倍あれば、元の音声に戻せますので6KHz以上ということになります。WiFiでパケットとして送受するため、サンプリング周波数8KHz、1回の転送数を160フレームで考えると、サンプリング間隔は、1/8KHz=0.125msになり、160フレームを1パケットで送るには、20ms(0.125ms×160フレーム)間隔で送ることになります。
しかし、SPRESENSEのPCMサンプリングは、最低16KHz、最小フレーム数240となっており、240フレームを1/16KHz=0.0625msで15ms(0.0625ms×240フレーム)間隔となります。
15ms間隔で240フレームを転送することになり、時間的には、かなり厳しい状況です。加えて、16ビットデータでは、1フレームに2バイト必要になり、240フレームは、480バイトのデータ転送になります。

2. WiFi部分

WiFi add-onボードは、ESP-WROOM-02(技適取得済)を搭載しており、ソフトウェアは、ESP8266ライブラリとしてMITライセンスの範囲で使用できます。ESP8266のプログラムを参考にPCとUDP通信を行うようにしました。当初、SPRESENSE側をサーバーにして、音声はUDP、トランシーバ操作のコマンドは、TCPと考えていましたが、使用したWiFi add-onボードでは、WiFi通信に以下の制限がありました。
ー サーバー設定にするとマルチコネクションができない。
ー トランスペアレント転送は、TCPに限られる。
リアルタイムに近い音声データ転送を送達確認があるTCPでは遅くなるため、最終的に音声、トランシーバ操作ともUDPで行い、UDPのシングルコネクションで、SPRESENSE側をクライアントとして構成することにしました。ただ、UDPでは、トランスペアレント転送ができないことから、1バイトのデータを転送する場合、2バイトのキャラクタ数字に置き換える必要があります。結果として、当初1フレーム2バイトと考えていた240フレームのデータは、480バイトとなり、キャラクタ変換で960バイトとなってしまいます。
そこで、1フレーム16ビット音声データを8ビットの1バイトに圧縮して240バイトとし、これをキャラクタ変換することにより、2倍の480バイトにしました。この圧縮は、標準的に使用されているITU-T G711で圧縮、伸長します。

3. トランシーバ操作のコマンドの送受

トランシーバ操作は、UARTのインターフェースと同じですが、メインボードを拡張ボードに載せた構成では、UARTは、二つあります。この二つのうち1つは、標準のSerialとしてUSBインターフェースとして接続されます。残るUARTのSerial2は、今回使用したWiFI add-onボードで使用されています。二つのUARTが使用されているため、トランシーバ操作のCATインターフェースは、デジタルピンを使用することにしました。UARTとして入出力をするための調歩同期は、プログラムでデジタルピンを変化させることでトランシーバを操作することにしました。
なお、WiFiで送られてくる音声信号とトランシーバの操作の信号は、パケットのサイズで判別することにしました。上記、音声信号は、100バイト以上は、必ずあります。また、トランシーバの操作コマンドは、長くても20バイトくらいで、100バイトを境に音声データとトランシーバの操作コマンドを判別するようにしました。

WiFiパケットモニター

Wire SharkというWiFi信号をモニタするアプリでとったものを以下に示します。IPアドレス192.168.0.221がPC、192.168.0.200がTRXtermです。
※注:試験中のモニターで1フレーム240バイトになっています。
左側のNo欄の表示で、No319(Time 17:54:52.779422)にPCからPOWER ONの信号がTRXtermに送信されます。
これでPCは、受信状態になりますのでNo339(Time 17:54:55.059963)以降音声パケットがTRXtermからPCへ連続的に送られます。
続けてPCの操作でPTT(Push To Talk:トランシーバの送信、受信を切り替えるスイッチ)をONにする信号が、No661(Time 17:55:04.396641)にTRXtermに送られると今度は、No467(Time 17:55:04.404835)以降、音声パケットがPCからTRXtermに連続的に送られています。
状態遷移では、電源オン→受信状態→PTTオンで送信状態となります。転送データ上も電源オンで、PCへの音声パケット送信開始、PTTオンで音声パケットの送信方向が変わることになります。
音声パケットの送信間隔は、モニタ表示の時間を見るとTRXtermからPCへは、30~60msに対し、PCからTRXtermへは、だいたい10ms間隔になっています。音声パケットの送信タイミングは、PC、TRXtermともオーディオ処理の240フレームを検知したときにパケットとして送信していますが、TRXtermからの送信は、30~60msとバラつきの幅が大きいのが課題です。加えて、当初想定していた15ms間隔の倍以上になっています。
※PCの送信間隔が10msと短いのも調査中です。

キャプションを入力できます

PCアプリ

Visual StudioでPCアプリを作成しました。
画面イメージを以下に示します。右上の電源ボタンを押すことにより、WiFiを通じてPOWER ONのコマンドが送られ、トランシーバの電源が入ります。電源オンでトランシーバから受信の音声が出ます。この音声をWiFIを通じて、PCのスピーカに出します。PTTオンボタンを押すと送信になり、PCのマイクの音声がWiFIを通じて送られ、TRXtermのSPRESENSE 拡張基板のHP端子からトランシーバに送られます。

キャプションを入力できます

ソースコード

シングルコア構成時のUDP送受信部分を抜粋して示します。Audio signal_process関数でPCMデータをvoice_effector処理に渡していますので、ここのデータをWiFi送信としました。送信時G711変換とバイナリーデータをキャラクタに変換して送ります。
受信データは、execute_aframe関数の中で設定します。
setupの中でAudio処理とWiFi処理の初期化を行います。UDP送受信部分は、sendUDP関数とrecvUDP関数を作りました。処理時間が短いため、送信時にコマンドデータを受信することがあり、sendUDP関数の中に受信処理が入っています。
状態は、statusNoという変数で持ち、0:電源オフ状態、1:トランシーバ受信状態(PTTオフ)、2:トランシーバ送信状態(PTTオン)としています。

シングルコア構成時のUDP送受信部分

/** * @brief Sample singnal Processing function * * @param [in] uint16_t ptr * @param [in] int size */ void signal_process(int16_t* ptr, int size) { static int frame_cnt = 0; int ret, i; uint8_t comressData[240];  uint8_t charData[240 * 2+100]; uint8_t *cData; cData = (uint8_t *)ptr; /////////////////////////////// ここでproc_bufferを送信 if(frame_cnt > 100){ frame_cnt = 0; for (i = 0; i < 240; i++) { comressData[i] = compress(*(cData + i));// G711圧縮 } for (i = 0; i < 240; i++) { bin2char(&comressData[i], &charData[i * 2]);// キャラクタ変換 } ledOn(LED1); charData[i] = 0x00; sendUDP(&charData[0]); } frame_cnt++; } <<省略>> /** * @brief Execute signal processing for one frame */ bool execute_aframe() { isCaptured = false; signal_process((int16_t*)proc_buffer, proc_size); AsPcmDataParam pcm_param; /* Alloc MemHandle */ while (pcm_param.mh.allocSeg(S0_REND_PCM_BUF_POOL, frame_size) != ERR_OK) { delay(1); } if (isEnd) { pcm_param.is_end = true; } else { pcm_param.is_end = false; } /* Set PCM parameters */ pcm_param.identifier = OutputMixer0; pcm_param.callback = 0; pcm_param.bit_length = bit_length; pcm_param.size = frame_size; pcm_param.sample = frame_sample; pcm_param.is_valid = true; // このproc_bufferに受信データをいれる memcpy(pcm_param.mh.getPa(), send_buffer, pcm_param.size); int err = theMixer->sendData(OutputMixer0, outmixer0_send_callback, pcm_param); if (err != OUTPUTMIXER_ECODE_OK) { printf("OutputMixer send error: %d\n", err); return false; } return true; } void setup() { int ret = 0; /* Initialize serial */ Serial.begin(115200); while (!Serial); /** * @brief Setup Audio Objects * * Set input device to Mic <br> * Initialize frontend to capture stereo and 48kHz sample rate <br> */ <<オーディオセットアップ処理>> //Open software serial for chatting to ESP Serial2.begin(115200); //////////////////////////////////////////////// // reset WiFi module Serial.println(F("Soft resetting...")); if (!espReset()) return WIFI_ERROR_RST; delay(500); <<WiFiセットアップ処理>> /////////////////////////////////////// } uint8_t sendUDP(String sendData ) { // Start sending data String cmd = "AT+CIPSEND="; // Send data cmd += sendData.length(); cmd.toCharArray(replybuffer, REPLYBUFFSIZ); sendCheckReply(replybuffer, ">"); Serial.print("Sending: "); Serial.println(sendData.length()); // Serial.println(F("*********SENDING*********")); // Serial.print(sendData); // Serial.println(F("*************************")); // sendData.toCharArray(replybuffer, REPLYBUFFSIZ); Serial2.println(sendData); while (true) { espreadline(3000); // this is the 'echo' from the data Serial.print(">"); //Serial.println(replybuffer); // probably the 'busy s...' // LOOK AT ALL THESE POSSIBLE ARBITRARY RESPONSES!!! if (strstr(replybuffer, "wrong syntax")) //continue; break; else if (strstr(replybuffer, "ERROR")) //continue; break; else if (strstr(replybuffer, "busy s...")) continue; //break; else if (strstr(replybuffer, "OK")) break; // else break; else if(strstr(replybuffer, "+IPD")){ if (strstr(replybuffer, "PWR,ON")){// 電源オンコマンド statusNo = 1; //printf("PWR,ON Detect"); ledOn(LED0); delay(1000); }else if (strstr(replybuffer, "PWR,OFF")){// 電源オフコマンド ledOff(LED0); statusNo = 0; } else if (strstr(replybuffer, "PTT,ON")){// PTTオンコマンド ledOn(LED3); statusNo = 2; memset(replybuffer, 0x00, 240); delay(2); } else if (strstr(replybuffer, "PTT,OFF")){// PTTオフコマンド ledOff(LED3); statusNo = 1; //memset(replybuffer, 0x00, 10); } else return; } } return true; } uint8_t recvUDP(){ uint16_t i = 0, len = 0; char recvBuffer[1000]; unint16_t recvData[1000]; // Receive data char c; while(Serial2.available()) { c = Serial2.read(); if (c == '\r') continue; if (c == '\n') continue; if (c == 0x00) continue; recvBuffer[i] = c; i++; delay(1); // これがないと0を読んでしまう } recvBuffer[i] = 0x00; if(i == 0) return false; if(char *s = strstr(recvBuffer, "+IPD,")){ // Receive network data len = atoi(s+5); Serial.print(len); Serial.println(" bytes total"); } if(len == 0) return false; if(len<100){ if (strstr(recvBuffer, "PWR,ON")){// 電源オンコマンド statusNo = 1; ledOn(LED0); delay(1000); }else if (strstr(recvBuffer, "PWR.OFF")){// 電源オフコマンド ledOff(LED0); statusNo = 0; } else if (strstr(recvBuffer, "PTT,ON")){// PTTオンコマンド ledOn(LED3); statusNo = 2; memset(recvBuffer, 0x00, 10); } else if (strstr(recvBuffer, "PTT,OFF")){// PTTオフコマンド ledOff(LED3); statusNo = 1; memset(recvBuffer, 0x00, 10); } else return; }else{ printf("Data recieved\n"); for(i=0; i<240; i++){ char2bin((uint8_t*)(recvBuffer+i+9), (uint8_t*)(recv_buffer+i));// キャラクタをバイナリに変換 } for (i = 0; i < 240; i++) { recvData[i] = decode((*(recv_buffer+i));// G711伸長 } printf("Data convered\n"); return; } printf("\nstatusNo = %x\n", statusNo); // Serial.println(F("*********RECIEVED********")); Serial.println(recvBuffer); // Serial.println(F("*************************")); //delay(100); } void loop() { int ret; int8_t msgid; uint32_t msgdata; /* Echo back */ // 無線機送信 PC MIC→ 無線機マイク端子へ // 無線機受信 受信スピーカー → PC スピーカーへ recvUDP(); if(statusNo == 1){ if (isCaptured) { if (!execute_aframe()) { printf("Rendering error!\n"); goto exitCapturing; } } }else if(statusNo == 2){ recvUDP();// 専用受信に変える予定 } /////////////////// if (ErrEnd) { puts("Error End"); theFrontEnd->stop(); goto exitCapturing; } if (isEnd && !isCaptured) { puts("isCaptured"); isEnd= false; goto exitCapturing; } exitCapturing: board_external_amp_mute_control(true); theFrontEnd->deactivate(); theMixer->deactivate(OutputMixer0); theFrontEnd->end(); theMixer->end(); puts("Exit."); exit(1); }

最後に

当初、メインコアにオーディオ機能とWiFi機能を実装しましたが、ばらつきが大きいのと送信間隔が長いことから、VoiceChanger処理を参考にメインコアとサブコア1に分離しました。しかし、コア間の情報の受け渡しがうまくいっていないようで、Panicが発生し、時間切れとなってしまいました。
不本意ですが、途中の状態で提出します。改善は、継続したいと思います。
また、現状のトランスペアレント転送をキャラクタに変換するのは、効率が悪く、改めてWiFiモジュールを再検討して、転送方法を見直したいと思います。

ログインしてコメントを投稿する