はじめに
この記事はWMMC Advent Calendar 2024 15日目の記事です。
adventar.org
こんにちは!Jamesです。
昨日の記事はYuki 先輩の第39回全日本学生マイクロマウス大会 参戦レポ でした。ロボット学会 学生特別賞の受賞おめでとうございます!!
VSCode 上で迷路の壁情報を表示させたい!!
なぜ壁情報を表示させたいのか
先輩たちが実際に表示させていてかっこいいと思った
マイクロマウスが探索走行で得た壁情報を可視化して客観的に見たい
今後探索系をいじる時やデバッグ の時に使いたい
実装方法
実装に当たって、2つの方法を思いつきました。
センサ値をprintfを使用してteraterm などに表示させる時と同様に、機体側に迷路情報を表示するプログラムを書く。
迷路情報をデータとしてPCに送り、PC側に迷路情報を表示させるプログラムを書く。
前者の方がシンプルな仕様になりますが、そのうち迷路情報に関するデータをコネコネしてみたいなと思い(使い道は未定)、後者を採用しました。printfではなく、配列を送ることになるので、以下のプログラムはシリアル通信に関するものが多くなっています。
ということで、ChatGPT(以下チャッピー)と相談しながらVSCode 上で迷路情報を表示させるプログラムを作成しました。(エディタは別にVSCode に限らず、正確にはターミナル上で迷路情報を表示)
実装した結果
PCと機体をUSB接続し、PC側でプログラムを実行した後、機体のスイッチを押してやると迷路情報が表示されます。
実際に自分の機体が学生大会2024で得た迷路情報
いい感じに表示できました。(語彙力)
?が表示されている部分は、機体が行っていない未探索の部分で、未知の領域には壁があるものとして表示しています。
↓は公開されている迷路情報です
学生大会2024 クラシックマウス競技の迷路 🔴部分がスタートとゴール
tech-kotalog.hatenablog.com
プログラムを見ていく
概要
最初に、動作環境はWindows なので注意してください。
main文の流れは大体こんな感じです。
シリアル通信に関する変数、二次元配列を作成
シリアルポートを開く
シリアルポートを設定
残留データをクリア
マップデータを受信
受信した生データを表示(デバッグ 用に残している)
シリアルポートを閉じる
迷路情報に変換し表示
Tanaport先輩がこの記事 で、VSCode 上でST-LINKを使った書き込みやprintfの話をしていて「ST-LINK最高!」みたいな感じなんですけど、そんな中でUARTの話をするのってアリなんですかね? (ST-LINKとマイコン 間の通信はUARTを使用しているそうです 2024/12/16)
for ( int i = 0 ; i < 16 ; i ++ ){
for ( int j = 0 ; j < 16 ; j ++ ){
map_data [i][j] = ( uint8_t ) eeprom_read_halfword (i * 16 + j);
}
}
HAL_UART_Transmit ( & huart2 , ( uint8_t * ) map_data , sizeof (map_data), HAL_MAX_DELAY); // send data
配列map_dataにEEPROM(不揮発性メモリ)に格納された迷路情報を格納し、それを送るシンプルなものです。
PC側のプログラム(関数系)
シリアル通信に関わる部分は大体チャッピーに教えてもらいました。
シリアルポートを開く関数
// 引数 : portName - オープンするCOMポート名
// 戻り値 : 成功時 - シリアルポートのハンドル, 失敗時 - INVALID_HANDLE_VALUE
HANDLE Open_SerialPort ( const char * portName ) {
HANDLE hSerial = CreateFile (
portName , // 開くシリアルポートの名前
GENERIC_READ | GENERIC_WRITE , // 読み書き両方のアクセス権を要求
0 , // 共有モード(0は共有無し)
NULL , // セキュリティ属性(デフォルト)
OPEN_EXISTING , // 既存のポートを開く
FILE_ATTRIBUTE_NORMAL , // 通常のファイル属性
NULL // テンプレートファイル(使用しない)
);
if ( hSerial == INVALID_HANDLE_VALUE ) {
printf ( "Error: Unable to open COM port ( %s ) \n " , portName );
}
return hSerial ;
}
自分の場合、使用するCOMポートはCOM8でした。
シリアルポートを設定する関数
// 引数 :
// hSerial - シリアルポートのハンドル
// dcbSerialParams - シリアルポート設定用構造体へのポインタ
// baudRate - 通信速度(ボーレート)
// 戻り値 : 成功時 - TRUE, 失敗時 - FALSE
BOOL Conf_SerialPort ( HANDLE hSerial , DCB * dcbSerialParams , DWORD baudRate ) {
dcbSerialParams -> DCBlength = sizeof ( DCB ); // DCB構造体のサイズ
dcbSerialParams -> BaudRate = baudRate ; // 通信速度(115200bps)
dcbSerialParams -> ByteSize = 8 ; // データビット長(8bit)
dcbSerialParams -> StopBits = ONESTOPBIT ; // ストップビット(1bit)
dcbSerialParams -> Parity = NOPARITY ; // パリティ (なし)
// シリアルポートの設定を適用
if ( ! SetCommState ( hSerial , dcbSerialParams )) {
printf ( "Error: Failed to configure COM port \n " );
return FALSE ;
}
return TRUE ;
}
SetCommState関数でシリアルポートを設定。
残留データをクリアする関数
機体とPCをUSB接続しているときに、PCでこのプログラムを実行する前に、機体のモード選択をすると、モード選択時に出るprintfなどにより送られるデータがPC側の配列に格納され、正しく迷路情報が表示されないのでこの関数を追加しました。(配列を送る時にヘッダーとフッターを付ける方法も検討しましたが、今回は手軽にできる方法にしました。)
// 引数 : hSerial - シリアルポートのハンドル
// 戻り値 : 成功時 - TRUE, 失敗時 - FALSE
BOOL Clear_SerialBuffer ( HANDLE hSerial ) {
// 送受信バッファをクリア
if ( ! PurgeComm ( hSerial , PURGE_RXCLEAR | PURGE_TXCLEAR )) {
printf ( "Error: Failed to purge COM buffers \n " );
return FALSE ;
}
return TRUE ;
}
PurgeComm関数で残留データをクリア。
※PCとUSB接続した状態で、PCでプログラムを実行していないときにモード選択が出来るようにしたが、PCでプログラムを実行している際にモード選択をすることはこの方法ではできない。実行スイッチを押す前の状態にし、PCでプログラムを実行し、機体の実行スイッチを押す必要がある。
マップデータを受信し二次元配列に変換する関数
// 引数 :
// hSerial - シリアルポートのハンドル
// buffer - データを格納するバッファ
// size - 読み込むバイト数
// bytesRead - 実際に読み込んだバイト数を格納する変数へのポインタ
// 戻り値 : 成功時 - TRUE, 失敗時 - FALSE
BOOL Read_SerialData ( HANDLE hSerial , uint8_t * buffer , DWORD size , DWORD * bytesRead ) {
// シリアルポートからデータを読み込み
if ( ! ReadFile ( hSerial , buffer , size , bytesRead , NULL )) {
printf ( "Error: Failed to read data from serial port \n " );
return FALSE ;
}
return TRUE ;
}
ReadFile関数でデータを読み込み。
一次元配列を二次元配列に変換し表示する関数
// 引数 :
// buffer - 1次元配列のデータ
// size - バッファのサイズ(バイト数)
// 戻り値 : なし
void Process_Print_Data ( uint8_t map [ ROWS ][ COLS ], uint8_t * buffer , size_t size ) {
// サイズチェック
if ( size != ROWS * COLS ) {
printf ( "Error: Data size mismatch (expected %d , got %zu ) \n " , ROWS * COLS , size );
return ;
}
// 2次元配列に再構築
for ( int i = 0 ; i < ROWS ; i ++ ) {
for ( int j = 0 ; j < COLS ; j ++ ) {
map [ i ][ j ] = buffer [ i * COLS + j ];
}
}
// データを表示
for ( int i = ROWS - 1 ; i >= 0 ; i -- ) {
for ( int j = 0 ; j < COLS ; j ++ ) {
printf ( " %3d " , map [ i ][ j ]);
}
printf ( " \n " );
}
}
扱いやすくするために二次元配列へと変換し、デバッグ のためにと一応生データを表示させています。
左下を(0, 0)の区画、右上を(15, 15)の区画にしたいので行は減る方向、列は増える方向のfor文で書いています。
生データから迷路データに変換する関数
// 引数 :
// map - 受信したマップの生データ
// maze - mapを迷路データに変換したデータを格納する
// 戻り値 : なし
void Process_Maze_Data ( uint8_t map [ ROWS ][ COLS ], uint8_t maze [ ROWS ][ COLS ]){
for ( int i = 0 ; i < ROWS ; i ++ ){
for ( int j = 0 ; j < COLS ; j ++ ){
maze [ i ][ j ] = ( map [ i ][ j ]) >> 4 ;
}
}
}
弊サークルの標準プログラムでは、迷路情報は上位4ビットに格納されているので、ビットシフトしたものをmazeに格納します。
なお、迷路情報とは各区画における4方向の壁の有無で表現されており、
#define north 0x08
#define east 0x04
#define south 0x02
#define west 0x01
のように4ビット目に北壁、3ビット目に東壁、2ビット目に南壁、1ビット目に西壁があったら1を無かったら0を格納しています。
例えば、0x0C(1100)のとき、北壁(4ビット目)と東壁(3ビット目)が存在し、南壁(2ビット目)と西壁(1ビット目)が存在しないことを示しています。(0xは16進数の意)
0x05(0101)のとき、東壁と西壁が存在し、北壁と南壁は存在しません。
迷路情報を表示する関数
長いので全部は載せません。特に複雑なことはしていません。
for ( int i = ROWS - 1 ; i >= 0 ; i -- ) { //迷路データを16進数で表示
for ( int j = 0 ; j < COLS ; j ++ ) {
printf ( " %3x " , maze [ i ][ j ]);
}
printf ( " \n " );
}
今回もまたデバッグ 用に変換した迷路データを表示させています。
あとは体裁を整えていい感じに迷路情報を表示させます。
PC側のプログラム(main文)
int main () {
const char * portName = "COM8" ; // シリアルポート名の設定
HANDLE hSerial ; // シリアルポートのハンドル
DCB dcbSerialParams = { 0 }; // シリアルポートの設定用構造体(0で初期化)
uint8_t buffer [ ROWS * COLS ]; // 16x16マップデータを格納する1次元バッファ
DWORD bytesRead ; // 実際に読み取ったバイト数を格納する変数
// 2次元配列の定義
uint8_t map [ ROWS ][ COLS ];
uint8_t maze [ ROWS ][ COLS ];
// シリアルポートを開く
hSerial = Open_SerialPort ( portName );
// ポートが開けなかった場合はエラー終了
if ( hSerial == INVALID_HANDLE_VALUE ) {
return 1 ;
}
// シリアルポートのパラメータを設定(ボーレート:115200)
if ( ! Conf_SerialPort ( hSerial , & dcbSerialParams , CBR_115200 )) {
CloseHandle ( hSerial ); // エラー時はポートを閉じて終了
return 1 ;
}
// 受信バッファに残っているデータをクリア
if ( ! Clear_SerialBuffer ( hSerial )) {
CloseHandle ( hSerial ); // エラー時はポートを閉じて終了
return 1 ;
}
// マップデータを受信
if ( Read_SerialData ( hSerial , buffer , sizeof ( buffer ), & bytesRead )) {
// 期待したサイズのデータを受信できた場合
if ( bytesRead == sizeof ( buffer )) {
printf ( "Received map data successfully. \n " );
// 受信データを2次元配列に変換して表示
Process_Print_Data ( map , buffer , bytesRead );
} else {
// 受信サイズが一致しない場合はエラーメッセージを表示
printf ( "Error: Data size mismatch (expected %zu , got %lu ) \n " , sizeof ( buffer ), bytesRead );
}
}
// 使用したシリアルポートを解放
CloseHandle ( hSerial );
// マップデータを迷路データに変換して表示
Print_Maze ( map , maze );
return 0 ;
}
こんな感じになりました。
現状と展望
良い点
エラーが何なのかが分かりやすい。
綺麗に表示される。
1番について、PCと機体を接続するの忘れてプログラムを実行して、Error: Unable to open COM port (8) の表示を既に何回か見ました。
改善したい点
ヘッダーとフッターを追加し、プログラム実行中でも機体を操作できるようにしたい。
実は未探索の区画だけど?が表示されていない区画がある。
2番について、区画には入っていないけれど横を通過し、そこに壁がなかった場合に?が表示されません。解決するためには、入った区画を格納する配列を機体側で用意してそれを基にやるのがいいかと。まだ、全面探索などをする段階にないのでいったん放置します。
追加したい機能
最短経路を表示したい。
歩数マップを表示して、経路導出を視覚的にイメージしやすくしたい。
データをコネコネするにあたって、データをファイル出力して、いつでも使えるようにする機能を追加したい。
終わりに
今回はVSCode 上で迷路情報を表示させるということで、完全に自己満足でプログラムを作成しました。とりあえず今は満足しているのでそのうちアップデートします。もっといいやり方あるのに~って方はぜひ教えて欲しいです!
なんかもっと解説っぽい記事にしたかったのですが、ただコードを貼っ付けたみたいな記事になってしまいました…。書き方も少しずつ習得していきたいですね。
明日はPotewo先輩の、自宅サーバーの紹介 です。お楽しみに!