プログラムはGoogle Geminiにこねくり回して貰いました。
Xの動画
https://x.com/i/status/2084622576526184793
画面レイアウト変更(大画面 0.96→2.42インチ)
BEEPを追加
BLEキーボードでスマホにタイム出力
キーボードと子機接続表示
外部アンテナ使用
その他安定化を追加
親機
```cpp
#include <esp_now.h>
#include <wifi.h>
#include <esp_wifi.h>
#include <wire.h>
#include <u8g2lib.h>
#include <hijelhid_blekeyboard.h>
#include <esp_timer.h>
// ==========================================
// デバッグ設定 (1にするとシリアルモニタ出力)
// ==========================================
#define DEBUG 0
#if DEBUG
#define DEBUG_PRINT(x) Serial.print(x)
#define DEBUG_PRINTLN(x) Serial.println(x)
#else
#define DEBUG_PRINT(x)
#define DEBUG_PRINTLN(x)
#endif
// ==========================================
// ディスプレイ設定 (U8g2)
// ==========================================
// ★2.42 / 2.54 インチ (SSD1309) の場合:
U8G2_SSD1309_128X64_NONAME0_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// ★0.96 インチ (SSD1306) で使用する場合は上の行を有効化してください:
// U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// ==========================================
// アンテナ制御設定 (XIAO ESP32C6 専用)
// 1: 外部アンテナ(U.FL) / 0: 内蔵アンテナ
// ==========================================
#define USE_EXTERNAL_ANTENNA 1
#define PIN_RF_PWR 3 // RFスイッチ電源制御ピン (GPIO 3)
#define PIN_RF_SEL 14 // アンテナ選択ピン (GPIO 14: HIGH=外部, LOW=内蔵)
// ==========================================
// ピン・ハードウェア設定
// ==========================================
#define I2C_SDA 22
#define I2C_SCL 23
#define PIN_RESET 1 // リセット / ストップボタン
#define PIN_BUZZER 18 // 圧電ブザー接続ピン (D10 / GPIO 18)
const bool ACTIVE_LEVEL = LOW;
// BLEキーボード
HijelHID_BLEKeyboard keyboard("Gymkhana_Android_KB", "Seeed", 100);
// ==========================================
// 定数設定(タイムアウト・間隔)
// ==========================================
const unsigned long SENSOR_TIMEOUT_MS = 6000; // 子機切断とみなす時間 (ms)
const unsigned long BTN_LONG_PRESS_MS = 500; // 長押し判定時間 (ms)
const unsigned long RESET_SIGNAL_DUR_MS = 300; // 子機へのリセット信号送信期間 (ms)
const unsigned long RESET_SIGNAL_INT_MS = 50; // リセット信号送信間隔 (ms)
const unsigned long DISPLAY_UPDATE_INT_MS = 50; // 走行中の画面描画間隔 (ms)
// ==========================================
// 状態定義・グローバル変数
// ==========================================
enum State {
STATE_READY,
STATE_RUNNING,
STATE_GOAL,
STATE_TX_ERROR,
STATE_INIT_ERROR
};
State currentState = STATE_READY;
// 高精度計測(マイクロ秒: us)
int64_t localStartTimeUs = 0;
uint64_t elapsedTimeUs = 0;
// ボタン処理用
bool lastBtnState = true;
unsigned long btnPressedTime = 0;
bool isLongPressedProcessed = false;
// 子機リセット処理用
unsigned long signalActiveStartTime = 0;
unsigned long lastResetSendTime = 0;
bool isAfterRunActive = false;
// 生存チェック & 電波強度用
unsigned long lastHeartbeatTime = 0;
bool isSensorConnected = false;
bool lastSensorConnectedState = false;
int lastRssi = -100; // 直近の電波強度 (dBm)
// 画面更新フラグ
bool updateDisplayRequest = true;
// ==========================================
// ESP-NOW 通信設定(ユニキャスト & パッキング)
// ==========================================
uint8_t slaveAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
typedef struct __attribute__((packed)) {
char mode; // 'S': Start, 'G': Goal, 'R': Reset, 'H': Heartbeat, 'A': ACK
uint64_t time; // マイクロ秒
} struct_message;
struct_message incomingData;
struct_message myData;
esp_now_peer_info_t peerInfo;
// ==========================================
// ビープ音再生処理 (D10 / BZ-01A 用)
// ==========================================
enum BuzzerSound { SND_START, SND_GOAL, SND_RESET, SND_ERROR };
void playBuzzer(BuzzerSound sound) {
switch (sound) {
case SND_START:
// スタート:ピッ!(高音・短め)
tone(PIN_BUZZER, 2000, 100);
break;
case SND_GOAL:
// ゴール:ファンファーレ音
tone(PIN_BUZZER, 1500, 100); delay(100);
tone(PIN_BUZZER, 2000, 100); delay(100);
tone(PIN_BUZZER, 2500, 250);
break;
case SND_RESET:
// リセット / 起動:ピッ、ピッ
tone(PIN_BUZZER, 2400, 50); delay(80);
tone(PIN_BUZZER, 2400, 50);
break;
case SND_ERROR:
// エラー:ブッ、ブッ(低音)
tone(PIN_BUZZER, 500, 200); delay(250);
tone(PIN_BUZZER, 500, 200);
break;
}
}
// ==========================================
// アンテナ・送信出力初期化関数
// ==========================================
void setupAntenna() {
pinMode(PIN_RF_PWR, OUTPUT);
pinMode(PIN_RF_SEL, OUTPUT);
WiFi.setTxPower(WIFI_POWER_20dBm);
#if USE_EXTERNAL_ANTENNA
digitalWrite(PIN_RF_PWR, LOW);
digitalWrite(PIN_RF_SEL, HIGH); // 外部アンテナ
#else
digitalWrite(PIN_RF_PWR, LOW);
digitalWrite(PIN_RF_SEL, LOW); // 内蔵アンテナ
#endif
delay(10);
}
// ==========================================
// ユーティリティ関数
// ==========================================
String formatTimeUs(uint64_t us) {
uint64_t ms = us / 1000;
unsigned long totalSec = ms / 1000;
unsigned long milliSec = ms % 1000;
unsigned long seconds = totalSec % 60;
unsigned long minutes = totalSec / 60;
char buf[16];
snprintf(buf, sizeof(buf), "%02lu'%02lu\"%03lu", minutes, seconds, milliSec);
return String(buf);
}
String getStatusStr() {
switch (currentState) {
case STATE_READY: return "READY";
case STATE_RUNNING: return "RUNNING";
case STATE_GOAL: return "GOAL!!";
case STATE_TX_ERROR: return "R-ERR";
default: return "ERROR";
}
}
// ==========================================
// オープニング画面描画関数 (テキスト大型表示)
// ==========================================
void showOpeningScreen() {
u8g2.clearBuffer();
// "GYMKHANA" を大きな太字フォントで中央描画
u8g2.setFont(u8g2_font_helvB14_tr);
int strWidth1 = u8g2.getStrWidth("GYMKHANA");
u8g2.drawStr((128 - strWidth1) / 2, 26, "GYMKHANA");
// "TIMER SYSTEM" を中型フォントで中央描画
u8g2.setFont(u8g2_font_helvB10_tr);
int strWidth2 = u8g2.getStrWidth("TIMER SYSTEM");
u8g2.drawStr((128 - strWidth2) / 2, 50, "TIMER SYSTEM");
u8g2.sendBuffer();
// 起動音を再生(ピッ、ピッ)
playBuzzer(SND_RESET);
// 2秒間オープニングテキストを表示して待機
delay(2000);
}
// ==========================================
// 画面描画 & 外部出力 (U8g2版・文字大型化)
// ==========================================
void displayTimeMax(uint64_t us) {
u8g2.clearBuffer();
// 1. ヘッダー情報(右上:[B] と [S] を最小フォントで配置してスペース確保)
u8g2.setFont(u8g2_font_5x7_tf);
// BLE接続状態 [B] / [-]
if (keyboard.isPaired()) {
u8g2.drawStr(92, 7, "[B]");
} else {
u8g2.drawStr(92, 7, "[-]");
}
// 子機センサー状態 [S] / [!] / [NC]
if (!isSensorConnected) {
u8g2.drawStr(108, 7, "[NC]");
} else if (lastRssi < -80) {
u8g2.drawStr(113, 7, "[!]");
} else {
u8g2.drawStr(113, 7, "[S]");
}
// 2. ステータス・エラー表示(限界サイズの超大型極太フォント)
u8g2.setFont(u8g2_font_helvB18_tr);
String st = getStatusStr();
int xPos = 0;
// 各ステータス・エラーに応じた最適位置(中央寄せ)の計算
if (st == "READY") xPos = 18;
else if (st == "RUNNING") xPos = 0; // 画面横幅いっぱい
else if (st == "GOAL!!") xPos = 14;
else if (st == "R-ERR") xPos = 18; // リセット/送信エラー時
else if (st == "ERROR") xPos = 15; // 初期化エラー時
else {
int strWidth = u8g2.getStrWidth(st.c_str());
xPos = (128 - strWidth) / 2;
if (xPos < 0) xPos = 0;
}
u8g2.drawStr(xPos, 28, st.c_str());
// 3. タイム表示(下段:巨大数字フォント)
uint64_t ms = us / 1000;
unsigned long totalSec = ms / 1000;
unsigned long milliSec = ms % 1000;
unsigned long seconds = totalSec % 60;
unsigned long minutes = totalSec / 60;
char timeBuf[16];
snprintf(timeBuf, sizeof(timeBuf), "%lu:%02lu.%03lu", minutes, seconds, milliSec);
u8g2.setFont(u8g2_font_logisoso28_tn);
u8g2.drawStr(0, 62, timeBuf);
u8g2.sendBuffer();
}
// スマホへタイムを出力 (BLE Keyboard)
void sendTimeToSmartphone(uint64_t us, const char *prefix = nullptr) {
if (!keyboard.isPaired()) return;
if (prefix != nullptr && *prefix != '\0') {
keyboard.print(prefix);
}
keyboard.print(formatTimeUs(us));
keyboard.println("");
keyboard.releaseAll();
}
// ==========================================
// ESP-NOW コールバック処理
// ==========================================
void OnDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) {}
void OnDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *incomingDataRaw, int len) {
if (len != sizeof(struct_message)) {
DEBUG_PRINT("Len Mismatch: ");
DEBUG_PRINTLN(len);
return;
}
memcpy(&incomingData, incomingDataRaw, sizeof(incomingData));
// 生存タイマーとRSSIの更新
lastHeartbeatTime = millis();
isSensorConnected = true;
lastRssi = recv_info->rx_ctrl->rssi;
DEBUG_PRINT("Recv Mode: ");
DEBUG_PRINTLN(incomingData.mode);
// ACK(受信確認)の返送
if (incomingData.mode == 'S' || incomingData.mode == 'G') {
struct_message ackMsg;
ackMsg.mode = 'A';
ackMsg.time = 0;
esp_now_send(slaveAddress, (uint8_t *)&ackMsg, sizeof(ackMsg));
}
// 受信シグナルに応じた状態遷移
// 【スタート処理】
if (currentState == STATE_READY || currentState == STATE_TX_ERROR) {
if (incomingData.mode == 'S') {
localStartTimeUs = esp_timer_get_time();
currentState = STATE_RUNNING;
updateDisplayRequest = true;
playBuzzer(SND_START); // ★スタート音再生
}
}
// 【ゴール処理】
else if (currentState == STATE_RUNNING) {
if (incomingData.mode == 'G' || incomingData.mode == 'S') {
if (incomingData.mode == 'G' && incomingData.time > 0) {
elapsedTimeUs = incomingData.time;
} else {
int64_t currentTimeUs = esp_timer_get_time();
elapsedTimeUs = (uint64_t)(currentTimeUs - localStartTimeUs);
}
currentState = STATE_GOAL;
updateDisplayRequest = true;
playBuzzer(SND_GOAL); // ★ゴール音再生
// スマホ(BLE)へ確定タイムを送信
sendTimeToSmartphone(elapsedTimeUs);
// ゴール後、子機へリセットシグナル('R')を連送する
signalActiveStartTime = millis();
isAfterRunActive = true;
}
}
}
// ==========================================
// セットアップ
// ==========================================
void setup() {
#if DEBUG
Serial.begin(115200);
#endif
// ピン初期化
pinMode(PIN_RESET, (ACTIVE_LEVEL == LOW) ? INPUT_PULLUP : INPUT_PULLDOWN);
pinMode(PIN_BUZZER, OUTPUT);
Wire.begin(I2C_SDA, I2C_SCL);
// U8g2 OLED表示初期化
u8g2.begin();
// ★オープニングロゴ画面の表示処理
showOpeningScreen();
// BLE起動
keyboard.begin();
// Wi-Fi & アンテナ & ESP-NOW 初期化 (チャンネル1指定)
WiFi.disconnect(true);
delay(100);
WiFi.mode(WIFI_STA);
setupAntenna(); // アンテナ切替 & 出力最大化 (20dBm)
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
if (esp_now_init() != ESP_OK) {
currentState = STATE_INIT_ERROR;
updateDisplayRequest = true;
playBuzzer(SND_ERROR); // ★エラー音
return;
}
esp_now_register_recv_cb(OnDataRecv);
esp_now_register_send_cb(OnDataSent);
// ピア登録(子機MACアドレス指定)
memcpy(peerInfo.peer_addr, slaveAddress, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
DEBUG_PRINTLN("Failed to add peer (Slave)");
}
lastHeartbeatTime = millis();
updateDisplayRequest = true;
DEBUG_PRINTLN("Master Setup Complete");
}
// ==========================================
// メインループ
// ==========================================
void loop() {
unsigned long currentTime = millis();
int64_t currentTimeUs = esp_timer_get_time();
bool currentBtnState = digitalRead(PIN_RESET);
// 初期化エラー時
if (currentState == STATE_INIT_ERROR) {
if (updateDisplayRequest) {
updateDisplayRequest = false;
displayTimeMax(0);
}
return;
}
// 1. 子機の生存確認
if (currentTime - lastHeartbeatTime > SENSOR_TIMEOUT_MS) {
isSensorConnected = false;
}
if (isSensorConnected != lastSensorConnectedState) {
updateDisplayRequest = true;
lastSensorConnectedState = isSensorConnected;
}
// 2. リセット/ストップボタン処理
if (currentBtnState == ACTIVE_LEVEL && lastBtnState != ACTIVE_LEVEL) {
btnPressedTime = currentTime;
isLongPressedProcessed = false;
}
// 長押し判定 (リセット)
if (currentBtnState == ACTIVE_LEVEL) {
if (!isLongPressedProcessed && (currentTime - btnPressedTime > BTN_LONG_PRESS_MS)) {
currentState = STATE_READY;
elapsedTimeUs = 0;
updateDisplayRequest = true;
isLongPressedProcessed = true;
signalActiveStartTime = currentTime;
isAfterRunActive = true;
playBuzzer(SND_RESET); // ★リセット操作音
}
}
// ボタンを離したときの処理 (短押し = 手動ストップ)
if (currentBtnState != ACTIVE_LEVEL && lastBtnState == ACTIVE_LEVEL) {
if (!isLongPressedProcessed) {
if (currentState == STATE_RUNNING) {
elapsedTimeUs = (uint64_t)(currentTimeUs - localStartTimeUs);
currentState = STATE_GOAL;
updateDisplayRequest = true;
signalActiveStartTime = currentTime;
isAfterRunActive = true;
playBuzzer(SND_GOAL); // ★手動ストップ時のゴール音
sendTimeToSmartphone(elapsedTimeUs, "M-");
}
}
isLongPressedProcessed = false;
}
lastBtnState = currentBtnState;
// 3. 子機へのリセット要求送信処理 (連送)
if (isAfterRunActive) {
if (currentTime - signalActiveStartTime < RESET_SIGNAL_DUR_MS) {
if (currentTime - lastResetSendTime > RESET_SIGNAL_INT_MS) {
memset(&myData, 0, sizeof(myData));
myData.mode = 'R';
myData.time = 0;
esp_now_send(slaveAddress, (uint8_t *)&myData, sizeof(myData));
lastResetSendTime = currentTime;
}
} else {
isAfterRunActive = false;
}
}
// 4. 走行中のリアルタイム表示更新
if (currentState == STATE_RUNNING) {
elapsedTimeUs = (uint64_t)(currentTimeUs - localStartTimeUs);
static unsigned long lastDisplayUpdateTime = 0;
if (currentTime - lastDisplayUpdateTime > DISPLAY_UPDATE_INT_MS) {
updateDisplayRequest = true;
lastDisplayUpdateTime = currentTime;
}
}
// 5. Bluetooth接続状態の変化検出
static bool lastBleConnected = false;
bool currentBleConnected = keyboard.isPaired();
if (currentBleConnected != lastBleConnected) {
updateDisplayRequest = true;
lastBleConnected = currentBleConnected;
}
// 6. 画面描画の実行
if (updateDisplayRequest) {
updateDisplayRequest = false;
displayTimeMax(elapsedTimeUs);
}
}
```
Posted at 2026/08/13 02:16:03 | |
トラックバック(0) |
ジムカーナ | 日記