Skip to content
EN

変更履歴 — デバイスファームウェア

This content is not available in your language yet.

バージョン運用 (2026-05-10 改定): FIRMWARE_VERSIONscripts/build_version.py が git 状態から自動生成する。

  • リリースタグ commit (例: v0.1.1): suffix なし — 0.1.1
  • 開発 commit: <NEXT_RELEASE_VERSION>d<N> — N は直近 v* タグからの commit 数 (例: 0.1.2d3)

開発期間中の手動 bump 不要。リリース時は CHANGELOG にエントリ追加 → git tag vX.Y.Z → push のみ。 次リリース予定 version の更新だけ src/hapbeat_config.hNEXT_RELEASE_VERSION を 1 行書き換える (リリース直後に 1 回)。

追加 — PLAY (FIRE) に pan を追加し、ステレオ出力機で L/R バランスを制御 (DEC-055)

Section titled “追加 — PLAY (FIRE) に pan を追加し、ステレオ出力機で L/R バランスを制御 (DEC-055)”

これまで FIRE(0x01 PLAY)はモノラル扱いで、左右の振り分けは CLIP(ストリーム)でしか できなかった。contractsmessage-format.md §0x01 改定に対応し、PLAY でも pan が効く。

  • wire: PLAY ペイロード末尾に pan float32 LE(-1.0=L … 0=中央 … +1.0=R)を任意で付加。 無ければ 0(中央) として扱うので、旧 SDK からの送信はそのまま動く。範囲外は ±1 にクランプ。
  • ミキサー: voice ごとに gain_l = gain × (pan<=0 ? 1 : 1−pan) / gain_r = gain × (pan>=0 ? 1 : 1+pan) のリニアバランス。モノ音源は両 ch に複製してから適用。1ch 出力機(band_v4_pwm)は pan を無視。
  • HIL テストハーネスの build_play を helper と同形(末尾 pan)に更新 + wire-format テスト追加。
  • リリース対象: Hapbeat 本体の 11 env(necklace_v3 / duowl_v4 / band_v2 / band_v3 / band_v4 / 各 _mqtt / 各 _stream_espnow)。band_v4_pwm(0.0.1 実験系)・broker / sensor は据え置き。
  • 実機検証: DuoWL v3 で Unreal SDK から FIRE pan -1 / 0 / +1 の左右変化を確認。

Fixed - ESP32-S3 USB Serial identification/config after reconnect

Section titled “Fixed - ESP32-S3 USB Serial identification/config after reconnect”

Arduino-ESP32 2.0.14 could stop receiving host commands on Windows after a USB Serial open/close cycle. Studio auto-identification would then receive no get_info response and repeatedly recover a stale port handle.

  • Updated PlatformIO Espressif32 from 6.5.0 to 6.11.0 (Arduino-ESP32 2.0.17).
  • Released the 12 variants using native ESP32-S3 USB as 0.3.1.
  • Kept atom_lite_sensor at 0.3.0 because its classic ESP32 USB-UART is unaffected.
  • Suppressed the harmless gain NOT_FOUND warning for an unset ESP-NOW stream gain.
  • Verified repeated open/close and get_info on DuoWL v4 ESP-NOW hardware.

変更(挙動) — デバイスアドレスを常に正準形で保持 / group を address に一本化 (DEC-048)

Section titled “変更(挙動) — デバイスアドレスを常に正準形で保持 / group を address に一本化 (DEC-048)”

デバイスアドレスが常に [prefix/]player_<N>/<position>/group_<M> のフル正準形になり、 group の source of truth が address 文字列のみになった。contractsdevice-addressing.md §2/§4.2/§6-8 改定に対応。

  • 背景: group には独立した 2 系統(NVS address 内の group_N セグメント=マッチング用 / NVS group_id uint8=OLED・get_info 用)が並存し、同期する仕組みが無かった。さらに boot 時に 正準化済みアドレスが NVS 生文字列で上書きされていたため、工場出荷機は OLED に Gr:01 と 出るのにアドレスに group が無く、group_1 宛のコマンドを一切受け取れない状態だった (addressMatch は target のセグメントがデバイス側に全部必要)。
  • 旧系統を削除: NVS group_id キー / set_group コマンド(TCP・Serial)/ boot 時の group 単独ロード / 正準化を上書きしていた boot ブロック。
  • Gr:NN OLED 表示・get_info.group・mDNS TXT groupaddress 由来の導出値として存続 (ルーティングには関与しない)。送信側 target は従来どおり group 省略可(前方一致で全 group)。
  • boot 時・set_address 時に非正準アドレスを正準形へ自由修復して NVS に書き戻す。 free prefix は保持。正準化で 63 文字を超える入力・不正な pos/group セグメントは エラー応答して旧状態を復元(NVS 未変更)。
  • 出荷時アドレス: DuoWL player_1/pos_neck/group_1 / BandWL player_1/pos_r_wrist/group_1
  • MQTT 経路も対象mqtt_transport.cpp が同じ addressMatch を使う)。ESP-NOW の レガシー group バイトは wire 不変。
  • 実機検証: DuoWL v3 / DuoWL v4 / BandWL v3 の 3 機種で HIL 自動テスト全通過 (工場出荷書込み直後の自己修復・正準形保持・永続化・不正入力拒否・UDP ルーティングの match/no-match)。

修正 — 未命名機の OLED デバイス名が既定名にならない

Section titled “修正 — 未命名機の OLED デバイス名が既定名にならない”

OLED の device_name 表示要素だけが、NVS dev_name 未設定時に固定文字列 "Hapbeat" を 表示していた(他経路の mDNS / PONG / get_info は既定名にフォールバック済み)。表示は 左から N 文字(4/5/8/16)の切り出しなので、同型機を並べると全機 Hapb になり弁別不能 だった。既定名 <MAC4>-<board>(例 AB12-duo-wl-v3)に統一。

  • あわせて名前解決を固定バッファでキャッシュ(deviceResolvedNameC / deviceNameInvalidate)。 OLED は 200ms 毎に再描画するため、毎フレームの NVS open + WiFi.macAddress() + String alloc を排除。dev_name の書き込みは cmdSetName の 2 経路のみで両方 invalidate。
  • element-registry.jsondevice_name を実装に追従(variant 4/5/8/16、既定幅 5)。

修正 — DuoWL v4 の音声DSP コマンド 8 個が LAN 経由で “unknown cmd” になる

Section titled “修正 — DuoWL v4 の音声DSP コマンド 8 個が LAN 経由で “unknown cmd” になる”

set_eq_band / set_eq_iir / set_dsp_profile / set_drc / set_beep / set_3d / set_agc / set_av_delay が Serial dispatch にのみ実装されており、TCP(Studio → helper → LAN)経由では unknown cmd になっていた。serial 側の 2 引数ハンドラを serial_config.h 経由で共有し、TCP dispatch へ登録(ロジックの二重実装を回避)。 duowl_v4 系のみ

追加 — PLAY / STOP の seq による重複排除(unicast + broadcast 併送を可能にする)

Section titled “追加 — PLAY / STOP の seq による重複排除(unicast + broadcast 併送を可能にする)”

PLAY / STOP / STOP_ALL について (送信元 IP, seq) の組を 500ms 記憶し、同じ組を 再受信した場合 2 回目以降を無視するようにした(udp_receiver.cpp、8 スロットの リングバッファ、動的確保なし)。

  • これにより送信側は同一コマンドを意図的に複数経路へ送れる: 既知デバイスへ unicast(DTIM 遅延なし)+ 未知デバイス用に broadcast 1 発(保険)。デバイス側が 二重発火を吸収するため、SDK は「どちらが届いたか」を意識しなくてよい
  • キーは seq 単独ではなく (送信元 IP, seq)。複数の送信アプリがそれぞれ独立した seq カウンタを持つ構成で互いを誤抑制しないため
  • 送信側が 1 コマンドごとに seq をインクリメントする限り誤抑制は起きない(連打でも seq が異なる)。TTL 経過後の同一 seq は通常どおり発火する(送信アプリ再起動で seq が巻き戻った場合に恒久無視されるのを避けるため)
  • STREAM 系(0x30/0x31/0x32)は対象外 — seq が「ストリーム内の位置」を意味し、 コマンドの同一性を表さないため
  • 常時有効(設定項目ではない)。現状どの SDK も重複送信しないため挙動は変わらず、 SDK が将来 dual-send を選べるようにするための土台
  • 実機検証(band v3): 異 seq 連射→2 回発火 / 同 seq 連射→1 回 / TTL 経過後→再発火。 送信間隔 0ms・20ms・200ms で確認。contracts message-format.md §7.1 に仕様追記

追加 — Wi-Fi 接続直後の自発 PONG アナウンス(SDK の unicast 一本化の前提)

Section titled “追加 — Wi-Fi 接続直後の自発 PONG アナウンス(SDK の unicast 一本化の前提)”

Wi-Fi サービス(UDP/TCP/mDNS)が立ち上がった直後に、デバイス自身が PONG を サブネットブロードキャストして「ここに居る」と名乗るようにした。

  • 背景: Wi-Fi broadcast は AP に省電力クライアントが 1 台でもいると DTIM ビーコン (100〜300ms)まで保留されるため、各 SDK の送信は unicast に統一する方針。ただし unicast は「送信側が IP を知らないデバイスには届かない」ため、セッション途中で 電源を入れ直した機体が次の PING まで(Unity 既定で最大 5 秒)不達になる穴があった。 デバイス側から名乗ることで、この穴を数百 ms に縮める(常時 unicast+broadcast 併送 でトラフィックを倍にする必要が無くなる)。
  • udpReceiverBroadcastPong() 自体は既存(音量変更時に使用)。呼び出しを checkWifiServices() のサービス起動完了エッジ(false→true)に追加した。再接続・ AP ローミング後も切断時にフラグがクリアされるため再度アナウンスされる。
  • 取りこぼし対策として 300ms 間隔で 3 回送る(連投にはせず、起動直後の輻輳を回避)。 切断時は保留中のアナウンスをキャンセルする。
  • スタック制約: udpReceiverBroadcastPong() は NVS から dev_name を読む深い呼び出し (~2-3KB)のため浅いコンテキストからのみ安全に呼べる。checkWifiServices()loop() 直下で、既存の volumeServiceTick() と同じ深さのため要件を満たす。
  • アナウンス実行時に [Main] Announce PONG (N left) を Serial に出力(呼び出し側で ログするため、音量変更経由のブロードキャストはログしない)。

修正 — kit/file/OTA 転送の進行 watchdog 追加(詰まるとデバイス再起動しか手が無かった問題)

Section titled “修正 — kit/file/OTA 転送の進行 watchdog 追加(詰まるとデバイス再起動しか手が無かった問題)”

Studio から Kit deploy 中に相手(PC / helper)が消えても、TCP スロットを握ったまま 永久に待ち続け、デバイス再起動以外に復帰手段が無かった問題を修正。tcp_server.cpp の idle-timeout は OTA / kit install / file transfer 中は「独自の進行タイムアウトを 持つ」という前提でスキップされていたが、実際にはその進行タイムアウトが実装されて いなかった(コメントと実装の不一致)。

  • processFileData()(kit/file の chunk 受信)がチャンク受信時に s_last_recv_ms を更新していなかった点を修正(OTA 側 processOtaData() は既に更新済みだった)。
  • OTA / kit file 転送それぞれの chunk 受信ループに、s_last_recv_ms からの経過が TRANSFER_WATCHDOG_MS5000ms)を超えたら転送を中断してスロットを解放する watchdog を追加(cleanup は既存の disconnect 分岐と同じロジックを再利用)。 判定は受信バッファが空のときだけ行う(未読チャンクがある状態を stall と誤認して ほぼ完了した kit を破棄する不具合を self-review で検出・修正)。
  • kit install 中でファイル間(file_begin 待ち)の無音区間も、メインループの idle-timeout 判定に s_kit_active 用の同じしきい値を追加してカバー (従来は s_kit_active の間まるごとスキップされていた)。
  • 待機時間は実測から決定。band_wl_v3 へ 1.78MB の実 OTA を流して計測した結果、 スループット ~254 KB/s・1 チャンク(4KB) の sendall 最大ブロックは 0.094 秒 (99%ile 0.077 / 中央値 0.013)。sendall のブロックはリンク速度に反比例するため 実測の 30 倍遅い劣化リンクでも ~0.5 秒で、5 秒発火は実質「1KB/s 級か死亡」に相当。 当初案の 15〜20 秒は根拠が無く、helper 側の旧 10 秒も「デバイスが 1KB/回しか drain していなかった時代」の防御値の残存だった(4KB drain 化で解消済み)。
  • helper 側の chunk 送信タイムアウトを 3 秒に短縮(helper 61a4ddc)。 helper 3s < device 5s の順序を維持する(送信側が先に諦める方がデバイスの即時 disconnect 経路が走ってクリーン。device watchdog は helper が signal できない ケース=プロセス kill / PC 引き抜き のための最終手段)。
  • 実機検証(band v3): kit / OTA とも転送停止から 5.1 秒で解放 + 再接続成功。 正常系(2 秒間隔・合計 14 秒の低速転送)は誤発火なしで完走。
  • tcp_server.cpp の idle-timeout コメントを実装に整合させた。

修正 — Wi-Fi UDP CLIP 既定経路の underrun 耐性強化(実機知見で再設計)

Section titled “修正 — Wi-Fi UDP CLIP 既定経路の underrun 耐性強化(実機知見で再設計)”

Wi-Fi UDP CLIP の既定受信(set_stream_buffer 未設定 = ms==0)で、通常の Wi-Fi ジッタでも時折可聴の途切れが出ていたのを根治。原因は under-run 時にリングを ハードストップして再 prime する挙動で、その re-prime gap が途切れとして聞こえて いた。既定を継続再生(continuous playout)に切り替える。

  • audioStreamSetBufferMs(0) の既定を「low-latency 無効(drain-naturally・ ハードストップ + 再 prime)」から「low-latency 有効(hold+decay under-run・ clock-drift 補正・re-prime 無し)」に変更。
  • ジオメトリは深く設定(CLIP_DEFAULT_* = prebuf 4ms / drift 目標 50ms / hard-trim ceiling 150ms)。当初 ESP-NOW mode7 と同じ浅い 64/64/96(4/4/6ms)に したところ、Unity SDK CLIP(フレームループ駆動で ~44ms チャンクを ~50ms リード でバースト送信)が到着毎に ceiling を大幅超過し hard-trim で大半破棄され、 かえって悪化した。浅い深さは精密ペーシング可能な送信者(ESP-NOW 送信機)専用 と結論し、既定は深いジオメトリ + hard-trim を 150ms 安全弁化した(44ms バースト + 50ms リード + ヒッチが収まる値。50ms 目標の drift 補正は ±1 frame/block で バーストの鋸波内で相殺され、恒常的なクロックドリフトのみを補正する)。
  • 3 か所(raw static / s_haptic_base_* / audioStreamSetBufferMs(0) 分岐)の cold-boot 一貫性を CLIP_DEFAULT_* 定数の単一ソースに集約。
  • set_stream_buffer(ms>0) の明示設定(浅い精密ペーシング用)・wire/protocol は 無変更。ESP-NOW 経路は起動時に audioStreamSetLowLatency() で常に上書きされる ため無影響。

追加 — ESP-NOW 受信機の省電力オンデマンド UI (DEC-033)

Section titled “追加 — ESP-NOW 受信機の省電力オンデマンド UI (DEC-033)”

ESP-NOW stream 受信機(necklace_v3_stream_espnow)に、Wi-Fi/UDP 機とは別系統の 省電力 UI を実装。60 台 2 時間のウェアラブル運用を想定し、表示・電力を最小化する。 contracts/specs/serial-config.md §4.19 準拠。

  • OLED 常時消灯 + LED 常時 offボタン押下 or ボリューム変更で、低輝度の 音量オーバーレイ(全幅 20+ 段セグメントバー + レベル + 電池% + FW)を数秒表示後、 自動スリープ。スリープは SSD1306 DISPLAYOFF コマンドのみ(EN 電源は切らず、 瞬時・再 init 不要で復帰)。
  • 固定レイアウト(Studio の display grid 編集対象外)。表示は drawEspnowScreen で bespoke 描画。輝度の既定は espnow ビルドのみ低(level 1)。
  • 挙動ポリシーを serial 設定化set_espnow_ui、NVS espnow_ui 永続): auto_off_ms / wake_on_button / wake_on_volume / led_enabled / low_batt_pct。 輝度は既存 set_oled_brightness。低電池(既定 ≤15%)で表示を自動点灯(dither 防止のヒステリシス付き)。
  • stream デバッグ readout: get_info の stream object でライブ統計(損失率・ 最大連続欠落・piggyback 復元・drop・handoff・lock 状態・推定遅延)を返す。 RSSI は legacy callback で取れないため損失率を強度の代理指標にする。
  • Wi-Fi/UDP ビルドの UI は無改変(専用 taskUIEspnow#ifdef で分離)。

修正 — ESP-NOW 受信の遅延を参照 firmware 並みに短縮

Section titled “修正 — ESP-NOW 受信の遅延を参照 firmware 並みに短縮”

受信→再生のレイテンシが体感で「ワンテンポ」遅かったのを根治。原因は audio が loop()(最低優先度)にあり、それを誤魔化すための大 DMA(4×128=512 frame=32ms)。 参照 firmware(hapbeat-wireless-firmware)の構成に整合させた(espnow ビルドのみ):

  • audio を 専用高優先度タスク taskAudioEspnow(prio 19, Core 1)に分離。loop() から audioPlayerUpdate() を撤去(serial/UI と別経路に)。
  • DMA を 2×16=32 frame=2ms、mix 粒度を 32 frame(2ms)、ring prebuffer を 32 frame に縮小(いずれも espnow のみ gate、Wi-Fi/UDP は 32ms DMA のまま無改変)。
  • stream 受信機は 0xAA 音声のみ処理(固定長コマンド経路を排除)→ 最短経路 + g_voices 競合の排除。受信→スピーカ ~40-50ms → ~6-8ms(参照 firmware 並み)。

改良 — ESP-NOW UI の細部(実機調整)

Section titled “改良 — ESP-NOW UI の細部(実機調整)”
  • 自動消灯既定を 4s → 8s に延長。
  • ボリュームウェイク閾値 vol_wake_step(既定 1、≥1)でノブ揺れを無視可能に。
  • ボリューム段数 volume_steps(1〜64)を serial 設定化 + NVS 永続化 (volume_control 自体は永続しないため espnow_ui が保持)。
  • 既定画面 2 行目を VOL n BAT xxx% に(FW 版は既定画面から削除)。
  • いずれかのボタン長押し中だけデバッグ画面(FW 版 + ch/loss%/sources/delay)。
  • 競合修正(self-review): serial 起点 wake の s_display 競合を UI タスクへ遅延、 FW 版表示の truncation 修正。get_info の serial 応答は実機で全コマンド確認済み。

追加 — ESP-NOW ストリーミング受信のマルチソース化 (DEC-033)

Section titled “追加 — ESP-NOW ストリーミング受信のマルチソース化 (DEC-033)”

会場同報(PA ライブ音声を ESP-NOW broadcast → 観客の Hapbeat が同時触覚)の 受信機(necklace_v3_stream_espnow)を、複数 transmitter(音声ソース機 + リピータ機)構成に対応させた。contracts/specs/espnow-stream.md §3.2 / §7 準拠。

  • piggyback 単発ロス回復(§3.2): seq 欠番 1 個 + piggyback prev_seq 一致で 欠落パケットを継ぎ目なく復元。2 個以上の欠番は有界の無音(最大 64 frame)で埋める。
  • マルチソース MAC ロック選択(§7.1): 複数送信元 MAC を同時受信しても 常に 1 つにロックして再生(ミキシング禁止 = 二重再生防止)。最先着ロック + ロック喪失(150ms 途絶)で次の生存ソースへ切替、切替時 3ms クロスフェード。 RSSI ヒステリシス経路を実装(arduino-esp32 2.0.x の legacy callback は RSSI 非提供のため現状は inert、将来の toolchain で「最強ロック」が有効化)。
  • 低遅延ピン留め: ESP-NOW 経路は audio_stream の opt-in 低遅延モードで ring を ~8ms に保ち、超過分を破棄して遅延累積を防止(UDP 経路は無改変)。
  • 純 ESP-NOW 運用: stream 受信ビルドは Wi-Fi STA に接続しない(同チャンネル 同居で >80% ロスする問題を回避、§5)。設定は USB serial 経由。
  • StreamStats: 損失率 / 最大連続欠落 / piggyback 復元数 / drop / handoff / 推定遅延を get_info(stream_*)と定期 serial ログで取得(DEC-033 検証用)。

v0.1.x(UDP 受信機のみ)から、施設アラート用途・通信モード別ノード・周辺機器・ legacy ハードまでをカバーする大型リリース。Studio はこの Release を集約して ファームウェアライブラリに表示する。

追加 — 施設アラート (facility-alert / MQTT)

Section titled “追加 — 施設アラート (facility-alert / MQTT)”
  • MQTT 経路のアラート配信: sensor → broker → 受信機の end-to-end。
  • アラート episode の aid lifecycle(同一 aid 再送は 1 回のみ作用 / 新 aid で再アラート)、 OLED 永続表示(ack まで保持)、意図的 button-ack(release→約 1s hold で誤停止防止)、 receiver multi-topic 購読(recv_topics)。
  • restricted / critical alert mode(critical payload + receiver 制限トグル)、 CUSTOM_TEXT / ALERT_LIMIT_MODE / mqtt_status の OLED 表示要素。

追加 — ノード役割・通信モード (DEC-034)

Section titled “追加 — ノード役割・通信モード (DEC-034)”
  • 役割アプリ: MQTT broker / color-sensor sender(M5 ATOM Lite, classic ESP32)。
  • 受信機 transport: MQTT receiver、ESP-NOW audio stream receiver。
  • get_info で role / transport / board を報告。ビルドは variant.json + manifest fragment を出力。

追加 — ハードウェアバリアント

Section titled “追加 — ハードウェアバリアント”
  • BandWL v2 (legacy) 対応: 2 ボタン(中央 SW4=GPIO0/BOOT は実行時入力に使用不可)、 MCP4018 なしのため PAM8003 VOLUME を PWM 駆動、BQ27220 自動検出。
  • band_v2 / band_v3 / band_v4 の MQTT receiver env。
  • loopTask スタックオーバーフロー根治: フル ui-config 書き込み時の reboot を解消。 volume_changed broadcast を同期呼び出しから loop() へ遅延、applyUiConfig の display-layout ディープコピーを直接 serialize に置換、ARDUINO_LOOP_STACK_SIZE=16384。
  • broker mDNS 再解決をループから分離(他通信のスタベーション根治)。
  • device_name 表示 variant を 4 サイズ(4/5/8/16)化し既定幅 8→5(DuoWL レイアウト整合)。
  • 開発中(dirty tree)は tag 上でも dN を付与する版管理修正。

Bug fix: TCP / OTA 安定性の 2 段階修正

Section titled “Bug fix: TCP / OTA 安定性の 2 段階修正”

リリース v0.1.0 で発覚した「OTA を連続実行すると詰まる + 途中で json parse error: InvalidInput が出る」現象を完全に解消。

1. lwIP socket fd リーク (s_client.stop() 漏れ)

  • processOtaData() の disconnect / validation 失敗 / write エラー / finalize 失敗 / boot-partition flip 失敗の 全 return パスs_client.stop() を呼ぶよう修正
  • accept displacement で s_client.stop()unconditional 化 (!connected() でも fd を release)
  • OTA 中に accept queue を drain (helper の log_tail supervisor が 2s 周期で投げてくる SYN が lwIP socket pool を枯渇させていた)
  • netstatFIN_WAIT_2 が累積する症状を解消

2. cmdOtaBegin 後の dispatch race

  • tcpServerUpdate() の JSON dispatch while ループが cmdOtaBegin 後の binary mode 遷移を尊重せず、helper の第 1 chunk バイトを JSON として誤食する race condition を修正
  • processLine 後に s_ota_active || s_kit_active || s_file_active をチェックして即 return。次の tcpServerUpdate() で適切な process*Data() short-circuit に入る
  • OTA / kit install / file transfer の 3 経路すべてで同時にカバー

影響範囲: OTA / kit install / file transfer を行うすべてのデバイス。v0.1.0 ユーザーは v0.1.1 への更新を強く推奨。

(開発中は 0.1.0→0.1.1→0.1.2 と 2 段階で bump したが、リリース時に 0.1.1 に集約)

Tooling: バージョン違いを物理的に防ぐビルド検証 (CI / post-build)

Section titled “Tooling: バージョン違いを物理的に防ぐビルド検証 (CI / post-build)”
  • merge_firmware.py post-build に「ソース FIRMWARE_VERSION が生成バイナリ内のバイト列に含まれているか」検証を追加。stale build を即 fail させる。
  • .github/workflows/release.yml を改修: env 名を実在値 (necklace_v3 / band_v3 / band_v4) に修正、master push でもビルド + artifact 保存 (10 日)、tag push 時のみ GitHub Release publish。

(firmware バイナリ自体の機能変更は無し — bump rule に従う patch +1)

UX: Wi-Fi 選択画面のレイアウト改善

Section titled “UX: Wi-Fi 選択画面のレイアウト改善”
  • drawWifiSelect() を改修。SSID をできる限り長く表示するため:
    • Line 1 (y=0): SSID の先頭 16 文字(旧: SSID: プレフィックスで 11 文字しか入らなかった)
    • Line 2 (y=16): SSID の 17 文字目以降を左詰め + "N/M" カウンターを右詰めで配置
    • 最大約 27 文字の SSID が表示可能。"N/M" は文字幅に応じて x 座標を動的計算(5/12 でも 1/3 でも常に右端揃え)
  • “Exit” エントリは Line 1 に “Exit”、Line 2 右下に "N/M" のみ。

Bug fix: OTA rollback バグの根治 (Update.end 失敗時の force-set を撤去)

Section titled “Bug fix: OTA rollback バグの根治 (Update.end 失敗時の force-set を撤去)”
  • 真因: Update.end(true) が false を返した際 (MD5/SHA256 検証失敗) に無条件で esp_ota_set_boot_partition() を呼んでいた。bootloader が起動時に image SHA256 を弾いて旧スロットに rollback → 「古い版で起動」の root cause だった。
  • 修正: Update.end(true) 失敗時は force-set を一切行わず、旧スロットを維持してエラー報告のみ。Studio から即再試行可能。Update.end(true) 成功時のみ boot partition mismatch チェック + force-set を行う (image 検証通過済みなので rollback リスクなし)。
  • magic byte 検証: 受信開始チャンクで chunk[0] == 0xE9 (ESP32 image magic) + chunk[12] == 0x09 (ESP32-S3 chip ID) を検証。bootloader.bin など誤ったファイルを送ると即停止し partition を汚さない。
  • サイズ検証: cmdOtaBegin で OTA サイズが 100KB 未満 / partition サイズ超過なら拒否。
  • s_ota_first_chunk_validated を全 error / disconnect / reconnect path で確実にリセット。

Feature: Hold 発火予告を線形 fade-out に変更 + brightness 独立化

Section titled “Feature: Hold 発火予告を線形 fade-out に変更 + brightness 独立化”
  • Hold 予告 LED を固定色 flash → 線形 fade-out に刷新。押下 hold_feedback_start_ms から hold_ms にかけて feedback_color × brightness × (1 - progress) で 0 まで落ちる。
  • ledControllerSetFadeOverride(r, g, b, brightness, progress) / ledControllerClearFadeOverride() を新設(fade は flash より高優先)。
  • hold 発火時 / ボタン release 時に ledControllerClearFadeOverride() を呼び rule engine に即復帰。
  • hold_feedback_brightness (default 30, 0–255) を button_handler + applyUiConfig に追加(色と輝度を独立調整可能に)。
  • デフォルト色 {0,30,30}{0,255,255} (純 cyan) に変更(brightness=30 で実際の輝度は約12%)。

Feature: Hold 発火予告のタイミング・色を ui-config で可変に

Section titled “Feature: Hold 発火予告のタイミング・色を ui-config で可変に”
  • hold_feedback_start_ms (default 300ms) と hold_feedback_color (default [0,30,30]) を applyUiConfigui セクションで受け入れ。
  • button_handlerbuttonSetHoldFeedbackStartMs() / buttonHoldFeedbackStartMs() / buttonSetHoldFeedbackColor() / buttonHoldFeedbackColor() を追加。
  • main.cpp の hold pending 処理を runtime 値 (buttonHoldFeedbackStartMs() / buttonHoldFeedbackColor()) で書き換え。旧 HOLD_FEEDBACK_MS=500 const を撤廃。

UX: LED リフレッシュレート 5Hz → 30Hz(fade 滑らか化)

Section titled “UX: LED リフレッシュレート 5Hz → 30Hz(fade 滑らか化)”
  • main.cpp の LED update 間隔を 200ms → 33ms に変更(ledControllerUpdate() 呼出し頻度 5Hz → 30Hz)。
  • app_connected breathe や battery_low fade のサイン波が階段状に見えた問題を解消。
  • 変数名 last_battery_checklast_led_update に改名(誤称の修正)。

Bug fix: LED 設定が Wi-Fi 接続後に無効化される問題を修正

Section titled “Bug fix: LED 設定が Wi-Fi 接続後に無効化される問題を修正”
  • checkWifiServices() 内で Wi-Fi 接続確立 / 切断時に s_led[0] を直書き + FastLED.show() していたため、毎ループ Studio の LED config が上書きされていた。該当 2 箇所を削除し、LED 状態管理を ledControllerUpdate() の rule engine に完全委譲。
  • 起動直後 (Wi-Fi 未接続) は正常で、Wi-Fi 接続後のみ色が固定されていた症状と一致。

Feature: ledControllerInit() デフォルト rules を Studio DEFAULT_LED_RULES (9 件) に揃える

Section titled “Feature: ledControllerInit() デフォルト rules を Studio DEFAULT_LED_RULES (9 件) に揃える”
  • 旧: battery_critical / battery_low / app_connected / idle_fix / idle_volume の 5 件のみ。idle_wifi / wifi_disconnected / volume_mute / always が欠けていたため、ui-config deploy 前は LED が反応しない領域があった。
  • 新: priority 1–9 の 9 ルール (Studio と同一セット)。idle_wifi=dim green / wifi_disconnected=dim yellow / volume_mute=dim red / always=off を追加。

Feature: applyUiConfig ログを TCP log stream に tee

Section titled “Feature: applyUiConfig ログを TCP log stream に tee”
  • [UiConfig] sections: display=N led=N volume=N ui=N および [UiConfig] led.rules=N global=NNN を Serial + tcpLogSend 両方に出力。Studio LogDrawer から apply 状況を直接確認できるようになった。
  • ledControllerLoadConfig[LED] Loaded N rule(s) も tcpLogSend に tee。

Feature: ui-config セクション (hold_ms / oled_brightness / hold_show_oled_indicator) 受け入れ

Section titled “Feature: ui-config セクション (hold_ms / oled_brightness / hold_show_oled_indicator) 受け入れ”
  • applyUiConfigconfig.ui セクション処理を追加。Studio UI 設定モーダルから hold 時間・輝度・OLED インジケーターを runtime 変更可能に。
  • buttonSetHoldFireMs(ms) / buttonHoldFireMs() / buttonSetHoldShowOledIndicator(bool) / buttonHoldShowOledIndicator()button_handler に追加。

UX: Hold タイミング変更 + OLED “Hold…” インジケーターをデフォルト無効化

Section titled “UX: Hold タイミング変更 + OLED “Hold…” インジケーターをデフォルト無効化”
  • HOLD_FEEDBACK_MS: 200ms → 500ms(LED 点滅開始タイミング)
  • hold_fire_ms デフォルト: 400ms → 1000ms(短押し誤発火を解消)
  • OLED “Hold…” 表示をデフォルト無効 (hold_show_oled_indicator: false)。LED 点滅のみで通知するため、短押し直後の pos/group 切替表示が遮られなくなった。

UX: OLED コントラスト Mid / High の差を拡大

Section titled “UX: OLED コントラスト Mid / High の差を拡大”
  • displaySetBrightness(): mid の contrast 値を 0x80 (50%) → 0x40 (25%) に変更。High=0xFF との比が 2× → 4× になり、肉眼で明確に差がわかるようになった。

UX: FW_VERSION を右詰め描画に変更

Section titled “UX: FW_VERSION を右詰め描画に変更”
  • 任意幅の FW_VERSION スロットに対応:
    • n ≤ w: 左側スペース pad で右詰め (例 width=8 で " v0.0.14")
    • n > w: 右端 w 文字を残して左を truncate (例 width=6 で "v0.0.13""0.0.13")
  • 6-cell の旧レイアウトでも patch 番号を残せるようになる。

UX: FW_VERSION 表示桁を 6 → 8 に拡張

Section titled “UX: FW_VERSION 表示桁を 6 → 8 に拡張”
  • patch-only バージョンスキームで 2 桁 patch (v0.0.10 以降 7 文字) が 6 セルに収まらず “v0.0.1” と truncate されていた問題を修正。
  • default_size[8, 1] に変更、renderer は elem.width を honor。
  • v0.0.123 / v9.99.9 / v10.10.5 まで余裕を持って表示できる。

Bug fix: OTA 後 otadata 切替失敗の防御的対応

Section titled “Bug fix: OTA 後 otadata 切替失敗の防御的対応”
  • Update.end(true) 成功後に boot partition を明示検証し、期待と不一致ならば esp_ota_set_boot_partition() で強制上書き。症状: Update.end() が true を返すが otadata が書き込まれず再起動後に旧バージョンのまま残る。
  • pre/post finalize の partition 状態ログを拡充 ([OTA] Pre-finalize: running=? target=? / Post-finalize: boot=? expected ? / [OTA] Success! boot=? — rebooting)。

Feature: hold 閾値延長 (150ms → 400ms) + 待機フィードバック

Section titled “Feature: hold 閾値延長 (150ms → 400ms) + 待機フィードバック”
  • hold 発火閾値を 150 ms → 400 ms に延長(意図しない hold 誤発動を解消)。
  • 待機ウィンドウ (200–400 ms): OLED に "Hold..." toast を表示 + LED を dim cyan でフラッシュ。「今 hold 中、もう少しで発動」の可視化。
  • ledControllerFlash(r, g, b, duration_ms) を新設(一時的な色オーバーライド API)。ルール優先度より常に上位。
  • long_press (1000 ms) と AP combo (3000 ms) には影響なし。

Feature: display 要素の variant 4/8/16 対応 + address prefix-only

Section titled “Feature: display 要素の variant 4/8/16 対応 + address prefix-only”
  • address 要素: /player_ の前半 prefix のみを表示するように変更(MyHapbeatGroup/player_5/pos_r_armMyHapbeatGroup)。variant 4/8/16 でそれぞれ左切出し + 右パディング。
  • wifi_ssid: variant 4/8/16 で左切出し + 右パディング。
  • ip_address: variant 4/6/13 で 右切出し + 左パディング(末尾オクテット重視)。
  • position: NVS の pos_xxx 名を表示する仕様に変更。compact=4 (“l_wr”), standard=8 (“pos:l_wr”), wide=16 (“pos: l_wrist____”)。
  • display_layout_loader"wide" variant パーサを追加(既存の compact / percent / bar と並列)。DisplayElement::variant の意味を 0=standard / 1=compact / 2=wide に拡張。
  • getDefaultSize 更新: ip_address 15→6 / position 7→8 / address 10→8。
  • element-registry.jsondefault_size も追従。

Feature: button action position_inc/decgroup_inc/dec に置換

Section titled “Feature: button action position_inc/dec を group_inc/dec に置換”
  • actionPositionInc / actionPositionDec を削除し、actionGroupInc / actionGroupDec を追加(getDeviceGroup() ± 1、wrap 0–255、NVS group_id 即時保存)。
  • s_action_table / legacy aliases / NECKLACE デフォルト割当 / 3-button fix mode を group ベースに更新。
  • element-registry.jsonbutton_actionsdevice_layouts.necklace_v3group_inc/dec に置換。
  • position_inc/dec を含むレイアウトは unknown action として黙ってスキップされるので壊れない(既存の handler 仕様)。

Bug fix: 初回 deploy 失敗の根本対応(TCP 7701 single-slot pre-empt)

Section titled “Bug fix: 初回 deploy 失敗の根本対応(TCP 7701 single-slot pre-empt)”
  • tcpServerUpdate の accept ロジックを変更: 既存 s_client長期セッション中でなく 1.5s 以上 idle の場合、新規接続が来たら既存を pre-empt して新規を即 accept する。
  • 症状: Wi-Fi 接続済み Hapbeat に 後から Helper を起動すると、起動直後の LAN scanner / mDNS conformance test 等が TCP 7701 を「stuck connected」させ、Helper deploy が IDLE_TIMEOUT 5 秒経過まで失敗する。
  • これにより Helper 側の RECOVERY_SLEEP=6.0s workaround は撤去可能。
  • NVS フラグ + restart 切替 で boot 時に STA / AP モードを選択(hot switch しない)。Arduino-ESP32 の channel jump や ESP-NOW peer state 残留問題を構造的に回避。
  • AP SSID は MAC ベース自動生成: Hapbeat-XXXXXX(MAC 末尾 6 桁 hex)。
  • AP password は NVS ap_pass: デフォルトオープン(pass 無し)、Studio から set_ap_pass で WPA2 設定可能(8-63 chars)。
  • mDNS は STA と共通: _hapbeat._tcp をそのまま AP IP (192.168.4.1) で publish。
  • 自動 timeout: STA クライアント associated している間は無期限。全 disconnect 後 10 分で STA mode に restart。
  • ボタン combo で双方向切替:
    • BAND v3 / v4: SW0 + SW2 を 3 秒同時長押し
    • NECKLACE v3: SW0 + SW4 を 3 秒同時長押し
    • 押下中は OLED にカウントダウン表示(AP in 3...)。combo 中は対象ボタンの long_press / hold アクションは抑制。
  • 新コマンド (TCP / Serial 共通):
    • enter_ap_mode / enter_sta_mode: NVS フラグを書いて restart
    • set_ap_pass {pass}: WPA2 pass 設定(空文字で open に戻す)
    • clear_ap_pass: open AP に戻す
    • get_ap_status: 現在のモード / SSID / IP / has_pass / client_count
  • get_info 拡張: mode (“sta” | “ap”) + AP mode 時は ap_ssid / ap_ip / ap_has_pass / ap_client_count を追加。
  • LED: AP モード中は magenta (5, 0, 5)。
  • AP モード中は ESP-NOW 無効(MVP では co-existence 未実装)。AP channel 固定なので将来追加可能。
  • Studio / Helper 側 UI 対応は別指示書 (hapbeat-studio/instructions/instructions-softap-mode-202605071800.md)。
  • app_name 要素の未接続 placeholder を幅対応に変更(旧: ---- / -- のみ)。
    • width 16: App: unconnected(16 文字 exact)
    • width 8: App N.C.(8 文字)
    • width 4: N.C.(4 文字)
    • width 1-3: --
    • 4 / 8 / 16 がレイアウトプリセット幅。中間幅は次に小さい bucket に fallback。
  • ディスプレイレイアウトに app_name 要素を追加(DEC-029)。Unity SDK が CONNECT_STATUS で送ってくる appName を独立要素として配置可能になった。device_name の隣に Unity アプリ名を表示するなどのレイアウトが可能。
    • 未接続 / タイムアウト時は -- (compact) または ---- を表示。
    • デフォルトサイズは 8x1(contracts spec 準拠)。"size": [16, 1] 等で 16 文字まで拡張可能。
  • 副作用: connection_status 要素から appName 埋め込みを削除(責務分離)。標準時 [OK]<appname>[OK] のみ表示。Wi-Fi 接続中で Hapbeat client から CONNECT_STATUS が来ていない場合の表示を [OK][--] に修正(意味的に正しい)。
  • TCP 7701 の障害・復旧状態を OLED に表示するようにした。
    • watchdog 発火(listen socket が runtime に落ちた場合)で "TCP LOST" を表示(tcpServerUpdate() 内)。
    • tcpServerStart() が 3 回以上連続失敗した場合に "TCP retry N..." を表示(checkWifiServices() 内)。
    • 障害後に TCP が正常起動した場合に "TCP OK" を表示(fail_count > 0 時のみ、初回起動では表示しない)。
  • TCP 7701 が起動時にランダムで listen していない問題を修正(電源 OFF→ON で復旧していた症状)。
    • WiFiServer::begin() は socket/bind/listen 失敗時に silent に return し _listening = false のまま戻るが、tcpServerStart() は結果をチェックせず常に true を返していたため、checkWifiServices の retry ロジックが効かなかった。operator bool() で listen 状態を検証して失敗時 false を返すように修正。
    • begin() 直前に s_server.end() を defensive に呼び、過去の半端な socket fd の leak を解消。
    • tcpServerUpdate() に listen socket dropped watchdog を追加(runtime に socket が剥がれたら tcpServerStop() 経由でクリーンに停止し、main の retry loop で自動再 bind)。watchdog 発火時に s_client.stop() も呼ぶことで orphan client が次回 accept をブロックする問題を回避。
    • tcpServerStop()Update.abort() を追加(OTA 進行中に Wi-Fi 切断 / watchdog 発火で停止した場合、staging partition の lock を解放して次回 OTA を成功させる)。
    • main.cpp::checkWifiServices()s_tcp_startedtcpServerIsRunning() と同期するように修正(watchdog 検出後の再 bind を可能にする)。
  • Wi-Fi 接続成功 / 切断 / 再接続失敗 / プロファイル切替の OLED メッセージで SSID をフル表示するように変更(IP アドレス削除、ssidShort ヘルパ撤去)。OLED の wrap で 2 行目まで使い、長い SSID も識別しやすくなる。
    • 旧: WiFi OK <ssid5> <ip> / WiFi LOST <ssid5> / WiFi FAIL <ssid5> retry Ns / WiFi -> <ssid5> ...
    • 新: WiFi OK <ssid> / WiFi LOST <ssid> / WiFi FAIL <ssid> retry Ns / WiFi -> <ssid>
  • list_wifi_profiles レスポンスから pass(平文パスワード)フィールドを削除。代わりに has_pass(boolean)のみ返す write-only 設計に変更。TCP 7701 は認証なしで LAN に開いているため、平文パスワードを返すことで同一 Wi-Fi 上の任意のデバイスからパスワードが取得可能だった(TCP / Serial 両経路で対応)。
  • 配布物を 2 ファイル化(firmware_app_ota.bin / firmware_full_serial.bin
  • GitHub Actions firmware release workflow + manifest 生成スクリプト
  • cmdScanWifi 追加、NVS 保護 split flash、TCP 再試行リファクタ
  • Initial release