MS_Windows10IoTCore - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki

Windows 10 IoT Core

概要

  • Windows 10 IoT Core の評価を実施中。
  • 最近は、Rasbian(後述の「On Rasbian」)が主流っぽくなってきた。

移行メモ(戻り先): 移行元にはパンくず(戻り先)の記載がなかったため、
内容と Tags(インフラストラクチャ / Windows / IoT)から
組み込み系のWindowsを調査 を戻り先として補った。

補足(Windows 10 IoT Core は実質的に終息した): 本ページは
2017〜2021 年頃の検証記録である。前提が大きく変わっているため、
先に現況を示す。

【製品としての終了】★
   ・Windows 10 IoT Core(バージョン 1809, LTSC)
       → 【2029年1月まで延長サポート】はあるが、
         【新バージョンは出ていない】
       → Windows 11 IoT Core は【存在しない】★
   ・Windows 10 IoT Core Services(有償の更新提供)
       → 【2023年で新規受付終了】
   ・IoT Core Dashboard
       → 配布が終了し、入手が難しい ★
   ・Raspberry Pi 4 への正式対応は
       → 【ついに提供されなかった】
         (Pi 2 / Pi 3 止まり)

【現在の Microsoft の IoT 製品】
   ・【Windows IoT Enterprise】★
       → フル Windows。産業用 PC / キオスク端末向け
       → x86/x64 のみ(ARM の Raspberry Pi は対象外)
   ・【Azure IoT Edge】
       → OS を問わず、コンテナでモジュールを動かす
       → Linux が主戦場 ★
   ・【Azure Sphere】
       → セキュア MCU。ただし
         2027年でサービス終了が発表されている
【本ページが「Rasbian が主流」と書いた見立ては正しかった】★
   Raspberry Pi での開発は
     → 【Raspberry Pi OS(旧 Raspbian)】+
       【.NET(.NET 8/9)】が現在の定番
     → GPIO は【System.Device.Gpio】
       (.NET IoT Libraries)★
         dotnet add package System.Device.Gpio
         dotnet add package Iot.Device.Bindings
     → 本ページの Lチカ/センサー読み取りは、
       この組み合わせで【ほぼ同じことができる】
     → 詳細は [.NET Core On Rasbian](MS_DotNetCoreOnRaspbian) を参照

基本

Headモード、Headlessモード

Headモード

  • GUI 有りモード

Headlessモード

  • GUI 無しモード
  • リソース消費を抑えることが出来る。

切替方法

以下のコマンドで切り替え可能。

  • Headless モードに変更
setbootoption.exe headless
shutdown /r /t 0
  • Head モードに変更
setbootoption.exe head
shutdown /r /t 0

移行メモ(正誤): 移行元では 2 つ目の見出しも
Headless モードに変更」となっていたが、
コマンドが setbootoption.exe head であることから
Head モードに変更」の誤記と判断し修正した。

ドライバの種類

Inboxドライバ

  • プラグ・アンド・プレイに対応した機器をパソコンに接続すると、
    Windows に同梱されるドライバの中から自動的にドライバが選択され組み込まれる。

  • このため、すぐに周辺機器が使えるようになる。

  • この、Windows に同梱されているドライバを、Inbox ドライバと呼ぶ。

DMAPドライバ

  • DMAP : Direct Memory Mapped

  • 前述の Inbox ドライバよりも GPIO パフォーマンスの向上を実現する開発用ドライバ

  • 以下のケースでは、安全に DMAP ドライバを使用できる。

  • 悪意のあるアプリは H/W、Security の問題を引き起こす可能性がある。

補足(Inbox と DMAP の性能差の理由): 「パフォーマンスの向上」の
中身を補っておく。

【なぜ DMAP が速いのか】★
   Inbox ドライバ経由
     アプリ → API → ドライバ → カーネル → GPIO
     → 【カーネル遷移(システム コール)】が
       毎回発生する
     → GPIO のトグルで数十 kHz 程度が限界

   DMAP(Direct Memory Mapped)
     アプリ → 【メモリに直接書く】 → GPIO
     → SoC の GPIO レジスタを
       ユーザー空間にマップしてしまう
     → カーネル遷移なし
     → 【数 MHz 級】まで出せる ★

【引き換えに失うもの】
   ・【保護がない】
     → アプリが任意のレジスタを叩ける
     → 原文の「悪意のあるアプリは H/W、Security の
       問題を引き起こす可能性がある」はこの意味 ★
   ・SPI / I2C の一部機能が使えなくなることがあった

【Linux 側の対応物】
   ・sysfs(/sys/class/gpio)… 遅い(Inbox 相当)
   ・【libgpiod(/dev/gpiochipN)】… 現在の標準 ★
   ・/dev/mem 直叩き(pigpio 等)… DMAP 相当。速いが危険
   → 【同じトレードオフが Linux にもある】

準備

開発用PC

OS

Windows 10 とする。

ネットワーク

企業内なら、開発用 NW とインターネットへの接続が許可されているもの。

  • 開発用 NW : Windows 10 IoT Core はイントラのセキュリティ・ポリシーと異なるため。
  • インターネット・アクセス: NuGet や Web サービス、クラウドの利用にあたって必要になる。

なお、PC-IoT 間は開発用 NW を使用し、
インターネット・アクセスには AP を経由して出る(後述)と良い。

IoT機器

本体

Raspberry Pi 3 MODEL B

電源

micro USB(Android と同じ)

周辺機器

ストレージ

  • microSD メモリーカード(Raspberry Pi 3 に挿す)
  • microSD から メモリーカード への 変換アダプター(開発用 PC に挿す)

ネットワーク

スイッチとして機能する機器(開発用 PC と Raspberry Pi 3 を接続する)

  • 有線 LAN の場合: L2 スイッチと LAN ケーブル
  • 無線 LAN の場合: プライバシー・セパレータ機能を搭載した Wi-Fi ルーター

移行メモ(正誤): 「プライバシー・セパレータ機能を搭載した Wi-Fi ルーター」
とあるが、プライバシー セパレータは端末間の通信を遮断する機能であり、
開発用 PC と Raspberry Pi を通信させたい本用途では
無効にする必要がある(「搭載した」=
「機能の有無を確認し、オフにできるもの」の意と解される)。
作者の記述はそのまま残したが、設定時は注意されたい。

入出力

  • モニタと HDMI ケーブル。
  • USB マウス, USB キーボード

セットアップ

Windows 10 IoT Coreのインストール

Windows 10 IoT Core Dashboardのダウンロード

移行メモ(リンク切れ): developer.microsoft.com/windows/iot/ 配下の
ドキュメントおよびダウンロードは Microsoft Learn へ統合・整理され、
IoT Core Dashboard の配布も終了しているため、
本ページ中の当該ドメインのリンクは到達しない可能性が高い
記録として残す。

microSDにWindows 10 IoT CoreのOSイメージを書き込む。

  • インストール後、そのまま Windows 10 IoT Core Dashboard が起動する。

  • 「microSD から メモリーカード への 変換アダプター」を開発用 PC に挿す。

  • [新しいデバイスのセットアップ] で以下の入力を行う。

  • [ダウンロードとインストール] をクリックして microSD に OS イメージを書き込む。

セットアップ

  • はじめに、Windows 10 IoT Core がダウンロードされる。

  • 次に、microSD に OS イメージが書き込まれる(フラッシング)。
    この際、コマンド・プロンプトで進捗表示がされる。

  • 書き込みが完了すると「SD カードは準備が完了しています」と表示される。

  • この画面で表示されている「3.デバイスを検索します」の「自分のデバイス」ボタンを押下し、
    「Dashboard」と「Raspberry Pi 3」との通信を許可しておく。
    Windows10IoTCoreDashboard.exe のインバウンドが許可される。

Raspberry Pi 3へ機器を接続

  • 電源

    • micro USB(Android と同じ)
  • 周辺機器

    • OS イメージを書き込んだ microSD を挿す。

    • 有線接続の場合、LAN ケーブルに接続する。

    • 入出力

      • USB マウス、USB キーボード
      • モニタ(HDMI ケーブル)

Windows 10 IoT Coreの初期設定をする。

起動時

  • 言語を選択する(日本語を選択)。

  • 無線 LAN の場合、

    • Wi-Fi を選択可能
    • IP アドレスは DHCP で自動配布される。
  • 有線 LAN の場合、

    • 起動前に LAN ケーブルを接続しておく。
    • IP アドレスは APIPA のアドレスになっている。

その他、適宜

  • コマンド・プロンプト
    • コマンド・ラインを選択して、
    • Administrator のログイン・アカウント情報を入力する。
    • コマンド・プロンプトを使用できるようになる。

Windows 10 IoT Core Dashboardから接続・操作する。

  • 開発用 PC 側の IP アドレスの設定

    • 無線 LAN の場合、
      DHCP で自動配布されるアドレスで通信可能なので設定は不要。

    • 有線 LAN の場合、
      APIPA のアドレスを確認し、開発用 PC のアドレスを設定・変更する。

      • IP アドレス : 169.254.x.x
      • サブネットマスク : 255.255.0.0
  • Windows 10 IoT Core Dashboard を起動する。

  • 「自分のデバイス」から Raspberry Pi 3 を選択、

補足(APIPA とは): 手順中に説明なく出てくるため補っておく。

【APIPA(Automatic Private IP Addressing)】★
   ・DHCP サーバが見つからないときに
     OS が【自分で勝手に付ける】アドレス
   ・範囲は【169.254.0.0/16】
   ・別名【リンクローカル アドレス】

   → L2 スイッチで PC と直結しただけの環境では
     DHCP がいないため、
     両方が APIPA になる
   → 【同じ 169.254.0.0/16 に居れば通信できる】ので
     本手順が成立する ★

【この手順が今も使える場面】
   ・産業機器・ネットワーク機器の初期設定
   ・ルータのない現場での 1 対 1 接続
   → 【169.254.x.x が見えたら
     「DHCP に届いていない」のサイン】と覚えておくとよい ★

Device Portal

  • 接続

    • 右クリック、[Device Portal で開く] を選択する。
    • Administrator のログイン・アカウント情報を入力する。
    • ログイン後、Device Portal が表示される。
  • 操作

    • Device Portal から操作する。
    • HOME: タイムゾーンの変更
    • PROCESS: 電源(再起動など)
    • ,etc.

SMB

  • 接続

    • 右クリック、[ネットワーク共有を開く] を選択する。
      上手く行かない時は、\\[Raspberry Pi 3のIPアドレス]\C$ を直打ち。
    • Administrator のログイン・アカウント情報を入力する。
    • \\[Raspberry Pi 3のIPアドレス]\C$ が開く。
  • 操作

  • 接続

    • 右クリック、[PowerShell を起動] を選択する。
    • Administrator のログイン・アカウント情報を入力する。
  • 操作

    • コマンドやコマンドレットを実行できる。

    • 前述の Head モード、Headless モードの切換

    • IP アドレスの設定(APIPA アドレスなら、必要に応じて変更すると良い)

      • 設定方法
ipconfig /all
netsh interface ip show config
netsh interface ip set address "Ethernet" static [IPアドレス] [サブネットマスク] [デフォゲ]
- 設定例は後述。
  • Administrator のパスワードを設定/変更する。
net user Administrator [パスワード]
  • ローカルコンピュータの新しい NetBIOS 名を設定
setcomputername [新しいマシン名]

補足(既定の資格情報は必ず変更する): 手順中で
「Administrator のログイン・アカウント情報を入力する」が繰り返されるが、
既定値のまま運用してはならない

【Windows 10 IoT Core の既定】★
   ユーザー : Administrator
   パスワード: p@ssw0rd
     → 【全世界で共通の初期値】である ★
     → net user Administrator [新パスワード] で
       必ず変更する(原文にも記載あり)

【IoT 機器の一般則】
   ・出荷時の共通パスワードは
     【Mirai ボットネット】等の
     大規模な感染の主因になった
   ・各国で規制が進んでいる
     - 英国 PSTI 法(2024年〜)
       → 【共通初期パスワードの禁止】★
     - 米国 Cyber Trust Mark
     - 日本 NOTICE / 技適の要件強化
   → 「LAN の中だから大丈夫」は通用しない
     (IoT 機器は踏み台にされる)★

開発用PC側のIPアドレスを設定/変更する。

IPアドレス、サブネットマスクなどを変更する。

  • Windows 10 IoT Core 側の設定に合わせて、
    IP アドレス、サブネットマスクなどを変更する。
netsh interface ip set address "Ethernet" static 192.168.1.1 255.255.255.0
  • 設定変更後、
    • PowerShell が切断される。
    • 前述の Dashboard の「自分のデバイス」の IP アドレスが更新される。

モバイルホットスポットでインターネット接続を共有する。

Windows 10 IoT Core とモバイルホットスポット(Wi-Fi)で
開発用 PC のインターネット接続を共有する。

この設定を行えば、開発用 PC と、Windows 10 IoT Core が、
192.168.137.0/24 のサブネットで接続できるようになり、
インターネット接続も共有可能になる。

ただし、Dashboard から操作するために、有線 LAN 接続は引き続き必要になる。

補足(192.168.137.0/24 は ICS の固定値): このアドレスが
唐突に出てくるが、Windows が決め打ちで使う値である。

【ICS(インターネット接続の共有)】★
   ・モバイル ホットスポットの実体は ICS
   ・共有側のホスト IP は
     【192.168.137.1】に固定される ★
     (レジストリで変更可能だが非推奨)
   ・DHCP で 192.168.137.x を配る
   ・NAT でインターネットへ出す

   → 「なぜか 192.168.137.x が振られる」
     という現象を見たら【ICS が有効】と判断できる ★

【本構成の意味】
   有線 LAN(169.254.x.x)… Dashboard / デプロイ用
   Wi-Fi(192.168.137.x)… インターネット アクセス用
     → 【2 系統を併用する】
     → 企業内 NW に IoT 機器を直接繋がずに済む ★
     → 「イントラのセキュリティ ポリシーと異なる」
       という前述の懸念への現実的な回答になっている

参考

後述の「参考 > インストール」を参照。

開発環境

開発用PCの準備

Windows 10 がインストールされた開発用 PC を準備する。

開発環境のセットアップ

Visual Studio 2017をインストールする。

  • ターゲットが Creators Update 対象になっているようで、
    Visual Studio 2015 ではなく 2017 が必要。
  • インストール時に、ユニバーサル Windows アプリ開発ツールをインストールすること。

移行メモ(誤字): 移行元の「Creators Updade」を
「Creators Update」に修正した。

開発者モードを有効にする。

Windows SDKのインストール

Windows 10 用の Windows SDK をダウンロードしてインストールする。

  • Windows 10 用の Windows SDK をダウンロード

  • Windows 10 用の Windows SDK をインストール

    • winsdksetup.exe をダブルクリックしてインストーラを実行。
    • インストーラーが起動したら既定値でインストールする。
  • インストールが完了したら、OS を再起動する。

“Hello World”的にサンプルを実行する準備

サンプル・プログラムをGitHubからDownloadZIPする。

HelloWorldする。

  • HelloWorld.sln をダブルクリックして VS 2017 を起動する。

  • [Debug] ボタンのドロップダウンから [リモート コンピューター] を選択。

  • 以下の入力を行う。

    • アドレス: IP アドレスを手打ち(自動検出できなかった場合)
    • 認証モード: ユニバーサル(暗号化されていないプロトコル)
    • [選択] ボタンを押下する。
  • [Debug] ボタンを押下する。

  • Hello World のメッセージが表示される。

HelloBlinkyする。

  • HelloWorld.sln と同じ手順で、HelloBlinky.sln を Debug 実行する。
    • (2 回目なので、)自動検出されている Raspberry Pi 3 を選択する。
    • GPIO pins の初期化が正しく行われた旨のメッセージが表示される。

サンプル・プログラムをデプロイして起動する。

デプロイ

  • [ソリューション構成] を「Release」に変更し、
  • [ビルド] から「xxの配置」を選択する。
  • [配置が完了しました]・・・の [完全パッケージ名] をメモしておく。

スタートアップアプリに指定

  • 前述の Dashboard から PowerShell で接続する。

  • 以下のコマンドを実行する。

iotstartup list
  • アプリが配置されていることを確認する。

  • 以下のコマンドを実行してスタートアップアプリに登録する。

iotstartup add headed 完全パッケージ名の先頭部分
  • shutdown /r /t 0 で再起動すれば、次回起動時から自作の IoT アプリが起動する。

  • 元の IoT Core メイン画面に戻すには、以下でスタートアップアプリを切り替える。

iotstartup add headed IoTCoreDefaultApp

補足(iotstartup の headed / headless): このコマンドは
前述の「Head モード / Headless モード」と対応している

【iotstartup の主なサブコマンド】★
   iotstartup list                  … 登録済みアプリの一覧
   iotstartup list headed           … 起動アプリ(GUI 有り)
   iotstartup list headless         … バックグラウンド アプリ
   iotstartup add headed <pkg>      … GUI アプリを既定に ★
   iotstartup add headless <pkg>    … バックグラウンドで常駐
   iotstartup remove headless <pkg> … 解除

【headed と headless は排他ではない】★
   ・headed  … 【1 つだけ】が前面に表示される
   ・headless… 【複数】が同時に常駐できる
   → 「画面は既定アプリ、
     裏でセンサー収集を常駐させる」構成が可能

【元に戻せなくなったときの保険】
   ・自作アプリがクラッシュ ループすると
     画面から操作できなくなる
   → 【Device Portal】または【PowerShell】から
     IoTCoreDefaultApp に戻す(原文の最後のコマンド)★
   → だから【ネットワーク経由の口を必ず確保しておく】

処理の実装

空のプロジェクトから実装を始める。

プロジェクトの新規作成

UWP バージョンを選択する

[ターゲットバージョン] と [最小バージョン] を選択する。

  • UWP バージョンの説明

    • ターゲットバージョン : 一般ユーザー向けのコードを新しく開発する場合、
      常に最新ビルドの Windows (14393) を使う。
    • 最小バージョン : エンタープライズ アプリを開発する場合は、
      最小バージョンで古いバージョンをサポートすることを検討する。
  • 参考

Debug実行

  • この状態だと、前述の選択した UWP バージョン次第で、
    ローカルでの Debug 実行も可能。
    しかし、以降、H/W に関する処理を実装していくと
    ローカルでの Debug 実行はできなくなる。

  • 前述の HelloWorld.sln、HelloBlinky.sln と同じ手順でコレを Debug 実行する。

    • [ターゲット CPU] を [x86] から [ARM] に変更してから、
    • [リモート コンピューター] で Debug 実行する。
  • 空のウィンドウが表示されることを確認する。

Writing apps開発の設定を行う

  • 参照設定で [Universal Windows]-[拡張]-[Windows IoT Extension for the UWP] を
    チェックする。

  • NuGet から「Direct Memory Mapped Driver」の「Microsoft.IoT.Lightning」を
    インストールして参照設定を追加。

  • 以下の「Direct Memory Mapped Driver」の「Microsoft.IoT.Lightning」を
    利用するための設定を行う。

    • 前述の Device Portal からドライバを切替える。

      • [Devices] をクリックしてページを切り替える。
      • [Default Controller Driver] を [Direct Memory Mapped Driver] に設定する。
      • 「Warning: This option will reduce Windows security
        in exchange for performance using an in development driver.」と表示される。
      • [Update Driver] をクリックしてドライバ設定を更新する。
      • リブートするかどうかのメッセージが表示されるのでリブートする。
    • 以下のコードをマニフェストファイル(Package.appxmanifest)に追加。

      • Capabilities 要素に以下を追加する。
<Capabilities>
    <!-- アプリケーションがカスタムデバイスにアクセスできるようにする -->
    <iot:Capability Name="lowLevelDevices" />
    <!-- LightningインターフェースのデバイスGUID ID -->
    <DeviceCapability Name="109b86ad-f53d-4b76-aa5f-821e2ddf2141"/>
</Capabilities>
- package 要素に上記の iot 名前空間を追加する。
 xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
 xmlns:iot="http://schemas.microsoft.com/appx/manifest/iot/windows10"
 IgnorableNamespaces="uap mp iot">

補足(UWP のケーパビリティ モデル): Package.appxmanifest への
追記が必要な理由を補っておく。

【UWP はサンドボックスで動く】★
   ・既定では【ファイル システムもデバイスも触れない】
   ・必要な権限を【マニフェストで宣言する】
     → 宣言していない API は
       実行時に UnauthorizedAccessException

【本ページで追加している 2 つ】
   ・lowLevelDevices
       → GPIO / I2C / SPI / PWM への
         【低レベル アクセスを許可する】★
       → これは【制限付きケーパビリティ】であり、
         ストア配布には申請・審査が要る
         (IoT Core の自社デバイスなら問題ない)
   ・DeviceCapability(GUID 指定)
       → Lightning ドライバの
         【特定インターフェースへのアクセス】を許可

【後述の Twitter で追加するもの】
   ・internetClient … 外向きの HTTP を許可 ★
     → これを入れ忘れると
       「デバッグでは動くのに配置すると通信できない」
       という現象になる
【現在の Raspberry Pi + .NET なら不要】★
   Raspberry Pi OS + .NET 8/9 では
     ・サンドボックスがないため
       マニフェストの宣言は不要
     ・代わりに【Linux のパーミッション】で制御する
       (gpio / i2c グループへの所属)
         sudo usermod -aG gpio,i2c,spi $USER

Lチカを実装する。

デバイス

Apple Piの LED の GPIO number については、下記を参照のこと。

移行メモ(リンク切れ): Yahoo! ブログは 2019 年 12 月にサービス終了
しており、上記 URL は到達しない。記録として残す。

MainPage.xaml.cs

using System.Threading.Tasks;
using Windows.Devices;
using Windows.Devices.Gpio;
using Microsoft.IoT.Lightning.Providers;

namespace App1
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        /// <summary>
        /// GpioPin #5は青色
        /// GpioPin #6は白色
        /// </summary>
        private readonly int LED_PIN = 6;

        /// <summary>GpioPin</summary>
        private GpioPin _gpioPin;

        /// <summary>Lチカを実装する。</summary>
        public MainPage()
        {
            this.InitializeComponent();

            // LightningProviderが利用可能かどうかチェックする。
            if (LightningProvider.IsLightningEnabled)
            {
                // LowLevelDevicesAggregateProviderを取得
                LowLevelDevicesController.DefaultProvider = LightningProvider.GetAggregateProvider();
            }

            // GpioControllerを取得
            GpioController gpioCtrl = GpioController.GetDefault();

            if (gpioCtrl == null)
            {
                return;
            }

            // GpioControllerでGpioを取得
            this._gpioPin = gpioCtrl.OpenPin(LED_PIN);
            // GpioPinでGpioPinをコントロール。
            this._gpioPin.SetDriveMode(GpioPinDriveMode.Output);
            this._gpioPin.Write(GpioPinValue.High);

            this.loop();
        }

        /// <summary>
        /// UIをハングさせないよう、asyncなloop内でawaitを使用する。
        /// System.Threading.Threadが無いので、こうなる。
        /// </summary>
        private async void loop()
        {
            while (true)
            {
                // GpioPinでGpioPinをコントロール。
                await Task.Delay(1000);
                this._gpioPin.Write(GpioPinValue.Low);
                await Task.Delay(1000);
                this._gpioPin.Write(GpioPinValue.High);
            }
        }
    }
}

補足(コメントの「System.Threading.Thread が無い」について): これは
UWP の制約を正しく指摘している。

【UWP には Thread クラスがなかった】★
   ・.NET Core(UWP のプロファイル)では
     System.Threading.Thread の
     【公開コンストラクタが提供されなかった】
     → new Thread(...) が書けない
     → Thread.Sleep も使えない
   ・代わりに
     → Task.Run / Task.Delay
     → ThreadPool
     → Windows.System.Threading.ThreadPoolTimer

   ※ .NET Standard 2.0 対応(2017年頃)以降、
     UWP でも Thread が使えるようになったが、
     【async / await を使うのが正しい作法】である ★
【このコードの注意点】
 ① 【async void の無限ループ】
     → 例外が発生しても【どこにも捕まらない】★
     → try-catch で囲むか、
       Task を返して呼び出し側で監視する
 ② 【終了手段がない】
     → CancellationToken を渡して
       アプリ終了時に止められるようにする ★
 ③ 【コンストラクタで長い処理を始めている】
     → Loaded イベントで開始する方が安全
       (後述の LCD / センサーの例では
         そうしており、そちらが正しい)
 ④ Task.Delay(1000) は【正確な 1 秒ではない】
     → 精密な周期が要るなら
       ハードウェア タイマや PWM を使う
【現在の .NET(Raspberry Pi OS)での同等コード】★
   using System.Device.Gpio;

   using var ctrl = new GpioController();
   ctrl.OpenPin(6, PinMode.Output);
   while (!token.IsCancellationRequested) {
       ctrl.Write(6, PinValue.High);
       await Task.Delay(1000, token);
       ctrl.Write(6, PinValue.Low);
       await Task.Delay(1000, token);
   }
   → 【Lightning プロバイダも
     マニフェストの宣言も不要】になる

LCDに文字を表示する。

デバイス

  • 電子工作 - 開発基盤部会 Wiki > 周辺機器 > LCD(液晶ディスプレイ)> I2C LCD AQM0802
    電子工作の該当節)

MainPage.xaml.cs

using System.Text;
using System.Threading.Tasks;
using Windows.Devices;
using Windows.Devices.I2c;
using Microsoft.IoT.Lightning.Providers;

// 空白ページの項目テンプレートについては、https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x411 を参照してください

namespace App1
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        /// <summary>
        /// Raspberry Pi 3で GPIO端子の I2C機能を有効化する方法
        ///  (ラズパイ3の GPIO端子の I2C機能を有効にして各種センサーを繋げる方法まとめ)
        /// http://www.neko.ne.jp/~freewing/raspberry_pi/raspberry_pi_3_gpio_enable_i2c/
        /// </summary>
        private readonly byte _Lcd_Addr = 0x3e;

        /// <summary>I2cDevice</summary>
        private I2cDevice _Lcd;

        public MainPage()
        {
            this.InitializeComponent();
            this.Loaded += MainPage_Loaded;
        }

        /// <summary>Loaded</summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            await this.InitLcd();
            await this.DisplayLcd();
        }

        /// <summary>
        /// LCDを初期化する。
        /// 電子工作 - 開発基盤部会 Wiki > 周辺機器 > LCD(液晶ディスプレイ)> I2C LCD AQM0802
        /// </summary>
        private async Task InitLcd()
        {
            // LightningProviderが利用可能かどうかチェックする。
            if (LightningProvider.IsLightningEnabled)
            {
                // LowLevelDevicesAggregateProviderを取得
                LowLevelDevicesController.DefaultProvider = LightningProvider.GetAggregateProvider();
            }

            // I2cControllerを取得
            I2cController i2cCtrl = await I2cController.GetDefaultAsync();

            if (i2cCtrl == null)
            {
                return;
            }

            // I2cControllerでLCDを取得。
            this._Lcd = i2cCtrl.GetDevice(new I2cConnectionSettings(this._Lcd_Addr));

            // 初期化
            await WriteLcdCmd(0x38, 1); // 行数の設定
            await WriteLcdCmd(0x39, 1); // 拡張コマンドの設定開始
            await WriteLcdCmd(0x14, 1); // 内部OSC周波設定
            await WriteLcdCmd(0x70, 1); // コントラスト設定
            await WriteLcdCmd(0x56, 1); // パワー/アイコン コントラスト設定
            await WriteLcdCmd(0x6c, 250); // Follower設定
            await WriteLcdCmd(0x38, 1); // 拡張コマンドの設定終了
            await WriteLcdCmd(0x0c, 1); // ディスプレイ オン
            await WriteLcdCmd(0x01, 2); // ディスプレイ クリア
        }

        /// <summary>LCDに表示する</summary>
        /// <returns>Task</returns>
        private async Task DisplayLcd()
        {
            await WriteLcdCmd(0x01, 2); // ディスプレイ クリア

            // 一行目を指定して
            await WriteLcdCmd(0x80, 1); // Set DDRAM Address
            await WriteLcdDisplay("RzPi3"); // Write data to RAM

            // 二行目を指定して
            await WriteLcdCmd(0xc0, 2); // Set DDRAM Address
            await WriteLcdDisplay("ApplePi"); // Write data to RAM
        }

        /// <summary>LCDにコマンドを送る</summary>
        /// <param name="cmd">byte</param>
        /// <param name="waitTime_msec">int</param>
        /// <returns>Task</returns>
        private async Task WriteLcdCmd(byte cmd, int waitTime_msec)
        {
            // I2cDeviceでLCDをコントロール。
            // 10 bit 書くので2 byte で先頭は空なので 0.
            this._Lcd.Write(new byte[] { 0, cmd });
            await Task.Delay(waitTime_msec);
        }

        /// <summary>LCDに表示する</summary>
        /// <param name="msg">string</param>
        /// <returns>Task</returns>
        private async Task WriteLcdDisplay(string msg)
        {
            byte[] bytesMSG = Encoding.ASCII.GetBytes(msg);

            // I2cDeviceでLCDをコントロール。
            foreach (byte b in bytesMSG)
            {
                // 10 bit 書くので2 byte で先頭は・・・
                this._Lcd.Write(new byte[] { 0x40, b }); // 先頭0x40は?
            }
            await Task.Delay(1);
        }
    }
}

補足(コード中の「先頭 0x40 は?」への回答): 作者が疑問符を付けた
この 1 バイトには明確な意味がある。

【AQM0802(ST7032 系コントローラ)の I2C プロトコル】★
   送信フォーマットは
     [スレーブアドレス] [制御バイト] [データ]

   【制御バイト】の意味
     bit7 = Co(Continuation)
       0 … これ以降は全部データ(連続送信)
       1 … 次の 1 バイトの後にまた制御バイトが来る
     bit6 = RS(Register Select)★
       【0 … コマンド】(命令レジスタ)
       【1 … データ】(DDRAM = 表示文字)
     bit5-0 = 0 固定

   → 0x00 = 0b0000_0000 → Co=0, RS=0 →【コマンド】
   → 0x40 = 0b0100_0000 → Co=0, RS=1 →【表示データ】★

   → 原文のコメント
     「10 bit 書くので 2 byte で先頭は空なので 0」は
     正確には【制御バイト】であり、
     「空」ではなく【コマンドであることを示す値】である ★
【初期化シーケンスの意味(データシート由来)】
   0x38 … Function Set(8bit, 2行, 通常命令)
   0x39 … Function Set(拡張命令セットへ切替)★
   0x14 … Internal OSC frequency
   0x70 … Contrast set(下位 4bit)
   0x56 … Power/ICON/Contrast(Booster ON)
   0x6C … Follower control ← 【250ms 待つ】★
           (内部昇圧回路の安定待ち。省くと表示が乱れる)
   0x38 … 通常命令セットへ戻す
   0x0C … Display ON(カーソル非表示)
   0x01 … Clear Display ← 【1.08ms 以上待つ】

   → 【待ち時間はデータシートの規定値】であり、
     短縮すると動いたり動かなかったりする ★
【今なら Iot.Device.Bindings に実装がある】★
   dotnet add package Iot.Device.Bindings
     → Iot.Device.CharacterLcd 名前空間に
       Hd44780 / Lcd1602 / Lcd2004 等の実装がある
     → I2C アダプタ(PCF8574)経由も対応
   → 上記のようなレジスタ叩きを自分で書かずに済む

センサーからデータを取得する。

デバイス

  • 電子工作 - 開発基盤部会 Wiki > 周辺機器 > センサー類 > BME280
    電子工作の該当節)

MainPage.xaml

<Page
    x:Class="App1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock x:Name="tbkTMP" Text="tbkTMP" HorizontalAlignment="Left" VerticalAlignment="Top" Height="50" Width="400" Margin="200, 140, 0, 0" TextWrapping="Wrap" FontSize="36" />
        <TextBlock x:Name="tbkHUM" Text="tbkHUM" HorizontalAlignment="Left" VerticalAlignment="Top" Height="50" Width="400" Margin="200, 210, 0, 0" TextWrapping="Wrap" FontSize="36" />
        <TextBlock x:Name="tbkPRE" Text="tbkPRE" HorizontalAlignment="Left" VerticalAlignment="Top" Height="50" Width="400" Margin="200, 280, 0, 0" TextWrapping="Wrap" FontSize="36" />
    </Grid>
</Page>

MainPage.xaml.cs

のコードを一部、修正して動かした。

using System.Threading;
using System.Diagnostics;

// 空白ページの項目テンプレートについては、https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x411 を参照してください

namespace App1
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        /// <summary>ライブラリ</summary>
        private BME280 bme280;
        /// <summary>タイマー</summary>
        private Timer periodicTimer;

        public MainPage()
        {
            this.InitializeComponent();
            this.Loaded += MainPage_Loaded;
        }

        /// <summary>Loaded</summary>
        /// <param name="sender">object</param>
        /// <param name="e">RoutedEventArgs</param>
        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            this.bme280 = new BME280();
            this.initBme280();
        }

        /// <summary>初期化</summary>
        private async void initBme280()
        {
            await this.bme280.Initialize();
            this.periodicTimer = new Timer(this.TimerCallback, null, 0, 1000);
        }

        /// <summary>TimerCallback</summary>
        /// <param name="state">object</param>
        private void TimerCallback(object state)
        {
            // ココの処理はBackground Threadで動作する。

            var temp = bme280.ReadTemperature();
            var press = bme280.ReadPreasure() / 100;
            var humidity = bme280.ReadHumidity();
            var alt = bme280.ReadAltitude(1013);   // 1013hPa = pressure at 0m

            Debug.WriteLine(
                "Temp:{0:F2}℃ Humidity:{1:F2}% Press:{2:F2}hPa Alt:{3:F0}m",
                temp, humidity, press, alt);

            // 主スレッドで処理する(以下はUWPでのControl.Invokeの書き方らしい)
            var task = this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                this.tbkTMP.Text = temp.ToString("F2") + "℃";
                this.tbkHUM.Text = humidity.ToString("F2") + "%";
                this.tbkPRE.Text = press.ToString("F2") + "hPa";
            });
        }
    }
}

補足(「UWP での Control.Invoke の書き方らしい」への回答): この理解は
正しい。UI サブシステムごとの対応物を整理しておく。

UI サブシステム UI スレッドへのマーシャリング
Windows Forms Control.Invoke / BeginInvoke
WPF Dispatcher.Invoke / BeginInvoke
UWP / WinUI Dispatcher.RunAsync(本コード)★
WinUI 3 は DispatcherQueue.TryEnqueue
ASP.NET 不要(UI スレッドの概念がない)
【なぜ必要か】★
   UI 要素は【作成したスレッドからしか触れない】
     → System.Threading.Timer のコールバックは
       【スレッド プール上】で動く(原文のコメントどおり)
     → そこから直接 TextBlock.Text に代入すると
       【例外】になる

   詳細は [コントロールのInvokeメソッド](MS_ControlInvoke) を参照。
【このコードの改善点】
 ① 【Dispatcher.RunAsync の戻り値を捨てている】
     var task = ... で受けているが await していない
     → 例外が握り潰される
     → 【await するか、明示的に破棄する】★
 ② 【Timer が 1 秒周期で、処理が重いと重なる】
     → I2C 読み取りが 1 秒を超えると
       コールバックが並行実行される
     → period を Timeout.Infinite にして
       処理の最後に Change で再設定する方が安全 ★
 ③ 【Timer が GC される可能性】
     → フィールドで保持しているので本コードは大丈夫
     → ローカル変数にすると回収されて止まる(典型的な罠)

BME280.cs

using System;
using System.Threading.Tasks;
using System.Diagnostics;

using Windows.Devices;
using Windows.Devices.I2c;
using Microsoft.IoT.Lightning.Providers;

namespace App1
{
    /// <summary>BME280キャリブレーション用</summary>
    public class BME280_CalibrationData
    {
        // BME280 Registers

        /// <summary></summary>
        public ushort dig_T1 { get; set; }
        /// <summary></summary>
        public short dig_T2 { get; set; }
        /// <summary></summary>
        public short dig_T3 { get; set; }

        /// <summary></summary>
        public ushort dig_P1 { get; set; }
        /// <summary></summary>
        public short dig_P2 { get; set; }
        /// <summary></summary>
        public short dig_P3 { get; set; }
        /// <summary></summary>
        public short dig_P4 { get; set; }
        /// <summary></summary>
        public short dig_P5 { get; set; }
        /// <summary></summary>
        public short dig_P6 { get; set; }
        /// <summary></summary>
        public short dig_P7 { get; set; }
        /// <summary></summary>
        public short dig_P8 { get; set; }
        /// <summary></summary>
        public short dig_P9 { get; set; }

        /// <summary></summary>
        public byte dig_H1 { get; set; }
        /// <summary></summary>
        public short dig_H2 { get; set; }
        /// <summary></summary>
        public byte dig_H3 { get; set; }
        /// <summary></summary>
        public short dig_H4 { get; set; }
        /// <summary></summary>
        public short dig_H5 { get; set; }
        /// <summary></summary>
        public sbyte dig_H6 { get; set; }
    }

    /// <summary>BME280 本体</summary>
    public class BME280
    {
        #region 各種列挙型

        /// <summary>キャリブレーションデータのレジスタアドレス</summary>
        enum eCalibrationDataRegistersAddress : byte
        {
            BME280_REGISTER_DIG_T1 = 0x88,
            BME280_REGISTER_DIG_T2 = 0x8A,
            BME280_REGISTER_DIG_T3 = 0x8C,

            BME280_REGISTER_DIG_P1 = 0x8E,
            BME280_REGISTER_DIG_P2 = 0x90,
            BME280_REGISTER_DIG_P3 = 0x92,
            BME280_REGISTER_DIG_P4 = 0x94,
            BME280_REGISTER_DIG_P5 = 0x96,
            BME280_REGISTER_DIG_P6 = 0x98,
            BME280_REGISTER_DIG_P7 = 0x9A,
            BME280_REGISTER_DIG_P8 = 0x9C,
            BME280_REGISTER_DIG_P9 = 0x9E,

            BME280_REGISTER_DIG_H1 = 0xA1,
            BME280_REGISTER_DIG_H2 = 0xE1,
            BME280_REGISTER_DIG_H3 = 0xE3,
            BME280_REGISTER_DIG_H4_L = 0xE4,
            BME280_REGISTER_DIG_H4_H = 0xE5,
            BME280_REGISTER_DIG_H5_L = 0xE5,
            BME280_REGISTER_DIG_H5_H = 0xE6,
            BME280_REGISTER_DIG_H6 = 0xE7,
        };

        /// <summary>測定データのレジスタアドレス</summary>
        enum eDataRegistersAddress : byte
        {
            BME280_REGISTER_PRESSUREDATA_MSB = 0xF7,
            BME280_REGISTER_PRESSUREDATA_LSB = 0xF8,
            BME280_REGISTER_PRESSUREDATA_XLSB = 0xF9, // bits <7:4>

            BME280_REGISTER_TEMPDATA_MSB = 0xFA,
            BME280_REGISTER_TEMPDATA_LSB = 0xFB,
            BME280_REGISTER_TEMPDATA_XLSB = 0xFC, // bits <7:4>

            BME280_REGISTER_HUMIDDATA_MSB = 0xFD,
            BME280_REGISTER_HUMIDDATA_LSB = 0xFE,
        };

        /// <summary>コマンド</summary>
        enum eCmd : byte
        {
            BME280_REGISTER_CHIPID = 0xD0,
            BME280_REGISTER_SOFTRESET = 0xE0,

            BME280_REGISTER_CONTROLHUMID = 0xF2,
            BME280_REGISTER_STATUS = 0xF3,
            BME280_REGISTER_CONTROL = 0xF4,
            BME280_REGISTER_CONFIG = 0xF5,
        };

        /// <summary>
        /// 
        /// Enables 2-wire I2C interface when set to ‘0’
        /// </summary>
        public enum interface_mode_e : byte
        {
            i2c = 0,
            spi = 1
        };

        /// <summary>
        /// スタンバイ時間のオプション
        /// t_sb standby options
        /// effectively the gap between automatic measurements when in "normal" mode
        /// </summary>
        public enum standbySettings_e : byte
        {
            tsb_0p5ms = 0,
            tsb_62p5ms = 1,
            tsb_125ms = 2,
            tsb_250ms = 3,
            tsb_500ms = 4,
            tsb_1000ms = 5,
            tsb_10ms = 6,
            tsb_20ms = 7
        };

        /// <summary>
        /// BME280のモード設定
        /// sensor modes,
        /// it starts off in sleep mode on power on forced is to take a single measurement now normal takes measurements reqularly automatically
        /// </summary>
        public enum mode_e : byte
        {
            smSleep = 0,
            smForced = 1,
            smNormal = 3
        };

        /// <summary>
        /// IIRフィルタの有効/無効
        /// Filter coefficients
        /// higher numbers slow down changes, such as slamming doors
        /// </summary>
        public enum filterCoefficient_e : byte
        {
            fc_off = 0, // OFF
            fc_2 = 1,
            fc_4 = 2,
            fc_8 = 3,
            fc_16 = 4
        };

        /// <summary>
        /// Oversampling options for humidity
        /// Oversampling reduces the noise from the sensor
        /// </summary>
        public enum oversampling_e : byte
        {
            osSkipped = 0,
            os1x = 1,
            os2x = 2,
            os4x = 3,
            os8x = 4,
            os16x = 5
        };

        #endregion

        #region メンバ変数

        /// <summary>
        /// Raspberry Pi 3で GPIO端子の I2C機能を有効化する方法
        ///  (ラズパイ3の GPIO端子の I2C機能を有効にして各種センサーを繋げる方法まとめ)
        /// http://www.neko.ne.jp/~freewing/raspberry_pi/raspberry_pi_3_gpio_enable_i2c/
        /// </summary>
        const byte BME280_Address = 0x76;

        /// <summary>
        /// BME280 device signature
        /// </summary>
        const byte BME280_Signature = 0x60;

        /// <summary>
        /// String for the friendly name of the I2C bus 
        /// </summary>
        private const string I2CControllerName = "I2C1";

        /// <summary>
        /// Create an I2C device
        /// </summary>
        private I2cDevice bme280 = null;

        /// <summary>
        /// キャリブレーション
        /// Create new calibration data for the sensor
        /// </summary>
        private BME280_CalibrationData CalibrationData;
        
        /// <summary>インターフェイスモード</summary>
        private byte spi3w_en = (byte)interface_mode_e.i2c;

        /// <summary>スタンバイ時間のオプション</summary>
        private byte t_sb;
        /// <summary>BME280のモード設定</summary>
        private byte mode;
        /// <summary>IIRフィルタの有効/無効(サンプリング回数)</summary>
        private byte filter;
        
        /// <summary>温度測定の有効(オーバーサンプリング値)/無効</summary>
        private byte osrs_t;
        /// <summary>気圧測定の有効(オーバーサンプリング値)/無効</summary>
        private byte osrs_p;
        /// <summary>湿度測定の有効(オーバーサンプリング値)/無告</summary>
        private byte osrs_h;

        /// <summary>データ校正用変数</summary>
        private Int32 t_fine;

        #endregion

        /// <summary>Constructor</summary>
        /// <param name="t_sb">スタンバイ時間のオプション</param>
        /// <param name="mode">BME280のモード設定</param>
        /// <param name="filter">IIRフィルタの有効/無効(サンプリング回数)</param>
        /// <param name="osrs_t">温度測定の有効(オーバーサンプリング値)/無効</param>
        /// <param name="osrs_p">気圧測定の有効(オーバーサンプリング値)/無効</param>
        /// <param name="osrs_h">湿度測定の有効(オーバーサンプリング値)/無告</param>
        public BME280(standbySettings_e t_sb = standbySettings_e.tsb_0p5ms,    // 0.5msec
                      mode_e mode = mode_e.smNormal,                           // Normal
                      filterCoefficient_e filter = filterCoefficient_e.fc_16,  // サンプリング回数 * 16
                      oversampling_e osrs_t = oversampling_e.os2x,             // オーバーサンプリング * 2
                      oversampling_e osrs_p = oversampling_e.os16x,            // オーバーサンプリング * 16
                      oversampling_e osrs_h = oversampling_e.os1x)             // オーバーサンプリング * 1
        {
            this.t_sb = (byte)t_sb;
            this.mode = (byte)mode;
            this.filter = (byte)filter;
            this.osrs_t = (byte)osrs_t;
            this.osrs_p = (byte)osrs_p;
            this.osrs_h = (byte)osrs_h;
        }

        #region 初期化

        /// <summary>
        /// Method to initialize the BME280 sensor
        /// </summary>
        /// <returns></returns>
        public async Task Initialize()
        {
            Debug.WriteLine("BME280::Initialize");

            try
            {
                ////Instantiate the I2CConnectionSettings using the device address of the BME280
                //I2cConnectionSettings settings = new I2cConnectionSettings(BME280_Address);
                
                ////Set the I2C bus speed of connection to fast mode
                //settings.BusSpeed = I2cBusSpeed.FastMode;
                
                ////Use the I2CBus device selector to create an advanced query syntax string
                //string aqs = I2cDevice.GetDeviceSelector(I2CControllerName);
                
                ////Use the Windows.Devices.Enumeration.DeviceInformation class to create a collection using the advanced query syntax string
                //DeviceInformationCollection dis = await DeviceInformation.FindAllAsync(aqs);
                
                ////Instantiate the the BME280 I2C device using the device id of the I2CBus and the I2CConnectionSettings
                //bme280 = await I2cDevice.FromIdAsync(dis[0].Id, settings);
                
                // LightningProviderが利用可能かどうかチェックする。
                if (LightningProvider.IsLightningEnabled)
                {
                    // LowLevelDevicesAggregateProviderを取得
                    LowLevelDevicesController.DefaultProvider = LightningProvider.GetAggregateProvider();
                }

                // I2cControllerを取得
                I2cController i2cCtrl = await I2cController.GetDefaultAsync();

                if (i2cCtrl == null)
                {
                    return;
                }

                // I2cControllerでbme280を取得。
                this.bme280 = i2cCtrl.GetDevice(new I2cConnectionSettings(BME280.BME280_Address));

                //Check if device was found
                if (this.bme280 == null)
                {
                    Debug.WriteLine("Device not found");
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine("Exception: " + e.Message + "\n" + e.StackTrace);
                throw;
            }

            byte[] readChipID = new byte[] { (byte)eCmd.BME280_REGISTER_CHIPID };
            byte[] ReadBuffer = new byte[] { 0xFF };

            // Read the device signature
            bme280.WriteRead(readChipID, ReadBuffer);
            Debug.WriteLine("BME280 Signature: " + ReadBuffer[0].ToString());

            // Verify the device signature
            if (ReadBuffer[0] != BME280.BME280_Signature)
            {
                Debug.WriteLine("BME280::Begin Signature Mismatch.");
                return;
            }

            // Read the coefficients table
            // キャリブレーション読み取り
            this.CalibrationData = this.ReadCoefficeints();

            #region レジスタの設定

            // Set configuration registers
            this.WriteConfigRegister();               // configレジスタ
            this.WriteControlMeasurementRegister();   // ctrl_measレジスタ
            this.WriteControlRegisterHumidity();      // ctrl_humレジスタ
            //Set configuration registers again to ensure configuration of humidity
            this.WriteConfigRegister();
            this.WriteControlMeasurementRegister();
            this.WriteControlRegisterHumidity();

            #endregion

            //Dummy read temp to setup t_fine
            this.ReadTemperature();
        }

        #region キャリブレーション読み取り

        /// <summary>
        /// BME280キャリブレーション読み取り
        /// Method to read the caliberation data from the registers
        /// </summary>
        /// <returns></returns>
        private BME280_CalibrationData ReadCoefficeints()
        {
            // 16 bit calibration data is stored as Little Endian, the helper method will do the byte swap.
            this.CalibrationData = new BME280_CalibrationData();

            // Read temperature calibration data
            this.CalibrationData.dig_T1 = this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_T1);
            this.CalibrationData.dig_T2 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_T2);
            this.CalibrationData.dig_T3 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_T3);

            // Read presure calibration data
            this.CalibrationData.dig_P1 = this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_P1);
            this.CalibrationData.dig_P2 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_P2);
            this.CalibrationData.dig_P3 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_P3);
            this.CalibrationData.dig_P4 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_P4);
            this.CalibrationData.dig_P5 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_P5);
            this.CalibrationData.dig_P6 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_P6);
            this.CalibrationData.dig_P7 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_P7);
            this.CalibrationData.dig_P8 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_P8);
            this.CalibrationData.dig_P9 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_P9);

            // Read humidity calibration data
            this.CalibrationData.dig_H1 = this.ReadByte((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_H1);
            this.CalibrationData.dig_H2 = (short)this.ReadUInt16_LittleEndian((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_H2);
            this.CalibrationData.dig_H3 = this.ReadByte((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_H3);
            short e4                    = this.ReadByte((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_H4_L);    // Read 0xE4
            short e5                    = this.ReadByte((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_H4_H);    // Read 0xE5
            this.CalibrationData.dig_H4 = (short)((e4 << 4) + (e5 & 0x0F));
            short e6                    = this.ReadByte((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_H5_H);    // Read 0xE6
            this.CalibrationData.dig_H5 = (short)((e5 >> 4) + (e6 << 4));
            this.CalibrationData.dig_H6 = (sbyte)this.ReadByte((byte)eCalibrationDataRegistersAddress.BME280_REGISTER_DIG_H6);

            return CalibrationData;
        }

        #endregion

        #region レジスタの設定

        /// <summary>
        /// configレジスタの設定
        /// ・t_sb     : スタンバイ時間
        /// ・filter   : IIRフィルタの有効/無効
        /// ・spi3w_en : 3 wire SPIの有効/無効
        /// ----------
        /// Method to write the config register (default 16)
        /// 000  100  00
        /// ↑  ↑   ↑I2C mode
        /// ↑  ↑Filter coefficient = 16
        /// ↑t_sb = 0.5ms
        /// </summary>
        private void WriteConfigRegister()
        {
            byte value = (byte)((this.t_sb << 5) + (this.filter << 2) + this.spi3w_en);
            byte[] WriteBuffer = new byte[] { (byte)eCmd.BME280_REGISTER_CONFIG, value };
            bme280.Write(WriteBuffer);
            return;
        }

        /// <summary>
        /// ctrl_measレジスタの設定
        /// ・osrs_t : 温度測定の有効(オーバーサンプリング値)/無効
        /// ・osrs_p : 気圧測定の有効(オーバーサンプリング値)/無効
        /// ・mode   : BME280の測定モード設定
        /// ----------
        /// Method to write the control measurment register (default 87)
        /// 010  101  11 
        /// ↑  ↑   ↑ mode
        /// ↑  ↑ Pressure oversampling
        /// ↑ Temperature oversampling
        /// </summary>
        private void WriteControlMeasurementRegister()
        {
            byte value = (byte)((this.osrs_t << 5) + (this.osrs_p << 2) + this.mode);
            byte[] WriteBuffer = new byte[] { (byte)eCmd.BME280_REGISTER_CONTROL, value };
            bme280.Write(WriteBuffer);
            return;
        }

        /// <summary>
        /// ctrl_humレジスタの設定
        /// ・osrs_h : 湿度測定の有効(オーバーサンプリング値)/無告
        /// ----------
        /// Method to write the humidity control register (default 01)
        /// </summary>
        private void WriteControlRegisterHumidity()
        {
            byte value = this.osrs_h;
            byte[] WriteBuffer = new byte[] { (byte)eCmd.BME280_REGISTER_CONTROLHUMID, value };
            bme280.Write(WriteBuffer);
            return;
        }

        #endregion

        #endregion
        
        #region センサーを読む

        /// <summary>気温を読む</summary>
        /// <returns>float</returns>
        public float ReadTemperature()
        {
            //Read the MSB, LSB and bits 7:4 (XLSB) of the temperature from the BME280 registers
            byte tmsb = this.ReadByte((byte)eDataRegistersAddress.BME280_REGISTER_TEMPDATA_MSB);
            byte tlsb = this.ReadByte((byte)eDataRegistersAddress.BME280_REGISTER_TEMPDATA_LSB);
            byte txlsb = this.ReadByte((byte)eDataRegistersAddress.BME280_REGISTER_TEMPDATA_XLSB); // bits 7:4

            //Combine the values into a 32-bit integer
            int t = (tmsb << 12) + (tlsb << 4) + (txlsb >> 4);

            //Convert the raw value to the temperature in degC
            double temp = this.BME280_compensate_T_double(t);

            //Return the temperature as a float value
            return (float)temp;
        }

        /// <summary>気圧を読む</summary>
        /// <returns>float</returns>
        public float ReadPreasure()
        {
            //Read the MSB, LSB and bits 7:4 (XLSB) of the pressure from the BME280 registers
            byte pmsb = this.ReadByte((byte)eDataRegistersAddress.BME280_REGISTER_PRESSUREDATA_MSB);
            byte plsb = this.ReadByte((byte)eDataRegistersAddress.BME280_REGISTER_PRESSUREDATA_LSB);
            byte pxlsb = this.ReadByte((byte)eDataRegistersAddress.BME280_REGISTER_PRESSUREDATA_XLSB); // bits 7:4

            //Combine the values into a 32-bit integer
            int p = (pmsb << 12) + (plsb << 4) + (pxlsb >> 4);

            //Convert the raw value to the pressure in Pa
            long pres = this.BME280_compensate_P_Int64(p);

            //Return the pressure as a float value
            return ((float)pres) / 256;
        }
        
        /// <summary>湿度を読む</summary>
        /// <returns></returns>
        public float ReadHumidity()
        {
            //Read the MSB and LSB of the humidity from the BME280 registers
            byte hmsb = this.ReadByte((byte)eDataRegistersAddress.BME280_REGISTER_HUMIDDATA_MSB);
            byte hlsb = this.ReadByte((byte)eDataRegistersAddress.BME280_REGISTER_HUMIDDATA_LSB);

            //Combine the values into a 32-bit integer
            int h = (hmsb << 8) + hlsb;

            //Convert the raw value to the humidity in %
            double humidity = this.BME280_compensate_H_double(h);

            //Return the humidity as a float value
            return (float)humidity;
        }

        #endregion

        #region 表示用文字列の作成

        /// <summary>
        /// ℃で温度を戻すメソッド
        /// 分解能は0.01℃。
        /// Method to return the temperature in DegC. Resolution is 0.01 DegC.
        /// Output value of “51.23” equals 51.23 DegC.
        /// </summary>
        /// <param name="adc_T"></param>
        /// <returns></returns>
        private double BME280_compensate_T_double(int adc_T)
        {
            double var1, var2, T;

            //The temperature is calculated using the compensation formula in the BME280 datasheet
            var1 = (adc_T / 16384.0 - CalibrationData.dig_T1 / 1024.0) * CalibrationData.dig_T2;
            var2 = ((adc_T / 131072.0 - CalibrationData.dig_T1 / 8192.0) *
                (adc_T / 131072.0 - CalibrationData.dig_T1 / 8192.0)) * CalibrationData.dig_T3;

            this.t_fine = (int)(var1 + var2);

            T = (var1 + var2) / 5120.0;
            return T;
        }

        /// <summary>
        /// Q24.8形式(24の整数ビットと8の小数ビット)のPaの圧力を返すメソッド。
        /// Method to returns the pressure in Pa, in Q24.8 format (24 integer bits and 8 fractional bits).
        /// Output value of “24674867” represents 24674867/256 = 96386.2 Pa = 963.862 hPa
        /// </summary>
        /// <param name="adc_P">int</param>
        /// <returns>long</returns>
        private long BME280_compensate_P_Int64(int adc_P)
        {
            long var1, var2, p;

            //The pressure is calculated using the compensation formula in the BME280 datasheet
            var1 = (long)this.t_fine - 128000;
            var2 = var1 * var1 * CalibrationData.dig_P6;
            var2 = var2 + ((var1 * CalibrationData.dig_P5) << 17);
            var2 = var2 + ((long)CalibrationData.dig_P4 << 35);
            var1 = ((var1 * var1 * CalibrationData.dig_P3) >> 8) + ((var1 * CalibrationData.dig_P2) << 12);
            var1 = (((long)1 << 47) + var1) * CalibrationData.dig_P1 >> 33;
            if (var1 == 0)
            {
                Debug.WriteLine("BME280_compensate_P_Int64 Jump out to avoid / 0");
                return 0; //Avoid exception caused by division by zero
            }

            //Perform calibration operations as per datasheet: 
            p = 1048576 - adc_P;
            p = (((p << 31) - var2) * 3125) / var1;
            var1 = ((long)CalibrationData.dig_P9 * (p >> 13) * (p >> 13)) >> 25;
            var2 = ((long)CalibrationData.dig_P8 * p) >> 19;
            p = ((p + var1 + var2) >> 8) + ((long)CalibrationData.dig_P7 << 4);
            return p;
        }

        /// <summary>
        /// %rHの湿度をdoubleとして返す。
        /// Returns humidity in %rH as as double. Output value of “46.332” represents 46.332 %rH
        /// </summary>
        /// <param name="adc_H"></param>
        /// <returns></returns>
        private double BME280_compensate_H_double(int adc_H)
        {
            double var_H;

            var_H = this.t_fine - 76800.0;
            var_H = (adc_H - (CalibrationData.dig_H4 * 64.0 + CalibrationData.dig_H5 / 16384.0 * var_H)) *
                CalibrationData.dig_H2 / 65536.0 * (1.0 + CalibrationData.dig_H6 / 67108864.0 * var_H *
                (1.0 + CalibrationData.dig_H3 / 67108864.0 * var_H));
            var_H = var_H * (1.0 - CalibrationData.dig_H1 * var_H / 524288.0);

            if (var_H > 100.0)
            {
                Debug.WriteLine("BME280_compensate_H_double Jump out to 100%");
                var_H = 100.0;
            }
            else if (var_H < 0.0)
            {
                Debug.WriteLine("BME280_compensate_H_double Jump under 0%");
                var_H = 0.0;
            }

            return var_H;
        }

        /// <summary>
        /// 気圧から高度を計算するメソッド
        /// Method to take the sea level pressure in Hectopascals(hPa) as a parameter and calculate the altitude using current pressure.
        /// </summary>
        /// <param name="seaLevel">float</param>
        /// <returns>float</returns>
        public float ReadAltitude(float seaLevel)
        {
            //Read the pressure first
            float pressure = this.ReadPreasure();
            //Convert the pressure to Hectopascals(hPa)
            pressure /= 100;

            //Calculate and return the altitude using the international barometric formula
            return 44330.0f * (1.0f - (float)Math.Pow((pressure / seaLevel), 0.1903f));
        }

        #endregion

        #region 読み取り

        /// <summary>
        /// Method to read an 8-bit value from a register
        /// </summary>
        /// <param name="register">byte</param>
        /// <returns>byte</returns>
        private byte ReadByte(byte register)
        {
            byte value = 0;
            byte[] writeBuffer = new byte[] { 0x00 };
            byte[] readBuffer = new byte[] { 0x00 };

            writeBuffer[0] = register;

            bme280.WriteRead(writeBuffer, readBuffer);
            value = readBuffer[0];
            return value;
        }

        /// <summary>
        /// Method to read a 16-bit value from a register and return it in little endian format
        /// </summary>
        /// <param name="register">byte</param>
        /// <returns>ushort</returns>
        private ushort ReadUInt16_LittleEndian(byte register)
        {
            ushort value = 0;
            byte[] writeBuffer = new byte[] { 0x00 };
            byte[] readBuffer = new byte[] { 0x00, 0x00 };

            writeBuffer[0] = register;

            bme280.WriteRead(writeBuffer, readBuffer);
            int h = readBuffer[1] << 8;
            int l = readBuffer[0];
            value = (ushort)(h + l);
            return value;
        }

        #endregion
    }
}

補足(BME280 のキャリブレーションが必要な理由): 600 行を超える
このクラスの大半は補正計算である。何をしているのかを補っておく。

【BME280 が返す生データは「そのままでは使えない」】★
   ・センサーは【個体ごとに特性がばらつく】
   ・工場出荷時に測定した【補正係数】が
     チップ内の ROM(0x88〜0xE7)に焼かれている
     → dig_T1..T3(温度)
     → dig_P1..P9(気圧)
     → dig_H1..H6(湿度)
   ・生の ADC 値 + 補正係数 →
     データシート記載の【多項式】に通して
     ようやく物理量になる

   → 本クラスの ReadTemperature / ReadPreasure /
     ReadHumidity の中の
     【一見わけの分からない演算】は、
     すべて Bosch のデータシートの
     リファレンス実装をそのまま移したもの ★
     → 【自分で導出するものではない】
【押さえておくべき依存関係】★
   ・気圧・湿度の補正には
     【温度補正の中間結果(t_fine)】が必要
     → 必ず【温度を先に読む】
     → ReadPreasure だけ呼ぶと値がおかしくなる

【dig_H4 / dig_H5 の読み取りが変則的】
   ・0xE4/0xE5/0xE6 の 3 バイトに
     12bit 値が 2 つ【ニブル単位で詰まっている】
     → 本クラスの H4_L / H4_H / H5_L / H5_H という
       列挙値が重複して見えるのはこのため ★
【今なら既製のバインディングがある】★
   dotnet add package Iot.Device.Bindings

   using Iot.Device.Bmxx80;
   using var i2c = I2cDevice.Create(
       new I2cConnectionSettings(1, Bme280.DefaultI2cAddress));
   using var bme = new Bme280(i2c);
   bme.SetPowerMode(Bmx280PowerMode.Normal);
   var t = bme.ReadTemperature();
   var p = bme.ReadPressure();
   var h = bme.ReadHumidity();

   → 上記 600 行が【数行になる】
   → Iot.Device.Bindings には
     100 種類以上のセンサー実装が含まれる ★

移行メモ(誤記の保持): メソッド名 ReadPreasure
ReadPressure の綴り誤りだが、
コード全体で一貫しているため、移行元のまま残した。

結果

SensorData

Twitterにセンサー情報をTweetする。

上記のセンサー情報を CoreTweet というライブラリを使用して Twitter に Tweet する。

CoreTweet は、Twitter API のクライアント・ライブラリなので、
非常に多機能っぽいが、ここでは Tweet する API を利用するだけ。

Twitter からエラーが返った場合、以下のエラー一覧が参考になる。

補足(Twitter API は無料では使えなくなった): 本節の手順は
現在そのままでは実行できない

【Twitter → X の API 有料化】★
   2023年2月  無料 API の提供終了を発表
   2023年3月  新料金体系
     ・Free      … 【投稿のみ】月 1,500 件、
                   読み取りは実質不可
     ・Basic     … 月 $100〜
     ・Pro       … 月 $5,000〜
     ・Enterprise… 応相談
   → 本ページのような
     「センサー値を定期 Tweet する」用途は
     【Free 枠でぎりぎり可能】だが、
     レート制限が厳しい ★

【手順面の変化】
   ・dev.twitter.com / apps.twitter.com は
     【developer.x.com】に統合された
   ・Consumer Key / Access Token の 4 点セットは
     【OAuth 1.0a】方式で今も使えるが、
     新規は【OAuth 2.0 + Bearer Token】が主
   ・CoreTweet は
     → API v1.1 前提のライブラリであり、
       【v2 への追随が止まっている】★
【IoT の通知先として、今なら】★
   ・【Slack / Discord の Incoming Webhook】
       → 最も手軽。URL に POST するだけ ★
   ・【LINE Notify】
       → ただし【2025年3月末で終了】。
         後継は LINE Messaging API
   ・【MQTT ブローカ】(Mosquitto / HiveMQ)
       → IoT の標準的な選択 ★
   ・【Azure IoT Hub】/ AWS IoT Core
       → デバイス管理・双方向通信・スケール
   ・【InfluxDB + Grafana】
       → 時系列データの蓄積と可視化
       → センサー値は「流す」より
         【貯めて見る】方が実用的 ★

Twitter登録

  1. サインアップして、
  2. サインインする。
  3. Twitterの開発者サイト に移動し、
  4. My Appsのリンク をクリックする。
  5. そして、Create New App ボタンを押下する。
  6. [Create an application] 画面で以下のように入力して、

Twitter1

  1. [Create youre Twitter application] ボタンを押下する。
  2. アプリケーション登録後、登録したアプリケーションの画面に遷移するので、
  3. そこで、[Keys and Access Tokens Permissions] タブを押下する。
  4. 表示される [Consumer Key (API Key)], [Consumer Secret (API Secret)] をメモ。

Twitter2

  1. 次に、当該画面の下部にある [Create my access token] ボタンを押下、
    [Access Token], [Access Token Secret] を入手してコレをメモする。

Twitter3

  1. この 4 つのキー [Consumer Key], [Consumer Secret], [Access Token],
    [Access Token Secret] を使用して Tweet を行う。

移行メモ(リンク切れ): dev.twitter.com / apps.twitter.com
X(旧 Twitter)の開発者ポータル(developer.x.com)へ移行しており、
上記リンクおよび画面(図)は現在の UI と一致しない。記録として残す。

MainPage.xaml.cs

  • 前述のセンサー取得のコードに Tweet 処理を追加する。

  • はじめに以下を行う、

    • CoreTweet を NuGet から入手。
    • CoreTweet を using に追加する。
    • そして、UWP から Tweet 可能にするようにネットワーク・アクセスを許可する。
      • Windows10でVPN接続するとストアアプリが外に出られなくなる - tmytのらくがき
        http://blog.tmyt.jp/entry/2015/11/23/154702
      • 以下が、その設定画面(Package.appxmanifest を選択、機能タブに遷移)。
        ここでは、以下の 2 つのチェック・ボックスにチェックを入れ、機能をオンにしてみた。

Twitter4

移行メモ(誤字): 移行元の「UWP の UI の情報」を
「UWP の UI の情報」に修正した。

  • 以下、前述のセンサー取得のコードからの差分。
using System.Threading;
using System.Diagnostics;
using CoreTweet;
・・・
        /// <summary>ライブラリ</summary>
        private BME280 bme280;
        /// <summary>SensorのTimer</summary>
        private Timer periodicTimer;
        /// <summary>TwitterのTimer</summary>
        private DispatcherTimer twitterTimer;
        /// <summary>TwitterのToken</summary>
        private Tokens tokens;

        public MainPage()
        {
            this.InitializeComponent();
            this.Loaded += MainPage_Loaded;
        }

        /// <summary>Loaded</summary>
        /// <param name="sender">object</param>
        /// <param name="e">RoutedEventArgs</param>
        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            this.bme280 = new BME280();
            this.initBme280();
            this.initTwitter();
        }

        /// <summary>Twitterの初期化</summary>
        private void initTwitter()
        {
            // 実際の値を入力する。その際、{}は削除する。
            this.tokens = Tokens.Create(
                "{API key}"
                , "{API secret}"
                , "{Access token}"
                , "{Access token secret}");

            this.twitterTimer = new DispatcherTimer();
            this.twitterTimer.Interval = TimeSpan.FromSeconds(10); // Tweet間隔を調整
            this.twitterTimer.Tick += this.TimerCallback;
            this.twitterTimer.Start();
        }

        /// <summary>Bme280の初期化</summary>
        private async void initBme280()
        {
            await this.bme280.Initialize();
            this.periodicTimer = new Timer(this.TimerCallback, null, 0, 1000);
        }

        /// <summary>TwitterのTimerCallback</summary>
        /// <param name="state">object</param>
        private void TimerCallback(object sender, object e)
        {
            string text =
                this.tbkTMP.Text +"\r\n"
                + this.tbkHUM.Text + "\r\n"
                + this.tbkPRE.Text;

            Debug.WriteLine("Tweet :" + text);

            this.tweet(text);
        }

        /// <summary>tweetする</summary>
        /// <param name="text">tweetのテキスト</param>
        private async void tweet(string text)
        {
            Status _status = await this.tokens.Statuses.UpdateAsync(status => text);
        }

        /// <summary>Bme280のTimerCallback</summary>
        /// <param name="state">object</param>
        private void TimerCallback(object state)
・・・

補足(DispatcherTimer を選んだ判断は正しい): 「Tweet 元の情報を
UI 上の情報から取得する必要があった」という理由は、的確である。

【.NET の主な Timer】★
   System.Threading.Timer
     → 【スレッド プール】上で発火
     → UI に触れない ★
     → 本コードのセンサー読み取り側
   System.Timers.Timer
     → 同上(SynchronizingObject で緩和可能)
   DispatcherTimer(WPF / UWP)
     → 【UI スレッド】上で発火 ★
     → そのまま UI を読み書きできる
     → だから Tweet 側はこちらが正しい
   PeriodicTimer(.NET 6 以降)★
     → await foreach で回せる現代的な API
         while (await timer.WaitForNextTickAsync(token)) { ... }
     → 【重なり実行が起きない】のが最大の利点

【DispatcherTimer の注意】
   ・UI スレッドで動くため
     【重い処理を書くと画面が固まる】★
   ・正確な周期は保証されない
     (UI が忙しいと遅延する)
【このコードの問題点】★
 ① 【API キーをソースに直書きしている】★★
     → 配置したアプリを解析すれば
       【誰でも取り出せる】
     → 実運用では
       設定ファイル / セキュア ストレージ /
       Azure Key Vault 等に置く
     → GitHub に push すると
       【自動検出されて即座に失効させられる】
 ② 【メソッド名 TimerCallback の多重定義】
     → 引数の違いで区別しているが、
       意図が読みにくい
     → TweetTimerCallback / SensorTimerCallback と
       分けるべき ★
 ③ 【async void tweet】
     → Twitter 側のエラーが
       どこにも捕まらない ★
     → try-catch で囲み、
       レート制限(429)を扱う必要がある
 ④ 【10 秒間隔で Tweet】
     → 現在の API 制限では即座に弾かれる
     → また同一文言の連投は
       【重複投稿として拒否される】

結果

SensorTweet

Wio Nodeを使用する。

(Wio Node については 電子工作の該当節を参照)

Wio Nodeのデバイスのデータを RESTで取得する。

Wio Nodeのデバイスを RESTで操作する。

移行メモ(未執筆): 移行元では本節(Wio Node の 2 つの小見出し)が
見出しのみで本文がない。見出しは残した。

補足(Wio Node とは): 未執筆のため、対象を簡単に補っておく。

【Wio Node】★
   Seeed Studio の ESP8266 ベースの小型 IoT ボード
     ・Grove コネクタが 2 つ
     ・【コードを書かずに】スマホアプリで
       センサーを選ぶと、
       【REST API が自動生成される】★
     ・生成された API を叩けば
       値の取得・デバイスの操作ができる

   例(イメージ)
     GET  https://us.wio.seeed.io/v1/node/GroveTempHumD0/temp
          ?access_token=XXXX
     POST https://us.wio.seeed.io/v1/node/GroveRelayD1/onoff/1
          ?access_token=XXXX

【現況】
   ・Wio Node / Wio Link のクラウド サービスは
     【提供終了】している ★
   ・Seeed の後継は
     Wio Terminal / Wio-E5 等(クラウド前提ではない)
   ・「センサーを REST で読む」という発想自体は
     → 現在は【MQTT】や
       【Home Assistant】経由が主流 ★

監視カメラを実装する。

  • カメラ・デバイスは、USB の前述の Inbox ドライバ経由で操作する。
  • カメラ・デバイスの操作には、Windows.Media.Capture 名前空間の API を使用する。
  • 顔の検出には、Windows.Media.FaceAnalysis.FaceDetector 名前空間の API を使用する。

OpenCVを利用する。

  • 画像処理とコンピュータビジョン - 開発基盤部会 Wiki

OpenCvSharp が、.NET Core / Mono に対応しているようなので、
OpenCvSharp + Windows10 IoT Core ができたら良さそ気。

補足(OpenCvSharp の現況): 作者の見立てはその後実現している

【OpenCvSharp4】★
   ・.NET Standard 2.0 / .NET 6+ に対応
   ・NuGet で
       OpenCvSharp4
       OpenCvSharp4.runtime.win        (Windows)
       OpenCvSharp4.runtime.linux-arm  (Raspberry Pi)★
     を入れれば動く
   ・つまり
     → 【Raspberry Pi OS + .NET + OpenCvSharp4】で
       画像処理ができる ★
     → 本ページの構想は
       【Windows 10 IoT Core ではなく Linux 側で】
       実現された

【顔検出について】
   ・原文の FaceDetector(UWP)は
     Windows 専用かつ機能が限定的
   ・現在は
     → OpenCV の DNN モジュール
     → 【ONNX Runtime】+ 学習済みモデル ★
     → Raspberry Pi 5 なら実用的な速度が出る
     → さらに Coral USB Accelerator 等の
       アクセラレータを足す選択肢もある

On Rasbian

移行メモ(表記): 移行元のページ名は「Rasbian」だが、
正しくは「Raspbian」(Raspberry + Debian)である。
見出しは移行元のページ名に合わせたが、
移行先ファイル名は正しい綴り
.NET Core On Rasbian / Mono On Rasbian)で作成した。

参考

Qiita

補足(Android Things も終了した): 上記で言及されている
Android Things(Google の IoT 向け Android)は、
2021 年 1 月 5 日にサービス終了した。
Windows 10 IoT Core と Android Things は
ほぼ同時期に登場し、ほぼ同時期に終息したという点で対照的である。
「汎用 OS を IoT デバイスへ持ち込む」というアプローチ自体が、
軽量な Linux + コンテナという潮流に置き換わったと言える。

ASCII.jp

@IT

MONOist(モノイスト)

Build Insider

Microsoft

Windows 10 IoT Samples

Windows 10 IoTCore ハンズオントレーニング

Microsoft Docs

インストール

Raspberry Pi 2

Raspberry Pi 3

Raspberry Pi 4

補足(Raspberry Pi 4 の項が「Windows 10 on ARM64」である理由): この節だけ
IoT Core ではない点に注意が必要である。

【Raspberry Pi 4 に IoT Core は入らない】★
   ・Windows 10 IoT Core の対応は
     【Raspberry Pi 2 / 3 まで】
   ・Pi 4 向けは【ついに提供されなかった】

【代わりに使われたのが「Windows on ARM」】
   ・Windows 10/11 の【フル版】を
     ARM64 で動かす
   ・非公式ツール【WoR(Windows on Raspberry)】
     でインストールする
   ・Microsoft のサポート対象【外】★
   ・動きはするが実用速度とは言い難い
     (Pi 5 でようやく実用の入口)

   → 上記の ITmedia 記事はこの手順を扱っている
   → 【IoT Core の後継ではない】ことに注意

電子工作(MAKE)

ラズベリーパイWindows | 記事
http://www.denshi.club/make/raspberrypi/

開発基盤部会 Wiki


Tags: 移行, インフラストラクチャ, Windows, IoT

⚠️ **GitHub.com Fallback** ⚠️