05 ソフトウェア - drmus0715/trinitybullet GitHub Wiki
執筆者:ひろたけ(ソフト担当)
この項では、ソフトウェアが関わった4つのモジュールについて説明します。
- Panel
- Master(Wi-Fi Module - ESP32)
- Game Management
- WebServer
目次
環境
組み込み系
項目 | 種別 | バージョン |
---|---|---|
Windows10 | OS | 1903 |
VSCode | 開発環境 | 1.37.1 |
PlatformIO Core | 開発環境 | 4.0.1rc1 |
PlatformIO IDE | 開発環境 | 1.8.2 |
contrib-piohome | 開発環境 |
- PlatformIO + VSCodeを中心に開発した。
WebServer
後述。
GameplayManagement
後述。
Panel
人が乗ったのを検知し、光るモジュール。アトラクションのメインとなる部位で、複数個必要となる。
使用ライブラリ等は以下のとおりです。
項目 | 種別 | バージョン |
---|---|---|
Adafruit NeoPixel | ライブラリ | 1.2.5 |
CAN Bus Shield | ライブラリ | commit 1935258(13 Aug 2019) |
MsTimer2 | ライブラリ | 1.1 |
Atmel AVR | プラットフォーム | 1.15.0 |
- CAN Bus ShieldのライブラリをPlatformIO経由で導入すると古いバージョンがぶち込まれるのでGithubから最新をクローンしました。古いバージョン(v1.20?)は製作したハードウェアに対応していなかった(MCP2515を8MHzで動作させた->v1.20は対応していない)。
動作
基本的には、
- CAN Msgを待つ
- LEDを点灯、フェードアウトさせつつスイッチ待機
- スイッチが押されたらMasterへCAN Msgを送信
という流れです。
以下、開発前のメモ書き。
- 主な挙動
- パネルが光っている間に踏まれたらCANのメッセージを送信
- LEDに点灯指令を出し続ける
- 色は受信したCANのメッセージに従う(R / G / B / W)
- 踏まれたら消灯する
- 踏んでる間はLEDをかすかに点灯させる
- 踏んでる間はCANのメッセージを無視する(パネル押下通知を出さない)
- パネルが踏まれているのに、押下許可が降りて反応してしまうのを防ぐ
インターフェース
CAN Bus
- 受信/送信で扱うメッセージ形式を統一した。最終的に受信/送信に使う構造体は以下の通りになる。
/*
* CAN Massage Definitions
* CAN rx, tx共通メッセージ形式
*/
typedef struct CAN_MESSAGE {
uint32_t ulCanId;
byte bPanelId; // PanelID
byte bColorInfoR; // 色情報 - R
byte bColorInfoG; // 色情報 - G
byte bColorInfoB; // 色情報 - B
byte bBtnFlag; // パネル押下許可/押下フラグ
byte bLightTime; // LED点灯時間
byte bStartSwFlag;
} canCommMsg_t;
- 色は外部からR / G / Bで指定できる。
- LEDのフェードアウトする長さを変更できる。
現在の光量 -= 減衰量
で計算。- 減衰量は
減衰量 = 現在の光量 / 指定した点灯時間
で計算。 - RGBそれぞれにこの計算式を入れている。
LED
- Neopixelを使用(単線シリアル)
- 光らせるLEDの位置とRGBを指定して光らせる
GPIO
- Panel接点(コンパレータ経由)とDIP SW(CAN ID設定用)がある。
- すべてのパネル基板を統一し、DIP SWでIDを設定する。最初にDIP SWの値を読み込み、CAN Driver初期化時にその値を設定している。
不具合
- MasterからCAN Msgを飛ばしたとき、違うIDに設定したはずのパネルが同時に同じメッセージを受信する場合がある。
- 未解明。
- ID設定時になにか起きてる?
- CAN Driver IC の不具合なのか基板不具合なのか切り分けられていない。
- 同じパネルで起きていることが多いのでおそらくハードウェアの問題か…?
Master(ESP32)
PCから見たClientで、Panelから見たMasterの役割。主にPanelの管理とServerとの通信に使う。
使用ライブラリ等は以下のとおりです。
項目 | 種別 | バージョン |
---|---|---|
arduino-esp32 | ライブラリ | 2019/08頃の最新 |
DFRobotDFPlayerMini | ライブラリ | 0.1.5 |
Espresssif 32 | プラットフォーム | 1.10.0 |
-
今回はESP-IDFを使用して開発を進めた。
- 理由:会社でFreeRTOSを使い始めた、シングルタスクで開発すすめるのがしんどかったから
- ESP-IDFはAPIが充実している。リファレンスマニュアルも充実しているので開発中はそれとにらめっこだった。
- ライブラリが豊富なのは良いが、ビルドにそれなりの時間がかかる。PlatformIOでビルドすると、差分ビルドしてくれる時と1からビルドするときがあり謎を呼んでいる。ヘッダファイル追加時か…?
-
arduino-esp32を導入し、ESP-IDFと併用しました。
- 理由:Arduinoでラッパーされたモジュールのほうが使い勝手が良い時がある
- To use as a component of ESP-IDFに従って導入した記憶があります。
動作
※いろいろなことをしているので詳細な遷移図は勘弁してください。
タスク構成
※通信メッセージは大まかです(ガバ設計)。ちゃんと図を書いてからコーディングを始めるべきでした。
xCanRxTask
PanelからCANメッセージを受信し、上位のタスクに渡す。 主にCANメッセージの変換をしている。
xCanTxTask
上位のタスクから来たCANメッセージをPanelに渡す。 主にCANメッセージの変換をしている。
tcp_server_task
Game play Managerと通信するタスク。 動作は以下の通り。
- ソケットを作る。
- Gameplay Managementとソケットを繋ぐ。
- ゲーム開始の通知(GameMng)/上位タスクからのメッセージを待機
- Gameplay Management上でMaster(ESP32)の状況が把握できるようになっています。
- ゲーム開始の通知→上位タスクへ送信
- 上位タスクからのメッセージ→GameMngへ送信
- メッセージ形式(GameMng→Master)
- JSON形式
key | value | Detail |
---|---|---|
"startFlag" | bool | ゲーム開始フラグ(念の為) |
"name" | str | プレイヤー名 |
"difficulty" | int | 難易度 |
"team" | int | 所属チーム |
- メッセージ形式(Master→GameMng)
CMD | name | direction |
---|---|---|
"esp_wait" | Notice finish prepare | ESP32 -> GM |
"esp_gamestart" | Notice game start | ESP32 -> GM |
xHpdltbTask
HPと時間を表示するディスプレイを制御するタスク。
I2Cで接続されている。
※デバイスはArduinoで動いています。この部分は基板担当にお願いしました。
- アドレス:0x1E
- コマンド
times | Data | structure |
---|---|---|
0 | EventID | 0x00: 通常動作モード(N)0x01: Countdownモード(C)0x02: Finishモード(F)0x03: 無点灯(B) |
1 | HP | N,C: 1 ~ 5 (それ以外は消灯)F,B: NOP |
2 | TIME_H | N: 0 ~ 99 (2桁まで)C,F,B: NOP |
3 | TIME_L | N: 0 ~ 9 (1桁まで)C: 0~9(1桁), 0xFF(GO表示)F,B: NOP |
xDfplayerTask
DFplayer Mini(MP3プレイヤー)を制御するタスク。
enumで再生するSEを定義しています。 このタスクに以下の構造体でメッセージを送ると音声が再生されます。
// DFPlayerコマンド定義
typedef enum DFPLAYER_PLAYLIST
{
SE_COUNTDOWN_1 = 1,
SE_COUNTDOWN_2,
SE_ENTRY,
SE_PANEL,
SE_FINISH,
SE_PINCH,
// SE_GAMEOVER,
SE_DAMAGE,
} ePlaylist_t;
typedef struct DFPLAYER_CTRL_MSG
{
ePlaylist_t eSound;
uint16_t uiVolume = DFPLAYER_DEFAULT_VOLUME; // volume : 0 to 30
} dfpCtrlMsg_t;
http_client_task
プレイヤー情報をWebServerに送るタスク。
このタスクに以下の構造体でメッセージを送るとWebServerのDBに登録されます。
/* Game情報格納構造体 */
struct GAME_INFO
{
eTeamcl_t team;
eDfclt_t difficuty;
char name[configPLAYERNAME_LENGTH];
uint32_t redPoint; // 赤いパネルが踏まれた回数
uint32_t bluePoint; // 青いパネルが踏まれた回数
uint32_t greenPoint; // 緑のパネルが踏まれた回数
int32_t hitPoint; // 残り体力
uint32_t remainingTime; // 残り時間
};
xCtrlTxTask
ゲームのシーケンスを作るタスク。 各タスクと協調してゲームの流れを作っていきます。
タイマはハンドラとして動作しており、ハンドラの中で時間をデクリメントしています。 「初期化」フェーズの中でタイマを作成し、「ゲームプレイ中」フェーズでタイマを開始しています。
void prvGameTimerHandle(TimerHandle_t xTimer)
{
hpdltb_t tHpdltb;
sulTimer--;
ESP_LOGD(TAG, "Timer Count:%d", sulTimer);
...
xCtrlRxTask
ゲームプレイ中にPanelから送信されたメッセージを処理するタスクです。 「ゲーム開始待ち/ゲームプレイ中」の部分でxCtrlTxTaskと同期をとっています。
GameplayManagement
Master(ESP32)にプレイヤー情報を送信するアプリケーション。
環境
項目 | 種別 | バージョン |
---|---|---|
Windows10 | OS | 1903 |
Python3 | インタプリタ | 3.6.8 |
tkinter | ライブラリ | - |
動作
Masterと同じネットワークに繋いで起動するとソケットが繋がれます。 あとはプレイヤー情報を入力し、「Player Entry」を押すとスタートします。
WebServer
Master(ESP32)から情報を受け取り、情報をブラウザ経由で表示するアプリケーション。
環境
項目 | 種別 | バージョン |
---|---|---|
Windows10 Pro | OS | 1903 |
docker | コンテナ | 19.03.5 |
- 今回はdockerで環境構築を行いました。Ruby2.6.4(debian)をベースに進めました。dockerfileはこんな感じで書きました。
FROM ruby:2.6.4-buster
RUN apt-get update -y && apt-get upgrade -y
RUN apt-get install -y sqlite3 libsqlite3-dev
RUN apt-get clean
WORKDIR /app
COPY Gemfile .
RUN bundle install && bundle clean
COPY . /app
EXPOSE 4567
CMD ["bundle", "exec", "ruby", "app.rb", "-o", "0.0.0.0", "-p", "4567"]
- WebServerにはRuby + sinatra + SQLite3の構成を取りました。環境もWebアプリ作成もシンプルに書けるためとても使いやすいです。Gemfileはこんな感じです。
source 'https://rubygems.org'
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem 'sinatra', '~> 1.4.7'
gem 'sinatra-contrib'
gem 'sinatra-activerecord'
gem 'activerecord'
gem 'sqlite3'
gem 'eventmachine'
gem 'thin'
gem 'json'
動作
今回は主に以下のページを作成しました。
ページ名 | メソッド | 内容 |
---|---|---|
/result | GET | プレイヤーリザルトのの表示 |
/result | POST | Master(ESP32)からプレイヤーデータを送信 |
/ranking | GET | 総合スコア上位10プレイを表示(/infoへ自動推移) |
/ranking_rd | GET | 総合スコア上位10プレイを表示(ポーリング) |
/info | GET | チーム別のウイルス討伐数などを表示(/rankingへ自動推移) |
/info_rd | GET | チーム別のウイルス討伐数などを表示(ポーリング) |
/dbdebug | GET | とりあえずDBの中身を表示する |
/receipt | GET | プレイ結果をレシートに印刷できる形にして表示 |
ルート(index)
ゲートウェイページを作りました。 (NO REDI.)は自動で推移しないページです。
/result
プレイヤー情報のアップロード先、および表示に使用しました。
POSTで情報をアップロードすると、スコアを計算しDBに保存されます。
/ranking
上位10名が表示されます。難易度別/チーム別で分けても良かったんですが、時間の都合上厳しかったため全プレイヤーを総合スコアでソートさせて表示しています。
また、ディスプレイの都合上/infoと同じディスプレイを使用して表示させたかったので、表示してから数秒後に/infoに推移します。
/info
残りウイルス数とチーム別の撃破数が表示されます。
/rankingと同じく、表示してから数秒後に/rankingに推移します。
/receipt
レシートプリンタで結果を印刷したら面白いな、と思ったので作りました。 当初は自動で印刷する仕組みを検討していましたが、時間と技術力の都合上Webブラウザから直接印刷する形を取りました。
ページサイズはCSSで指定し、印刷時はレシートプリンタを直接指定して印刷します。ビットマップイメージだと汚く出力されてしまうため、全てベクタ画像で出力するように工夫しました。
不具合
- 極稀にサーバーがストップする。データベースにアクセスできない(ActiveRecord関連)という旨のエラーが表示される。
やらかしがありました。本番運用中/テスト中は全く気づかなかったのですが、データベース運用に問題があったようです。
おそらく原因と思われる部分は以下のとおりです。
# [app.rb]
get '/ranking' do
@player = PlayerData.order(score: :desc)
@player ||=genEmptyResult()
erb :ranking
end
/rankingページはポーリング・自動推移でアクセスが常に発生します。しかし、私はアクセスするたびにいちいちソート処理が発生するように設計していました。
そして当日、「ランキングが常に見えていたほうが良いよね」ということで接続するPCの数を増やしました。増やしてから2回位落ちました。あーあ。
対策としては
- ソートしたテーブルは分けて保存し、ソート処理が発生する頻度を抑える
- インデックスをつける
です。
総括
大きく分けて4つの部分を作りました。とっても大変でした。数々の優秀なツール群や、充実したドキュメントがなければここまで作れなかったでしょう。
今回は組み込みからフロントエンドまで幅広く担当しましたが、やはり知見が全然足りていないことを実感しています。つまるところ全然わからんといった感じです。特に、Webの部分に関しては世界がとっても広いことが最近ようやくわかってきました。
これからの展開次第では、どんどん要求されることが増えていくんじゃないかと思っています。ありがたい限りです。今回の反省点を生かして、もっと効率よく、安定して動く作品に仕上げていきたい所存です。
ありがとうございました。俺たちの戦いはこれからだ!!!
(完)