💡

Embedded Swift & Matterでデバイスを操作する:LED制御編

に公開

はじめに

9月に行われたiOSDC Japan 2025にてEmbedded SwiftとMatterを組み合わせて、Appleの標準ホームアプリからデバイスを操作するLT(5分)を発表しました。

https://fortee.jp/iosdc-japan-2025/proposal/42a001a0-ddac-46dc-8d76-6836c7a0c6d9

当日は、短い時間ながらもみなさん楽しんでいただけたようでよかったです。

しかし、5分という短い時間で技術的な部分を伝えるのは、かなり厳しかったので今回はテックブログに残していこうかと思います。

Embedded SwiftとMatterを組み合わせると、これまでiOSアプリ開発を中心に使われてきたSwift言語でも気軽にデバイス開発ができるようになり、ものづくりの幅が広がると思います。
今回はその基本であるLED制御について紹介していきます💡

技術について

Embedded Swift

Embedded Swiftは組み込み向けのSwiftのサブセット言語となっています。
WWDC24にて紹介されました。

Embedded Swiftでは、Swiftの一部の言語機能や標準ライブラリの使用を制限して、コードサイズを大幅に縮小しています。
これにより、メモリが限られているデバイス上でSwiftコードを動作させることが可能になりました。

ドキュメントも日々アップデートされており、非常に充実してきました。
https://www.swift.org/get-started/embedded/

Matter

Matterはスマートホーム機器向けのオープンソース共通規格となっています。
WWDC21で紹介されました。

Matterが登場する以前は、規格や設定が異なるデバイスとアプリとの連携は非常に困難でした。
しかし、このMatterによって様々なデバイスをスマートホームアプリ(Appleホームアプリ、Google Home等)から制御することが容易となりました。

最近では、SwitchBotやNature Remoなど多くのスマートホームデバイスからもMatter対応のデバイスが増えてきています。
matter

このMatterとEmbedded Swiftを組み合わせて、スマートホームアプリからデバイスを制御することが可能です。
WWDC24では、swift-matter-examplesという公式のチュートリアルが紹介され、環境構築からLED制御まで細かく説明されています。

使用するデバイス

今回は一番容易にできるM5シリーズのNanoC6というデバイスを使用します。
こちらは内部にESP32-C6FH4を搭載しており、フルカラーLEDや赤外線LEDなども付いています。
非常に小型のため場所もとらず、1200円程度で購入できるので、初めてなら試しやすいかなと思います。
https://docs.m5stack.com/ja/core/M5NanoC6

もちろん他のデバイスを使うことも可能です。
以下のようなケースがあります。

  • ケース1
    • M5 NanoC6(今回)
  • ケース2
  • ケース3
    • ESP32C6
    • フルカラーLED
    • ジャンパー線、抵抗等

環境構築

ここからは以下の公式チュートリアルに沿って環境構築を進めていきます。
https://swiftlang.github.io/swift-matter-examples/tutorials/swiftmatterexamples/setup-macos

以下のような流れとなっています。

  1. Xcodeのインストール
  2. Swift Nightly Toolchainのインストール
  3. ESP-IDFとESP-Matter SDKのインストール
  4. シェル環境のセットアップ

ちなみに公式からはDockerを利用したセットアップも紹介されています。
ただし30GB近い容量を必要とするため、試す場合は空き容量に注意です⚠️
https://swiftlang.github.io/swift-matter-examples/tutorials/swiftmatterexamples/setup-docker

1.Xcodeのインストール

もはや皆さんインストールしてると思うので説明はいらないと思います笑
SwiftコンパイラおよびツールチェーンをMac上で機能させるために必要になります。

2.Swift Nightly Toolchainのインストール

Swift6からEmbedded Swiftのサポートが含まれているため、それより前のバージョンの場合は別途インストールが必要です。
以下は自分がインストールした際の例です。(Swift 6.1.2)

# Locate the toolchain
$ ls ~/Library/Developer/Toolchains/
swift-6.1.2-RELEASE.xctoolchain/
swift-latest.xctoolchain

# Find the toolchain identifier
$ plutil -extract CFBundleIdentifier raw \
  -o - \
  ~/Library/Developer/Toolchains/swift-6.1.2-RELEASE.xctoolchain/Info.plist
org.swift.612202505261a

# Test the toolchain
$ TOOLCHAINS=org.swift.612202505261a swift --version
Apple Swift version 6.2-dev (LLVM bba436f29ae40dd, Swift 6b297005546e5ab)
Target: arm64-apple-macosx26.0
Build config: +assertions

3.ESP-IDFとESP-Matterのインストール

Espressif社が提供しているESP-IDFとESP-Matterの2つのSDKをインストールします。
それぞれ、ESP32向けの開発環境、Matterデバイス開発環境を提供しています。

# Install CMake, Ninja, and dfu-util
$ brew install cmake ninja dfu-util

# Create an ESP SDK directory
$ mkdir -p ~/esp

# Download the ESP-IDF SDK
$ cd ~/esp
$ git clone \
  --branch v5.2.1 \
  --depth 1 \
  --shallow-submodules \
  --recursive https://github.com/espressif/esp-idf.git \
  --jobs 24

# Install the ESP-IDF SDK
$ cd ~/esp/esp-idf
$ ./install.sh

# Download the ESP-Matter SDK
$ cd ~/esp
$ git clone \
    --branch release/v1.4 \
    --depth 1 \
    --shallow-submodules \
    --recursive https://github.com/espressif/esp-matter.git \
    --jobs 24

# Install the ESP-Matter SDK
$ cd ~/esp/esp-matter
$ ./install.sh

4.シェル環境のセットアップ

インストールされたツールを見つけられるようにシェルを設定します。
shellの設定ファイル(.zshrc等)に記述しても大丈夫です。

# Setup the Swift nightly toolchain
$ export TOOLCHAINS=org.swift.612202505261a

# Setup the ESP-IDF environment
$ . ~/esp/esp-idf/export.sh

# Setup the ESP-Matter environment
$ . ~/esp/esp-matter/export.sh

Embedded SwiftでLEDを制御する

いよいよLEDを光らせてみましょう💡
公式チュートリアルが提供しているコードをもとに試していきます

コード構成

まずはリポジトリをクローンし、'code/led-blink'に移動します

# Clone the repo
$ cd ~
$ git clone https://github.com/apple/swift-matter-examples.git

# Navigate to the led blink example
$ cd ~/swift-matter-examples/led-blink

'led-blink'のコード構成は以下のようになっています

led-blinkのコード構成
led-blink/
├── CMakeLists.txt
├── main
│   ├── BridgingHeader.h
│   ├── CMakeLists.txt
│   ├── idf_component.yml
│   ├── LED.swift
│   └── Main.swift
├── partitions.csv
├── README.md
├── sdkconfig
└── sdkconfig.defaults

重要になるmainフォルダについて説明します

  • BridgingHeader.h
    • SwiftからC/C++のAPIを呼び出すためのブリッジ
    • SwiftコードがこれらのC/C++ APIを直接呼び出せるようにする
  • CMakeLists.txt
    • Embedded Swiftのコンパイル設定
    • C/C++ヘッダーパスの設定
    • BridgingHeader.hのインポート設定
    • ビルド対象のSwiftファイル(LED.swift、Main.swift)の指定
    • ルートディレクトリにもCMakeLists.txtは存在するが、こちらはプロジェクト全体のビルド設定を定義している
  • idf_component.yml
    • ESP-IDFコンポーネントの依存関係管理ファイル
  • LED.swift
    • LEDを制御するためのヘルパークラス(ON/OFF, 明るさ、色等)
  • Main.swift
    • アプリケーションのメインエントリーポイント

M5 NanoC6で光らせるための修正

今回使用するM5 NanoC6で光らせるためにコードを一部修正します。

まず、GPIO制御(デジタル入出力)をするために必要なヘッダ(driver/gpio.h)とWS2812 LEDを制御するために必要なヘッダ(driver/rmt.h)を追加します。

BridgingHeader.h
// ESP IDF
// =======

#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <led_strip.h>
#include <sdkconfig.h>
#include <nvs_flash.h>
#include <led_driver.h>
#include <device.h>
+ #include "driver/rmt.h"
+ #include "driver/gpio.h"

次に、LEDクラスのイニシャライザを修正します。
ここでは、M5 nanoC6に搭載されているLED WS2812を光らせるための設定を追加します。
設定内容としては、GPIO, RMT, LED Stripです。

LED.swift
final class LED {
  init() {
    // GPIO19をHIGHに設定してLED電源を有効化
    gpio_reset_pin(GPIO_NUM_19)
    gpio_set_direction(GPIO_NUM_19, GPIO_MODE_OUTPUT)
    gpio_set_level(GPIO_NUM_19, 1)

    // RMT設定(led_driver_initのC実装を基に)
    var rmtConfig = rmt_config_t()
    rmtConfig.channel = rmtChannel
    rmtConfig.gpio_num = gpioPin
    rmtConfig.mem_block_num = 1
    rmtConfig.clk_div = 2
    rmtConfig.tx_config.loop_en = false
    rmtConfig.tx_config.carrier_en = false
    rmtConfig.rmt_mode = RMT_MODE_TX

    // LED strip設定
    var stripConfig = led_strip_config_t(
      max_leds: 1,
      dev: UnsafeMutableRawPointer(bitPattern: Int(rmtConfig.channel.rawValue))
    )

    // LED strip初期化
    let strip = led_strip_new_rmt_ws2812(&stripConfig)
  }
}

これにより、LEDクラス内にある制御用プロパティから、LEDを制御できるようになります。
なお、led_driver_set_powerなどの関数は、ESP-Matterから提供されています。

LED.swift
final class LED {
  // ON/OFF制御
  var enabled: Bool = true {
    didSet {
      led_driver_set_power(handle, enabled)
    }
  }

  // 明るさ制御(0-100)
  var brightness: Int = 100 {
    didSet {
      brightness = max(0, min(100, brightness))
      led_driver_set_brightness(handle, UInt8(brightness))
    }
  }

  // カラー制御
  var color: Color = .hueSaturation(0, 100) {
    didSet {
      switch color {
      case .hueSaturation(let hue, let saturation):
        led_driver_set_hue(handle, UInt16(hue))
        led_driver_set_saturation(handle, UInt8(saturation))
      case .temperature(let temperature):
        led_driver_set_temperature(handle, UInt32(temperature))
      }
    }
  }

  enum Color {
    // "Hue + saturation" color representation. Hue range is 0 ..< 360.
    // Saturation is 0 ... 100.
    case hueSaturation(Int, Int)

    // Temperature color representation in Kelvins (range 600 ... 10000).
    case temperature(Int)

    // Hue range is 0 ..< 360.
    var hue: Int {
      switch self {
      case .hueSaturation(let hue, _):
        return hue
      case .temperature:
        return 0
      }
    }

    // Saturation is 0 ... 100.
    var saturation: Int {
      switch self {
      case .hueSaturation(_, let saturation):
        return saturation
      case .temperature:
        return 0
      }
    }
  }
}

最後にMain.swiftから、LEDオブジェクトを生成し、LED点滅させます。
今回のコードではLEDを1秒おきに点滅させてみます。
Swiftでは、Bool値をtoggle()で切り替えることができます。

Main.swift
@_cdecl("app_main")
func main() {
  // LEDオブジェクトを生成
  let led = LED()

 // 1秒間隔でLEDのON/OFFを切り替える
  while true {
    led.enabled.toggle()
    sleep(1)
  }
}

以上でLEDを光らせる準備は完了です。

ビルド結果

実際にビルドしてみます。こちらもチュートリアル通り進めれば問題ないです。
一回目のビルドは少々時間がかかりますし、コードのエラーもこのビルドで検出されます。

# Configure the target microcontroller
$ idf.py set-target esp32c6

# Build the application
# (Project build complete.が表示されたら完了)
$ idf.py build

# Flash and monitor the application
$ idf.py flash monitor

結果的に以下のように光るようになりました!💡

result-led-blink

Matterと連携する

さて、光るようになったLEDを次はスマートホームアプリから操作できるようにしてみます。
ここでついにMatterが登場します。

Matterのデータモデル階層

Matterでは、デバイスの機能や状態を共通化するためにデータモデルという階層構造が定義されています。
Matterのデータモデルは、以下の4階層で構成されています。

  • Node
    • 1つの物理デバイス全体
  • Endpoint
    • Nodeに登録されたデバイス類
  • Cluster
    • Endpointが持つ機能のまとまり
  • Attribute
    • Clusterが持つ状態や設定値

例えば、M5NanoC6デバイスに対して

  • LEDライト
  • 温湿度センサ

を搭載した構成を想定し、この構成をデータモデルに当てはめると、以下のような構成になります。

この構成をもとに、Embedded Swiftでどのように階層を定義し、LEDをMatter経由で制御するか見ていきます。

コード構成

'code/smart-light'に移動します

# Navigate to the smart light example
$ cd ~/swift-matter-examples/smart-light

'smart-light'のコード構成は以下のようになっています

smart-lightのコード構成
smart-light/
├── CMakeLists.txt
├── main
│   ├── BridgingHeader.h
│   ├── CMakeLists.txt
│   ├── idf_component.yml
│   ├── LED.swift
│   ├── linker.lf
│   └── Main.swift
├── Matter
│   ├── Attribute.swift
│   ├── Clusters.swift
│   ├── Matter.swift
│   ├── MatterInterface.cpp
│   ├── MatterInterface.h
│   └── Node.swift
├── partitions.csv
├── README.md
├── sdkconfig
└── sdkconfig.defaults

先程のled-blinkとの違いは、mainフォルダにlinker.lfが追加されている点、Matterフォルダが存在する点です。各ファイルを説明します。

  • main/linker.lf
    • SwiftコードをESP32で実行する際のメモリレイアウトをカスタマイズするための設定ファイル
    • smart-lightはMatterフレームワークを使用しており、C++とSwiftのコードが混在しているため、より複雑なメモリレイアウトが必要(といって特別いじる必要はない)
  • Matter/Attribute.swift
    • MatterのAttributeを扱うためのコード
  • Matter/Clusters.swift
    • MatterのClusterを扱うためのコード
  • Matter/Matter.swift
    • Matterインターフェースを扱うための高レベルAPI
  • Matter/MatterInterface.cpp、Matter/MatterInterface.h
    • C++のESP Matter APIをSwiftから使いやすくするためのラッパー層
  • Matter/Node.swift
    • MatterのNodeを扱うためのコード

M5 NanoC6で光らせるための修正

修正する点は、先程と同様に、BridgingHeader.hとLED.swiftのイニシャライザを変更すればOKです。
Main.swiftではLEDオブジェクトを生成したのちに、Matterで操作できるようにするために1~4の処理を行います。(特に変更はしてないです)

  1. Matterプロトコルのrootノードを作成します
  2. エンドポイントを作成し、アプリから操作したときのイベントハンドラーを設定します
    各イベント(ON/OFF制御、明るさ制御、色制御)はenmu化されており、それらをswitch文で制御分岐しています
  3. 2.で作成したエンドポイントをrootノードに追加します
  4. Matterアプリケーションを起動させます
@_cdecl("app_main")
func main() {
  // LEDオブジェクトを生成
  let led = LED()

  // 1. rootノードを作成
  let rootNode = Matter.Node()
  rootNode.identifyHandler = {
    print("identify")
  }

  // (2) エンドポイントを作成
  let lightEndpoint = Matter.ExtendedColorLight(node: rootNode)
  lightEndpoint.eventHandler = { event in
    print("lightEndpoint.eventHandler:")
    print(event.attribute)
    print(event.value)

    switch event.attribute {
    case .onOff: // ON/OFF制御
      led.enabled = (event.value == 1)

    case .levelControl: // 明るさ制御(0-255を0-100%に変換)
      led.brightness = Int(Float(event.value) / 255.0 * 100.0)

    case .colorControl(.currentHue): // 色制御(色相)
      let newHue = Int(Float(event.value) / 255.0 * 360.0)
      led.color = .hueSaturation(newHue, led.color.saturation)

    case .colorControl(.currentSaturation): // 色制御(彩度)
      let newSaturation = Int(Float(event.value) / 255.0 * 100.0)
      led.color = .hueSaturation(led.color.hue, newSaturation)

    case .colorControl(.colorTemperatureMireds): // 色制御(色温度)
      let kelvins = 1_000_000 / event.value
      led.color = .temperature(kelvins)

    default:
      break
    }
  }

  // (3) エンドポイントをrootノードに追加
  rootNode.addEndpoint(lightEndpoint)

  // (4) Matterアプリの起動
  let app = Matter.Application()
  app.rootNode = rootNode
  app.start()

  // ローカル変数が関数の終了とともに破棄されるのを防ぐため、無限ループを追加
  // https://github.com/apple/swift-matter-examples/issues/10
  while true {
    sleep(1)
  }
}

ビルド結果

ビルドの流れは先程と同様です。

# Configure the target microcontroller
$ idf.py set-target esp32c6

# Build the application
# (Project build complete.が表示されたら完了)
$ idf.py build

# Flash and monitor the application
$ idf.py flash monitor

Flashまで完了したら、ホームアプリを開いてデバイスの登録を行います。
デバイスの登録は、チュートリアルSection2 - Connect using Matterの手順通りに進めれば問題ないです。

最終的には以下のようになります。

ON/OFF制御 明るさ制御 色制御
result-onoff result-brightness result-color

少し反応は遅いですが、ホームアプリからLEDが制御されるようになりました💡

まとめ

本記事では、公式のswift-matter-examplesを参考に、導入からAppleの標準ホームアプリからLEDライトを操作するまでを説明しました。
公式のコードを見ながら自分で気になる点を少しずつ加えていけば理解も深まると思います!
次回は、応用編ということでMatter経由で温湿度センサからの値を確認する方法について紹介します。お楽しみに〜👋(なんとか1ヶ月以内に出したい...)

参考

https://speakerdeck.com/tussy5969/iosdc-2025-b2e1e9d3-2bc6-4fe5-a104-7810607d41c0
https://developer.apple.com/jp/videos/play/wwdc2024/10197/
https://docs.swift.org/embedded/documentation/embedded/
https://github.com/swiftlang/swift-matter-examples

Discussion