Skip to content

About

高度化PICS信号機から送出されているビーコンをGARMINのデバイスで見る。

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Advanced PICS Traffic Signal Viewer for GARMIN GPSMAP H1i Plus

高度化PICS ビューア for GARMIN GPSMAP H1i Plus

A Connect IQ application that receives and displays BLE advertisements from Japan's Advanced PICS (Pedestrian Information and Communication System) pedestrian traffic signals in real-time — no smartphone required.

Example

image

Features

  • Real-time reception of PICS BLE advertisements (company ID 0x01CE)
  • Displays signal state (Red / Green / Blinking Green / Uncontrolled) for up to 16 pedestrian channels with dynamic scrolling and empty signal filtering
  • Shows receive status, last receive time, packet count, and current GPS position in a compact header
  • Shows the Type 1 transmitter/device ID (for example SAPPORO_STN001) under the resolved intersection name when available
  • Resolves intersection name from GPS coordinates using a pre-built national intersection database (596 intersections)
  • Displays device GPS coordinates, nearest intersection coordinates, and calculated distance + bearing
  • Keeps nearby intersection names and addresses clear of the right-aligned distance and bearing using pixel-aware truncation, including for wide Japanese glyphs
  • Works standalone on the GPSMAP H1i Plus without any companion app
  • SD Card logging: Record raw BLE payloads and packet data directly to the device's internal storage

File Structure

Advanced-PICS-Traffic-Signals-for-GARMIN/
├── manifest.xml                     ← App definition, permissions (BLE + Positioning)
├── monkey.jungle                    ← Project build configuration
├── source/
│   ├── ApiFlags.mc                  ← Auto-generated compile-time feature flags
│   ├── PicsBleApp.mc                ← App entry point, lifecycle, menu handling
│   ├── PicsBleDelegate.mc           ← BLE scanning & packet reception
│   ├── PicsData.mc                  ← Data models, PICS parser, intersection DB
│   └── PicsMainView.mc              ← Screen rendering (282×470 px)
├── resources/
│   ├── data/
│   │   ├── intersections.json       ← Intersection database (auto-generated)
│   │   └── jsonResources.xml        ← JSON resource declaration
│   ├── drawables/
│   │   ├── drawables.xml            ← Drawable resource declarations
│   │   └── launcher_icon.png        ← App launcher icon
│   └── strings/
│       └── strings.xml              ← UI strings (Japanese; add translations here)
├── tools/
│   ├── build.ps1                    ← Windows build helper
│   ├── clean_intersections.py       ← Address cleanup helper
│   ├── csv_to_resource.py           ← CSV → JSON resource converter
│   ├── debug_project.py             ← Project diagnostics & API flag generator
│   ├── ble_pics_advertiser.py       ← macOS BLE test advertiser
│   ├── make_ble_advertiser_app.sh   ← Builds the macOS app bundle for the advertiser
│   └── BlePicsAdvertiser.cs         ← Windows BLE test advertiser source
└── tests/
    └── test_csv_to_resource.py       ← CSV converter regression tests

Technical Specifications

Target Devices

  • GARMIN GPSMAP H1i Plus (primary)
  • GARMIN GPSMAP H1 (inReach-free variant)

Minimum Connect IQ API Level: 5.0.0

  • Toybox.BluetoothLowEnergy (BLE scanning): API Level 3.1.0+
  • ScanResult.getManufacturerSpecificData(companyId): API Level 3.2.0+
  • Toybox.Position (GPS): API Level 1.0.0+

Required Permissions

Permission Purpose
BluetoothLowEnergy Receive PICS BLE advertisements
Positioning Read device GPS coordinates for intersection lookup

BLE Scanning

Item Details
Mode Central (scan only)
Filter Manufacturer ID = 0x01CE (PICS-specific)
Pairing Not required (advertisement reception only)
GATT connection Not established
API ScanResult.getManufacturerSpecificData(companyId) returns ByteArray directly

PICS Packet Format (bytes after company ID 0x01CE):

Bytes Content
[0..1] Header (version / flags)
[2] Message type (0 = identifier, 1 = location, 2 = signal state)
[3] Message ID
[4..5] Reserved
[6..9] Intersection ID (4-byte hex)
[10+i] Signal state (Type 2 only): upper 4 bits = remaining time, lower 4 bits = state

Build Instructions

1. Install Connect IQ SDK

Download from https://developer.garmin.com/connect-iq/sdk/

2. Generate a Developer Key

openssl genrsa -out developer_key.pem 4096
openssl pkcs8 -topk8 -inform PEM -outform DER \
  -in developer_key.pem -out developer_key.der -nocrypt

3. Generate API Feature Flags

python3 tools/debug_project.py --generate-api-flags

This auto-generates source/ApiFlags.mc with compile-time constants based on the installed SDK.

4. Generate Intersection Database Resource

# Default: reads ~/Downloads/250501-zenkoku-kousaten.csv
python3 tools/csv_to_resource.py

# Or specify paths explicitly:
python3 tools/csv_to_resource.py /path/to/input.csv resources/data/intersections.json

5. Build

monkeyc -d gpsmaph1 -f monkey.jungle -o bin/pics-viewer.prg -y developer_key.der

On Windows, you can also use the repository build helper. It looks for monkeyc on PATH and in the default Garmin SDK Manager install locations:

.\tools\build.ps1

# Or specify an SDK path explicitly:
.\tools\build.ps1 -SdkPath "C:\Path\To\connectiq-sdk-win-9.2.0"

To build for a different device, replace gpsmaph1 with the target device ID.

Run the repository tests with:

python3 -m unittest discover -s tests -v

6. Deploy to Device (USB Mass Storage)

Ensure USB Mass Storage Mode

cp bin/pics-viewer.prg /GARMIN/APPS/

SD Card Logging (Debug)

The app automatically logs every received PICS packet with a wall-clock timestamp, monotonic receive tick, and raw HEX payload. However, Garmin OS discards these logs unless you manually create a text file to capture them:

  1. Connect your Garmin device via USB.
  2. In the GARMIN/APPS/LOGS/ directory, create an empty text file named exactly the same as your app executable but with a .txt extension (e.g. pics-viewer.txt).
  3. Launch the app. All BLE traffic logs will be continuously appended to that text file.

BLE Test Advertiser (macOS)

tools/ble_pics_advertiser.py broadcasts a sample PICS packet (company ID 0x01CE, Type 2, intersection ID DEADBEEF) for 15 seconds, so the Connect IQ app can be exercised without real traffic-signal hardware. The Windows equivalent is tools/BlePicsAdvertiser.cs.

Dependencies

python3 -m venv .venv
.venv/bin/python -m pip install pyobjc-framework-CoreBluetooth

Create the virtual environment with the Python installation you intend to keep. A venv whose base interpreter is later removed (for example after switching from Intel to Apple Silicon Homebrew) leaves broken shebangs; pip3 then falls through to the system Python and fails with externally-managed-environment. Recreate the venv in that case.

macOS Bluetooth permission limitation

macOS refuses CoreBluetooth access unless the responsible application declares NSBluetoothAlwaysUsageDescription in its Info.plist. When a script is started from a shell, the responsible application is the terminal emulator — not Python and not the script. Terminal.app and Warp do not declare that key, so the interpreter is killed by TCC before it can advertise:

This app has crashed because it attempted to access privacy-sensitive data
without a usage description. The app's Info.plist must contain an
NSBluetoothAlwaysUsageDescription key ...

The process aborts with exit code 134 and writes a crash report to ~/Library/Logs/DiagnosticReports/. This is platform policy, not a bug in the script: no Python-side change can bypass it, including patching NSBundle.mainBundle() at runtime.

Option 1 — Run via the bundled application (recommended)

Build a small app bundle that declares the usage description and embeds the framework interpreter:

tools/make_ble_advertiser_app.sh          # build only
tools/make_ble_advertiser_app.sh --run    # build, launch, and print the log

Launch it again at any time with:

open -a build/PICSBLEAdvertiser.app

Launching through open makes the bundle its own responsible application, so its Info.plist satisfies the check. macOS shows the Bluetooth permission prompt on first launch, and the grant appears under System Settings → Privacy & Security → Bluetooth as PICS BLE Advertiser. The bundle runs without a terminal, so output goes to build/ble-advertiser.log:

Publisher status: Started
Broadcasting CompanyId=0x01CE ID=DEADBEEF Type=2 for 15 seconds
Final status: Stopped

The bundle is a local build artifact (ignored by git). Rebuild it after upgrading Python or recreating the virtual environment.

Option 2 — Run from a terminal that declares the usage description

Running the script directly works only from a terminal application that declares the key and has been granted Bluetooth access:

.venv/bin/python tools/ble_pics_advertiser.py

Check a terminal before relying on it:

/usr/libexec/PlistBuddy -c "Print :NSBluetoothAlwaysUsageDescription" \
  "/Applications/Visual Studio Code.app/Contents/Info.plist"

Visual Studio Code declares the key, so its integrated terminal works once Bluetooth access is granted to VS Code. Terminal.app and Warp do not declare it and cannot run the advertiser directly — use Option 1 there. Editing Homebrew's own Python.app/Contents/Info.plist also works, but is not recommended: it affects every Python process, invalidates the framework signature, and is reverted by each brew upgrade.

Implementation note

The delegate class must call objc.super(...), not the built-in super(). PyObjC subclasses of NSObject raise AttributeError: 'super' object has no attribute 'init' with the plain built-in.

Notes & Limitations

  • BLE: Central (scan-only) mode. No GATT connection is established.
  • macOS test advertiser: Requires an application bundle that declares NSBluetoothAlwaysUsageDescription; see BLE Test Advertiser (macOS).
  • Widget type: The app runs as a Connect IQ widget, accessible via the PAGE button on the device. BLE scanning runs continuously while the widget is active.
  • Widget vs watch-app: As a widget, the app is launched directly from the widget glance view (PAGE button) rather than navigating to Main Menu → Connect IQ → Apps.
  • UI verification mode: Press MENU to toggle a mock-data display mode for checking rendering on hardware when BLE/simulator behavior is unreliable.
  • Screen layout: The footer is removed; receive metadata is shown in the compact header and cards use the remaining screen height.
  • Nearby rows: Intersection names and addresses are truncated by rendered pixel width, leaving a fixed gap before distance and bearing values instead of wrapping or overlapping them.
  • Intersection database: Built from the e-Gov national intersection list (596 intersections). Re-run csv_to_resource.py to update.
  • Identification of traffic signs: Need to dump network telemetry of NIPPON SIGNAL's App. ref.
  • String resources: Status and empty-state labels are defined in resources/strings/strings.xml. To add a language translation, create a locale-specific strings file (e.g. resources-eng/strings/strings.xml).
  • Connect IQ SDK: ScanResult.getManufacturerSpecificData() requires the company ID as an argument and returns a ByteArray directly (no longer returns a Dictionary).
  • Simulator: The Connect IQ SDK 9.1.0 simulator may crash on macOS 26+ (Tahoe) in the ant_main thread when loading apps that use BLE. Deploy to actual hardware for testing until Garmin releases a compatible SDK update.
  • Legal compliance: PICS BLE advertisements are public-infrastructure transmissions broadcast to all. This app is receive-only and does not transmit or modify any signal. Reception is compliant with Japanese radio and telecommunications law.

Acknowledgments

This project is deeply inspired by and based on the work of yumu19/ble-pics-viewer (Python version). We express our sincere gratitude to the original author for making their packet analysis and implementation public, which made this Garmin port possible.

References


高度化PICS ビューア for GARMIN GPSMAP H1i Plus

Connect IQ アプリとして実装した、歩行者用信号機 BLE アドバタイズのリアルタイム受信・表示ツールです。スマートフォン不要・単体で高度化PICS信号機のBLEアドバタイズを受信し、交差点の信号状態(赤・青・青点滅)をリアルタイム表示します。

ble-pics-viewer(Python版)の機能を GARMIN GPSMAP H1i Plus 上で動作する Connect IQ アプリとして移植したものです。


ファイル構成

Advanced-PICS-Traffic-Signals-for-GARMIN/
├── manifest.xml                     ← アプリ定義・権限宣言(BLE + Positioning)
├── monkey.jungle                    ← プロジェクトビルド設定
├── source/
│   ├── ApiFlags.mc                  ← 自動生成されるコンパイル時フラグ
│   ├── PicsBleApp.mc                ← エントリポイント・ライフサイクル管理
│   ├── PicsBleDelegate.mc           ← BLEスキャン・パケット受信
│   ├── PicsData.mc                  ← データモデル・PICSパーサ・交差点DB
│   └── PicsMainView.mc              ← 画面描画(282×470px)
├── resources/
│   ├── data/
│   │   ├── intersections.json       ← 交差点データベース(自動生成)
│   │   └── jsonResources.xml        ← JSONリソース宣言
│   ├── drawables/
│   │   ├── drawables.xml            ← 画像リソース宣言
│   │   └── launcher_icon.png        ← アプリアイコン
│   └── strings/
│       └── strings.xml              ← UIラベル文字列(日本語)
├── tools/
│   ├── build.ps1                    ← Windows向けビルド補助
│   ├── clean_intersections.py       ← 住所データの整形補助
│   ├── csv_to_resource.py           ← CSV → JSON リソース変換スクリプト
│   ├── debug_project.py             ← プロジェクト診断・APIフラグ生成
│   ├── ble_pics_advertiser.py       ← macOS BLEテスト送信ツール
│   ├── make_ble_advertiser_app.sh   ← 上記を実行する macOS アプリバンドルの生成
│   └── BlePicsAdvertiser.cs         ← Windows BLEテスト送信ツールのソース
└── tests/
    └── test_csv_to_resource.py       ← CSV変換ツールの回帰テスト

技術仕様

動作デバイス

  • GARMIN GPSMAP H1i Plus(メインターゲット)
  • GARMIN GPSMAP H1(inReach なし版も対応)

必要な Connect IQ API Level

  • API Level 5.0.0 以上
    • Toybox.BluetoothLowEnergy(BLEスキャン機能)API Level 3.1.0〜
    • ScanResult.getManufacturerSpecificData(companyId) API Level 3.2.0〜
    • Toybox.Position(GPS)API Level 1.0.0〜

必要なパーミッション

パーミッション 用途
BluetoothLowEnergy PICSのBLEアドバタイズ受信
Positioning GPS座標取得(交差点名の解決に使用)

BLEスキャン動作仕様

項目 内容
動作モード セントラル(スキャン専用)
フィルタ条件 メーカーID = 0x01CE(高度化PICS固有)
ペアリング 不要(アドバタイズ受信のみ)
GATT接続 行わない
API getManufacturerSpecificData(companyId) で ByteArray を直接取得(SDK 8.x)

PICSパケット解析

BLEアドバタイズのメーカー固有データ(company ID 0x01CE)の後続バイト列を解析:

バイト 内容
[0..1] ヘッダー(バージョン・フラグ)
[2] メッセージタイプ(0/1/2)
[3] メッセージID
[4..5] 予約
[6..9] 交差点ID(4バイト hex)
[10+i] 信号状態(タイプ2のみ)上位4bit=残り時間, 下位4bit=状態

ビルド方法

前提条件

  1. Garmin Connect IQ SDK をインストール
  2. Visual Studio Code + Monkey C 拡張機能 を導入

開発者キーの生成

openssl genrsa -out developer_key.pem 4096
openssl pkcs8 -topk8 -inform PEM -outform DER -in developer_key.pem -out developer_key.der -nocrypt

APIフラグの自動生成

python3 tools/debug_project.py --generate-api-flags

これにより、インストールされているSDKに基づいてコンパイル時定数を持つ source/ApiFlags.mc が生成されます。

交差点データベースの生成

# デフォルト: ~/Downloads/250501-zenkoku-kousaten.csv を読み込む
python3 tools/csv_to_resource.py

# パスを明示する場合:
python3 tools/csv_to_resource.py /path/to/input.csv resources/data/intersections.json

ビルド手順

monkeyc -d gpsmaph1 -f monkey.jungle -o bin/pics-viewer.prg -y developer_key.der

実機転送(USBマスストレージ)

USBマスストレージモードにすること

cp bin/pics-viewer.prg /GARMIN/APPS/

テスト

python3 -m unittest discover -s tests -v

ログ保存機能 (SDカード記録)

受信したPICSパケットの生データ(HEXダンプ)、信号状態、電波強度が、実時刻と単調増加する受信Tickとともに記録されます。GarminのOS仕様上、以下の手順を実施しないとログはデバイスに保存されません。

  1. GarminデバイスをPCにUSB接続します。
  2. デバイス内の /GARMIN/APPS/LOGS/ フォルダを開きます。
  3. アプリの実行ファイル名と同じ名前を持つ、空のテキストファイル(例: pics-viewer.txt)を作成します。
  4. アプリを起動すると、このテキストファイルに受信ログが追記され続けます(長時間の連続記録によるファイル容量肥大化にご注意ください)。

BLEテスト送信ツール (macOS)

tools/ble_pics_advertiser.py は、company ID 0x01CE・Type 2・交差点ID DEADBEEF のサンプルPICSパケットを15秒間送信します。実際の信号機がなくても Connect IQ アプリの受信動作を確認できます(Windows版は tools/BlePicsAdvertiser.cs)。

依存パッケージ

python3 -m venv .venv
.venv/bin/python -m pip install pyobjc-framework-CoreBluetooth

仮想環境は、今後も使い続ける Python で作成してください。ベースとなる Python が後から削除されると(例: Homebrew を Intel 版から Apple Silicon 版へ移行した場合)venv の shebang が壊れ、pip3 がシステム Python にフォールバックして externally-managed-environment エラーになります。その場合は venv を作り直してください。

macOS の Bluetooth 権限による制約

macOS では、責任アプリケーション (responsible application) の Info.plist に NSBluetoothAlwaysUsageDescription が宣言されていない限り、CoreBluetooth の利用が拒否されます。シェルからスクリプトを起動した場合、責任アプリケーションは Python でもスクリプトでもなくターミナルアプリです。Terminal.app や Warp はこのキーを宣言していないため、送信を開始する前に TCC によってプロセスが強制終了されます。

This app has crashed because it attempted to access privacy-sensitive data
without a usage description. The app's Info.plist must contain an
NSBluetoothAlwaysUsageDescription key ...

プロセスは終了コード 134 で異常終了し、クラッシュレポートが ~/Library/Logs/DiagnosticReports/ に出力されます。これはスクリプトの不具合ではなく OS のポリシーです。実行時に NSBundle.mainBundle() を書き換える等の Python 側の回避策では解決できません。

方法1 — アプリバンドル経由で実行(推奨)

使用目的の説明を宣言し、フレームワーク版インタプリタを内包した小さなアプリバンドルを生成します。

tools/make_ble_advertiser_app.sh          # 生成のみ
tools/make_ble_advertiser_app.sh --run    # 生成・起動してログを表示

2回目以降は次のコマンドで起動できます。

open -a build/PICSBLEAdvertiser.app

open で起動するとバンドル自身が責任アプリケーションになるため、そのバンドルの Info.plist で権限チェックを満たせます。初回起動時に Bluetooth の許可ダイアログが表示され、システム設定 → プライバシーとセキュリティ → Bluetooth に PICS BLE Advertiser として登録されます。ターミナルを介さずに動作するため、出力は build/ble-advertiser.log に記録されます。

Publisher status: Started
Broadcasting CompanyId=0x01CE ID=DEADBEEF Type=2 for 15 seconds
Final status: Stopped

このバンドルはローカルのビルド成果物です(git 管理外)。Python の更新や venv の作り直し後は再生成してください。

方法2 — 使用目的を宣言しているターミナルから実行

スクリプトを直接実行できるのは、当該キーを宣言し、かつ Bluetooth へのアクセスを許可されたターミナルアプリのみです。

.venv/bin/python tools/ble_pics_advertiser.py

事前確認:

/usr/libexec/PlistBuddy -c "Print :NSBluetoothAlwaysUsageDescription" \
  "/Applications/Visual Studio Code.app/Contents/Info.plist"

Visual Studio Code はこのキーを宣言しているため、VS Code に Bluetooth 権限を許可すれば統合ターミナルから実行できます。Terminal.app と Warp は宣言していないため直接実行できません(方法1を使用してください)。Homebrew の Python.app/Contents/Info.plist を編集する方法も動作しますが、すべての Python プロセスに影響し、フレームワークの署名が無効化され、brew upgrade のたびに元へ戻るため推奨しません。

実装上の注意

デリゲートクラスでは組み込みの super() ではなく objc.super(...) を使用する必要があります。PyObjC の NSObject サブクラスで組み込みの super() を使うと AttributeError: 'super' object has no attribute 'init' が発生します。


注意事項・制約

Connect IQ BLE の制約

  • GATTクライアントのみ:アドバタイズの送信・GATTサーバーは不可
  • ウィジェット動作: 本アプリはWidgetとして動作し、PAGEボタンでアクセスします。
  • スキャンはフォアグラウンド動作時のみ安定:バックグラウンドではOSが間引く場合あり
  • ScanResult のフィルタリングはアプリ側で実装(Connect IQ はUUIDフィルタ非対応)
  • SDK 8.x/9.x 対応:ScanResult.getManufacturerSpecificData() はcompany IDを引数で指定し ByteArray を直接返す(旧APIは Dictionary を返していたが廃止)
  • エミュレータ確認モード:BLEやシミュレーターが不安定な場合でも、MENUボタンでモックデータ表示に切り替えてUI描画を確認できます。
  • 画面レイアウト:フッターは削除し、受信情報はコンパクトなヘッダーへ集約しています。
  • 周辺信号の表示:交差点名と住所は、文字数ではなく実際の描画幅に合わせて省略されます。日本語の全角文字を含む場合も、右寄せの距離・方角と重なったり折り返したりしません。

交差点名称について

実測ログ上の PICS Type0 パケットには、交差点名称ではなく UTMS_PICS0001 のような ASCII の発信器識別子が入っています。交差点名は Type1 パケット(緯度経度)とバンドルされた全国交差点DBを使って、GPS座標マッチングで名称解決します。

文字列リソース

状態表示と空表示のUIラベルは resources/strings/strings.xml で管理しています。ロケール別のディレクトリ(例:resources-eng/strings/strings.xml)を作成することで多言語対応が可能です。

シミュレーターについて

Connect IQ SDK 9.1.0 のシミュレーターは macOS 26 (Tahoe) 以降で ant_main スレッドがクラッシュする既知の問題があります。テストは実機(GPSMAP H1)で行ってください。Garmin が対応 SDK をリリース次第、更新予定です。

法令遵守

本アプリが受信するPICSのBLEアドバタイズは公共インフラから不特定多数に向けて送信される公開電波であり、電波法・電気通信事業法・個人情報保護法の「通信の秘密」には該当しません。本アプリは受信専用であり、信号の送信・改変は行いません。


謝辞

本プロジェクトは、yumu19/ble-pics-viewer (Python版) の実装およびパケット解析手法を全面的に参考にし、その成果をGarminデバイス向けに移植したものです。貴重な知見とソースコードを公開してくださった原作者の方に、この場を借りて深く感謝申し上げます。

参考リンク

About

高度化PICS信号機から送出されているビーコンをGARMINのデバイスで見る。

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages