Raspberry Pi 3 B+, Adafruit RGB Matrix + RTC HATにキーイベント関数を走らせてスロットゲームを作った

# 初めに
「Radian_N」（ラジアン）と申します。
今回の記事は、Linuxでのキーイベント取得方法（[引用コード][Linuxでのキーイベント取得関数]）について知見を得たので、Raspberry Pi 3とLEDマトリクスパネルを用いたスロットゲームの製作手法について記載しようと思います。
完成したものは こちらです。

https://twitter.com/radiann_kswg/status/1529705473393721344

※なお今回紹介するコードは上記Twitterのものからさらに調整していますので、この動画と仕様が多少異なります、ご注意ください。

***
# ハードウェア（動作環境構築）
## 使用パーツ
* Raspberry Pi 3 model B+　（この記事では初期設定方法については触れません）
* [Adafruit RGB Matrix + RTC HAT](https://www.adafruit.com/product/2345)
* [Adafruit 64x32 RGB LED Matrix - 3mm Pitch](https://www.adafruit.com/product/2279)
* ACアダプタ 5V4A
* USBキーボード

Adafruit RGB Matrix + RTC HATについては、[マルツ電商](https://www.marutsu.co.jp/pc/i/574341/)からでも入手可能です
セットアップなどについては[前回][]のブログに解説リンクを掲載しております、ご参照ください。

# ソフトウェア（Cソースコードなど）
## 使用ライブラリ概要
実行ファイルの作成には、 [H.Zeller氏のHAT用Cライブラリ(英語解説)][HAT用ライブラリ] を使用しC言語で書きました（ `rpi-rgb-led-matrix/example-api-use/c-example.c` を参照しましょう）。 [HATの初期設定解説][HAT初期設定] でこのライブラリがインストールされるようです。

ソースコードのコンパイル・ビルド方法については、上記リンクよりご参照ください。


## ソースコード
参考にしたサイトおよびソースコード引用先など

* [H.Zeller氏のHAT用ライブラリ][HAT用ライブラリ]　
* [Linuxでのキーイベント取得関数][]　

なおGPIOの処理や予備の変数など今回不要となっているコードについては、改良する際に良きに計らっていただければ幸いです。
```c:c-source
/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*-
 *
 * Using the C-API of this library.
 *
 */
#include "led-matrix-c.h"

#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h> // 今回は不要です
#include <unistd.h>

#include <termios.h>
#include <fcntl.h>

#include <pigpio.h> // 今回は不要です

// キーイベント取得関数
// コードについてはこちらを引用しました
// https://hotnews8.net/programming/tricky-code/c-code03
int kbhit(void) {
	struct termios oldt, newt;
	int ch;
	int oldf;
	
	tcgetattr(STDIN_FILENO, &oldt);
	newt = oldt;
	newt.c_lflag &= ~(ICANON | ECHO);
	tcsetattr(STDIN_FILENO, TCSANOW, &newt);
	oldf = fcntl(STDIN_FILENO, F_SETFL, 0);
	fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
	
	ch = getchar();
	
	tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
	fcntl(STDIN_FILENO, F_SETFL, oldf);
	
	if (ch != EOF) {
		ungetc(ch, stdin);
		return 1;
	}
	
	return 0;
}

// 初期化関数 今回は不要
static int init_dev(void)
{
	if (gpioInitialise() < 0)
	{
		return 1;
	}
    return 0;
}

// 数字は1～9を用いています

// 表示する数字の色（RGB値）の生成関数
// 今回は表示用の文字列を引数にし、1文字目の値を参照しています
/* 数字-色 対応表
   偶数(2,4,6,8): 青(#607FFF)
   1,5: 黄緑(#7FA000)  9: 白(#FFFFFF)
   3: 黄(#FFA000)  7: 赤(#FF0000) */
uint8_t returnR(char* buff){ // Red 赤
	switch(buff[0]){
		// ここではcase文を複数同時に処理させてます、returnG関数、returnB関数も同様
		case '1': // 黄緑
		case '5': // 黄緑
		return 128;
		case '3': // 黄
		case '7': // 赤
		case '9': // 白
		return 255;
		default: // 青(偶数)
		return 96;
	}
	return 0;
}
uint8_t returnG(char* buff){ // Green 緑
	switch(buff[0]){
		case '3': // 黄
		return 160;
		case '9': // 白
		return 255;
		case '1': // 黄緑
		case '5': // 黄緑
		return 160;
		case '7': // 赤
		return 0;
		default: // 青(偶数)
		return 127;
	}
	return 0;
}
uint8_t returnB(char* buff){ // Blue 青
	switch(buff[0]){
		case '1': // 黄緑
		case '3': // 黄
		case '5': // 黄緑
		case '7': // 赤
		return 0;
		default: // 白(9),青(偶数)
		return 255;
	}
	return 0;
}

volatile bool interrupt_received = false;
static void InterruptHandler(int signo) {
	interrupt_received = true;
}

// 現在のスロットの回転状態
// 停止順は「左、右、真ん中」、変数phaseの遷移順はenum内に記載の順番と同様
enum PHASE {
	STOP, // 全部止まってる
	ROLLING_ALL, // 全部回ってる
	ROLLING_SIDE, // 左を除く2つだけ回ってる
	ROLLING_CENTER, // 真ん中の1つだけ回ってる
} phase;

// 回転の所要時間（1つ数字が回転するのにかかる時間）[μs]
const long ROLLSPEED_HIGH = 25000; // 高速
const long ROLLSPEED_LOW = 100000; // 低速

// 1～9の数字をランダムに生成する関数
// 今回はシード値を操作してません
int generate_num(void) {
	return random()%9+1;
}

// main関数、ここから処理に移ります
int main(int argc, char **argv) {
	// LEDマトリクスの変数設定
	struct RGBLedMatrixOptions options;
	struct RGBLedMatrix *matrix;
	struct LedCanvas *offscreen_canvas;
	int width, height;
	struct LedFont *font_main;
	struct LedFont *font_sub;

	memset(&options, 0, sizeof(options));
	options.rows = 32;
	options.cols = 64;
	options.chain_length = 1;
	
	// GPIO初期化、今回は不要です
	if(init_dev()==1)  return 1;
	
	// フォントデータの準備、subの方は使ってません
	const char *bdf_font_main= "../fonts/9x18B.bdf";
	const char *bdf_font_sub = "../fonts/6x13.bdf";

	font_main= load_font(bdf_font_main);
	font_sub = load_font(bdf_font_sub);
	if (font_main == NULL) {
		fprintf(stderr, "Couldn't load font '%s'\n", bdf_font_main);
		return 1;
	}
	if (font_sub == NULL) {
		fprintf(stderr, "Couldn't load font '%s'\n", bdf_font_sub);
		return 1;
	}
	
	// 変数の設定
	phase = STOP; // 回転状況、最初はすべて止まってる状態
	int num[3] = {3,7,5}; // 表示している数字、num[0]から順に 左、右、真ん中
	// 最初に表示する数字をランダムにする場合はこちらで初期化してください
	// ※ただしシード値を変更していないので、シード値の更新も併せて行ってください
/*	int num[3] = {generate_num(), generate_num(), generate_num()};
	while(num[0] == num[1]) num[1] = generate_num();
	while(num[0] == num[2] || num[1] == num[2]) num[2] = generate_num(); */

	// 表示する文字列、text_buff0から順に 左、右、真ん中
	char text_buff0[1], text_buff1[1], text_buff2[1];
	// 回転の所要時間、初期状態は高速
	long rollspeed_us = ROLLSPEED_HIGH;
	
	// LEDマトリクスの初期設定（GPIOを使うときはgpioInitialise関数の後にしましょう）
	/* This supports all the led commandline options. Try --led-help */
	matrix = led_matrix_create_from_options(&options, &argc, &argv);
	if (matrix == NULL)
		return 1;

	/* Let's do an example with double-buffering. We create one extra
	* buffer onto which we draw, which is then swapped on each refresh.
	* This is typically a good aproach for animations and such.
	*/
	offscreen_canvas = led_matrix_create_offscreen_canvas(matrix);

	led_canvas_get_size(offscreen_canvas, &width, &height);

	fprintf(stderr, "Size: %dx%d. Hardware gpio mapping: %s\n",
		width, height, options.hardware_mapping);
	
	// こちらの記載は任意、省略する場合 次のwhile文の引数は1にする
	signal(SIGTERM, InterruptHandler);
	signal(SIGINT, InterruptHandler);

	// ループ開始
	while(!interrupt_received) {
		// 表示している数字を文字列に変換
		sprintf(text_buff0, "%1d", num[0]);
		sprintf(text_buff1, "%1d", num[2]);
		sprintf(text_buff2, "%1d", num[1]);
		
		// 描画
		led_canvas_fill(offscreen_canvas, 0,0,0); // 文字が重ならないよう黒画面に
		draw_text(offscreen_canvas, font_main,
				17, 7+baseline_font(font_main),
				returnR(text_buff0), returnG(text_buff0), returnB(text_buff0),
				text_buff0, 0); // 左の数字
		draw_text(offscreen_canvas, font_main,
				27, 7+baseline_font(font_main),
				returnR(text_buff1), returnG(text_buff1), returnB(text_buff1),
				text_buff1, 0); // 右の数字
		draw_text(offscreen_canvas, font_main,
				37, 7+baseline_font(font_main),
				returnR(text_buff2), returnG(text_buff2), returnB(text_buff2),
				text_buff2, 0); // 真ん中の数字
		
		// キーイベントの取得、なにかの文字入力が1キー分行われたときに処理されます
		if (kbhit()) {
			// 今回は入力キーの識別はしてませんが、バグの原因になるので
			// 入力キーを取得するgetchar関数の呼び出しを1回だけ実行しましょう
			getchar();
		//	printf("hitted key of %c\n", getchar()); // キーイベントのモニタリング
			
		//	led_canvas_fill(offscreen_canvas, 64,64,64); // キー入力時にフラッシュ（任意）
			
			// 回転状況を遷移
			switch (phase) {
				// 回転開始（すべて止まっていた場合）
				case STOP:
					rollspeed_us = ROLLSPEED_HIGH; // 高速回転にする
					num[0] = generate_num(); // 回転開始と同時に数字をランダムなものにする
					num[1] = generate_num();
					num[2] = generate_num();
					phase = ROLLING_ALL;
					break;
				// 1つ目（左）を止める
				case ROLLING_ALL: // （すべて回っていた場合）
					rollspeed_us = ROLLSPEED_LOW; // 目押しできるように低速回転にする
					phase = ROLLING_SIDE;
					break;
				// 2つ目（右）を止める
				case ROLLING_SIDE: // （左以外の2つだけ回っていた場合）
					phase = ROLLING_CENTER;
					break;
				// 3つ目（真ん中）を止める
				case ROLLING_CENTER: // （真ん中の1つだけ回っていた場合）
				default: // 念のため
					phase = STOP; // 3つ全部が止まった状態にする
					break;
			}
		}
		// 備考：キーイベント処理の後に回転の処理を実行すると目押ししやすかったです

		// 数字を回転させる（1つ次の数字にする）
		switch (phase) {
			// breakを挟まないことで次以降のcase文も処理させてます
			// 以下、すべて（左＋右＋真ん中）回転しているとき
			case ROLLING_ALL:
				num[0] %= 9;
				num[0] += 1;
			// 以下、2つ（右＋真ん中）以上回転しているとき
			case ROLLING_SIDE:
				num[1] %= 9;
				num[1] += 1;
			// 以下、1つ（真ん中）以上回転しているとき
			case ROLLING_CENTER:
				num[2] %= 9;
				num[2] += 1;
			// 念のため他のケースも記載して、switch文を離脱
			case STOP:
			default:
				break;
		}
		
		// 回転速度に応じて待機
		usleep(rollspeed_us);

		// LEDマトリクスを更新
		/* Now, we swap the canvas. We give swap_on_vsync the buffer we
		 * just have drawn into, and wait until the next vsync happens.
		 * we get back the unused buffer to which we'll draw in the next
		 * iteration.
		 */
		offscreen_canvas = led_matrix_swap_on_vsync(matrix, offscreen_canvas);
	}

	// 終了
	/*
	 * Make sure to always call led_matrix_delete() in the end to reset the
	 * display. Installing signal handlers for defined exit is a good idea.
	 */
	led_matrix_delete(matrix);
	
	gpioTerminate(); // 今回は不要

	return 0;
}
```

***
# あとがき
LEDマトリクスパネルについては、[Adafruit公式サイト(英語)](https://www.adafruit.com/)で積極的に制御ボードや専用ボードを扱っているそうです。もしLEDマトリクスパネルの運用に興味があるときは、そちらも併せて閲覧してみてもいいかもしれません。
今回のスロットゲームのように、LEDマトリクスパネルを使ったミニアーケードゲームの製作をしてみても面白いので、ぜひともこの記事が読者の皆さんに興味を引いていただけたら幸いです。

## 参考・引用URLまとめ

* [完成品（今回紹介したコードとわずかに異なります）][完成品]
* [前回のLEDマトリクスパネルについてのブログ][前回]
* [HAT初期設定(英語)][HAT初期設定]
* [HAT用ライブラリ(英語)][HAT用ライブラリ]
* [Linuxでのキーイベント取得関数][]

[完成品]:https://twitter.com/radiann_kswg/status/1529705473393721344
[前回]:https://qiita.com/Radian_N/items/4ced68cee55032964ae2 
[HAT初期設定]:https://learn.adafruit.com/adafruit-rgb-matrix-plus-real-time-clock-hat-for-raspberry-pi
[HAT用ライブラリ]:https://github.com/hzeller/rpi-rgb-led-matrix/tree/master/examples-api-use
[Adafruit製のLEDマトリクスパネル]:https://www.adafruit.com/product/2345
[Linuxでのキーイベント取得関数]:https://hotnews8.net/programming/tricky-code/c-code03