2026年8月2日日曜日

WPFのGrid内に動的に要素を配置する

WPFのGrid内に動的に要素を配置したいと思ったことはありませんか?

WPFのGridは、基本的な使い方では静的になります。RowDefinitionsプロパティColumnDefinitionsプロパティはバインディングできず、XAMLからは静的にしか設定することができません。そのため、実行時まで行や列の数が定まらないようなものはそのままでは作ることができません。

これを動的に作れるように作っていきましょう。

没案:ItemsContorl in ItemsControl

Gridは2次元に要素を並べるものなので、配列の配列を作って2次元のデータを用意し、例えば、行ごとのItemsControlをさらにItemsControlで縦に並べればそもそもGridなんていらないのでは?と思うかもしれません。

実際はそれでも何とかなりますが、行や列数が増えるとめちゃめちゃ重たくなります。そもそもItemsControlがそこそこ重たいコントロールなので、それを何百個も作ったら重くなるのは想像に難くないでしょう。

今回の記事では、そのような構造は取らず、あくまでもGridコントロールを動的に制御するというアプローチで2次元の要素を並べるようにしていきたいと考えています。

ViewModelの準備

まずは、セル一つ分を表現するViewModelを準備していきます。

public class CellViewModel : ViewModel
{
    #region Text

    public string Text
    {
        get;
        set {
            if(field == value)
                return;
            field = value;
            RaisePropertyChanged();
        }
    } = "";

    #endregion

    #region Row

    public int Row
    {
        get;
        set {
            if(field == value)
                return;
            field = value;
            RaisePropertyChanged();
        }
    }

    #endregion

    #region Column

    public int Column
    {
        get;
        set {
            if(field == value)
                return;
            field = value;
            RaisePropertyChanged();
        }
    }

    #endregion
}

シンプルなViewModelです。Row / Columnは、このセルを配置するRow / Columnのインデックスを示したものです。Textは今回の本質とは関係ありませんが、何も表示されないと味気ないので表示内容のテキストを保持するものになります。

余談ですが、このコードはC#14のfieldキーワードを使用することで、バッキングフィールドに直接アクセスするようにしてコードを最小化しています。まさに変更通知プロパティのための機能ですね。

続いて、MainWindowViewModelです。少しややこしく見えるかもしれませんが、行数 / 列数を示すRows / ColumnsプロパティをCellsの内容変更に自動で追従するようにしたためのコードが多いだけでそんなに変なことはやっていませんので、よく読んでいただければ内容がすぐにわかるでしょう。

public class MainWindowViewModel : ViewModel
{
    public MainWindowViewModel()
    {
        _Cells = new ObservableCollection<CellViewModel>();
        Cells = new ReadOnlyObservableCollection<CellViewModel>(_Cells);

        ((INotifyCollectionChanged)Cells).CollectionChanged += Cells_CollectionChanged;
    }

    public void Initialize()
    {
        _Cells.Add(new CellViewModel() { Row = 0, Column = 0, Text = "A1" });
        _Cells.Add(new CellViewModel() { Row = 1, Column = 1, Text = "B2" });
        _Cells.Add(new CellViewModel() { Row = 2, Column = 0, Text = "A3" });
    }

    readonly ObservableCollection<CellViewModel> _Cells;
    public ReadOnlyObservableCollection<CellViewModel> Cells { get; }

    #region Cells_CollectionChanged

    private void Cells_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        switch(e.Action) {
            case NotifyCollectionChangedAction.Add:
                HandleAdd(e.NewItems!);
                break;

            case NotifyCollectionChangedAction.Remove:
                HandleRemove(e.OldItems!);
                break;

            case NotifyCollectionChangedAction.Replace:
                HandleRemove(e.OldItems!);
                HandleAdd(e.NewItems!);
                break;

            case NotifyCollectionChangedAction.Reset:
                RecalculateRows();
                RecalculateColumns();
                break;
        }
    }

    private void HandleAdd(IList newItems)
    {
        foreach(CellViewModel cell in newItems) {
            if(cell.Row + 1 > Rows)
                Rows = cell.Row + 1;

            if(cell.Column + 1 > Columns)
                Columns = cell.Column + 1;
        }
    }

    private void HandleRemove(IList oldItems)
    {
        bool needRecalcRows = false;
        bool needRecalcColumns = false;

        foreach(CellViewModel cell in oldItems) {
            if(cell.Row + 1 == Rows)
                needRecalcRows = true;

            if(cell.Column + 1 == Columns)
                needRecalcColumns = true;

            if(needRecalcRows && needRecalcColumns)
                break;
        }

        if(needRecalcRows)
            RecalculateRows();

        if(needRecalcColumns)
            RecalculateColumns();
    }

    private void RecalculateRows()
    {
        int maxRow = 0;

        foreach(var cell in Cells) {
            if(cell.Row + 1 > maxRow)
                maxRow = cell.Row + 1;
        }

        Rows = maxRow;
    }

    private void RecalculateColumns()
    {
        int maxColumn = 0;

        foreach(var cell in Cells) {
            if(cell.Column + 1 > maxColumn)
                maxColumn = cell.Column + 1;
        }

        Columns = maxColumn;
    }

    #endregion

    #region Rows

    public int Rows
    {
        get;
        set {
            if(field == value)
                return;
            field = value;
            RaisePropertyChanged();
        }
    }

    #endregion

    #region Columns

    public int Columns
    {
        get;
        set {
            if(field == value)
                return;
            field = value;
            RaisePropertyChanged();
        }
    }

    #endregion
}

Behaviorの準備

さて、ここで用意したViewModelからの情報をGridに伝搬してあげたいのですが、最初に説明した通りRowDefinitionsプロパティColumnDefinitionsプロパティは直接バインディングすることができずXAML経由で扱えません。そのような場合はそう、Behaviorですね。

public class DynamicGridBehavior : Behavior<Grid>
{
    protected override void OnAttached()
    {
        OnRowChanged(Rows);
        OnColumnChanged(Columns);

        base.OnAttached();
    }

    #region Columns

    public int Columns
    {
        get { return (int)GetValue(ColumnsProperty); }
        set { SetValue(ColumnsProperty, value); }
    }

    public static readonly DependencyProperty ColumnsProperty =
        DependencyProperty.Register(nameof(Columns), typeof(int), typeof(DynamicGridBehavior), new PropertyMetadata(default(int), (sender, e) => {
            if(sender is DynamicGridBehavior me && e.NewValue is int newValue)
                me.OnColumnChanged(newValue);
        }));

    void OnColumnChanged(int newValue)
    {
        if(AssociatedObject == null)
            return;

        if(AssociatedObject.ColumnDefinitions.Count > newValue) {
            AssociatedObject.ColumnDefinitions.RemoveRange(newValue, AssociatedObject.ColumnDefinitions.Count - newValue);
        } else if(AssociatedObject.ColumnDefinitions.Count < newValue) {
            for(int i = AssociatedObject.ColumnDefinitions.Count; i < newValue; i++)
                AssociatedObject.ColumnDefinitions.Add(new ColumnDefinition() { Width = ColumnWidth });
        }
    }

    #endregion

    #region ColumnWidth

    public GridLength ColumnWidth
    {
        get { return (GridLength)GetValue(ColumnWidthProperty); }
        set { SetValue(ColumnWidthProperty, value); }
    }

    public static readonly DependencyProperty ColumnWidthProperty =
        DependencyProperty.Register(nameof(ColumnWidth), typeof(GridLength), typeof(DynamicGridBehavior), new PropertyMetadata(default(GridLength), (sender, e) => {
            if(sender is DynamicGridBehavior me && e.NewValue is GridLength newValue)
                me.OnColumnWidthChanged(newValue);
        }));

    void OnColumnWidthChanged(GridLength newValue)
    {
        if(AssociatedObject == null)
            return;

        foreach(var column in AssociatedObject.ColumnDefinitions)
            column.Width = newValue;
    }

    #endregion

    #region Rows

    public int Rows
    {
        get { return (int)GetValue(RowsProperty); }
        set { SetValue(RowsProperty, value); }
    }

    public static readonly DependencyProperty RowsProperty =
        DependencyProperty.Register(nameof(Rows), typeof(int), typeof(DynamicGridBehavior), new PropertyMetadata(default(int), (sender, e) => {
            if(sender is DynamicGridBehavior me && e.NewValue is int newValue)
                me.OnRowChanged(newValue);
        }));

    void OnRowChanged(int newValue)
    {
        if(AssociatedObject == null)
            return;

        if(AssociatedObject.RowDefinitions.Count > newValue) {
            AssociatedObject.RowDefinitions.RemoveRange(newValue, AssociatedObject.RowDefinitions.Count - newValue);
        } else if(AssociatedObject.RowDefinitions.Count < newValue) {
            for(int i = AssociatedObject.RowDefinitions.Count; i < newValue; i++)
                AssociatedObject.RowDefinitions.Add(new RowDefinition() { Height = RowHeight });
        }
    }

    #endregion

    #region RowHeight

    public GridLength RowHeight
    {
        get { return (GridLength)GetValue(RowHeightProperty); }
        set { SetValue(RowHeightProperty, value); }
    }

    public static readonly DependencyProperty RowHeightProperty =
        DependencyProperty.Register(nameof(RowHeight), typeof(GridLength), typeof(DynamicGridBehavior), new PropertyMetadata(default(GridLength), (sender, e) => {
            if(sender is DynamicGridBehavior me && e.NewValue is GridLength newValue)
                me.OnRowHeightChanged(newValue);
        }));

    void OnRowHeightChanged(GridLength newValue)
    {
        if(AssociatedObject == null)
            return;

        foreach(var column in AssociatedObject.RowDefinitions)
            column.Height = newValue;
    }

    #endregion
}

Columns / ColumnWidth / Rows / RowHeightプロパティを実装しました。こちらもまた依存関係プロパティのせいでコード量が多くなっていますが、やっていることとしては、Columns / Rowsプロパティで列数 / 行数を設定して、それに対応するようにGrid.ColumnDefinitionsとGrid.RowDefinitionsの中身を調整しているにすぎません。新たに行や列を作る場合は、その行幅 / 列幅を共通で設定できるようColumnWidth / RowHeightプロパティも用意しています。

XAMLの実装

さて、最後にXAMLを実装していきます。 

<Window x:Class="GridTest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:GridTest.Views"
        xmlns:vb="clr-namespace:GridTest.Views.Behaviors"
        xmlns:vm="clr-namespace:GridTest.ViewModels"
        Title="MainWindow" Width="640" Height="480">

    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>

    <i:Interaction.Triggers>
        <!--  Dispose method is called, when Window closing.  -->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction />
        </i:EventTrigger>

        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize" />
        </i:EventTrigger>

        <!--  If you make user choose 'OK or Cancel' closing Window, then please use Window Close cancel Behavior.  -->

    </i:Interaction.Triggers>

    <ItemsControl ItemsSource="{Binding Cells}" >
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Grid>
                    <i:Interaction.Behaviors>
                        <vb:DynamicGridBehavior Rows="{Binding Rows}" Columns="{Binding Columns}" RowHeight="50" ColumnWidth="200" />
                    </i:Interaction.Behaviors>
                </Grid>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="{x:Type vm:CellViewModel}">
                <Border BorderBrush="Gray" BorderThickness="1" >
                    <TextBlock Text="{Binding Text}" HorizontalAlignment="Center" VerticalAlignment="Center" />
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Grid.Row" Value="{Binding Row}" />
                <Setter Property="Grid.Column" Value="{Binding Column}" />
            </Style>
        </ItemsControl.ItemContainerStyle>
    </ItemsControl>
</Window>

一つひとつ説明していきます。

まず、動的な要素をパネルにマッピングする場合はItemsControlを使います。具体的なパネルはItemsControl.ItemsPanelで指定しますので、ここでGridを指定してあげています。Gridの内容を動的に設定する必要があるので、先ほど作ったDynamicGridBehaviorを設定しています。 

DataTemplateは特段説明する必要はないと思います。なお、静的に配置する場合は各要素にGrid.Row / Grid.Column添付プロパティを追加することになりますが、ここでは指定しません。なぜならば、BorderはさらにContentPresenterでラッピングされるため、Borderで指定したところで正しく反映されないのです。

ContentPresenterにどうやって添付プロパティを設定するかと言えば、ItemContainerStyleになります。これによってGrid.Row / Grid.Column添付プロパティを設定することができるわけですね。

これによって、このように動的に正しく配置されたGridができました。

まとめ

ポイントをまとめると以下の通りとなります。

  • Grid.RowDefinitions / Grid.ColumnDefinitionsはバインディングできないため、Behaviorを作ってC#のコードから操作をする
  • パネルに動的に要素を配置するためにItemsControlを使用する
  • ItemsControlを使った場合、Grid直下の要素はContentPresenterになるため、ItemContainerStyleを使用してGrid.Row / Grid.Column添付プロパティを設定する 
以上となります。

2026年5月1日金曜日

古い.NETで新しいC#の機能を使う(Polyfill)

 C#は日々進化していて、年々便利な機能が追加されて行っています。しかし、一部のC#の新機能は.NETランタイムのサポートが必要なものもあり、特にライブラリの開発だったりすると古めのターゲットフレームワークを指定しなければならないこともしばしばあるため、その「便利な新機能」が使えないことがあります。

一部の機能は公式よりライブラリが提供されることがあり、それを追加することで対応できます。例えば、C#7で導入されたタプルはSystem.ValueTuple、C#7.2で導入されたSpan<T>はSystem.Memoryを入れることで比較的古いフレームワークにも対応させることができます。しかし、C#8.0(.NET Core 3.0)で導入されたIndex/RangeやNullableのアノテーション属性などは公式ライブラリが無く、サポートされません。

PolySharp

そのような隙間を埋める非公式ライブラリ、通称PolyfillがC#(.NET)にもあります。Polyfillって私も最近知った用語なのですが、もともとはJavaScriptで発達した概念みたいですね。

いろいろなライブラリがあるようですが、有名どころはこの2つでしょうか。

いずれもコードジェネレーターとして動作するので、これらを導入したところで出力アセンブリに変化はありません。

今回はPolySharpの使い方を備忘録的に残していきたいと思います。

導入

Nugetからインストールするだけです。

Nullableアノテーション属性(.NET Core 3.0~)

Nullableアノテーション属性とは、参照型のNull許容性を静的に示すための属性です。コンパイラがこの情報をもとに異常なNull状態があるかどうかを判別するためのものです。

代表的な使用例としてはTryParseでしょうか。例えばIPAddress.TryParseメソッドはパースに成功した際はtrueを返し、かつ第2引数のout IPAddress? addressは非nullとなります。しかし、falseを返した時はnullになりますので、全体としてはnullの可能性があるということで'?'が付いています。これを「常にnullの可能性がある」扱いしてはかなり不都合が大きいので、属性を付けて静的解析の助けにするわけです。

public static bool TryParse(string input, [NotNullWhen(true)] out string? result)
{
    if(string.IsNullOrEmpty(input)) {
        result = null;
        return false;
    } else {
        result = input;
        return true;
    }
}

PolySharpさえ導入すれば、何も気にせずに古いバージョンでも動くようになります。ライブラリの公開メソッドにも害なく使えます(コンパイラが見るだけなので)。

Index / Range(.NET Core 3.0~)

上のNullableアノテーション属性はPolySharpを入れるだけで使えるようになっていましたが、Index / Rangeの導入には一癖あります。

まず、Index / Rangeを使うためにはValueTupleが必要です。.NET Framework 4.6.2以前では最初に述べた通りパッケージの導入が必要です。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFrameworks>net462;net470;net10.0</TargetFrameworks>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    <LangVersion>14</LangVersion>
    </PropertyGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'net462'">
    <PackageReference Include="System.ValueTuple" Version="4.5.0" />
  </ItemGroup>
  
    <ItemGroup>
      <PackageReference Include="PolySharp" Version="1.15.0">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      </PackageReference>
    </ItemGroup>

</Project>

さらに、Rangeを動かすためにはRuntimeHelpers.GetSubArray()メソッドも必要になります。ソースコードはここにありますが、これをそのままちゃんと動かそうとすると面倒なので、簡略化したコードにしておきましょう。

#if NETFRAMEWORK || !NETCOREAPP3_0_OR_GREATER
namespace System.Runtime.CompilerServices
{
    internal static class RuntimeHelpers
    {
        public static T[] GetSubArray<T>(T[] array, Range range)
        {
            var (offset, length) = range.GetOffsetAndLength(array.Length);
            var result = new T[length];
            Array.Copy(array, offset, result, 0, length);
            return result;
        }
    }
}
#endif

これでちゃんとIndex / Rangeが動くようになりました。

int[] array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
var range = array[5..];

Console.WriteLine(string.Join(", ", range));
5, 6, 7, 8, 9

Init-Onlyプロパティ(.NET 5~)

C#9.0/.NET5で導入されたInit-Onlyプロパティは、実体としてはただのsetアクセッサで、コンパイラが初期化時以外に触れないようにチェックしているに過ぎません。そのsetアクセッサにはIsExternalInitクラスという属性のようなものが付けられており、これによってinitプロパティであることを示しています。

余談ですが、属性のようなものであって属性ではないのは、上のNullableアノテーション属性とは異なって、「解釈できない場合は触らないで」というタイプのものだからです(解釈できないまま触られたらsetアクセッサとなってしまい、initよりも緩い制約になってしまいます)。ですので、modreq+IsExternalInitという特殊なマーキングを行っていて、このマーキングが解釈できない古いコンパイラでは勝手に触らないでもらうようにしているようです。ですが、役割としてはただのマーキングなので、クラスを定義するだけで古いバージョンのフレームワークで使えるようになります。

internal class Program
{
    static void Main(string[] args)
    {
        var test = new TestClass() { TestProperty = 1 };
    }
}

public class TestClass
{
    public int TestProperty
    {
        get; init;
    }
}

Record型(.NET 5~)

上記のInit-Onlyプロパティが使えるようになったことを受けて使えるようになります。 

Required修飾子(.NET 7~)

プロパティやフィールドにrequired修飾子を付けることによって、初期化時の代入を強制できます。null非許容参照型のプロパティをコンストラクタで初期化しなくて良くなるので便利ですね。

これも実体はRequiredMemberAttributeなので、PolySharpでその属性が実装されています。

internal class Program
{
    static void Main(string[] args)
    {
        var test1 = new TestClass() { Name = "Test1" };
    }
}

public class TestClass
{
    public required string Name { get; set; }
}

まとめ

少しIndex / Rangeに癖がありましたが、主要な機能は使えることがわかりました。

もともとPolyfillを入れなくても使える機能や、公式にパッケージが提供されているものもあり、今回紹介した機能がPolyfillでカバーできるのならば割と充分なのではないでしょうか。 

2026年3月18日水曜日

ESP32+NimBLEでBLE Advertisingを受信する

皆さんは紛失防止タグを使っていますか?私は使ったことがありません。

この紛失防止タグに使われているのがBLE Advertisingという技術です。もともとはBluetoothのペアリング前にペリフェラル(Bluetooth子機)が周囲のセントラル(親機)に自分の存在を伝えるための機能ですが、

  • ペリフェラルは周囲のセントラルに対して一方的にデータをブロードキャストできる
  • セントラルはペリフェラルの信号レベルを基準に ペリフェラルまでの距離感を知ることができる

という特徴を使って、紛失防止タグが定期的にBLE Advertisingを送信することにより、それを受信した周囲のスマホ等の位置情報を通じて紛失防止タグがどの辺にいるかがわかるわけです。

ですが、BLE Advertisingの可能性はそのような紛失防止タグにとどまりません。上述の通り任意のデータをブロードキャストできるため、上手いこと使えばちょっとしたデータ送信にも使えるわけです。超省電力で定期的にセンサーの値を周囲に発信するデバイスを作るとかね。

今回はそのAdvertisingを受信するときの話です。 

環境

さて、如何せん組み込み周りは流行り廃りが早いので、今回の環境の確認です。

Seeed XIAOシリーズは個人的には結構気に入っています。USB Type-C搭載、小型、フォームファクタの統一あたりがかなり私のツボにドンピシャです。今回はESP32S3の搭載されているものを使いますが、これはアンテナをつけないとまともに通信できないのが玉に瑕ですね…。

NimBLEはESP32の標準BLEライブラリライクな使い方ができるように作られた軽量なライブラリです。標準ライブラリより安定しているようです。

同期受信

まずは基本の同期受信から。同期受信(勝手に私がそう呼んでいるだけです)とは、受信中は関数から制御が返ってこない受信方法です。チュートリアルにコードがしっかり書いてありますので、その通りに作ればOKです。

Creating a Client - New User Guide

#include <NimBLEDevice.h>

void setup()
{
    Serial.begin(115200);
    Serial.println("Started!");

    NimBLEDevice::init("");

    NimBLEScan *scan = NimBLEDevice::getScan();
    NimBLEScanResults results = scan->getResults(10 * 1000);

    for(int i = 0; i < results.getCount(); i++) {
        const NimBLEAdvertisedDevice *device = results.getDevice(i);
        
        Serial.printf("#%d: %s", i, device->toString().c_str());
        Serial.println();
    }
}

void loop()
{
}

scan->getResults()関数は与えられた時間(ミリ秒)の間待機し、その間に受信したBLE Advertisingを返します。待機している間はこの関数は制御を返さない(blocking function)ので「同期受信」です。

非同期受信

さて、続いて非同期受信です。こちらはあまり体系だった情報は見つけられませんでしたが、NimBLEのver.1.x系がESP32標準BLEライブラリの名称を踏襲していて、以下の公式ドキュメントにver.1.x系からver.2.x系に移行する際のクラス/関数名の置き換えが書いてあるので、その情報を組み合わせたり、NimBLE自身のソースコードを参考にしながらコードを書いていきます。

Migrating from 1.x to 2.x

#include <NimBLEDevice.h>

class AdvCallback : public NimBLEScanCallbacks
{
    void onResult(const NimBLEAdvertisedDevice* device) override
    {
        if(!updated) {
            deviceText = device->toString().c_str();
            updated = true;
        }
    }

public:
    String deviceText;
    bool updated = false;
};

AdvCallback callback;

void setup()
{
    Serial.begin(115200);
    Serial.println("Started!");

    NimBLEDevice::init("");

    NimBLEScan *scan = NimBLEDevice::getScan();
    scan->setScanCallbacks(&callback);
    scan->setActiveScan(true);
    scan->setInterval(100);
    scan->setWindow(80);
    scan->setDuplicateFilter(true);

    scan->start(0, false, false);
}

void loop()
{
    if(callback.updated) {
        Serial.println(callback.deviceText);
        callback.updated = false;
    }
}

今度は少し長くなりました。

NimBLEでは、Advertising受信時のコールバックはクラスを作ることで対応します。昔のJavaスタイルですね。親クラス名はNimBLEScanCallbacksで、このonResult関数をオーバーライドすると、BLE Advertisingを受信した際にこの関数が呼ばれるようになります。このサンプルコードでは、受信した情報をテキスト化して同じクラス内にデータとして保持するようにしています。

重複フィルター

さて、上述の非同期受信のコードを実行していると気づくかもしれませんが、同じペリフェラルから複数回Advertisingを送っても2回目以降は検知しません。これはもともとAdvertisingはペリフェラルがセントラルに自分の存在を知らせるためのものなので、同じペリフェラルから複数回の通知を受ける必要が無いからです。

ですが、先ほどの例のようにセンサーの値を送るようなデバイスを作った場合、同じデバイスからの信号でも複数回受信したくなってきます。そういう場合は重複フィルターをOFFにしましょう。

void setup()
{
    Serial.begin(115200);
    Serial.println("Started!");

    NimBLEDevice::init("");

    NimBLEScan *scan = NimBLEDevice::getScan();
    scan->setScanCallbacks(&callback);
    scan->setActiveScan(true);
    scan->setInterval(100);
    scan->setWindow(80);
    scan->setDuplicateFilter(false);

    scan->start(0, false, false);
}

簡単ですね。これにて重複があっても問答無用でonResult関数が呼ばれまくるようになりました。

再スタート

重複フィルターをOFFにしてみるとわかりますが、滝のようにAdvertisingがやってきます。そうすると、まあここまでいらないと。例えば、10秒に1回重複フィルターをリセットするくらいがちょうどいいみたいなニーズもあるかもしれません。その場合は、NimBLEScan::start関数の最後の引数をtrueにして再度呼び出せば良いです。これをすると、重複フィルターがリセットされまたイチから受信を始めます。

#include <NimBLEDevice.h>

class AdvCallback : public NimBLEScanCallbacks
{
    void onResult(const NimBLEAdvertisedDevice* device) override
    {
        if(!updated) {
            deviceText = device->toString().c_str();
            updated = true;
        }
    }

public:
    String deviceText;
    bool updated = false;
};

AdvCallback callback;

void setup()
{
    Serial.begin(115200);
    Serial.println("Started!");

    NimBLEDevice::init("");

    NimBLEScan *scan = NimBLEDevice::getScan();
    scan->setScanCallbacks(&callback);
    scan->setActiveScan(true);
    scan->setInterval(100);
    scan->setWindow(80);
    scan->setDuplicateFilter(true);

    scan->start(0, false, false);
}

void loop()
{
    static unsigned long last10sec = 0;
    unsigned long now10sec = millis() / 10000;

    if(callback.updated) {
        Serial.println(callback.deviceText);
        callback.updated = false;
    }
    if(now10sec != last10sec) {
        NimBLEDevice::getScan()->start(0, false, true);     // restart
        Serial.println("Restarted!!");

        last10sec = now10sec;
    }
}

データを送る

BLE Advertisingを使ってセンサー値などの任意のデータを送る場合、Manufacture Dataにくっつけて送ることが多いようです。Manufacture Dataは最初の2バイトがそのデバイスの製造企業を表す識別子、以降が任意のデータになります。 

以下のサンプルは、Manufacture Dataが4バイト以上ある場合に、3, 4バイト目を16bit整数値として回収するサンプルです。

#include <NimBLEDevice.h>

class AdvCallback : public NimBLEScanCallbacks
{
    void onResult(const NimBLEAdvertisedDevice* device) override
    {
        if(!updated && device->haveManufacturerData()) {
            std::string mdata = device->getManufacturerData();
            if(mdata.length() >= 4) {
                data = ((uint16_t)mdata[2] << 8) | mdata[3];
                updated = true;
            }
        }
    }

public:
    uint16_t data;
    bool updated = false;
};

AdvCallback callback;

void setup()
{
    Serial.begin(115200);
    Serial.println("Started!");

    NimBLEDevice::init("");

    NimBLEScan *scan = NimBLEDevice::getScan();
    scan->setScanCallbacks(&callback);
    scan->setActiveScan(true);
    scan->setInterval(100);
    scan->setWindow(80);
    scan->setDuplicateFilter(true);

    scan->start(0, false, false);
}

void loop()
{
    static unsigned long last10sec = 0;
    unsigned long now10sec = millis() / 10000;

    if(callback.updated) {
        Serial.printf("%04X", callback.data);
        Serial.println();
        
        callback.updated = false;
    }
    if(now10sec != last10sec) {
        NimBLEDevice::getScan()->start(0, false, true);     // restart
        Serial.println("Restarted!!");

        last10sec = now10sec;
    }
}

getManufactureData()関数はなぜかデータをstd::stringで返してくるようですが、配列の気分で特に気にせずにデータを取り出してしまえば大丈夫です。

ここまでやれば、Advertisingでのデータ受信は一通りできるようになったと言えるでしょう。

余談:AIコーディング

AIコーディング全盛期の今、私もその波に乗っかろうとひとまず「Seeed XIAO ESP32S3でNimBLEを使ってBLE Advertisingを受信するコードを書いて」などと注文してやってみましたが…うまく動かずAIと会話を続けているとだんだん腹が立ってきました。

というのも、AIが吐き出したコードがコンパイルエラーを起こすのでエラーの内容を聞いたら「NimBLEはesp32 ver. 3.3.7には対応していない」とか「NimBLEは安定しているver.1.xを使うべき」とか適当なことばかり言ってきて…。しかも、そんなはずはないと思って改めて聞き直してみたら「前も言った通りそうではない」「あなたは以前にもその間違いで苦労していましたね」とか言ってくるもんだから…。これがもしAIじゃなくて人間だったら、ろくに調べもしないで口先ばかりで適当なことを言う割に頑固という、絶対に相手にしたくないタイプですね。

結局、ちゃんと公式ドキュメンテーションを読んで、ライブラリのソースコードを読んでとやることで、私が自力で答えまでたどり着くのでした。

AIがドキュメンテーションやコードの読み込みが足りないのは少し時間がたてば解決してくれそうな気はしますが、そのコードがちゃんとデバイス上で意図したとおりに動くか検証できるようにはならないでしょうから、まだその側面においては人間有利といったところでしょうか。英語の海から集めた引き出しを日本語で提示してくれるのは助かりますが…せめてあまり適当なことは言わないようにしてもらいたい…。

あと数年経ってからこの余談を読み返したら、「黎明期はこんなのだったなー」と思うのでしょうか。それについては少し楽しみですね。

2025年11月23日日曜日

WPF用縦書きテキストブロック Tategaki ver.3.3.0

Tategakiをアップデートしました。

Github:

Nuget:
https://www.nuget.org/packages/Tategaki/

今回の変更点

今回はほぼほぼマイナーチェンジなのですが、Obsolete属性を付けていたTategakiMultilineを削除したので、2桁目のバージョンを上げて3.3.0としました。

変更内容は以下の通りです。

  • ターゲットフレームワークに.NET10.0を追加
  • フォントファイルの読み込みルーチンを微修正(.NET10.0で出るようになった警告CA2022を回避するため)
  • TategakiMultilineを削除(ver.3.1.0以降Obsolete属性が付いていたもの)

.NET10.0では、Stream.Readメソッドの戻り値を無視すると警告CA2022が出るようです。代替策としてStream.ReadExactlyメソッドを呼び出すことが推奨されていますが、これが.NET7.0以降でのみサポートされているので、それ以外のターゲットではオレオレReadExactlyを作って呼び出すことで対応しました。

TategakiMultilineはいつ消そうかとずっと様子を伺っていたのですが、Obsoleteを付けたver.3.1.0をリリースしてから1年半以上経ちましたし、.NET10.0対応を機に削除することにしました。パフォーマンスも機能性もTategakiTextのTextWrappingプロパティを使うほうが優れていますので、ぜひそちらを使用してください。

2025年7月7日月曜日

円周率を数値計算する

誰もが小学生の時に友達とこぞって暗記する数学の超有名定数こと円周率ですが、みなさんは円周率を数値計算したいと思ったことは無いですか?私は日ごろからしてみたいと思っていました(?)。

パソコンで数値計算をしようとしたら、超有名ソフト「SUPER PI」や、現在でも速度面で覇権をとっている「y-cruncher」などが有名ですね。当然ながらそういったソフトに勝てるようなソフトは作れないのですが、円周率の数値計算の世界を少し覗いてみましょう。

逆正接関数(arctan)のテイラー展開

数値計算といったらテイラー展開です。過去にネイピア数を求めたときもテイラー展開でしたね。円周率の数値計算の話をすると、arctan(tanの逆関数)やarcsin(sinの逆関数)を使う方法が2番目くらいに出てくるかと思います(1番目は内接多角形・外接多角形の話かなと思います。個人の意見です)。というわけで、早速arctanのテイラー展開に取り組んでいきましょう。

まず、唐突ですがarctanの微分を考えます。 

\[y=\arctan x\]

と置くと、定義より 

\[x=\tan y\] 

両辺微分して 

\[\frac{dx}{dy}=\frac{d}{dy}\tan y =\frac{1}{\cos^2 y}\]  

よって

\[\frac{dy}{dx}=\cos^2 y=\frac{1}{1+\tan^2 y}\]  

ここで$y=\arctan x$を代入すると

\[\frac{dy}{dx}=(\arctan x)'=\frac{1}{1+x^2}\]  

arctanの微分って意外とシンプルな表現になるんですね。両辺積分して、arctanの形で表現するようにしておきましょう。

\[\arctan x=\int_{0}^{x} \frac{1}{1+t^2} dt\]   

ここで、さらに唐突ですが、無限等比級数の公式を思い出します。

\[ 1+r+r^2+r^3+\cdots=\frac{1}{1-r} \qquad (|r|<1) \]

これに$r=-t^2$を代入すると

\[ 1-t^2+t^4-t^6+\cdots=\frac{1}{1+t^2} \qquad (t^2<1) \] 

あら不思議、右辺は先ほどのarctanの積分表記と同じですね。ということで代入して、

\[ \begin{align} \arctan x &=\int_{0}^{x} \frac{1}{1+t^2} dt \notag \\ &=\int_{0}^{x} (1-t^2+t^4-t^6+\cdots) dt \notag \\ &=\left [t-\frac{t^3}{3}+\frac{t^5}{5}-\frac{t^7}{7}+\cdots \right ]_{0}^{x} \notag \\ &= x-\frac{x^3}{3}+\frac{x^5}{5}-\frac{x^7}{7}+\cdots \notag \end{align} \]

無事テイラー展開ができました。

ついでに円周率を求めておきましょう。arctanの定義より、 

\[ \arctan 1 = \frac{\pi}{4} \]

となるので、テイラー展開を適用して、

\[ \frac{\pi}{4} = 1-\frac{1}{3}+\frac{1}{5}-\frac{1}{7}+\cdots \]

という級数が得られます(途中式で$t^2<1$という条件があったから$x=1$を入れられないじゃないかって?収束半径とかごにょごにょ改めて計算すると大丈夫らしいです)。見ての通り、この級数を計算して4倍すれば円周率ですね。

この級数はとてつもなく収束が遅いことで知られています。10桁計算するのに100億項まで計算する必要があるんだとか。そりゃ、分母が等差数列でしか増えていかなくて、+と-が交互に出てくるんじゃ収束は遅いですよね。

マチンの公式

1706年にイギリスの天文学者マチンは、次の公式を発表しました。

\[ \frac{\pi}{4} = 4 \arctan\frac{1}{5}-\arctan\frac{1}{239} \]

これを上のテイラー展開の式に当てはめて考えてみましょう。

\[ \frac{\pi}{4} = 4 \left (\frac{1}{5}-\frac{1}{3 \cdot 5^3}+\frac{1}{5 \cdot 5^5}-\frac{1}{7 \cdot 5^7}+\cdots \right) - \left (\frac{1}{239}-\frac{1}{3 \cdot 239^3}+\frac{1}{5 \cdot 239^5}-\frac{1}{7 \cdot 239^7}+\cdots \right ) \] 

$x^{2n}$で増えていく項が分母に付いたので、収束がぐんと早くなることは想像に難くありません。実際、この方法だと10桁を計算するのに10項ほどで事足ります。

せっかくなので、この公式も導出しておきましょう。

$\theta=\arctan(1/5)$と置いて、2倍角の公式を2回使って$\tan 4\theta$を求めます。

\[ \tan 2\theta = \frac{2 \tan \theta}{1-\tan^2 \theta} = \frac{2 \cdot \frac{1}{5}}{1-\frac{1}{25}}=\frac{5}{12} \]

\[ \tan 4\theta = \frac{2 \tan 2\theta}{1-\tan^2 2\theta} = \frac{2 \cdot \frac{5}{12}}{1-\left(\frac{5}{12}\right)^2}=\frac{120}{119} \] 

ここで、本命となる$\tan(4\theta-\pi/4)$を計算します。加法定理より 

\[ \tan \left(4\theta-\frac{\pi}{4}\right) = \frac{\tan 4\theta - \tan\frac{\pi}{4}}{1+\tan 4\theta \tan\frac{\pi}{4}}=\frac{\frac{120}{119}-1}{1+\frac{120}{119}}=\frac{1}{239}\]  

よって

\[ 4\theta-\frac{\pi}{4} = \arctan \frac{1}{239} \] 

\[ \frac{\pi}{4} = 4\arctan\frac{1}{5}-\arctan \frac{1}{239} \]  

実装

さて、マチンの公式を実装していきましょう。

static void Main()
{
    int digits = 100;
    int scale = digits + 10; // 十分な精度確保のため余裕を持たせる

    var pi = 16 * Arccot(5, scale) - 4 * Arccot(239, scale);
    var piText = ToDecimal(pi, scale).Substring(0, digits + 2);

    Console.WriteLine(piText);
}

static BigInteger Arccot(int x, int scale)
{
    BigInteger unity = BigInteger.Pow(10, scale);
    BigInteger xPower = unity / x;
    BigInteger sum = xPower;
    BigInteger term;
    int n = 3;
    bool negative = true;
    long x2 = (long)x * x;

    while(true) {
        xPower /= x2;
        term = xPower / n;

        if(term == 0)
            break;

        sum += negative ? -term : term;
        negative = !negative;
        n += 2;
    }

    return sum;
}

static string ToDecimal(BigInteger value, int scale)
{
    string s = BigInteger.Abs(value).ToString().PadLeft(scale + 1, '0');
    int decimalPosition = s.Length - scale;
    return (value.Sign < 0 ? "-" : "") + s.Insert(decimalPosition, ".");
}

Arccot関数はいわゆる逆余接関数(コタンジェントの逆関数)です。要するに、正接の逆数を与えることになります。上のテイラー展開の式を見てもわかる通り、マチンの公式だと各項の分子は常に1ですので、コタンジェントと見立てれば引数はすべて整数にできるのですね。

また、計算の過程でも、例えば小数点以下第100桁まで求めたければ、ひとまず10^100を用意して、それを各項の分母で割って足していくことで級数の和を計算します。最終的に項の値が0になってしまったら、それ以降は1未満の項で無視できるということで計算を終了させています。これによって、分数を使わずにすべて整数として計算できるようになっています。

また、求めたい桁数に対して、10桁ほど多めに計算するようにしています。これは、テイラー展開の各項の計算における割り算の誤差や、最後に足し合わせた時の繰り上げ/繰り下げ等で多少の誤差が出ることから、少し多めの桁数を計算しているといったところです。本当はちゃんと挟みうちをして誤差を評価すべきでしょうが、まあ、除算や足し算の誤差なんて知れているので、この程度で概ね充分でしょう。

マチンの公式の拡張

さて、上のコードは充分にシンプルで良いものですが、例えば私のパソコンで実行すると、30万桁で約47秒かかります。もう少し良い方法は無いでしょうか?

実は、マチンの公式に類似した公式、すなわちarctanの中身が1/nであるものを足し合わせただけの公式は多数あります。当然分母が大きいほうが収束が早いはずですので、そのようなものがあればより早く計算できることが期待できます。

というわけで次の公式を使って計算してみましょう。

\[ \frac{\pi}{4}=44 \arctan \frac{1}{109}+95 \arctan \frac{1}{239}-12 \arctan \frac{1}{682}+24 \arctan \frac{1}{12943}-44 \arctan \frac{1}{6826318}\] 

すべての分母が100超えということで、計算速度に期待できますね。

また、項が5つありますが、各項は独立して計算できるので並列処理してしまいましょう。

static void Main()
{
    int digits = 100;
    int scale = digits + 10; // 十分な精度確保のため余裕を持たせる

    var members = new BigInteger[5];
    Parallel.Invoke(
        () => members[0] = 176 * Arccot(109, scale),
        () => members[1] = 380 * Arccot(239, scale),
        () => members[2] = -48 * Arccot(682, scale),
        () => members[3] = 96 * Arccot(12943, scale),
        () => members[4] = -176 * Arccot(6826318, scale)
    );
    var pi = members[0] + members[1] + members[2] + members[3] + members[4];
    var piText = ToDecimal(pi, scale).Substring(0, digits + 2);

    Console.WriteLine(piText);
}

これで30万桁計算すると、約15秒で計算が終わりました。マチンの公式に比べて3倍ほど早いですね。

項数が増えると各項を足し合わせるのに時間がかかるのでは?と思うかもしれませんが、多倍長整数の和は簡単に計算できるのでここはそんなにボトルネックになりません。arccotの計算にかかるが支配的で、その次は10進変換です。

まとめ

ということで、マチンの公式及びその亜種を使っての円周率の計算をやってきました。高校数学くらいで理解できる式で、簡単かつそれなりに計算できるものなのですね。

ちなみに、私のコードで30万桁15秒でしたが、SUPER PIだと52万桁で3秒でした。y-cruncherだと1億桁計算して5秒なので、足元にも及ばないですね…。

2025年6月26日木曜日

WPF用縦書きテキストブロック Tategaki ver.3.2.3

Tategakiをアップデートしました。

Github:

Nuget:
https://www.nuget.org/packages/Tategaki/

今回の変更点

今回もマイナーチェンジです。というより、コードはほぼ変更していません。以下のターゲットフレームワークを追加しました。

  • .NET Framework 4.6.1(現行サポートされている最低バージョン)
  • .NET Framework 4.6.2(GlyphRunのオーバーロードが変更になるバージョン)
  • .NET Core 3.0(.NET CoreでWindows Desktopがサポートされた最低バージョン)
  • .NET8.0(最新のLTSバージョン)

これにより、今までサポートしていたのが.NET Framework 4.7.2以降 / .NET 6.0以降だったものが、.NET Framework 4.6.1以降 / .NET Core 3.0以降に緩和されました。ここまでならコードをほぼ変更せずに対応できましたが、これ以上はTupleがサポートされていないなど、かなり大幅な修正が必要になるため対応できません。

Tategakiはver.2.1.1が.NET Framework 4 Client Profileのサポートをしていて、未だにダウンロードしてくださる方がたくさんいます。現時点でも、ver.3.2.2よりもver.2.1.1のほうが3倍近く多くのダウンロードがあります。

Package Downloads for Tategaki

この緩和によって、過去のバージョンに縛られた方が少しでも新しいバージョンに移行できることを願うばかりです。

2025年6月22日日曜日

PIC32MXでUSB MicroSDカードリーダーを作る (USB Device MSD class)

今回はPIC32MXでUSB接続のMicroSDカードリーダーを作ります。

もっぱら、PIC32MXではUSB Hostも対応しておりますので、USBメモリーの読み書きなどにも使えますが、今回はDevice側となります。 

環境

  • PIC32MX250F128B
  • MPLAB X IDE v6.20
  • XC32 v4.60
  • MPLAB Code Configurator v5.5.1

MPLAB Code Configurator (MCC)はMPLAB Harmonyを包含したコード生成ツールで、最近はHarmonyはMCC経由で入れることになるようです。

回路

まずは回路図です。 

シンプルにUSBとMicroSDカードのソケットを接続した回路になります。インジケーターとしてD1のLEDも用意しました。

PIC32MXシリーズでは内蔵オシレーターを持っていますが、USBを使うためには外付け8MHzのクロックが必要となります。

ファームウェア

続いて、MPLAB Xにてファームウェアを作成していきます。

空のプロジェクトの作成 

プロジェクトを作った後にMCCを使ってコード生成を行うので、空のプロジェクト(Application Project)を作成すればOKです。

ちなみにMCCはプロジェクトフォルダと同じ階層にsrcフォルダを作る仕様になっていて、これは設定等で変更できないようです。ですので、自分でプロジェクト用のフォルダを作って、その中にプロジェクトを作ると良いでしょう(あまり良い感じの解決法ではないですが)。

MPLAB Code Configurator (MCC)の設定

プロジェクトを作成すると自動的にMCCが起動します。起動しない場合はMPLABの上のほうの「MCC」アイコンをクリックします。アイコンが無い場合はプラグインが入っていない可能性があるのでググりましょう。

クロック設定

まずはクロック設定をします。Project GraphタブのPluginsからClock Configurationを選択します。

そうすると、Clock Diagramが出てくるので設定を行います。

まずはPrimary OscillatorをXTに設定し、周波数を8,000,000Hz (=8MHz)に設定します。ただしこのままでは右上のUSB Clockが8MHzのままなので、UPLLEN (USB PLL Enable)をONして48MHzにしてあげましょう。

これでクロックの設定は完了です。コンフィグレーションビットなどを直接触ることなく設定できるので超簡単ですね。

ピン設定

続いてピン設定を行います。PIC32MXシリーズもPPS (Peripheral Pin Select)という周辺回路を好きなピン(実はそんなに自由度は高くないですが)に割り当てる機能がありますが、MCCではその設定を行うこともできます。

クロック設定と同様にProject Graphタブから「Pin Configuration」を開きます。3つタブが出てきますが、Pin Tableで設定をしていきましょう。 

横にポート番号が並んでいて、縦に周辺回路の機能が並んでいます。青色が割り当て可能、緑が割り当て済み、灰色が割り当て不可です。

こんな感じに設定してみました。

USBはUSBID及びVBUSONは使用しないので未選択です。SPIはSPI2を使用することとします。SDカードのCSとLEDはGPIOになるのでその設定をします。クロックも忘れずにポートを設定する必要があります。

続いてPin Settingsタブを開いてGPIOの設定をします。

SDカードのCSピン(RB3)は出力ピンで、非選択のときはHighなのでそのように設定します。LEDのRB4も出力で、不点灯のときはLowなのでそのように設定します。 

Harmonyの設定

続いて使用するHarmony(ライブラリ)の設定をします。左側のDevice Resourcesから、以下のライブラリを取り込み(『+』ボタンを押し)ます (もしも出てこない場合は、ライブラリがインストールされていないので、MCC Content Managerから対応するライブラリを事前にインストールしておいてください)。

  • MSD Function Driver (Libraries - Harmony - USB - Device Stack)
  • SPI (Libraries - Harmony - Drivers - SDCARD)
  • SD Card (Libraries - Harmony - Drivers - SDCARD)
  • SPI2 (Libraries - Harmony - Peripherals - SPI)
  • CORE TIMER (Libraries - Harmony - Peripherals - CORE TIMER)

途中で依存関係にあたるライブラリを一緒に有効化するかという問い合わせが出てきます。基本OKで良いですが、FreeRTOSだけはNoにしておきます(容量がでかすぎて入らなくなるので)。

一通り入れ終わると、Project Graphタブにいろいろなモジュールが並びます。

赤色の部分は何かしら接続しないといけませんので接続していきます。黄色は必須じゃないらしいのでそのままで。

ライブラリの設定

ライブラリにも一部設定が必要なものがあります。

SD Card (SPI)_Instance 0を選択し、Chip Select PinをRB3に設定してやりましょう。  

USB Device Layerを選択するとUSBの設定が出てきます。デフォルトのままでも問題ないですが、何かしら変更したければここに入力すれば良いでしょう。 

コード生成 

最後に上の「Generate」を押すとコードが自動生成されます。

無事コードが生成されたら、最後にビルドしてビルドが通ることを確認しておきましょう。

アプリケーションコードの作成

さて、ビルドが通ったからと喜んで書き込んでみると、うんともすんとも言わないプログラムが出来上がります。デバイスマネージャーからもUSBが認識されない状態です。なぜかと言うと、アプリケーションコードが一切無いからです。USBデバイスを開いて~みたいな作業を全くしていないため何も起こらないわけです。 

なお、このセクションはこのGithubリポジトリのコードを参考にしています。

app.h

まずはapp.hを開いて以下の3つのファイルをincludeに追加します。

#include "definitions.h"
#include "usb/usb_chapter_9.h"
#include "usb/usb_device.h"

つづいて、APP_STATESとAPP_DATAを以下のように修正します。

typedef enum
{
    /* Application's state machine's initial state. */
    APP_STATE_INIT=0,

    /* Application's state machine running state */
    APP_STATE_RUNNING,
} APP_STATES;


typedef struct
{
    /* The application's current state */
    APP_STATES state;

    /* USB Device Handle */
    USB_DEVICE_HANDLE usbDeviceHandle;

    /* Device configured state */
    bool isConfigured;
    
} APP_DATA;

これでapp.hは完了です。

app.c

続いてapp.cの修正をしていきます。まずは下準備としてLEDの制御関数を作っておきます。

void LED_Off(void)
{
    GPIO_RB4_Clear();
}

void LED_On(void)
{
    GPIO_RB4_Set();
}

次にUSBのイベントが発生したときのイベントハンドラーを作っておきます。

void APP_USBDeviceEventHandler( USB_DEVICE_EVENT event, void * pEventData, uintptr_t context )
{
    /* This is an example of how the context parameter
       in the event handler can be used.*/

    APP_DATA * appDataObject = (APP_DATA*)context;

    switch( event )
    {
        case USB_DEVICE_EVENT_RESET:
        case USB_DEVICE_EVENT_DECONFIGURED:
            appData.isConfigured = false;
            /* Device was reset or de-configured. Update LED status */
            LED_Off();
            break;

        case USB_DEVICE_EVENT_CONFIGURED:
            appData.isConfigured = true;
            /* Device is configured. Update LED status */
            LED_On();
            break;

        case USB_DEVICE_EVENT_SUSPENDED:
            
            LED_Off();
            break;

        case USB_DEVICE_EVENT_POWER_DETECTED:

            /* VBUS is detected. Attach the device. */
            USB_DEVICE_Attach(appDataObject->usbDeviceHandle);
            break;

        case USB_DEVICE_EVENT_POWER_REMOVED:
            appData.isConfigured = false;
            /* VBUS is not detected. Detach the device */
            USB_DEVICE_Detach(appDataObject->usbDeviceHandle);
            LED_Off();
            break;

        /* These events are not used in this demo */
        case USB_DEVICE_EVENT_RESUMED:
            if(appData.isConfigured == true)
            {
                LED_On();
            }
            break;
        case USB_DEVICE_EVENT_ERROR:
        case USB_DEVICE_EVENT_SOF:
        default:
            break;
    }
}

続いて、APP_Initialize関数で追加したAPP_DATA構造体のメンバーの初期化を追加しておきましょう。

void APP_Initialize ( void )
{
    /* Place the App state machine in its initial state. */
    appData.state = APP_STATE_INIT;
    
    /* Set device layer handle as invalid */
    appData.usbDeviceHandle = USB_DEVICE_HANDLE_INVALID;
    
    appData.isConfigured = false;
}

最後に、APP_Tasks関数にてUSBデバイスの初期化を行っておきましょう。

void APP_Tasks ( void )
{
    /* Check the application's current state. */
    switch ( appData.state )
    {
        /* Application's initial state. */
        case APP_STATE_INIT:
        {
             appData.usbDeviceHandle = USB_DEVICE_Open(USB_DEVICE_INDEX_0, DRV_IO_INTENT_READWRITE);

            if(appData.usbDeviceHandle != USB_DEVICE_HANDLE_INVALID)
            {
                /* Set the Event Handler. We will start receiving events after
                 * the handler is set */
                USB_DEVICE_EventHandlerSet(appData.usbDeviceHandle, APP_USBDeviceEventHandler, (uintptr_t)&appData);

                /* Move the application to the next state */
                appData.state = APP_STATE_RUNNING;
            }
             
            break;
        }

        case APP_STATE_RUNNING:
            /* The MSD Device is maintained completely by the MSD function
             * driver and does not require application intervention. So there
             * is nothing related to MSD Device to do here. */
            break;

        /* The default state should never be executed. */
        default:
            break;
        
    }
}

USB_DEVICE_Open関数でデバイスを開いたうえで、USB_DEVICE_EventHandlerSet関数で先ほど実装したAPP_USBDeviceEventHandler関数を登録しているだけです。ソースコードに記載されている通り、初期化さえしてしまえばAPP_STATE_RUNNINGで特段ルーチン処理をする必要はありません。 

---

これにてひとまずパソコンに差すとSDカードのフォルダーが表示されるような状態になったはずです。目標であるMicroSDカードリーダーの完成です。

ちなみに、ファイルの読み書きをしてみるとわかりますがめちゃめちゃ読み書きが遅いです。まあ、まあ、USB Full Speed (12MHz) + SPIアクセス(5MHz)で動作させているので仕方ないですね。今や数百円でカードリーダーが買える時代にわざわざこのためだけに電子工作をする必要はありませんね。

では、このデバイスをベースに市販のカードリーダーには無いものを作るにはどうするか。ここがスタート地点です。