diff --git a/.github/actions/setup-build-environment/action.yml b/.github/actions/setup-build-environment/action.yml index cba2d2e562..e292434b69 100644 --- a/.github/actions/setup-build-environment/action.yml +++ b/.github/actions/setup-build-environment/action.yml @@ -4,7 +4,7 @@ runs: steps: - name: Init Cache - uses: actions/cache@v5 + uses: actions/cache@v4 with: path: | ~/.cache/pip @@ -12,9 +12,9 @@ runs: key: ${{ runner.os }}-pio - name: Install Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v5 with: - python-version: '3.13' + python-version: '3.11' - name: Install PlatformIO shell: bash diff --git a/.github/workflows/jp-release.yml b/.github/workflows/jp-release.yml new file mode 100644 index 0000000000..5436200814 --- /dev/null +++ b/.github/workflows/jp-release.yml @@ -0,0 +1,174 @@ +name: Build JP Firmwares + +permissions: + contents: write + +on: + workflow_dispatch: + push: + tags: + - 'jp-v*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Clone Repo + uses: actions/checkout@v4 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Build JP Firmwares + env: + FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION || 'dev' }} + + PLATFORMIO_BUILD_FLAGS: >- + -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 + -D LORA_FREQ=920.800 + -D LORA_BW=125.0 + -D LORA_SF=10 + -D LORA_CR=6 + -D 'ALLOWED_REPEAT_FREQ_RANGE={921200,921200}' + -D ENABLE_ADVERT_ON_BOOT=1 + -D ENABLE_PRIVATE_KEY_IMPORT=1 + -D ENABLE_PRIVATE_KEY_EXPORT=1 + -D RADIOLIB_EXCLUDE_CC1101=1 + -D RADIOLIB_EXCLUDE_RF69=1 + -D RADIOLIB_EXCLUDE_SX1231=1 + -D RADIOLIB_EXCLUDE_SI443X=1 + -D RADIOLIB_EXCLUDE_RFM2X=1 + -D RADIOLIB_EXCLUDE_SX128X=1 + -D RADIOLIB_EXCLUDE_AFSK=1 + -D RADIOLIB_EXCLUDE_AX25=1 + -D RADIOLIB_EXCLUDE_HELLSCHREIBER=1 + -D RADIOLIB_EXCLUDE_MORSE=1 + -D RADIOLIB_EXCLUDE_APRS=1 + -D RADIOLIB_EXCLUDE_BELL=1 + -D RADIOLIB_EXCLUDE_RTTY=1 + -D RADIOLIB_EXCLUDE_SSTV=1 + + run: | + mkdir -p out_jp + # JP TX power制限 + # Wio-SX1262搭載機(工事設計認証201-250230、証明出力10dBm)は10dBmに制限 + sed -i 's/-D LORA_TX_POWER=22/-D LORA_TX_POWER=10/g' \ + variants/xiao_nrf52/platformio.ini \ + variants/xiao_s3_wio/platformio.ini \ + variants/wio-tracker-l1/platformio.ini + # T1000-E: Seeed確認済み証明上限19mW(≈12.79dBm)のため12dBmに制限 + sed -i 's/-D LORA_TX_POWER=22/-D LORA_TX_POWER=12/g' \ + variants/t1000-e/platformio.ini + # それ以外はARIB STD-T108上限の13dBmのまま + sed -i 's/-D LORA_TX_POWER=22/-D LORA_TX_POWER=13/g' \ + variants/rak_wismesh_tag/platformio.ini \ + variants/lilygo_techo_lite/platformio.ini \ + variants/heltec_t114/platformio.ini \ + variants/m5stack_unit_c6l/platformio.ini \ + variants/lilygo_tdeck/platformio.ini + bash build.sh build-firmware RAK_WisMesh_Tag_companion_radio_ble + bash build.sh build-firmware RAK_WisMesh_Tag_companion_radio_usb + bash build.sh build-firmware RAK_WisMesh_Tag_repeater + bash build.sh build-firmware RAK_WisMesh_Tag_room_server + bash build.sh build-firmware t1000e_companion_radio_ble + bash build.sh build-firmware t1000e_companion_radio_usb + bash build.sh build-firmware t1000e_repeater + bash build.sh build-firmware t1000e_room_server + bash build.sh build-firmware LilyGo_T-Echo-Lite_companion_radio_ble + bash build.sh build-firmware LilyGo_T-Echo-Lite_repeater + bash build.sh build-firmware LilyGo_T-Echo-Lite_room_server + bash build.sh build-firmware Xiao_nrf52_companion_radio_ble + bash build.sh build-firmware Xiao_nrf52_companion_radio_usb + bash build.sh build-firmware Xiao_nrf52_repeater + bash build.sh build-firmware Xiao_nrf52_room_server + bash build.sh build-firmware Xiao_S3_WIO_companion_radio_ble + bash build.sh build-firmware Xiao_S3_WIO_companion_radio_usb + bash build.sh build-firmware Xiao_S3_WIO_repeater + bash build.sh build-firmware Xiao_S3_WIO_room_server + bash build.sh build-firmware Heltec_t114_without_display_companion_radio_ble + bash build.sh build-firmware Heltec_t114_without_display_companion_radio_usb + bash build.sh build-firmware Heltec_t114_without_display_repeater + bash build.sh build-firmware Heltec_t114_without_display_room_server + bash build.sh build-firmware Heltec_t114_companion_radio_ble + bash build.sh build-firmware Heltec_t114_companion_radio_usb + bash build.sh build-firmware Heltec_t114_repeater + bash build.sh build-firmware Heltec_t114_room_server + bash build.sh build-firmware WioTrackerL1_companion_radio_ble + bash build.sh build-firmware WioTrackerL1_companion_radio_usb + bash build.sh build-firmware WioTrackerL1_repeater + bash build.sh build-firmware WioTrackerL1_room_server + bash build.sh build-firmware M5Stack_Unit_C6L_companion_radio_ble + bash build.sh build-firmware M5Stack_Unit_C6L_companion_radio_usb + bash build.sh build-firmware M5Stack_Unit_C6L_repeater + bash build.sh build-firmware M5Stack_Unit_C6L_room_server + bash build.sh build-firmware LilyGo_TDeck_companion_radio_ble + bash build.sh build-firmware LilyGo_TDeck_companion_radio_usb + bash build.sh build-firmware LilyGo_TDeck_repeater + + - name: Copy artifacts to out/ + run: | + ls -la .pio/build/*/firmware.uf2 2>/dev/null || echo "no uf2 found" + mkdir -p out_jp + HASH=$(git rev-parse --short HEAD) + TAG=$(git describe --tags --exact-match 2>/dev/null || echo "dev") + for ENV in RAK_WisMesh_Tag_companion_radio_ble RAK_WisMesh_Tag_companion_radio_usb RAK_WisMesh_Tag_repeater RAK_WisMesh_Tag_room_server t1000e_companion_radio_ble t1000e_companion_radio_usb t1000e_repeater t1000e_room_server LilyGo_T-Echo-Lite_companion_radio_ble LilyGo_T-Echo-Lite_repeater LilyGo_T-Echo-Lite_room_server Xiao_nrf52_companion_radio_ble Xiao_nrf52_companion_radio_usb Xiao_nrf52_repeater Xiao_nrf52_room_server Heltec_t114_without_display_companion_radio_ble Heltec_t114_without_display_companion_radio_usb Heltec_t114_without_display_repeater Heltec_t114_without_display_room_server Heltec_t114_companion_radio_ble Heltec_t114_companion_radio_usb Heltec_t114_repeater Heltec_t114_room_server WioTrackerL1_companion_radio_ble WioTrackerL1_companion_radio_usb WioTrackerL1_repeater WioTrackerL1_room_server; do + + NAME="${ENV}-${TAG}-${HASH}" + cp ".pio/build/${ENV}/firmware.uf2" "out_jp/${NAME}.uf2" 2>/dev/null || true + cp ".pio/build/${ENV}/firmware.zip" "out_jp/${NAME}_dfu.zip" 2>/dev/null || true + done + # ESP32系は.binと-merged.binをコピー + for ENV in Xiao_S3_WIO_companion_radio_ble Xiao_S3_WIO_companion_radio_usb Xiao_S3_WIO_repeater Xiao_S3_WIO_room_server M5Stack_Unit_C6L_companion_radio_ble M5Stack_Unit_C6L_companion_radio_usb M5Stack_Unit_C6L_repeater M5Stack_Unit_C6L_room_server LilyGo_TDeck_companion_radio_ble LilyGo_TDeck_companion_radio_usb LilyGo_TDeck_repeater; do + NAME="${ENV}-${TAG}-${HASH}" + cp ".pio/build/${ENV}/firmware.bin" "out_jp/${NAME}.bin" 2>/dev/null || true + cp ".pio/build/${ENV}/firmware-merged.bin" "out_jp/${NAME}-merged.bin" 2>/dev/null || true + done + ls -la out_jp/ + + - name: Upload Workflow Artifacts + uses: actions/upload-artifact@v4 + + with: + name: jp-firmwares + path: out_jp + + - name: Create Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + name: JP Firmware ${{ env.GIT_TAG_VERSION }} + generate_release_notes: true + body: | + ## 日本向け MeshCore ファームウェア + + ### 対応デバイス + | デバイス | ファイル | 書き込み方法 | + |---|---|---| + | WisMesh Tag | `RAK_WisMesh_Tag_companion_radio_ble-*.uf2` | UF2ドラッグ&ドロップ | + | T1000-E | `t1000e_companion_radio_ble-*.uf2` | UF2ドラッグ&ドロップ | + | T-Echo Lite | `LilyGo_T-Echo-Lite_companion_radio_ble-*.uf2` | UF2ドラッグ&ドロップ | + | XIAO nRF52840 | `Xiao_nrf52_companion_radio_ble-*.uf2` | UF2ドラッグ&ドロップ | + | Heltec T114 (ディスプレイなし) | `Heltec_t114_without_display_companion_radio_ble-*.uf2` | UF2ドラッグ&ドロップ | + | Heltec T114 (ディスプレイあり) | `Heltec_t114_companion_radio_ble-*.uf2` | UF2ドラッグ&ドロップ | + | Wio Tracker L1 Pro | `WioTrackerL1_companion_radio_ble-*.uf2` | UF2ドラッグ&ドロップ | + | M5Stack Unit C6L | `M5Stack_Unit_C6L_companion_radio_ble-*-merged.bin` | esptool書き込み | + | T-DECK | `LilyGo_TDeck_companion_radio_ble-*-merged.bin` | esptool書き込み | + + > ⚠️ Wio Tracker L1 Pro / T-DECK はCIビルド確認のみです。動作報告歓迎します。M5Stack Unit C6Lは起動時クラッシュループの原因を特定・修正しました(実機はflash_mode=QIO非対応、配布バイナリのヘッダをDIOに変更)。実機での動作を確認済みです。 + + 以上の表に載っていないデバイスや、リピーター、ルームサーバーもリリースしていますが、実験的です。 + + ### 日本での運用設定 + フラッシュ後、スマホアプリで以下を確認してください: + - 周波数: `920.8 MHz` + - TX出力: `13 dBm以下`(XIAO nRF52840/XIAO ESP32S3+Wio-SX1262/Wio Tracker L1 Proは`10 dBm以下`、T1000-Eは`12 dBm以下`) + - BW: `125 kHz` / SF: `10` / CR: `4/6` + + > ℹ️ Wio-SX1262搭載機(XIAO nRF52840/ESP32S3+Wio-SX1262、Wio Tracker L1 Pro)はモジュール単体で技適取得済み(工事設計認証201-250230、Seeed社確認済み・書面回答2026/7/20)。ただし証明出力は10dBmのみのため、本ビルドは当該機種のTX出力のデフォルトを10dBmに設定しています。アンテナは付属の低利得アンテナ(FPCまたはSpring)を使用してください。試験報告書に記載されている8dBiのダイポール型アンテナは、10dBm設定ではEIRP超過のため使用不可です。 + > + > ℹ️ T1000-Eも工事設計認証を取得済み(217-252754、Seeed社確認済み・書面回答2026/7/22)。証明出力は19mW(≈12.79dBm)が上限のため、本ビルドは当該機種のTX出力のデフォルトを12dBmに設定しています。 + + ### 技適について + カスタムファームウェア書き込み後、Wio-SX1262搭載機(XIAO nRF52840/ESP32S3+Wio-SX1262、Wio Tracker L1 Pro)とT1000-Eは技適の範囲内(それぞれ送信出力・アンテナ構成の条件あり)、それ以外の機種は技適の対象外です。詳細は [README_JP.md](https://github.com/jirogit/MeshCore/blob/dev/README_JP.md) を参照してください。 + draft: true + files: out_jp/* diff --git a/PENDING_NOTES_JP.md b/PENDING_NOTES_JP.md new file mode 100644 index 0000000000..a53448f91f --- /dev/null +++ b/PENDING_NOTES_JP.md @@ -0,0 +1,9 @@ +# 次回リリース向けメモ(直接push分のみ) + +PRを経由した変更は自動生成(generate_release_notes)で拾われるため、ここに書く必要はない。 +jirogit/dev への直接push(fork限定ドキュメント等)だけをここに1行で書き足す。 + +リリース時:この内容をドラフトリリース本文にコピーしてから、このファイルを空にする。 + +--- + diff --git a/QUICKSTART_JP.md b/QUICKSTART_JP.md new file mode 100644 index 0000000000..e015ec8cff --- /dev/null +++ b/QUICKSTART_JP.md @@ -0,0 +1,119 @@ +# MeshCore JP クイックスタート + +このガイドは、メーカーに「ファームウェアを書き換えても技適の証明範囲内」と +確認が取れている **4機種** に限定して、購入からコンパニオンアプリ接続までを +最短で説明します。技適・電波法の詳しい話は [README_JP.md](./README_JP.md) を参照してください。 + +**アンテナは、本体に最初からついているもの(付属アンテナ)を使います。** +市販の高利得アンテナに交換すると、技適の証明範囲(送信出力の上限)を超えてしまう +可能性があります。付属アンテナのままなら問題ありません。 + +(アンテナを変更したい場合は、メーカーの技適証明書に記載された条件等を自分で確認する必要があります。クイックスタートでは「付属アンテナのみ」とします。) + +## 対応4機種 + +| # | 機種 | 送信出力設定 | +|---|------|------------| +| 1 | Wio Tracker L1 Pro | 10dBm | +| 2 | XIAO nRF52840 + Wio-SX1262 | 10dBm | +| 3 | XIAO ESP32S3 + Wio-SX1262 | 10dBm | +| 4 | T1000-E | 12dBm | + +## 手順 + +### JP版ファームウェアを書き込む + +1. [jirogit/MeshCore Releases](https://github.com/jirogit/MeshCore/releases) から最新の `jp-v*` リリースを開く + +- NRF52系 +- WioTrackerL1_companion_radio_ble-*.uf2 +- t1000e_companion_radio_ble-*.uf2 +- Xiao_nrf52_companion_radio_ble-*.uf2 + +- ESP32系 +- Xiao_S3_WIO_companion_radio_ble-*-merged.bin + +2. 機種ごとの書き込み手順: + +**Wio Tracker L1 Pro** +- ファイル WioTrackerL1_companion_radio_ble-*.uf2 を、PCにダウンロードしておく。 +- USB CケーブルでPCと接続する。 +- USBポートのすぐ横の、本体側面のリセットボタンとスライド電源スイッチが有るのをしっかり確認する。 +- このリセットボタンを2回すばやく押す。 +- 電源スライドスイッチとUSBポートの間にある、オレンジ色のLEDが、約1秒毎にゆっくり点灯したり、消灯したりするのを確認する。 +- 「TRACKER L1」という名前のドライブがPCに表示されたら、ダウンロードしたファイル WioTrackerL1_companion_radio_ble-*.uf2 を、そこへコピーする。 +- コピーが完了すると、自動的に再起動する。インストール終了。 + +**T1000-E** +- ファイル t1000e_companion_radio_ble-*.uf2 を、PCにダウンロードしておく。 +- まず磁石ケーブルを分離する。 +- ボタンを押しながら磁石ケーブルを素早く、接続、分離、接続する。 +- 緑のLEDが点灯したままになるのを確認する。 +- ボタンから指を離す。 +- 「T1000-E」という名前のドライブがPCに表示されたら、ダウンロードしたファイル t1000e_companion_radio_ble-*.uf2 を、そこへコピーする。 +- コピーが完了すると、自動的に再起動する。インストール終了。 + +**XIAO nRF52840+Wio-SX1262** +- ファイル Xiao_nrf52_companion_radio_ble-*.uf2 を、PCにダウンロードしておく。 +- USB CケーブルでPCと接続する。 +- USBポートのすぐ横にある、「RST」とすぐ側に書いてある、とても小さな、まるでハンダ付けのような銀色部分の中央に、直径1ミリくらいの黒い点の有るリセットボタンが有るのをしっかり確認する。USBボートの反対側には「USR」と書いてあるユーザーボタンに見えそうな存在があるが、残念ながらこれはボタンではない。 +- このリセットボタンを2回すばやく押す。 +- 「XIAO-SENSE」という名前のドライブがPCに表示されたら、ダウンロードしたファイル Xiao_nrf52_companion_radio_ble-*.uf2 を、そこへコピーする。 +- コピーが完了すると、自動的に再起動する。インストール終了。 + +**XIAO ESP32S3+Wio-SX1262** +- ファイル Xiao_S3_WIO_companion_radio_ble-*-merged.bin を、PCにダウンロードしておく。 +- モジュールをしっかり観察して、1階と2階が端の部分にある黒い細長い四角い部分でつながっているのを確認する。 +- やさしくじわじわと、前後左右にほんの少しづつ動かして、1階と2階部分を分離する。 +- USB Cケーブルをモジュール側に接続する。PCとはまだ接続しない。 +- USBポートのすぐ横にある、とても小さな、まるでハンダ付けのような銀色部分の中央に、直径1ミリくらいの黒い点の有るボタンが存在するのをまずしっかり確認する。アンテナ側のボタンがUSRユーザーボタンで、アンテナの反対側に有るのがRSTリセットボタン。 +- このリセットボタンを爪で、5秒以上押し下げたままにする練習を、自信がつくまで繰り返す。 +- リセットボタンを押し下げたまま、USBコネクターを、PCのポートに差し込む。 +- Google Chrome系のウェブブラウザを使用して、[https://meshcore.io/flasher](https://meshcore.io/flasher) を開く。 +- ページの一番下の方に「Custom Firmware」というボタンがあるので、それをクリックする。 +- ダウンロードしたファイル Xiao_S3_WIO_companion_radio_ble-*-merged.bin を開く。 +- 「merged.bin」というファイルは設定ファイなどすべて消去してしまいますよというポップアップのOKボタンを押す。 +- さらに「Erase device」のチェックボックスがあるので、チェックを入れてオンにする。 +- 「Flash」ボタンを押す。 +- シリアルポートを選択するポップアップが出てくるので、「USB JTAG/serial debug unit (*) - Paired」という名前のシリアルポートを選択して、接続する。 +- コピーが完了すると、自動的に再起動する。インストール終了。 + + +### スマホアプリをスマホにインストールする + +- iPhoneならApp Store、AndroidならPlay Storeへ行ってMeshCoreをインストールする。 + +### 接続する + +- アプリを起動すると、右上にConnectボタンがあるので押す。 +- Meshcore-ではじまるデバイスをクリックする +- Wio Tracker L1 Pro だけはスクリーンがあるので、ランダムなBLEのPin番号が表示される。それ以外のデバイスのPin番号は123456. +- ここで止まってしまうデバイスもあるが、スマホのBLEをオフにして10秒待ち、またオンにする。 + +### LoRa確認 + +- 右上の歯車のようなシンボル「設定」をクリックする +- Redio Settingをしっかり確認する + +920.8 MHz +BW 125.0 kHz +SF 10 +CR 6 +Transmit Power: T1000-eが12dBm それ以外のデバイスは10dBm + +### DMの送り方 + +- 右上の歯車の左横にある、けん玉の頭、もしくはリボンのようなシンボルがあるのでクリックする。 +- Advert・Zero Hopをクリックする +- 付近に同様な、スマホに接続したコンパニオンラジオチャットクライアントがあると、Contactsに、Advert広報したデバイスが追加される。 +- コンタクトの設定によっては、自動で追加されないので、その時は、一番右上、縦に3つの点が並んだシンボルをクリックする。 +- 表示されるメニューの中の「Discover Contacts」をクリックする +- Addボタンを押してContactsに追加する。 +- 反対側のデバイスも、同じことをする。 +- 両側が反対側のデバイスをContactsに登録し終わった後にだけ、相手のContactをクリックしてDMが送れるようになる。相手のデバイスのコンタクトを持っていても、相手のデバイスが自分のデバイスを登録していない場合は、DMが送れない。 + +--- + +もっと詳しい技術的背景(技適の考え方、LBT仕様など)が知りたい方は +[README_JP.md](./README_JP.md) をご覧ください。 + diff --git a/README_JP.md b/README_JP.md new file mode 100644 index 0000000000..9a7a6943e4 --- /dev/null +++ b/README_JP.md @@ -0,0 +1,289 @@ +# MeshCore JP ファームウェア + +> **jirogit/MeshCore** の日本向けビルドです。 +> ARIB STD-T108(920MHz帯)準拠の **LBT(Listen Before Talk)実装** を含みます。 + +本家 [meshcore-dev/MeshCore](https://github.com/meshcore-dev/MeshCore) へのPR #2218 がマージされるまでの、日本ユーザー向け暫定ビルドです。 + +--- + +👉 まずはこれだけ読めば始められます: [QUICKSTART_JP.md](./QUICKSTART_JP.md) + +--- + +## 📥 ファームウェアのダウンロードと書き込み + +### 最新リリース → [Releases ページ](https://github.com/jirogit/MeshCore/releases/latest) + +| デバイス | MCU | Radio | ファイル | 書き込み方法 | +|---|---|---|---|---| +| WisMesh Tag | nRF52840 | SX1262 | `RAK_WisMesh_Tag_companion_radio_ble-jp-v*.uf2` | ✅ UF2ドラッグ&ドロップ | +| T1000-E | nRF52840 | LR1110 | `t1000e_companion_radio_ble-jp-v*.uf2` | ✅ UF2ドラッグ&ドロップ | +| T-Echo Lite | nRF52840 | SX1262 | `LilyGo_T-Echo-Lite_companion_radio_ble-jp-v*.uf2` | ✅ UF2ドラッグ&ドロップ | +| XIAO nRF52840 | nRF52840 | SX1262 | `Xiao_nrf52_companion_radio_ble-jp-v*.uf2` | ✅ UF2ドラッグ&ドロップ | + +> 両デバイスとも MCU は nRF52840 のため、どちらのフォーマットでも書き込めます。 +> 初心者には `.uf2`(ドラッグ&ドロップ)を推奨します。 + +--- + +### ⚠️ 送信できる文字数について(jp-v1.8.0〜) + +jp-v1.8.0でSF10/CR4/6に移行し、JP専用の文字数切り詰めロジックを廃止しました。**送信できるバイト数は本家と同じ(DM150バイト/チャンネルメッセージ147-デバイスに付けた名前のバイト数)で、コーディングレートに依存しません。** + +コーディングレートは**自分のデバイスだけで設定するもの**で、CRの情報はパケットのヘッダーに含まれるため、ネットワーク内の他のデバイスの設定を変える必要はありません。 + +--- + +## WisMesh Tag (RAKwireless+MOKOSMART / MCU: nRF52840 / Radio: SX1262) の書き込み手順 + +### 必要なもの +- WisMesh Tag本体 +- USB-Cケーブル(データ転送対応のもの) +- PC(Windows / Mac / Linux) + +### 手順(UF2 — 推奨) + +**1. ファームウェアをダウンロード** +[Releasesページ](https://github.com/jirogit/MeshCore/releases/latest)から `RAK_WisMesh_Tag_companion_radio_ble-jp-v*.uf2` をダウンロード。 + +**2. ブートローダーモードに入る** +WisMesh Tagのリセットボタンをすばやく2回押す(ダブルクリック)。 +PCにUSBドライブとして表示されればOK(ドライブ名は環境により異なります)。 + +**3. ファームウェアを書き込む** +ダウンロードした `.uf2` ファイルをそのドライブにドラッグ&ドロップする。 +自動的に再起動して完了。 + +--- + +## T1000-E (MCU: nRF52840 / Radio: LR1110) の書き込み手順 + +### 必要なもの +- T1000-E本体 +- USB-Cケーブル(データ転送対応のもの) +- PC + +### 手順(UF2 — 推奨) + +**1. ファームウェアをダウンロード** +[Releasesページ](https://github.com/jirogit/MeshCore/releases/latest)から `t1000e_companion_radio_ble-jp-v*.uf2` をダウンロード。 + +**2. ブートローダーモードに入る** +ボタンを押し続けながら、磁石式充電ケーブルをすばやく2回、接続・切断する(磁石をくっつけて、はなす、をすばやく2回繰り返す)。 +緑色LEDが点灯し続け、PCに **`T1000-E`** という名前のUSBドライブが表示されればOK。 +※ この操作はすばやく行う必要があります。何度か試す必要がある場合があります。 + +**3. ファームウェアを書き込む** +ダウンロードした `.uf2` ファイルをドライブにドラッグ&ドロップする。 +自動的に再起動して完了。 + +--- + +## リピーター / ルームサーバーの設定 + +以下のウェブ設定ツールにUSBで接続して設定してください: +👉 https://config.meshcore.io + +### 初期パスワード + +リピーター・ルームサーバーの管理者パスワードのデフォルトは `password` です。必ず変更してください。 + +### リピーターの発見 + +リピーター側からadvertを送信していなくても、コンパニオンアプリの **Tools → Discover Nearby Nodes** から検出できます。 + +--- + +## ⚠️ 日本での法的地位について + +### T1000-E (Seeed Studio) + +T1000-E は **工事設計認証(技適)を取得しています**(認証番号:217-252754、Seeed Technology Co., Ltd.、2026年1月7日)。証明書上の型式名は以下の純正ファームウェア向けです: + +- SenseCAP Card Tracker(LoRaWAN版) +- SenseCAP Card Tracker T1000-E for Meshtastic + +Seeed社に正式問い合わせたところ(2026年7月22日付、書面回答)、**証明はT1000-E完成品としての構成に基づくが、送信出力・周波数などRFパラメータが証明範囲内である限り、ファームウェアの変更は証明の有効性に影響しない**との回答を得ています。証明書の「for Meshtastic」という表記は、厳密な法的制約ではなかった可能性が高いです。 + +証明された920MHz帯の出力上限は **19mW(≈12.79dBm)** で、これは厳格な上限(丸め誤差の余地なし)とのことです。本ファームウェアはこれを踏まえ、T1000-Eの送信出力のデフォルトを **12dBm**(≈15.85mW)に設定しています。 + +> **注:** Wio-SX1262(後述)については、証明された試験報告書が本ファームウェアの送信時間制御方式(5ms/4秒/50ms)ではなく別方式(128μs/0.4秒)でのみ試験されていたことが判明しています。T1000-Eについても同様の不一致があるかどうかは、該当する試験報告書を未入手のため確認できていません。 + +**証明内容の確認:** +- [総務省データベース:第8号 920MHz帯(LoRa)](https://www.tele.soumu.go.jp/giteki/SearchServlet?pageID=jg01_01&PC=217&TC=N&PK=1&FN=260303N217&SN=%E8%AA%8D%E8%A8%BC&LN=4&R1=*****&R2=*****) — F1D 920.6〜928MHz(200kHz間隔38波)**19mW**、920.7〜927.9MHz(37波)16mW +- [総務省データベース:第19号 2.4GHz帯(Bluetooth LE)](https://www.tele.soumu.go.jp/giteki/SearchServlet?pageID=jg01_01&PC=217&TC=N&PK=1&FN=260303N217&SN=%E8%AA%8D%E8%A8%BC&LN=3&R1=*****&R2=*****) +- [Seeed証明書PDF](https://files.seeedstudio.com/Seeed_Certificate/documents_certificate/114993369-TELEC.pdf) — 上記2種別をまとめて記載 + +> T1000-Eの技適は、920MHz帯(第8号)と2.4GHz帯(第19号)で総務省データベース上の行が分かれています。LoRaの運用に関係するのは第8号の方です。 + +### Wio-SX1262搭載機(XIAO nRF52840/ESP32S3+Wio-SX1262、Wio Tracker L1 Pro) + +Wio-SX1262モジュール単体で工事設計認証(技適)を取得しています(認証番号:**201-250230**、Seeed Technology Co., Ltd.、2026年4月30日)。証明された出力は **10.000mW(10dBm)** のみです。 + +Seeed社に正式問い合わせたところ(2026年7月20日付、書面回答)、以下の3条件を満たせば **ホスト側ファームウェアの変更(本リポジトリのカスタムファームウェア含む)は技適の有効性に影響しない** との回答を得ています: + +- Wio-SX1262モジュールのハードウェアを無改造であること +- アンテナ構成が証明範囲内であること +- 動作周波数・RFパラメータ(送信出力含む)が証明範囲内であること + +これを受けて、本ファームウェアはWio-SX1262搭載機(XIAO nRF52840+Wio-SX1262、XIAO ESP32S3+Wio-SX1262、Wio Tracker L1 Pro)の送信出力のデフォルトを **10dBm** に設定しています(他の対応機種は13dBm)。 + +**証明内容の確認:** +- [総務省データベース(認証番号 201-250230)](https://www.tele.soumu.go.jp/giteki/SearchServlet?pageID=jg01_01&PC=201&TC=N&PK=1&FN=250515N201&SN=%E8%AA%8D%E8%A8%BC&LN=34&R1=*****&R2=*****) +- [試験報告書 BL-SZ2531376-601](https://www.tele.soumu.go.jp/giteki/SearchServlet2?PageID=jt01&ATF=185316004)(証明対象アンテナ3種の記載あり) + +> **⚠️ 重要な留保(2026年8月追記):** 上記の試験報告書(BL-SZ2531376-601)を直接確認したところ、実際に試験・合格しているのは告示49号の128マイクロ秒キャリアセンス/0.4秒送信/1時間あたり360秒以下という方式のみで、本ファームウェアが実装している5ミリ秒連続キャリアセンス/4秒送信/50ミリ秒休止という方式の試験データは報告書内に見当たりませんでした。 +> +> Seeed社の「RFパラメータが範囲内ならファームウェア変更は証明の有効性に影響しない」という回答が、この送信時間制御方式(ルート)自体の変更まで含むのか、それとも周波数・出力といった物理的なパラメータのみを指すのかは、現時点で確認が取れていません。この点について2026年8月7日付でSeeed社へ追加確認を依頼しており、回答待ちです。 +> +> 結論として、Wio-SX1262搭載機の技適については、本ファームウェアでの動作が法的に問題ないとまだ言い切れる状態ではありません。回答があり次第、このセクションを更新します。それまでの間は、特例制度の届出と併用することを推奨します。 + +**アンテナについて:** 証明対象のアンテナは3種類です(試験報告書 BL-SZ2531376-601 より)。 + +| アンテナ | 利得 | 本ファームウェア(10dBm設定)での使用 | +|---|---|---| +| Seeed FPC Antenna (M01-0401030R0A) | 2.36 dBi | ✅ 使用可 | +| Seeed Spring Antenna (M04-0101870R0A) | 3.11 dBi | ✅ 使用可 | +| VLG Dipole Antenna (V1194-145-A-01) | 8.00 dBi | ❌ **使用不可** | + +試験報告書に記載されている8dBiのダイポール型アンテナは、証明対象ではありますが、10dBmの出力設定と組み合わせるとEIRPが18dBmとなり、920MHz帯の上限16dBmを超えます(試験時は出力を約7.7dBmまで下げて8.0dBiと組ませることでEIRP15.7〜15.8dBmに収め、合格しています)。**Wio-SX1262搭載機では付属の低利得アンテナ(FPCまたはSpring、概ね3dBi以下)を使用してください。** + +### WisMesh Tag (RAKwireless+MOKOSMART) + +WisMesh Tagの日本向け技適取得状況は現時点で確認できていません。 + +### アンテナ・送信出力について(全機種共通) + +日本の電波法では、次のすべてを同時に満たす必要があります(どれか1つ守ればいい、という関係ではありません)。 + +- **アンテナ利得**:3dBi以下(技適の対象外の機種、および技適済み機種でも証明範囲外のアンテナを使う場合の一般原則) +- **送信出力**:本ファームウェアが各機種に設定した値(10〜13dBm)を越えた値に変更しないでください +- **技適済み機器**:上記の一般原則より優先。認証時の出力・アンテナ構成の範囲内で使用してください(上記のT1000-E・Wio-SX1262搭載機の項を参照) + +高利得アンテナ(八木・指向性・ハイゲインコリニア等)は3dBiを超えるため使用できません。通常のホイップ/チップアンテナ(概ね0〜2dBi)の範囲で使ってください。 + +--- + +## カスタムファームウェアを日本で使うには + +### 技適対象外:実験試験局 or 特例制度 + +本ファームウェアで使われる無線機のうち、T1000-Eはメーカーから条件付きで技適の有効性が確認できています(送信出力・アンテナ構成の条件あり)。Wio-SX1262搭載機についても同様の回答を得ていますが、上記の通り試験ルートに関する未解決の論点があり、現時点では「確認済み」と言い切らない扱いにしています。詳細は上記の各機種の項を参照してください。それ以外の無線機は、2026年7月時点でどれも技適を通っていません。技適の対象外となる機種で日本国内で電波を発射する場合は、次のいずれかの手続きが必要だと思われます(開発は米国で行っているので、ちょっと自信がないです、すいません)。 + +**おすすめ:特例制度(届出のみ・180日)** + +外国認証や無線従事者確認がなくても、**ARIB STD-T108への適合**を根拠に届出できます(令和元年総務省告示第263号 五号3)。本ファームウェアはARIB STD-T108準拠を目的に設計されているため、これが一番現実的なルートです。 +👉 [総務省:特例制度](https://www.tele.soumu.go.jp/j/sys/others/exp-sp/) + +**実験試験局免許は非推奨** + +常設運用に向いていそうに見えますが、免許人以外の使用を認めない条件(電波法施行規則第6条)があり、不特定多数と通信するメッシュ用途とは相性が悪いです。加えて920MHz帯は簡易審査の「特定実験試験局」の対象外で、通常審査(無線従事者資格者の選任も必要)になります。 + +**まとめ**:特例制度で実地検証を重ね、運用が固まったら技適取得済みハードウェアへ移行していくのが現実的です。 + +### 免責事項 + +このファームウェアは技術検証・実験用途を主な想定としています。実際に電波を発射して使用する場合は、利用者自身が電波法を確認し、適切な手続きを行ってください。このリポジトリの作者は、利用者による電波法上の違反について責任を負いません。 + +--- + +## 📻 日本向け無線設定 + +現時点では本家MeshCoreに日本向けプリセットは存在しません([Issue #460](https://github.com/meshcore-dev/MeshCore/issues/460) で提案・議論中)。チャットアプリから以下の値を**手動で設定**してください。 + +| パラメータ | 推奨値 | 備考 | +|---|---|---| +| 周波数 | 920.8 MHz | ARIB STD-T108 CH25 | +| 帯域幅(BW) | 125 kHz | | +| スプレッディングファクター(SF) | 10 | jp-v1.8.0〜 | +| コーディングレート(CR) | 4/6 | jp-v1.8.0〜 | +| 送信出力 | ≤ 13 dBm(Wio-SX1262搭載機は ≤ 10 dBm、T1000-Eは ≤ 12 dBm) | ARIB STD-T108 上限/各機種の証明出力に準拠 | + +> 周波数を 920.8 / 921.0 / 921.2 MHz のいずれかに設定すると、このファームウェアが自動的に日本モードになります。 + +--- + +## ⚠️ ファームウェア更新時の注意:保存済み設定について + +TX出力などの設定値は、書き込み後に内部ファイルシステムへ保存され、次回起動時は +その保存値が使われます。新しいファームウェアでデフォルト値が変更されていても +(例:Wio-SX1262搭載機が13dBm→10dBmに変更)、**上書きインストールしただけでは +古い設定値が残ります。** + +**技適・法令遵守に関わる設定(TX出力等)は、機種を問わず、ファームウェア更新の +度に手動で確認・更新してください。** + +スマホアプリの **Export Config / Import Config** 機能で秘密鍵・連絡先・ +チャンネルをまとめてバックアップ・復元できます。**Import Configは古いTX出力設定も +そのまま復元してしまう**ため、Import Config後もTX出力の再確認・再設定が +必要です。バックアップ・復元があっても手動確認は省略できません。 + +> 💡 **リピーター/ルームサーバーでもExport Configは使えます。** リピーター/ +> ルームサーバーはコンパニオンラジオチャットクライアントと同じ設定ファイルを +> 使用しているため、コンパニオンラジオチャットクライアントをインストールし、 +> そのクライアントにあるExport Config機能を使ってバックアップを保存できます。 + +### イレースについて(任意・通常のアップグレードには不要) + +設定が壊れた場合などにクリーンな状態からやり直したい場合のみ、以下の方法が +使えます。通常のアップグレードでは不要です。 + +**nRF52系のデバイス(WisMesh Tag、T1000-E、T-Echo Lite、Heltec T114、XIAO nRF52840)** + +https://flasher.meshcore.io で対象デバイスを選択 → "Enter DFU Mode" → +デバイス接続を選択 → "Erase Flash"。 + +**ESP32系のデバイス(XIAO ESP32S3+Wio-SX1262、T3S3、Heltec V4、M5Stack Unit C6L、T-DECK、Heltec Wireless Paper)** + +flasher.meshcore.ioでは単体イレースができません("Flash"ボタンの上に +"Erase"チェックボックスがあるのみで、書き込みと同時にイレースする形に +なります)。手動での設定確認・更新(上記)で対応することを推奨します。 + +> ⚠️ **イレース前に上記のExport Config(コンパニオンラジオチャットクライアント経由)で +> バックアップを取っておかないと、アイデンティティ(秘密鍵)が失われ、復元できません。** +> バックアップなしでイレースすると、既存のメッシュ参加デバイスからは「別ノード」として認識されます。 + +### 公開鍵とノード名について + +公開鍵の先頭バイトが近隣ノードと衝突した場合は、アイデンティティを +作り直す(公開鍵を変える)のがおすすめです。公開鍵は運用上しょっちゅう +変わりうるものと考えて問題ありません。 + +一方、**ノード名は同じものを使い続けることを推奨します。** 近隣の利用者は +公開鍵ではなくノード名でノードを認識するため、公開鍵を変更した場合でも +`set name <ノード名>` などで同じ名前を設定し直せば、周囲の利用者からは +引き続き同じノードとして認識されます。 + +リピーター/ルームサーバーには、公開鍵の先頭1バイト(16進数2文字)を +指定して新しいアイデンティティを生成する機能もあります。この際に生成される +**seed** をテキストで保存しておけば、同じseedから同じ公開鍵を何度でも +再生成できます。イレースなどでアイデンティティが失われた場合の復元手段として +活用できます。 + +--- + +## JP LBT実装の概要 + +| 項目 | 設定値 | 根拠 | +|---|---|---| +| センシング方式 | RSSI連続測定(5ms) | ARIB STD-T108 エネルギー検出 | +| 閾値 | −80 dBm | 同上 | +| バックオフ | 指数(base 2000ms、最大16000ms) | 衝突回避 | +| ジッター | 0〜500ms | チャネル空き時 | +| 送信後待機 | 50ms | キャリアリリース | +| Japan mode判定 | 920.8 / 921.0 / 921.2 MHz | 周波数で自動検出 | + +詳細は [Issue #2079](https://github.com/meshcore-dev/MeshCore/issues/2079) および [PR #2218](https://github.com/meshcore-dev/MeshCore/pull/2218) を参照。 + +> **📡 注意:** 本実装はチャネルが空いていることを確認してから送信します(Listen Before Talk)。 +> 周囲に強い電波干渉がある環境では、送信の遅延や困難が生じる場合があります。 + +> バックオフの長さや、ジッターは私が調整、追加したものですが、それ以外の仕様は、ARIB STD-T108への準拠によるものです。詳しくは [Issue #2079](https://github.com/meshcore-dev/MeshCore/issues/2079) を参照してください。 +> 参考にした物:[ARIB STD-T108 v1.5](https://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf)(英訳、参考資料)。法的根拠としては[平成元年郵政省告示第49号](https://www.tele.soumu.go.jp/horei/law_honbun/72138100.html)(日本語原文)。内容に差がある場合は告示(日本語原文)を優先したつもりです。 +--- + +## ライセンス + +本リポジトリは [meshcore-dev/MeshCore](https://github.com/meshcore-dev/MeshCore) のフォークです。 +ライセンスは upstream と同じ **MIT License** です。 diff --git a/docs/notes/unit-c6l-flash-mode.md b/docs/notes/unit-c6l-flash-mode.md new file mode 100644 index 0000000000..65dac77cc6 --- /dev/null +++ b/docs/notes/unit-c6l-flash-mode.md @@ -0,0 +1,79 @@ +# M5Stack Unit C6L 起動不能の真因調査(2026-08) + +> **TL;DR (EN)** — The boot loop on Unit C6L is not a firmware-code problem. +> The board manifest `esp32-c6-devkitm-1.json` sets `flash_mode: qio`, and this +> chip does not boot reliably from a QIO image. `pio run -t upload` hardcodes +> `--flash_mode dio` on a separate code path, so the bug stays invisible during +> development while every distributed `merged.bin` ships broken. +> Fix: one line, `board_build.flash_mode = dio` (PR #3098). + +**結論: 真因はボードマニフェスト `esp32-c6-devkitm-1.json` の `flash_mode: qio`。修正は `variants/m5stack_unit_c6l/platformio.ini` に `board_build.flash_mode = dio` を1行加えるだけ(PR #3098)。** + +**以下は、PI4IO / TCXO電圧 / I2Cピン / TX_LED の4項目を真因と誤認して PR #3088 を出し、再bisectで撤回するまでの経緯。途中まで読むと誤結論を拾うので注意。** + +--- + +## 症状 + +USBシリアルポートが接続/切断を繰り返し、デバイスが起動しない。upstream `meshcore-dev/MeshCore` の dev ブランチ由来ファームで発生。Issue #2229 に複数の報告者(comsorg / vikkut / alestandby / svenrobbie)。 + +一方、M5Burner 経由で配布されている TheRealHaoLiu フォーク版(MeshCore v1.11.0)は正常起動する。 + +## 真因 + +`esp32-c6-devkitm-1.json`(ボードマニフェスト)が `flash_mode: qio` を指定している。ESP32-C6 はQIOイメージから安定して起動しない。 + +**なぜ開発中は見えなかったか** — PlatformIO の書き込み経路は2つあり、挙動が違う。 + +| 経路 | flash_mode | 結果 | +|---|---|---| +| `pio run -t upload` | `--flash_mode dio` をハードコード(マニフェスト無視) | 起動する | +| `mergebin`(= GitHub Releases 配布の merged.bin = Webフラッシャー経路) | マニフェストの `qio` を採用 | **起動しない** | + +つまり開発者は常に dio で書き込んでおり、ユーザーは常に qio を掴まされていた。 + +## 確認方法 + +1. 同一ファームウェアに対し `--flash_mode` だけを変えて書き込み。qio で2回クラッシュループを再現、dio で2回解消 +2. 書き込み後に `esptool image_info` でオンチップのヘッダを読み戻し、実際にどのモードが載ったかを確認 +3. 配布済み merged.bin(jp-v1.8.1 および upstream companion-v1.16.0)のヘッダを確認 → **両方ともQIO**。`--flash_mode` 指定なしで書き込むと同じクラッシュループを再現 + +## 誤認の経緯(PR #3088) + +M5Burner フォークとの差分6項目を洗い出した: + +1. TCXO電圧 1.8V → 3.0V +2. I2Cピン 16/17 → 10/8 +3. PI4IO `OUT_H_IM` レジスタ書き込み +4. TX_LED GPIO15 +5. `SX126X_RXEN` と `GPS_TX` の GPIO5 競合 +6. SSD1306 ディスプレイ対応の欠落 + +このうち1〜4を適用したところクラッシュが解消したように見えたため、PR #3088 を提出し jp-v1.8.1 をリリースした。 + +翌日クリーンな手順で再検証したところ、**upstream素のコード(変更ゼロ)でも dio なら起動する**ことが判明。4項目はいずれも起動可否に無関係だった。PR #3088 はクローズし、PR #3098 を提出。 + +**なぜ誤認したか(2つの汚染源)** + +- **ビルドキャッシュ**: PlatformIO のインクリメンタルビルドがヘッダファイルの変更を拾わず、bisect中に偽の失敗が混入していた +- **書き込み手順**: `esptool.py write_flash 0x0 firmware.bin` を merged / non-merged の区別なく使っていた + +## 教訓 + +- **ESP32系では「コードのbisect」より先に「書き込み条件(オフセット・flash_mode)の固定」を疑う。** 変数が固定されていない状態でのbisectは、結論ではなくノイズを生む +- 検証ビルドの前には必ず `pio run -t clean` + `rm -rf .pio/build/` +- 書き込みは `pio run -t upload` に統一。esptool 直書き・オフセット手動指定はしない +- 「開発環境では動くが配布物では動かない」類のバグは、ビルド成果物そのもの(ヘッダ等)を読み戻して確認する + +## 積み残し(この件とは別テーマ) + +- **PI4IO の LNA(P5) 有効化** — upstream では未初期化のまま。受信感度に影響する可能性があるが、起動問題とは無関係。別途検証の余地あり +- **GPIO5 競合**(`SX126X_RXEN=5` / `GPS_TX=5`)— 別PRとして分離(PR #3089、ブランチ `fix/unit-c6l-gpio5-conflict`) + +## 関連 + +- Issue #2229 — 症状の報告先。訂正コメント投稿済み +- PR #3088 — クローズ(誤った修正) +- PR #3098 — 正しい修正(1行) +- jp-v1.8.1 — Unit C6L 向け merged.bin がQIOヘッダのため実質破損版 +- jp-v1.8.2 — 4つのC6L merged.bin 資産すべてがDIOヘッダであることを確認したうえでリリース diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b66e19522a..73a5bf32e2 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -541,10 +541,22 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { + if (_radio->isAS923_1_JP()) { + // JP LBT: suppress txdelay to jitter-scale to avoid adding unnecessary + // latency on top of LBT backoff. A window equal to jitter_max gives + // ~33% collision reduction vs zero, scales naturally with airtime as + // CR changes, and keeps average added delay to ~56ms at SF12/BW125. + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { + if (_radio->isAS923_1_JP()) { + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 36978e808f..4c272d3122 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -272,10 +272,22 @@ const char *MyMesh::getLogDateTime() { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { + if (_radio->isAS923_1_JP()) { + // JP LBT: suppress txdelay to jitter-scale to avoid adding unnecessary + // latency on top of LBT backoff. A window equal to jitter_max gives + // ~33% collision reduction vs zero, scales naturally with airtime as + // CR changes, and keeps average added delay to ~56ms at SF12/BW125. + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { + if (_radio->isAS923_1_JP()) { + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 59c9aa0900..066b2e0544 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -313,12 +313,24 @@ int SensorMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) { + if (_radio->isAS923_1_JP()) { + // JP LBT: suppress txdelay to jitter-scale to avoid adding unnecessary + // latency on top of LBT backoff. A window equal to jitter_max gives + // ~33% collision reduction vs zero, scales naturally with airtime as + // CR changes, and keeps average added delay to ~56ms at SF12/BW125. + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); - return getRNG()->nextInt(0, 6)*t; + return getRNG()->nextInt(0, 5*t + 1); } uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { + if (_radio->isAS923_1_JP()) { + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); - return getRNG()->nextInt(0, 6)*t; + return getRNG()->nextInt(0, 5*t + 1); } int SensorMesh::getInterferenceThreshold() const { return _prefs.interference_threshold; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index c0610b7f8a..d2e6f266c3 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -60,9 +60,15 @@ uint32_t Dispatcher::getCADFailRetryDelay() const { return 200; } uint32_t Dispatcher::getCADFailMaxDuration() const { + if (_radio->isAS923_1_JP()) return UINT32_MAX; // ARIB STD-T108: never force TX during LBT return 4000; // 4 seconds } +uint32_t Dispatcher::getMaxTxAirtimeMs() const { + if (_radio->isAS923_1_JP()) return 4000; // ARIB STD-T108: single transmission must be <=4s + return UINT32_MAX; // no limit for other regions +} + void Dispatcher::loop() { if (millisHasNowPassed(next_floor_calib_time)) { _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); @@ -324,6 +330,16 @@ void Dispatcher::checkSend() { } else { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; + uint32_t send_airtime = _radio->getEstAirtimeFor(len); + if (send_airtime > getMaxTxAirtimeMs()) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): DROPPED, airtime %dms exceeds limit", getLogDateTime(), send_airtime); + n_tx_dropped_airtime++; + logTxFail(outbound, len); + releasePacket(outbound); + outbound = NULL; + return; + } + uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); @@ -387,4 +403,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const { return _ms->getMillis() + millis_from_now; } -} \ No newline at end of file +} diff --git a/src/Dispatcher.h b/src/Dispatcher.h index aad6cba3ec..192f1e3d62 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -78,6 +78,11 @@ class Radio { virtual float getLastRSSI() const { return 0; } virtual float getLastSNR() const { return 0; } + + virtual bool isAS923_1_JP() const { return false; } + + virtual int getMaxTextLen() const { return 10 * 16; } // default 160 bytes + virtual int getMaxGroupTextLen() const { return 10 * 16; } // default 160 bytes }; /** @@ -125,6 +130,7 @@ class Dispatcher { bool prev_isrecv_mode; uint32_t n_sent_flood, n_sent_direct; uint32_t n_recv_flood, n_recv_direct; + uint32_t n_tx_dropped_airtime; // packets dropped: would exceed getMaxTxAirtimeMs() unsigned long tx_budget_ms; unsigned long last_budget_update; unsigned long duty_cycle_window_ms; @@ -152,6 +158,7 @@ class Dispatcher { tx_budget_ms = 0; last_budget_update = 0; duty_cycle_window_ms = 3600000; + n_tx_dropped_airtime = 0; } virtual DispatcherAction onRecvPacket(Packet* pkt) = 0; @@ -167,6 +174,7 @@ class Dispatcher { virtual int calcRxDelay(float score, uint32_t air_time) const; virtual uint32_t getCADFailRetryDelay() const; virtual uint32_t getCADFailMaxDuration() const; + virtual uint32_t getMaxTxAirtimeMs() const; // single-transmission airtime cap (JP: 4000ms per ARIB STD-T108) virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual bool getCADEnabled() const { return false; } // hardware CAD disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default @@ -187,8 +195,10 @@ class Dispatcher { uint32_t getNumSentDirect() const { return n_sent_direct; } uint32_t getNumRecvFlood() const { return n_recv_flood; } uint32_t getNumRecvDirect() const { return n_recv_direct; } + uint32_t getNumTxDroppedAirtime() const { return n_tx_dropped_airtime; } void resetStats() { n_sent_flood = n_sent_direct = n_recv_flood = n_recv_direct = 0; + n_tx_dropped_airtime = 0; _err_flags = 0; } diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 972a97e9e6..75d27464ba 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -419,8 +419,9 @@ void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mes mesh::Packet* BaseChatMesh::composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack) { int text_len = strlen(text); - if (text_len > MAX_TEXT_LEN) return NULL; - if (attempt > 3 && text_len > MAX_TEXT_LEN-2) return NULL; + int max_len = _radio->getMaxTextLen(); + if (text_len > max_len) return NULL; + if (attempt > 3 && text_len > max_len - 2) return NULL; uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique @@ -460,7 +461,8 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout) { int text_len = strlen(text); - if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED; + int max_len = _radio->getMaxTextLen(); + if (text_len > max_len) return MSG_SEND_FAILED; uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique @@ -493,7 +495,8 @@ bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& chan char *ep = strchr((char *) &temp[5], 0); int prefix_len = ep - (char *) &temp[5]; - if (text_len + prefix_len > MAX_TEXT_LEN) text_len = MAX_TEXT_LEN - prefix_len; + int max_len = _radio->getMaxGroupTextLen(); + if (text_len + prefix_len > max_len) text_len = max_len - prefix_len; memcpy(ep, text, text_len); ep[text_len] = 0; // null terminator diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 0470e48f26..33c20cca20 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -82,4 +82,5 @@ class CustomLR1110 : public LR1110 { } uint8_t getSpreadingFactor() const { return spreadingFactor; } + uint8_t getCodingRate() const { return this->codingRate + 4; } // RadioLib stores 1-4, return 5-8 }; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 44230c61c3..ca86bc95f3 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -43,6 +43,8 @@ class CustomLR1110Wrapper : public RadioLibWrapper { float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); } uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); } + uint8_t getCodingRate() const override { return ((CustomLR1110 *)_radio)->getCodingRate(); } + float getFreqMHz() const override { return ((CustomLR1110 *)_radio)->getFreqMHz(); } bool setRxBoostedGainMode(bool en) override { return ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index bfea50ec9c..4d29e5f7e4 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -37,6 +37,8 @@ class CustomSX1262Wrapper : public RadioLibWrapper { return packetScoreInt(snr, sf, packet_len); } uint8_t getSpreadingFactor() const override { return ((CustomSX1262 *)_radio)->spreadingFactor; } + uint8_t getCodingRate() const override { return ((CustomSX1262 *)_radio)->codingRate + 4; } // RadioLib stores 1-4, return 5-8 + float getFreqMHz() const override { return ((CustomSX1262 *)_radio)->freqMHz; } virtual void powerOff() override { ((CustomSX1262 *)_radio)->sleep(false); } diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 7146e47b6a..467b2df8cd 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -2,6 +2,12 @@ #define RADIOLIB_STATIC_ONLY 1 #include "RadioLibWrappers.h" +#ifdef NRF52_PLATFORM + #define YIELD_TASK() vTaskDelay(1) +#else + #define YIELD_TASK() delay(1) +#endif + #define STATE_IDLE 0 #define STATE_RX 1 #define STATE_TX_WAIT 3 @@ -176,6 +182,9 @@ bool RadioLibWrapper::isSendComplete() { void RadioLibWrapper::onSendFinished() { _radio->finishTransmit(); _board->onAfterTransmit(); + if (isAS923_1_JP()) { + delay(50); // ARIB STD-T108 §3.4.1: >= 50ms between transmissions + } state = STATE_IDLE; } @@ -184,10 +193,40 @@ int16_t RadioLibWrapper::performChannelScan() { } bool RadioLibWrapper::isChannelActive() { - // int.thresh: RSSI-based interference detection (relative to noise floor) - if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + if (isAS923_1_JP()) { + // ARIB STD-T108: 5ms continuous RSSI sensing, -80dBm absolute threshold + uint32_t sense_start = millis(); + while (millis() - sense_start < 5) { + if (getCurrentRSSI() > -80.0f) { + _busy_count++; + uint32_t base_ms = 500; + uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)4000); + uint32_t backoff_until = millis() + random(max_backoff / 2, max_backoff); + while (millis() < backoff_until) { + YIELD_TASK(); + } + return true; + } + YIELD_TASK(); + } + // Channel free: reset busy counter and add airtime-scaled jitter. + // JP_LBT_JITTER_DIVISOR controls jitter upper bound: + // /8 -> SF12/BW125 ~975ms, SF7/BW62.5 ~50ms + // /16 -> SF12/BW125 ~490ms, SF7/BW62.5 ~25ms + // /32 -> SF12/BW125 ~245ms, SF7/BW62.5 ~12ms (default) + _busy_count = 0; + uint32_t airtime_ms = getEstAirtimeFor(MAX_TRANS_UNIT); + uint32_t jitter_until = millis() + random(0, airtime_ms / JP_LBT_JITTER_DIVISOR); + while (millis() < jitter_until) { + YIELD_TASK(); + } + // JP RSSI sensing passed; fall through to CAD if enabled + } else { + // Non-JP: RSSI-based interference detection (relative to noise floor) + if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + } - // cad: hardware channel activity detection + // hardware channel activity detection (JP and non-JP) if (_cad_enabled) { int16_t result = performChannelScan(); // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index e7fef91042..ebd3197a82 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -15,6 +15,7 @@ class RadioLibWrapper : public mesh::Radio { uint32_t n_recv, n_sent, n_recv_errors; int16_t _noise_floor, _threshold; bool _cad_enabled; + uint8_t _busy_count; uint16_t _num_floor_samples; int32_t _floor_sample_sum; uint8_t _preamble_sf; @@ -26,7 +27,7 @@ class RadioLibWrapper : public mesh::Radio { virtual void doResetAGC(); public: - RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; } + RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _busy_count(0), _preamble_sf(0) { n_recv = n_sent = 0; } void begin() override; virtual void powerOff() { _radio->sleep(); } @@ -52,6 +53,18 @@ class RadioLibWrapper : public mesh::Radio { virtual uint8_t getSpreadingFactor() const { return LORA_SF; } static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); } + virtual uint8_t getCodingRate() const { return 8; } // default CR4/8, override in subclass + virtual float getFreqMHz() const { return 0.0f; } // default unknown, override in subclass + + static constexpr uint8_t JP_LBT_JITTER_DIVISOR = 32; + + bool isAS923_1_JP() const override { + float freq = getFreqMHz(); + return (fabsf(freq - 920.800f) < 0.05f || + fabsf(freq - 921.000f) < 0.05f || + fabsf(freq - 921.200f) < 0.05f); + } + PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols); virtual int16_t performChannelScan(); diff --git a/variants/m5stack_unit_c6l/UnitC6LBoard.h b/variants/m5stack_unit_c6l/UnitC6LBoard.h index a4ea3ee6e5..c710634fea 100644 --- a/variants/m5stack_unit_c6l/UnitC6LBoard.h +++ b/variants/m5stack_unit_c6l/UnitC6LBoard.h @@ -1,12 +1,96 @@ #pragma once #include +#include #include +#define PI4IO_ADDR 0x43 + +// PI4IO registers +#define PI4IO_REG_CHIP_RESET 0x01 +#define PI4IO_REG_IO_DIR 0x03 +#define PI4IO_REG_OUT_SET 0x05 +#define PI4IO_REG_OUT_H_IM 0x07 +#define PI4IO_REG_IN_DEF_STA 0x09 +#define PI4IO_REG_PULL_EN 0x0B +#define PI4IO_REG_PULL_SEL 0x0D +#define PI4IO_REG_IRQ_STA 0x13 +#define PI4IO_REG_INT_MASK 0x11 + class UnitC6LBoard : public ESP32Board { +private: + bool i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.write(value); + return Wire.endTransmission() == 0; + } + + bool i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value) { + Wire.beginTransmission(addr); + Wire.write(reg); + if (Wire.endTransmission() != 0) return false; + if (Wire.requestFrom(addr, (uint8_t)1) != 1) return false; + if (!Wire.available()) return false; + *value = Wire.read(); + return true; + } + + // P0=button, P1=unused input, P5=LNA_EN, P6=ANT_SW, P7=NRST(LoRa reset) + void resetLoRaViaPI4IO() { + uint8_t in_data; + + // Reset the I/O expander to a known state + i2c_write_byte(PI4IO_ADDR, PI4IO_REG_CHIP_RESET, 0xFF); + delay(10); + i2c_read_byte(PI4IO_ADDR, PI4IO_REG_CHIP_RESET, &in_data); + delay(10); + + // Set P5, P6, P7 as outputs (0: input, 1: output) + i2c_write_byte(PI4IO_ADDR, PI4IO_REG_IO_DIR, 0b11100000); + delay(10); + + // Disable output high-impedance (i.e. enable the output driver) for + // every pin actually wired on this board: P0, P1, P5, P6, P7. + // Without this, IO_DIR + OUT_SET have no effect on the physical pin — + // it stays in the power-on Hi-Z state, so NRST never actually toggles. + i2c_write_byte(PI4IO_ADDR, PI4IO_REG_OUT_H_IM, 0b00011100); + delay(10); + + // Pull up/down select (0: down, 1: up) + i2c_write_byte(PI4IO_ADDR, PI4IO_REG_PULL_SEL, 0b11100011); + delay(10); + + // Pull up/down enable (0: disable, 1: enable) + i2c_write_byte(PI4IO_ADDR, PI4IO_REG_PULL_EN, 0b11100011); + delay(10); + + // Default input state for P0, P1 (buttons) + i2c_write_byte(PI4IO_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); + delay(10); + + // Enable interrupts for P0, P1 (0: enable, 1: disable) + i2c_write_byte(PI4IO_ADDR, PI4IO_REG_INT_MASK, 0b11111100); + delay(10); + + // Set P7 (NRST) high, P5/P6 will be set after + i2c_write_byte(PI4IO_ADDR, PI4IO_REG_OUT_SET, 0b10000000); + delay(10); + + // Clear IRQ status + i2c_read_byte(PI4IO_ADDR, PI4IO_REG_IRQ_STA, &in_data); + + // Enable RF switch (P6 high) and LNA (P5 high) + i2c_read_byte(PI4IO_ADDR, PI4IO_REG_OUT_SET, &in_data); + in_data |= (1 << 6); // P6 = RF Switch = HIGH + in_data |= (1 << 5); // P5 = LNA Enable = HIGH + i2c_write_byte(PI4IO_ADDR, PI4IO_REG_OUT_SET, in_data); + } + public: void begin() { - ESP32Board::begin(); + ESP32Board::begin(); // already calls Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL) + resetLoRaViaPI4IO(); } const char* getManufacturerName() const override { diff --git a/variants/m5stack_unit_c6l/platformio.ini b/variants/m5stack_unit_c6l/platformio.ini index 94083eb486..c959deeeeb 100644 --- a/variants/m5stack_unit_c6l/platformio.ini +++ b/variants/m5stack_unit_c6l/platformio.ini @@ -2,11 +2,12 @@ extends = esp32c6_base board = esp32-c6-devkitm-1 board_build.partitions = min_spiffs.csv ; get around 4mb flash limit +board_build.flash_mode = dio ; board manifest defaults to qio, which causes a boot crash-loop on this module build_flags = ${esp32c6_base.build_flags} ${sensor_base.build_flags} -I variants/m5stack_unit_c6l - -D P_LORA_TX_LED=15 +; -D P_LORA_TX_LED=15 -D P_LORA_SCLK=20 -D P_LORA_MISO=22 -D P_LORA_MOSI=21 @@ -15,11 +16,11 @@ build_flags = -D P_LORA_BUSY=19 -D P_LORA_RESET=-1 -D PIN_BUZZER=11 - -D PIN_BOARD_SDA=16 - -D PIN_BOARD_SCL=17 + -D PIN_BOARD_SDA=10 + -D PIN_BOARD_SCL=8 -D SX126X_RXEN=5 -D SX126X_DIO2_AS_RF_SWITCH=true - -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_DIO3_TCXO_VOLTAGE=3.0 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D USE_SX1262 @@ -42,6 +43,8 @@ build_src_filter = ${M5Stack_Unit_C6L.build_src_filter} +<../examples/simple_repeater/*.cpp> build_flags = ${M5Stack_Unit_C6L.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D ARDUINO_USB_MODE=1 -D ADVERT_NAME='"Unit C6L Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -59,6 +62,8 @@ build_src_filter = ${M5Stack_Unit_C6L.build_src_filter} +<../examples/simple_room_server> build_flags = ${M5Stack_Unit_C6L.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D ARDUINO_USB_MODE=1 -D ADVERT_NAME='"Unit C6L Room"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -106,6 +111,24 @@ lib_deps = densaugeo/base64 @ ~1.4.0 end2endzone/NonBlockingRTTTL@^1.3.0 +[env:M5Stack_Unit_C6L_companion_radio_wifi] +extends = M5Stack_Unit_C6L +build_flags = ${M5Stack_Unit_C6L.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D TCP_PORT=5000 +build_src_filter = ${M5Stack_Unit_C6L.build_src_filter} + + + - + +<../examples/companion_radio/*.cpp> +lib_deps = + ${M5Stack_Unit_C6L.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + [env:M5Stack_Unit_C6L_kiss_modem] extends = M5Stack_Unit_C6L build_src_filter = ${M5Stack_Unit_C6L.build_src_filter}