koktoh の雑記帳

気ままに書いていきます

基板発注するときの製造データをいい感じにするプログラム作った

はじめに

基板発注するとき、製造データ(BOM/POS)を出しますが、出てきた CSV なりのヘッダーだけ変更しても実は微妙だったりしますよね。
なので、それを解消するプログラムを書きました。

公開先

Web アプリとして公開しているので、こちらで利用できます。

koktoh.github.io

ソースコードはこちら。

github.com

対応状況

対応ソフト

  • KiCad

対応業者

  • JLC

使い方

全体像

基本

  1. ソフトと業者を選択する
  2. BOM / POS ファイルを読み込ませる
  3. 変換ルールを設定する
    • 直接編集する
    • すでにある JSON を読み込ませる
  4. 変換してダウンロードする

ルールが存在しない(キーが一致するルールがない)ものはそのまま出力されます。

変換ルールについて

BOM を読み込ませると、自動でテンプレートが生成されます。
そこから編集したり、用意していた JSON を読み込ませるといいと思います。

JSON を読み込ませると、ルールは JSON のものに上書きされるので注意。

仕様

面積をとるので折りたたんでおきます。
PCBAssemblyConverter の ℹ️ をクリックすると、同じものがポップアップで表示されます。

変換ルール仕様詳細

項目名 データ型 選択肢 説明
ルール名 文字列 - ルールの名前。
キー 文字列 - 変換ルールの適用対象となるフットプリントを検索するためのキー。
検索条件 選択
  • 完全一致
  • 部分一致
  • 正規表現
  • 完全一致
    • キー文字列と完全に一致するフットプリントに対してルールを適用する。
  • 部分一致
    • キー文字列を含むフットプリントに対してルールを適用する。
  • 正規表現
    • キー文字列に入力された正規表現と一致するフットプリントに対してルールを適用する。
Offset X 小数点 - フットプリント原点から実際の部品実装位置までの X オフセット。
Offset Y 小数点 - フットプリント原点から実際の部品実装位置までの Y オフセット。
実装面変更 選択
  • なし
  • トップ
  • ボトム
  • 反転
  • なし
    • 変更しない。
  • トップ
    • トップに変更。
  • ボトム
    • ボトムに変更。
  • 反転
    • 元の実装面から反転させる。
回転 選択
  • なし
  • 90° 反時計回り
  • 180° 反時計回り
  • 270° 反時計回り
フットプリントの回転角と実際の部品の回転角との差を補正する。
パーツ業者番号 文字列 - 実装するパーツの業者番号。
パーツ参考資料 文字列 - 実装するパーツの参考資料(データシートや調達先のリンクなど)が必要であればここに入力。
メモとしても使用可能。
ルール適用 選択
  • 変換して出力
  • そのまま出力
  • 出力しない
  • 変換して出力
    • キーと一致したフットプリントについて、変換ルールを適用して入稿ファイルに出力する。
  • そのまま出力
    • キーと一致したフットプリントについて、変換せずにそのまま入稿ファイルに出力する。
  • 出力しない
    • キーと一致したフットプリントは、入稿ファイルに出力しない。

  • キーと検索条件
    • キーと検索条件を合わせないと、正しくルールが適用できないので注意
    • 正規表現を使用するときは記法をちゃんと確認すること
    • 完全一致 -> 正規表現 -> 部分一致の順でルールを検索する
  • 回転
    • フットプリントと、業者側の角度が一致しないときなどに使用
    • 基本的に 90° ずつ回転させればいつかは一致すると思うので、それを選択する
  • パーツ業者番号
    • LCSC であれば C00000 みたいな番号を入れると楽
    • その他であれば、販売業者が振っている番号とか、部品の製造元が振っているパーツ番号を入れるなど柔軟に
  • パーツ参考資料
    • 代替パーツ候補、パーツのデータシートのリンク、販売業者のページなどなど、参考になるメモとかを入れると使いやすいかも

ルール例

こんな感じでルールが生成されます。
これを JSON で保存して読み込ませて使ったり、エディタで好きなルールを追加したりするのもいいです。

[
  {
    "Name": "D_SOD-123",
    "Key": "D_SOD-123",
    "MatchingStrategy": 0,
    "OffsetX": 0,
    "OffsetY": 0,
    "ChangeSide": 0,
    "RotationAdjustment": 0,
    "ManufacturerPartNumber": "C81598",
    "ManufacturerPartNotes": "",
    "RuleAction": 0
  },
  {
    "Name": "CherryMX_Hotswap",
    "Key": "CherryMX_Hotswap(_thin)?(_via)?_((\\d\u002B(\\.\\d\u002B)U)|(ISO_Enter))?(_.\u002B)?",
    "MatchingStrategy": 2,
    "OffsetX": -0.635,
    "OffsetY": 3.81,
    "ChangeSide": 3,
    "RotationAdjustment": 0,
    "ManufacturerPartNumber": "C49352235",
    "ManufacturerPartNotes": "kailh:C5156480(not available), HE(purple):C41430893, HE(white):C49352235",
    "RuleAction": 0
  },
  {
    "Name": "Kailh_Choc_V1_Hotswap",
    "Key": "Kailh_Choc_V1_Hotswap(_thin)?(_via)?",
    "MatchingStrategy": 2,
    "OffsetX": -2.5,
    "OffsetY": -7,
    "ChangeSide": 3,
    "RotationAdjustment": 0,
    "ManufacturerPartNumber": "",
    "ManufacturerPartNotes": "not available on LCSC",
    "RuleAction": 1
  },
  {
    "Name": "XIAO-RP2040",
    "Key": "XIAO-RP2040",
    "MatchingStrategy": 1,
    "OffsetX": 0,
    "OffsetY": 0,
    "ChangeSide": 0,
    "RotationAdjustment": 0,
    "ManufacturerPartNumber": "",
    "ManufacturerPartNotes": "",
    "RuleAction": 1
  }
]

使用例

仕様だのなんだの説明されたところで、これを使うと何が嬉しいのかわからないと思うので、実際に見てみることにします。

サンプルの KiCad プロジェクトを用意しました。
ガーバー、変換前後の BOM/POS ファイルも入っています。

github.com

サンプル基板

こんな感じの基板です。

これを JLC で PCBA するとします。
なお、 Choc ソケットは LCSC にないので PCBA に含んでいません。

そのまま進めてみる

KiCad から出力されたファイルのヘッダーを変更しただけで進んでいくとこうなります。

ダイオードの方向があべこべですね。
さらに、 MX ソケットもありません。(これに関しては私が公開しているフットプリントライブラリで、キースイッチのフットプリントの実装面が表裏反対なのが悪いんですが、3D モデルを登録してたりして色々直すの面倒なので……)

PCBAssemblyConverter で変換したデータで進めてみる

これを PCBAssemblyConverter で変換するとこうなります。
変換ルールは ルール例 に記載しているものと同じです。

ダイオードの方向が正しくなり、 MX ソケットも正しく配置されました。
嬉しい!! 🤗🤗🤗🤗

おわりに

というわけで、基板発注に便利そうなツール PCBAssemblyConverter を作ってみました。

対応業者を増やしたりとか、使いやすくしたりとか、ちょこちょこ改善出来たらいいなーと思っています。
「これに対応してくれ」みたいな要望にも応えられたらいいですね。

これで基板発注の手間が少しでも減ってくれれば嬉しいです。

時計ウィジェット作った

はじめに

欲しいと思ったので時計ウィジェット作りました。
思ったより規模が大きくなった……

せっかく作ったので公開してみようかなって。
テストとかあんましてないけど、まあ、おおむねちゃんと動いてるのでいいかなって。

ネイティブ API バチバチに使ってるので、 Windows のみです。
Impact と Yu Gothic UI Semibold のフォントを使ってるので、それがないと表示がおかしくなるかも。

公開場所

ここに置いてます。

github.com

注意事項

天気情報は気象庁の非公式 API を使用し取得しています。

非公式のため、予告なく変更・停止などされる可能性があります。

取得部分のコードの再利用、アプリの使用については 気象庁ホームページの利用規約 を確認の上、気象庁のサーバーに過度な負荷を与えないよう心がけ、自己責任のもと使用してください。

詳細

見た目はこんな感じになってます。

個人的な好みでアナログです。

表示できる情報

  • 時間(当然)
  • 日付
  • 国民の祝日
  • 天気(ほぼリアルタイム)

設定項目

設定ウィンドウで OK ボタンを押すまで反映されません。
キャンセル ボタンを押したり、設定ウィンドウを閉じると変更は破棄されます。

スタンドアロン

ネットワーク接続をしないモードです。
国民の祝日のリストと天気はネットから持ってきてるので、そういうのをしません。
天気は API をボコボコ叩いてるので、スタンドアロンでは意味がないので強制的に非表示になります。

サイズ

時計全体のサイズを変更します。

常に最前面

常に最前面に表示されるようにします。

日付表示

日付を表示するかどうかを決めます。

天気表示

天気を表示するかどうかを決めます。
非表示にすると API を叩きに行くのをやめます。

各部色設定

  • 文字盤
  • 時針
  • 分針
  • 秒針
  • 日付・天気

をそれぞれ設定できます。
特にプリセットみたいな機能は付けてません。(面倒だし別に要らんかなって)

その他

ドラッグで移動できます。

文字盤、それぞれの針は ClockResources 以下に SVG で置いてあるので、それを入れ替えれば好きなデザインにできます。
針の SVG は、針自体の色を 0xFF000000 、背景に文字盤と同じ大きさの 透明 0x00000000 な円か何かを置いてください。

コンテキストメニュー(右クリック or タスクトレイのアイコンをクリック)からもいくつか直接設定できます。

技術的な話

WPF もりもり書くのは久しぶりだったので、結構大変でした。 Prism/DryIoc で MVVM です。

ChatGPT に結構助けられました。すごいね、 ChatGPT 。

NuGet パッケージ

マテリアルデザインでいい感じにするやつとか、いい感じにログするやつとかを入れてます。
Hardcodet.NotifyIcon.Wpf はタスクトレイにアイコン表示するやつですね。
Polly は API 叩きに行くときのリトライ制御とかをいい感じにするやつです。

メインウィンドウ

何もしないと Alt + Tab のウィンドウ切り替え(個人的に多用する)の一覧に出てきて邪魔なのでツールウィンドウにしてます。
Win32 API とか叩きたくなかったけどしょうがない……

public MainWindow()
{
    InitializeComponent();

    this.SourceInitialized += this.MainWindow_SourceInitialized;
}

private void MainWindow_SourceInitialized(object sender, EventArgs e)
{
    IntPtr hwnd = new WindowInteropHelper(this).Handle;
    int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
    exStyle |= WS_EX_TOOLWINDOW;
    SetWindowLong(hwnd, GWL_EXSTYLE, exStyle);
}

private const int GWL_EXSTYLE = -20;
private const int WS_EX_TOOLWINDOW = 0x00000080;

[DllImport("user32.dll", SetLastError = true)]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);

[DllImport("user32.dll", SetLastError = true)]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

あとは WindowStyle="None" とか色々いい感じにしとけばいい感じになります。

天気取得

気象庁の非公式 API ってやつを叩いてます。
JSON が降ってくるので、デシリアライズ先のクラスとか、コンバーターとかを作るのが面倒でしたね。

天気データについて

取ってくるのはアメダスの天気データです。

アメダスのデータ構造とかはいろんなところで解説されてるので、詳しい説明はしません。デシリアライズ用クラスとか見たら雰囲気がわかるかもしれません。

このアプリで使うのは以下の3つです。

全部アメダス観測所の ID をキーとしたディクショナリになっています。つまり、全観測所のデータがいっぺんに降ってきます。

データ整理

アメダス観測所の ID で突合してます。
ついでに、アメダス観測所の緯度経度と現在地の緯度経度から距離を計算して、近い順に並び変えます。

var amedas = amedasLocations
    .Select(x => new
    {
        x.LocationId,
        Amedas = x,
        Data10m = amedasData10m.FirstOrDefault(y => y.LocationId == x.LocationId),
        Data1h = amedasData1h.FirstOrDefault(y => y.LocationId == x.LocationId),
        Distance = geoCoordinate.DistanceTo(x.Latitude, x.Longitude)
    })
    .OrderBy(x => x.Distance);

現在地の緯度経度

ip-api.com から取得してます。

ip-api.com

IP アドレスから取得するので、プロキシとか通して全然違うところからアクセスしたら変な緯度経度が返ってくるかもしれません。
初期化時にしかアクセスしないので、移動しながら現在地を更新とかもできません。

データ抽出

このアプリでは気温と天気(晴れとか雨とか)を表示しますが、それぞれ見るべきデータが違います。
10分データと1時間データで構造はどちらも同じですが、天気は1時間データにしか含まれず、天気の項目を配信する観測所も一部しかありません。

まとめると……

  • 気温
    • 10分データで一番近い観測所のデータ
  • 天気
    • 1時間データで、天気の項目が存在し、一番近い観測所のデータ

これをフィルターして、それぞれで見るべき観測所 ID を保存し、以降は保存した観測所 ID のデータを見ています。

var data10m = amedas.FirstOrDefault(x => x.Data10m is not null);
var data1h = amedas.FirstOrDefault(x => x.Data1h?.Weather is not null);

// _semaphoreAmedas10m -> _semaphoreAmedas1h の順で取得する(デッドロック対策)
await this._semaphoreAmedas10m.WaitAsync();
await this._semaphoreAmedas1h.WaitAsync();
try
{
    this._target10mAmedasLocationId = data10m?.LocationId;
    this._target1hAmedasLocationId = data1h?.LocationId;

    this._cacheAmedasData10m = data10m?.Data10m;
    this._lastAmedasData10mUpdated = now;

    this._cacheAmedasData1h = data1h?.Data1h;
    this._lastAmedasData1hUpdated = now;
}
finally
{
    // 逆順に開放(デッドロック対策)
    this._semaphoreAmedas1h.Release();
    this._semaphoreAmedas10m.Release();
}

取得タイミング

観測所一覧

そうそう変化しないと思うので、一度取得したらローカルに保存して、以降はローカルのデータを読むようにしてます。
更新は半年(180日)です。

10分データ

更新は5分に1回です。
現在時刻を基に取得しますが、タイミングによっては配信されてない場合もあるので、最大20分前まで遡って取得できるようにしてます。

public async Task<IEnumerable<AmedasData>> GetAllAmedasData10mAsync()
{
    using var _ = new LoggerScope(this._logger);

    var now = DateTime.Now;

    for (int i = 0; i < 3; i++)
    {
        var target = $"{now.AddMinutes(-10 * i).ToString("yyyyMMddHHmm")[..11]}000.json";
        var url = $"{AMEDAS_API_URL}{target}";

        try
        {
            this._logger.LogInformation("10分間天気データ({Target})取得開始", target);

            this._logger.LogInformation("{Url} アクセス実行", url);

            using var res = await this.GetWithRetryAsync(url, HttpCompletionOption.ResponseHeadersRead);
            res.EnsureSuccessStatusCode();

            this._logger.LogInformation("{Url} アクセス成功", url);

            this._logger.LogDebug("{Url} レスポンス取得", url);

            var json = await res.Content.ReadAsStringAsync();

            this._logger.LogDebug("JSON デシリアライズ");

            var amedasDataRaw = JsonSerializer.Deserialize<IDictionary<string, Json.Data.AmedasData>>(json);
            var amedasData = amedasDataRaw.MapToAmedasData();

            this._logger.LogInformation("10分間天気データ({Target})取得完了", target);

            return amedasData;
        }
        catch (HttpRequestException ex)
        {
            if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                this._logger.LogWarning("10分間天気データ({Target})未配信のため再取得", target);
                continue;
            }

            this._logger.LogError(ex, "10分間天気データ取得失敗({Url} アクセス失敗)", url);
            break;
        }
        catch (JsonException ex)
        {
            this._logger.LogError(ex, "10分間天気データ取得失敗(JSON デシリアライズ失敗)");
            break;
        }
        catch (Exception ex)
        {
            this._logger.LogError(ex, "10分間天気データ取得失敗(不明なエラー)");
            break;
        }
    }

    return Enumerable.Empty<AmedasData>();
}

5分間隔以上の頻度でアクセスする場合は、サービス側でキャッシュを返すようにしています。

1時間データ

更新は30分に1回です。
これもタイミングによっては配信されてない場合もあるので、最大1時間前まで遡って取得できるようにしてます。

public async Task<IEnumerable<AmedasData>> GetAllAmedasData1hAsync()
{
    using var _ = new LoggerScope(this._logger);

    var now = DateTime.Now;

    for (int i = 0; i < 2; i++)
    {
        var target = $"{now.AddHours(-1 * i):yyyyMMddHH0000}.json";
        var url = $"{AMEDAS_API_URL}{target}";

        try
        {
            this._logger.LogInformation("1時間天気データ({Target})取得開始", target);

            this._logger.LogInformation("{Url} アクセス実行", url);

            using var res = await this.GetWithRetryAsync(url, HttpCompletionOption.ResponseHeadersRead);
            res.EnsureSuccessStatusCode();

            this._logger.LogInformation("{Url} アクセス成功", url);

            this._logger.LogDebug("{Url} レスポンス取得", url);

            var json = await res.Content.ReadAsStringAsync();

            this._logger.LogDebug("JSON デシリアライズ");

            var amedasDataRaw = JsonSerializer.Deserialize<IDictionary<string, Json.Data.AmedasData>>(json);
            var amedasData = amedasDataRaw.MapToAmedasData();

            this._logger.LogInformation("1時間天気データ({Target})取得完了", target);

            return amedasData;
        }
        catch (HttpRequestException ex)
        {
            if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                this._logger.LogWarning("1時間天気データ({Target})未配信のため再取得", target);
                continue;
            }

            this._logger.LogError(ex, "1時間天気データ取得失敗({Url} アクセス失敗)", url);
            break;
        }
        catch (JsonException ex)
        {
            this._logger.LogError(ex, "1時間天気データ取得失敗(JSON デシリアライズ失敗)");
            break;
        }
        catch (Exception ex)
        {
            this._logger.LogError(ex, "1時間天気データ取得失敗(不明なエラー)");
            break;
        }
    }

    return Enumerable.Empty<AmedasData>();
}

30分間隔以上の頻度でアクセスする場合は、サービス側でキャッシュを返すようにしています。

国民の祝日取得

これも政府が CSV で配信してくれているので、そいつを取ってきてます。
Shift-JIS なのが微妙に面倒で、 UTF-8文字コードを変換して扱うようにしています。

当然、ローカルに保存して半年ごとに更新するようにしてます。

初期化

遅延初期化をしています。
スタートアップに登録してますが、とりあえず PC の電源付けて放置……みたいなことをよくやるので、画面ロックが開けるまで初期化を遅らせたりしてます。

Initializer クラスを作って、 IEnumerable<IAsyncInitializable> を DI で突っ込んでもらって、 foreach で初期化メソッドを叩いてるだけです。
DI 登録の時に優先度とか初期化フラグ突っ込んで、ある程度柔軟に制御できるようにしてます。
詳しくは ...Models.Initialization 以下を見てください。

設定によっては起動時にサービスの初期化がスキップされることもあるので、あとから初期化できるようにしてるし、初期化されてない状態でデータ取得メソッド叩かれても問題ないようにしてます。

システムモニター

天気とかは API を叩きに行きますが、画面ロックやらしてるときは見えない(= 取得しても意味ない)ので叩かないようにしてます。
そこら辺を確認するためのロジックです。ネイティブをバチバチに叩いてます。

internal class SystemMonitorService : ISystemMonitorService, IDisposable
{
    private const int DEBOUNCE_INTERVAL = 500; // ミリ秒

    private readonly ILogger _logger;
    private readonly IEventAggregator _eventAggregator;

    private bool _disposedValue;

    private HwndSource _hwndSource;
    private IntPtr _hwnd;

    private int _lastPowerMode = -1;
    private int _lastSessionChange = -1;
    private DateTime _lastPowerModeChangeTime = DateTime.MinValue;
    private DateTime _lastSessionChangeTime = DateTime.MinValue;

    public SystemMonitorService(ILogger<SystemMonitorService> logger, IEventAggregator eventAggregator)
    {
        this._logger = logger;
        this._eventAggregator = eventAggregator;
    }

    public void Initialize()
    {
        var window = System.Windows.Application.Current.MainWindow ?? throw new InvalidOperationException("MainWindow が初期化されていません");

        if (SystemMonitor.IsInteractiveSessionActive())
        {
            this._logger.LogInformation("起動時セッション状態: アクティブ");
            this._eventAggregator.GetEvent<SessionActiveEvent>().Publish();
        }
        else
        {
            this._logger.LogInformation("起動時セッション状態: 非アクティブ");
        }

        this._hwnd = new WindowInteropHelper(window).Handle;
        this._hwndSource = HwndSource.FromHwnd(this._hwnd);
        this._hwndSource?.AddHook(this.WndProc);
        WTSRegisterSessionNotification(this._hwnd, NOTIFY_FOR_THIS_SESSION);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this._disposedValue)
        {
            if (disposing)
            {
                if (this._hwnd != IntPtr.Zero)
                {
                    WTSUnRegisterSessionNotification(this._hwnd);
                }

                if (this._hwndSource is not null)
                {
                    this._hwndSource.RemoveHook(this.WndProc);
                    this._hwndSource.Dispose();
                    this._hwndSource = null;
                }
            }

            this._disposedValue = true;
        }
    }

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        switch (msg)
        {
            case WM_POWERBROADCAST:
                this.HandlePowerModeChange((int)wParam);
                break;
            case WM_WTSSESSION_CHANGE:
                this.HandleSessionChange((int)wParam);
                break;
        }

        return IntPtr.Zero;
    }

    private void HandlePowerModeChange(int mode)
    {
        var now = DateTime.UtcNow;

        if (this._lastPowerMode == mode && (now - this._lastPowerModeChangeTime).TotalMilliseconds < DEBOUNCE_INTERVAL)
        {
            // 同じモードの変更が短時間で連続して発生した場合は無視
            this._logger.LogDebug("同じモードの変更が短時間で発生: {Mode}", mode);
            return;
        }

        this._lastPowerMode = mode;
        this._lastPowerModeChangeTime = now;

        switch (mode)
        {
            case PBT_APMSUSPEND:
                this._eventAggregator.GetEvent<SystemSuspnedEvent>().Publish();
                this._logger.LogDebug("システムサスペンド");
                break;
            case PBT_APMRESUME:
                this._eventAggregator.GetEvent<SystemResumeEvent>().Publish();
                this._logger.LogDebug("システムレジューム");
                break;
        }
    }

    private void HandleSessionChange(int changeType)
    {
        var now = DateTime.UtcNow;

        if (this._lastSessionChange == changeType && (now - this._lastSessionChangeTime).TotalMilliseconds < DEBOUNCE_INTERVAL)
        {
            // 同じセッション変更が短時間で連続して発生した場合は無視
            this._logger.LogDebug("同じセッション変更が短時間で発生: {ChangeType}", changeType);
            return;
        }

        this._lastSessionChange = changeType;
        this._lastSessionChangeTime = now;

        switch (changeType)
        {
            case WTS_SESSION_LOGON:
                this._eventAggregator.GetEvent<SessionLogonEvent>().Publish();
                this._logger.LogDebug("セッションログオン");
                break;
            case WTS_SESSION_LOGOFF:
                this._eventAggregator.GetEvent<SessionLogoffEvent>().Publish();
                this._logger.LogDebug("セッションログオフ");
                break;
            case WTS_SESSION_LOCK:
                this._eventAggregator.GetEvent<SessionLockEvent>().Publish();
                this._logger.LogDebug("セッションロック");
                break;
            case WTS_SESSION_UNLOCK:
                this._eventAggregator.GetEvent<SessionUnlockEvent>().Publish();
                this._logger.LogDebug("セッションアンロック");
                break;
        }
    }

    private const int WM_POWERBROADCAST = 0x0218;
    private const int PBT_APMSUSPEND = 0x0004;
    private const int PBT_APMRESUME = 0x0007;

    private const int WM_WTSSESSION_CHANGE = 0x02B1;
    private const int WTS_SESSION_LOGON = 0x0005;
    private const int WTS_SESSION_LOGOFF = 0x0006;
    private const int WTS_SESSION_LOCK = 0x0007;
    private const int WTS_SESSION_UNLOCK = 0x0008;

    private const int NOTIFY_FOR_THIS_SESSION = 0x0000;

    [DllImport("Wtsapi32.dll")]
    private static extern IntPtr WTSRegisterSessionNotification(IntPtr hWnd, int dwFlags);

    [DllImport("Wtsapi32.dll")]
    private static extern bool WTSUnRegisterSessionNotification(IntPtr hWnd);
}
public static class SystemMonitor
{
    public static bool IsInteractiveSessionActive()
    {
        int sessionId = WTSGetActiveConsoleSessionId();

        if (sessionId == -1) return false;

        return IsActive(sessionId) && TryGetUserName(sessionId, out _) && !IsLocked(sessionId);
    }

    private static bool IsActive(int sessionId)
    {
        if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WTS_CONNECT_STATE, out var buffer, out _))
        {
            var state = Marshal.ReadInt32(buffer);
            WTSFreeMemory(buffer);

            return state == WTS_ACTIVE;
        }

        return false;
    }

    private static bool TryGetUserName(int sessionId, out string result)
    {
        result = null;

        if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WTS_USER_NAME, out var buffer, out _))
        {
            result = Marshal.PtrToStringAnsi(buffer);
            WTSFreeMemory(buffer);

            return true;
        }

        return false;
    }

    private static bool IsLocked(int sessionId)
    {
        if (WinStationQueryInformationW(IntPtr.Zero, sessionId, WIN_STATION_LOCK_STATE, out var isLocked, sizeof(int), out _))
        {
            return isLocked != 0;
        }

        return false;
    }

    private const int WTS_USER_NAME = 5;
    private const int WTS_CONNECT_STATE = 8;

    private const int WTS_ACTIVE = 0;

    private const int WIN_STATION_LOCK_STATE = 28;

    [DllImport("kernel32.dll")]
    private static extern int WTSGetActiveConsoleSessionId();

    [DllImport("Wtsapi32.dll")]
    private static extern bool WTSQuerySessionInformation(IntPtr hServer, int sessionId, int infoClass, out IntPtr buffer, out uint bytesReturned);

    [DllImport("Wtsapi32.dll")]
    private static extern void WTSFreeMemory(IntPtr pointer);

    // 非推奨 API
    // https://learn.microsoft.com/en-us/previous-versions/aa383827(v=vs.85)
    // 使えるので使う
    [DllImport("winsta.dll", CharSet = CharSet.Unicode)]
    private static extern bool WinStationQueryInformationW(IntPtr hServer, int sessionId, int infoClass, out int buffer, int bufferLength, out int returnedLength);
}

普通ならロックからの復帰やらはネイティブ叩かなくても SystemEvents.SessionSwitch とかで取れますが、「ツールウィンドウにしてる」「ShowInTaskbar="False", WindowStyle="None" にしてる」あたりのせいでうまく取れないみたいなのでネイティブ叩いてます。

よくわからんけど二重発火とかするのでデバウンス処理を入れてます。

SystemMonitor.IsInteractiveSessionActive() は起動時に一回だけ実行されますが、起動時点でロック画面かどうかというのはわからん(ロックに入った/復帰とかはイベントでわかる)ので、非推奨 API まで使ってます。(起動時にアクティブなら初期化が走り、そうでないならスキップしてロックやらから復帰したときに走る)
完全にダメになったらどうしようもないですね……

ネットワーク接続ポリシー

設定で機能が切られてたり、ロックやらでネットワーク接続を一時停止してるとかを管理してます。

ネットワーク接続が必要なサービスはみんなこいつを参照して内部で制御してるので、サービスを使う側はネットワーク接続を気にせず叩けます。

ロックやらでアクセスしていいかどうかは SystemMonitorService が発火するイベントで切り替えてます。

internal class NetworkAccessibilityService : INetworkAccessibilityService
{
    public const long ACCESSIBLE = 1;
    public const long NOT_ACCESSIBLE = 0;

    private readonly ILogger _logger;

    private long _accessibility = ACCESSIBLE;

    public bool IsAccessible => Interlocked.Read(ref this._accessibility) == ACCESSIBLE;

    public NetworkAccessibilityService(ILogger<NetworkAccessibilityService> logger, IEventAggregator eventAggregator)
    {
        this._logger = logger;

        eventAggregator.GetEvent<SessionLogonEvent>()
            .Subscribe(() => this.SetAccessibility(true, NetworkAccessibilityChangeReason.Logon), ThreadOption.BackgroundThread);
        eventAggregator.GetEvent<SessionLogoffEvent>()
            .Subscribe(() => this.SetAccessibility(false, NetworkAccessibilityChangeReason.Logoff), ThreadOption.BackgroundThread);

        eventAggregator.GetEvent<SessionUnlockEvent>()
            .Subscribe(() => this.SetAccessibility(true, NetworkAccessibilityChangeReason.Unlock), ThreadOption.BackgroundThread);
        eventAggregator.GetEvent<SessionLockEvent>()
            .Subscribe(() => this.SetAccessibility(false, NetworkAccessibilityChangeReason.Lock), ThreadOption.BackgroundThread);

        eventAggregator.GetEvent<SystemResumeEvent>()
            .Subscribe(() => this.SetAccessibility(true, NetworkAccessibilityChangeReason.SystemResume), ThreadOption.BackgroundThread);
        eventAggregator.GetEvent<SystemSuspnedEvent>()
            .Subscribe(() => this.SetAccessibility(false, NetworkAccessibilityChangeReason.SystemSuspend), ThreadOption.BackgroundThread);
    }

    private void SetAccessibility(bool isAccessible, NetworkAccessibilityChangeReason reason = NetworkAccessibilityChangeReason.Unknown)
    {
        Interlocked.Exchange(ref this._accessibility, isAccessible ? ACCESSIBLE : NOT_ACCESSIBLE);
        this._logger.LogInformation("ネットワークアクセス: {State} ({Reason})", isAccessible ? "有効" : "無効", reason);
    }
}

リポジトリ

観測所一覧とか国民の祝日一覧とか、そうそう変化がないデータはローカルに保存してローカルから読み込むようにしてます。
それを XxxRepository というクラスで制御しています。

動きとしてはこんな感じです。

  1. API アクセスが必要か判断
  2. 必要なら API から取得
  3. 不要ならローカルから取得

ここら辺は全部 XxxRepository 内部で完結してるので、使う側は外部 API からなのかローカルからなのか意識せずに取得できます。

排他制御

ほとんどのサービスは非同期に動くし、イベント駆動であっちこっちから呼ばれたりするし、内部キャッシュ持ってたりするし、 IAsyncInitializable の初期化とか設定切り替えるたびに走るし、初期化前中後関係なくデータ取得メソッド叩かれるし、なるべくスレッドセーフになるように作りました。

AmedasService とかなかなかなことになってます。(SemaphoreSlimInterlocked の乱れ打ち)

ログ

最近のイケてる構造化ログが出せるらしい Serilog というのを使ってます。

……いや、ぶっちゃけこの程度のアプリに構造化ログなんか要りません。

Microsoft.Extensions.Logging との連携が手軽にとれるらしいので使いました。(ChatGPT 情報)

ロガーなんてなんでもいいんですが、パッケージに直接依存するの嫌じゃないですか。パッケージ変えたら全部のログメソッド書き換えとか地獄でしょ。
Microsoft.Extensions.Logging を間に挟んでおけば、ログメソッドは Microsoft.Extensions.Logging.ILogger.LogXxx で統一できて、 DI コンテナ登録の時に差し替えるだけで済むんですから使わない手はないでしょう。

LoggerScope

ChatGPT に教えてもらったテクニックです。

「とりあえずメソッドの入口出口にデバッグログ仕込んどくかなー」と思ったとき、至る所で早期リターンとかしてると、やってらんないじゃないですか。「早期リターンしてるところ全部に出口ログ仕込むの……?」って絶望するじゃないですか。

ソースコード見てるとこんなのがいっぱい書いてあります。

using var _ = new LoggerScope(this._logger);

中身はこんな実装になっています。

#if DEBUG

using System;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;

namespace ClockWidget.Logging
{
    public struct LoggerScope : IDisposable
    {
        private readonly ILogger _logger;
        private readonly string _methodName;

        public LoggerScope(ILogger logger, [CallerMemberName] string methodName = "")
        {
            this._logger = logger;
            this._methodName = methodName;

            this._logger.LogDebug("{MethodName} 実行", this._methodName);
        }

        public void Dispose()
        {
            this._logger.LogDebug("{MethodName} 終了", this._methodName);
        }
    }
}

#endif

IDisposable があるので、メソッド脱出の時に勝手に「<メソッド名> 終了」ってログを出してくれるわけです。便利。

でも当然、こんなのデバッグ以外では使わないので、リリースビルドでは中身のないものに差し替えます。

#if !DEBUG

using System;

namespace ClockWidget.Logging
{
    public struct LoggerScope : IDisposable
    {
        public LoggerScope(object _ = null) { }
        public void Dispose() { }
    }
}

#endif

デバッグの時はメソッドの入口出口で漏れなくログが出せる、リリースの時は空っぽでパフォーマンスに影響しない、なかなかのテクニックですよね。 ChatGPT すごい。

おわりに

いろいろあって、「何かつくる」ということが嫌になってたけど、どうしても欲しくて気が付いたら作ってました。

まあ、満足いくものはできたので、いいかなって感じです。

というか、 ChatGPT に投げたらしっかりしたレビューしてくれるし、 Copilot はちょっとコード書いたら「こういうの書きたいんやろ?」ってめちゃくちゃ補完してくれるし、「AI すげー……!」ってなりまくりましたね。

RGB MATRIX でインジケータをいい感じに制御するコード書いた

はじめに

RGB MATRIX の LED にインジケータの機能を持たせようとするとちょっと面倒だったので、いい感じに制御するコードを書きました。

公開場所

以下で公開しています。

https://github.com/koktoh/rgb_matrix_indicatorgithub.com

使い方

基本的に、上記のリポジトリにある example_xxxx 系のファイルを見れば何となくわかると思います。

ファイルの配置

<keyboard> ディレクトリに rgb_matrix_indicator をコピペします。

<keyboard>
│  config.h
│  keyboard.json
│  readme.md
│  rules.mk
│
├─keymaps
│  └─default
│          keymap.c
│
└─rgb_matrix_indicator
        rgb_matrix_indicator.c
        rgb_matrix_indicator.h

config.h の記述

RGB_INDICATOR_EEPROM_CONFIG_ADDR の設定

独自の設定を EEPROM に保存しているので、それ用の設定です。
これがないと動かないので、そのままコピペしてください。

#ifdef DYNAMIC_KEYMAP_ENABLE
#    ifdef VIA_ENABLE
#        define DYNAMIC_KEYMAP_EEPROM_ADDR (VIA_EEPROM_CONFIG_END)
#    else // VIA_ANABLE
#        define DYNAMIC_KEYMAP_EEPROM_ADDR (EECONFIG_SIZE)
#    endif // VIA_ENABLE

#    define DYNAMIC_KEYMAP_EEPROM_MAX_ADDR (TOTAL_EEPROM_BYTE_COUNT - 1)
#    define DYNAMIC_KEYMAP_LAYER_COUNT 4
#    define DYNAMIC_KEYMAP_ENCODER_EEPROM_ADDR (DYNAMIC_KEYMAP_EEPROM_ADDR + (DYNAMIC_KEYMAP_LAYER_COUNT * MATRIX_ROWS * MATRIX_COLS * 2))

#    ifdef ENCODER_MAP_ENABLE
#        define DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR (DYNAMIC_KEYMAP_ENCODER_EEPROM_ADDR + (DYNAMIC_KEYMAP_LAYER_COUNT * NUM_ENCODERS * 2 * 2))
#    else // ENCODER_MAP_ENABLE
#        define DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR (DYNAMIC_KEYMAP_ENCODER_EEPROM_ADDR)
#    endif // ENCODER_MAP_ENABLE

#    define DYNAMIC_KEYMAP_MACRO_EEPROM_SIZE (DYNAMIC_KEYMAP_EEPROM_MAX_ADDR - DYNAMIC_KEYMAP_MACRO_EEPROM_ADDR + 1)

#    define RGB_INDICATOR_EEPROM_CONFIG_ADDR (DYNAMIC_KEYMAP_MACRO_EEPROM_SIZE)
#else // DYNAMIC_KEYMAP_ENABLE
#    define RGB_INDICATOR_EEPROM_CONFIG_ADDR (EECONFIG_SIZE)
#endif // DYNAMIC_KEYMAP_ENABLE

LED の設定

以下のインジケータとして使用する LED のインデックスを設定します。

  • レイヤー
  • Num Lock
  • Caps Lock
  • Scroll Lock
  • Compose
  • Kana

define されない場合、各インジケータは表示されません。
インジケータとして不要であれば、 define しないでおけば OK です。

XXXX_INDICATOR_LED_INDEXXXXX_INDICATOR_LED_INDEXES のどちらかだけを define してください。

1つの LED をインジケータとして使用する

一番普通の使い方だと思います。

設定されたインデックスの LED が、そのインジケータとして使用されます。

#define LAYER_INDICATOR_LED_INDEX 0

#define NUMLOCK_INDICATOR_LED_INDEX 1
#define CAPSLOCK_INDICATOR_LED_INDEX 2
#define SCROLLLOCK_INDICATOR_LED_INDEX 3
#define COMPOSE_INDICATOR_LED_INDEX 4
#define KANA_INDICATOR_LED_INDEX 5

複数の LED をインジケータとして使用する

例えば、「10番目と20番目の LED を Num Lock のインジケータとして使いたい」というときに設定します。(需要があるのかはわからんけど……)
指定されたインデックスの LED は全部同じ色に光ります。
LED はいくつでも指定できます。

#define LAYER_INDICATOR_LED_INDEXES { 0, 1 }

#define NUMLOCK_INDICATOR_LED_INDEXES { 2, 3 }
#define CAPSLOCK_INDICATOR_LED_INDEXES { 4, 5, 6 }
#define SCROLLLOCK_INDICATOR_LED_INDEXES { 7, 8, 9, 10 }
#define COMPOSE_INDICATOR_LED_INDEXES { 11, 12 }
#define KANA_INDICATOR_LED_INDEXES { 13, 14 }

その他設定

普通の RGB MATRIX とかであるようなやつを揃えています。

#define LAYER_INDICATOR_BASE_COLOR HSV_CYAN // Default is HSV_RED
#define LAYER_INDICATOR_STEP 17             // Default is 25
#define INDICATOR_HUE_STEP 4                // Default is 8
#define INDICATOR_SAT_STEP 8                // Default is 17
#define INDICATOR_VAL_STEP 8                // Default is 17
#define INDICATOR_LIMIT_VAL 127             // Default is 255

keymap.c の記述

基本的に example_keymap.c を見てもらえればわかるかなと思います。

初期化

rgb_matrix_indicator を初期化します。
EEPROM に保存していた設定を読み込みます。

void keyboard_post_init_user(void) {
    rgb_matrix_indicator_init();
}

EEPROM リセット

EE_CLR とかで EEPROM がリセットされた時に、デフォルト値を設定します。
これを書いておかないと EEPROM がリセットされても、 rgb_matrix_indicator の設定がリセットされません。

void eeconfig_init_user(void) {
    eeconfig_init_rgb_matrix_indicator();
}

キーコードの処理

これを忘れると rgb_matrix_indicator 用のキーコードの処理がされないので気を付けてください。

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    /* ---- とりあえずここにこれを書いておけば OK ---- */
    bool p_rgb_indicator = process_record_rgb_matirx_indicator(keycode, record);
    if (!p_rgb_indicator) {
        return false;
    }
}

カスタムキーコードを定義する場合は RGB_INDICATOR_SAFE_RANGE を使用してください。

enum custom_keycodes {
    CUSTOM1 = RGB_INDICATOR_SAFE_RANGE,
    CUSTOM2,
    CUSTOM3,
    CUSTOM4,
}

.
.
.

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    /* ---- とりあえずここにこれを書いておけば OK ---- */
    bool p_rgb_indicator = process_record_rgb_matirx_indicator(keycode, record);
    if (!p_rgb_indicator) {
        return false;
    }

    /* ---- 別にカスタムキーコードを定義して使う場合は、ここから下に書く ---- */
    switch (keycode) {
        case CUSTOM1:
            if (record->event.pressed) {
                // Do something when pressed
            } else {
                // Do something else when release
            }
            return false;
        case CUSTOM2:
            if (record->event.pressed) {
                // Do something when pressed
            } else {
                // Do something else when release
            }
            return false;
        case CUSTOM3:
            if (record->event.pressed) {
                // Do something when pressed
            } else {
                // Do something else when release
            }
            return false;
        case CUSTOM4:
            if (record->event.pressed) {
                // Do something when pressed
            } else {
                // Do something else when release
            }
            return false;
        default:
            return true; // Process all other keycodes normally
    }
}

インジケータの処理

インジケータを更新して正しく光らせるための処理です。
これを忘れると正しく動きません。

簡単な使用

とりあえずこう書いておけば動きます。

bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {
    update_layer_indicator(get_highest_layer(layer_state | default_layer_state));
    update_indicators(host_keyboard_led_state());

    return false;
}

カスタマイズ

ちょっとカスタマイズしたいと思ったら、以下のように書いてください。

bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {
    /* ---- レイヤーインジケータのカスタマイズ ---- */
    switch (get_highest_layer(layer_state | default_layer_state)) {
        case _BASE:
            layer_indicator_set_hsv(HSV_CYAN);
            break;
        case _LAYER1:
            layer_indicator_set_hsv(HSV_MAGENTA);
            break;
        case _LAYER2:
            layer_indicator_set_hsv(HSV_ORANGE);
            break;
        case _LAYER3:
            layer_indicator_set_hsv(HSV_PURPLE);
            break;
        default:
            break;
    }

    /* ---- その他インジケータのカスタマイズ ---- */
    if (host_keyboard_led_state().num_lock) {
        numlock_indicator_set_hsv(HSV_AZURE);
    } else {
        numlock_indicator_off();
    }

    if (host_keyboard_led_state().caps_lock) {
        capslock_indicator_set_hsv(HSV_YELLOW);
    } else {
        capslock_indicator_off();
    }

    return false;
}

レイヤーは update_layer_indicator(uint8_t layer_state) を使って、他のインジケータは独自で実装する(逆も然り)とかも可能です。

rules.mk の記述

ソースを追加するだけです。

SRC += rgb_matrix_indicator/rgb_matrix_indicator.c

レイヤーインジケータについて

レイヤーのインジケータは少し特殊になっています。

config.h で以下のように設定しました。

#define LAYER_INDICATOR_BASE_COLOR HSV_CYAN // Default is HSV_RED
#define LAYER_INDICATOR_STEP 17             // Default is 25

LAYER_INDICATOR_BASE_COLOR は、レイヤー0番が選択されているときの LED の色です。
レイヤー1番になると、LAYER_INDICATOR_STEP の分だけ Hue に加算された色になります。(ウェブとかの HSV のカラーパレットとかでどんな感じか見てもらえれば)
2番ではさらに LAYER_INDICATOR_STEP の分だけ加算され……というように、自動で色が設定されます。

あまりにもレイヤーが多かったり、 LAYER_INDICATOR_STEP の値が小さいと、色が一周して違うレイヤーが同じ色になったり、レイヤーが変わっても色の変化が小さすぎてわかりにくくなったりします。

インジケータの処理カスタマイズ に書いたように、レイヤーごとに自分で色を設定することもできるので、好みによって変えてください。

キーコードについて

rgb_matrix_indicator では、RGB MATRIX に関係するキーコードの動作を変更しています

キーコード 変更前の動作 変更後の動作
RGB_TOG すべての LED が点灯 / 消灯する インジケータ以外の LED が点灯 / 消灯する
RGB_MOD から RGB_M_SW まで それぞれの動作 インジケータ以外の LED が消灯している場合、何も実行されない

また、独自のキーコードを定義しています。

キーコード エイリアス 説明
RGB_INDICATOR_HUE_UP RGB_IHI インジケータ LED の Hue を加算する
RGB_INDICATOR_HUE_DOWN RGB_IHD インジケータ LED の Hue を減算する
RGB_INDICATOR_SATURATION_UP RGB_ISI インジケータ LED の Saturation を加算する
RGB_INDICATOR_SATURATION_DOWN RGB_ISD インジケータ LED の Saturation を減算する
RGB_INDICATOR_VALUE_UP RGB_IVI インジケータ LED の Value (明るさ)を加算する
RGB_INDICATOR_VALUE_DOWN RGB_IVD インジケータ LED の Value (明るさ)を減算する

これらのキーコードによる操作は、レイヤー以外のすべてのインジケータ LED に反映されます。
つまり、 RGB_IHI を実行すると、 Num Lock も Caps Lock もすべて同じ色に変化します。

RGB_IVI / RGB_IVD はレイヤー含めたすべてのインジケータ LED に反映されます。

API

void rgb_matrix_indicator_init(void)

rgb_matrix_indicator を初期化する。

void eeconfig_init_rgb_matrix_indicator(void)

rgb_matirx_indicator の EEPROM 領域をリセットする。

void disable_rgb_effect(void)

インジケータ以外の LED を消灯する。

bool process_record_rgb_matirx_indicator(uint16_t keycode, keyrecord_t *record)

rgb_matrix_indicator で定義されたキーコードの処理。

void update_layer_indicator(uint8_t layer_state)

レイヤーインジケータ LED の表示を更新する。

void update_indicators(led_t led_state)

レイヤー以外の全インジケータ LED の表示を更新する。

void layer_indicator_set_hsv(uint8_t hue, uint8_t sat, uint8_t val)

レイヤーインジケータ LED を指定した色に設定する。
HSV_RED などをそのまま使用可能。
valINDICATOR_LIMIT_VAL を超える値が指定された場合、 INDICATOR_LIMIT_VAL の値が使用される。

void numlock_indicator_set_hsv(uint8_t hue, uint8_t sat, uint8_t val)

Num Lock インジケータ LED を指定した色に設定する。
HSV_RED などをそのまま使用可能。
valINDICATOR_LIMIT_VAL を超える値が指定された場合、 INDICATOR_LIMIT_VAL の値が使用される。

これを使用した場合、 RGB_IHI, RGB_IHD, RGB_ISI, RGB_ISD のキーコードによる変更は反映されない。
RGB_IVI, RGB_IVD のキーコードは反映される。

void numlock_indicator_off(void)

Num Lock インジケータ LED を消灯する。

void capslock_indicator_set_hsv(uint8_t hue, uint8_t sat, uint8_t val)

Caps Lock インジケータ LED を指定した色に設定する。
HSV_RED などをそのまま使用可能。
valINDICATOR_LIMIT_VAL を超える値が指定された場合、 INDICATOR_LIMIT_VAL の値が使用される。

これを使用した場合、 RGB_IHI, RGB_IHD, RGB_ISI, RGB_ISD のキーコードによる変更は反映されない。
RGB_IVI, RGB_IVD のキーコードは反映される。

void capslock_indicator_off(void)

Caps Lock インジケータ LED を消灯する。

void scrolllock_indicator_set_hsv(uint8_t hue, uint8_t sat, uint8_t val)

Scroll Lock インジケータ LED を指定した色に設定する。
HSV_RED などをそのまま使用可能。
valINDICATOR_LIMIT_VAL を超える値が指定された場合、 INDICATOR_LIMIT_VAL の値が使用される。

これを使用した場合、 RGB_IHI, RGB_IHD, RGB_ISI, RGB_ISD のキーコードによる変更は反映されない。
RGB_IVI, RGB_IVD のキーコードは反映される。

void scrolllock_indicator_off(void)

Scroll Lock インジケータ LED を消灯する。

void compose_indicator_set_hsv(uint8_t hue, uint8_t sat, uint8_t val)

Compose インジケータ LED を指定した色に設定する。
HSV_RED などをそのまま使用可能。
valINDICATOR_LIMIT_VAL を超える値が指定された場合、 INDICATOR_LIMIT_VAL の値が使用される。

これを使用した場合、 RGB_IHI, RGB_IHD, RGB_ISI, RGB_ISD のキーコードによる変更は反映されない。
RGB_IVI, RGB_IVD のキーコードは反映される。

void compose_indicator_off(void)

Compose インジケータ LED を消灯する。

void kana_indicator_set_hsv(uint8_t hue, uint8_t sat, uint8_t val)

Kana インジケータ LED を指定した色に設定する。
HSV_RED などをそのまま使用可能。
valINDICATOR_LIMIT_VAL を超える値が指定された場合、 INDICATOR_LIMIT_VAL の値が使用される。

これを使用した場合、 RGB_IHI, RGB_IHD, RGB_ISI, RGB_ISD のキーコードによる変更は反映されない。
RGB_IVI, RGB_IVD のキーコードは反映される。

void kana_indicator_off(void)

Kana インジケータ LED を消灯する。

VIA 対応について

一応、VIA で使用する EEPROM 領域は侵さないようになっていると思います。(RGB_INDICATOR_EEPROM_CONFIG_ADDR の設定

しかし、面倒でちゃんとは確認していないので、不具合等があれば、 issue 立てたり X(旧 Twitter) で突っ込んだりしてください。

また、独自キーコードを設定する場合は Any(n) などで頑張ってください。

おわりに

以上、 RGB MATRIX をいい感じにするコードを書いた報告とその解説でした。

どんな動きするかは実際使ってみて確かめてみてください。

不具合があったり、要望があったらさらに手を入れるかもしれません。
もちろん PR も大歓迎です。

QMK ファームウェア 2MB の壁を超える

はじめに

先日投稿した、「QMK は 2MB を超えるファームウェアを作れない」という問題が解決できたので、その方法を記していきます。

問題の詳しい内容はこちら。

koktoh.hatenablog.com

前提

RP2040 をターゲットとしてファームウェアをビルドする場合です。

RP2040 をターゲットとしてビルドするための基本的な設定などは、別途 公式ドキュメント などで確認してください。

解決法 その1

rules.mk に以下を追加するだけです。

EXTRALDFLAGS = -Wl,--defsym,FLASH_LEN=16m

FLASH_LEN=16m で、フラッシュサイズを 16MB に設定しています。
8MB にするなら FLASH_LEN=8m にするなど、環境に合わせて変更していけば OK です。

解決法 その2

リンカスクリプトを配置する方法です。

RP2040 をターゲットとしたビルドでは、 <keyboard>.elf を作るために qmk_firmware/platforms/chibios/boards/common/ld/RP2040_FLASH_TIMECRIT.ld を使用します。
これを qmk_firmware/keyboards/<keyboard>/ld 以下にコピペして、それを参照するようにする方法です。

必要なファイルの配置

qmk_firmware/platforms/chibios/boards/common/ld 以下にあるファイルから、次に挙げる2つのファイルを qmk_firmware/keyboards/<keyboard>/ld にコピペします。

  • RP2040_FLASH_TIMECRIT.ld
  • RP2040_rules_data_with_timecrit.ld

RP2040_rules_data_with_timecrit.ldRP2040_FLASH_TIMECRIT.ld から参照されるファイルで、一緒に入れておかないとビルドできないので、とりあえず入れておきます。
編集などはしません。

RP2040_FLASH_TIMECRIT.ld はわかりやすいようにリネームしておくといいでしょう。
今回は RP2040_FLASH_16MB.ld とします。

ディレクトリ構成としては以下のようになります。

<keyboard>
│  config.h
│  keyboard.json
│  readme.md
│  rules.mk
│
├─keymaps
│  └─default
│          keymap.c
│
└─ld
        RP2040_FLASH_16MB.ld
        RP2040_rules_data_with_timecrit.ld

ファイルの編集

RP2040_FLASH_16MB.ld は、以下の部分を編集します。

/*
 * RP2040 memory setup.
 */
MEMORY
{
    flash0 (rx) : org = 0x00000000, len = 16k   /* ROM                  */
    flash1 (rx) : org = 0x10000000, len = 16m   /* ここを編集 DEFINED(FLASH_LEN) ? FLASH_LEN : 2048k -> 16m */
    flash2 (rx) : org = 0x00000000, len = 0
    flash3 (rx) : org = 0x00000000, len = 0
    flash4 (rx) : org = 0x00000000, len = 0
    flash5 (rx) : org = 0x00000000, len = 0
    flash6 (rx) : org = 0x00000000, len = 0
    flash7 (rx) : org = 0x00000000, len = 0
    ram0   (wx) : org = 0x20000000, len = 256k  /* SRAM0 striped        */
    ram1   (wx) : org = 0x00000000, len = 256k  /* SRAM0 non striped    */
    ram2   (wx) : org = 0x00000000, len = 0
    ram3   (wx) : org = 0x00000000, len = 0
    ram4   (wx) : org = 0x20040000, len = 4k    /* SRAM4                */
    ram5   (wx) : org = 0x20041000, len = 4k    /* SRAM5                */
    ram6   (wx) : org = 0x00000000, len = 0
    ram7   (wx) : org = 0x20041f00, len = 256   /* SRAM5 boot           */
}

配置したリンカスクリプトを参照させる

rules.mk に以下を追加します。

MCU_LDSCRIPT = RP2040_FLASH_16MB

これで 16MB までのファームウェアがビルドできるようになりました。
8MB にするなら、 flash1 の値を 8m にするなど、環境によって変更してください。

解決法 その3 (非推奨)

ライブラリ側のリンカスクリプトを直接編集する方法です。

submodule に含まれているファイルを編集するので非推奨です。

この方法でもできるので一応書いておくだけです。

qmk_firmware/platforms/chibios/boards/common/ld/RP2040_FLASH_TIMECRIT.ld を直接編集します。

編集内容は解決法 その2 でやったことと同じです。

/*
 * RP2040 memory setup.
 */
MEMORY
{
    flash0 (rx) : org = 0x00000000, len = 16k   /* ROM                  */
    flash1 (rx) : org = 0x10000000, len = 16m   /* ここを編集 DEFINED(FLASH_LEN) ? FLASH_LEN : 2048k -> 16m */
    flash2 (rx) : org = 0x00000000, len = 0
    flash3 (rx) : org = 0x00000000, len = 0
    flash4 (rx) : org = 0x00000000, len = 0
    flash5 (rx) : org = 0x00000000, len = 0
    flash6 (rx) : org = 0x00000000, len = 0
    flash7 (rx) : org = 0x00000000, len = 0
    ram0   (wx) : org = 0x20000000, len = 256k  /* SRAM0 striped        */
    ram1   (wx) : org = 0x00000000, len = 256k  /* SRAM0 non striped    */
    ram2   (wx) : org = 0x00000000, len = 0
    ram3   (wx) : org = 0x00000000, len = 0
    ram4   (wx) : org = 0x20040000, len = 4k    /* SRAM4                */
    ram5   (wx) : org = 0x20041000, len = 4k    /* SRAM5                */
    ram6   (wx) : org = 0x00000000, len = 0
    ram7   (wx) : org = 0x20041f00, len = 256   /* SRAM5 boot           */
}

方法によってビルド成果物に違いがないか確認

解決法 その1解決法 その2 で、ビルドした <keyboard>.uf2 に変わりがないか確認しました。

以下のコマンドで md5 チェックサムを表示してくれるということなので、各解決法で確認しました。

make <keyboard>:<keymap>:check-md5

実際は以下のコマンドを実行しました。

make VERBOSE_LD_CMD=yes <keyboard>:<keymap>:check-md5 > log

解決法 その1 のチェックサム

リンカスクリプト関係のログだけ抜粋して示します。

Linking: .build/qp_test_default.elf                                                                 [33;01m[WARNINGS][0m
 | 
 | Using built-in specs.
 | Reading specs from h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib/nano.specs
 | rename spec link to nano_link
 | rename spec link_gcc_c_sequence to nano_link_gcc_c_sequence
 | rename spec cpp_unique_options to nano_cpp_unique_options
 | COLLECT_GCC=H:\QMK_MSYS\mingw64\bin\arm-none-eabi-gcc.exe
 | COLLECT_LTO_WRAPPER=h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/lto-wrapper.exe
 | Target: arm-none-eabi
 | Configured with: ../configure --build=x86_64-w64-mingw32 --host=x86_64-w64-mingw32 --prefix=/mingw64 --target=arm-none-eabi --with-native-system-header-dir=/mingw64/include --libexecdir=/mingw64/lib --enable-languages=c,c++ --enable-plugins --disable-decimal-float --disable-libffi --disable-libgomp --disable-libmudflap --disable-libquadmath --disable-libssp --disable-libstdcxx-pch --disable-nls --disable-shared --disable-threads --disable-tls --disable-libada --with-gnu-as --with-gnu-ld --with-system-zlib --with-newlib --with-headers=/mingw64/arm-none-eabi/include --with-python-dir=share/gcc-arm-none-eabi --with-gmp --with-mpfr --with-mpc --with-isl --with-libelf --enable-gnu-indirect_function --with-multilib-list=rmprofile --with-host-libstdcxx='-static-libgcc -Wl,-Bstatic,-lstdc++,-Bdynamic -lm' --enable-linker-plugin-flags='LDFLAGS=-static-libstdc++\ -static-libgcc\ -pipe\ -Wl,--stack,12582912' LDFLAGS='-pipe -Wl,--disable-dynamicbase'
 | Thread model: single
 | Supported LTO compression algorithms: zlib zstd
 | gcc version 12.2.0 (GCC) 
 | COMPILER_PATH=h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/;h:/qmk_msys/mingw64/bin/../lib/gcc/;h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/bin/
 | LIBRARY_PATH=h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/thumb/v6-m/nofp/;h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib/thumb/v6-m/nofp/;h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/;h:/qmk_msys/mingw64/bin/../lib/gcc/;h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib/
 | COLLECT_GCC_OPTIONS='-D' 'THUMB_PRESENT' '-D' 'THUMB_NO_INTERWORKING' '-D' 'PICO_NO_FPGA_CHECK' '-D' 'NDEBUG' '-fomit-frame-pointer' '-ffunction-sections' '-fdata-sections' '-fshort-wchar' '-fno-builtin-printf' '-ggdb' '-Os' '-Wall' '-Wstrict-prototypes' '-Werror' '-std=gnu11' '-fcommon' '-o' '.build/qp_test_default.elf' '-L./lib/chibios-contrib/os/common/startup/ARMCMx/compilers/GCC/ld' '-T' './platforms/chibios/boards/common/ld/RP2040_FLASH_TIMECRIT.ld' '-L./platforms/chibios/boards/common/ld' '-nostartfiles' '-specs=nano.specs' '-mcpu=cortex-m0plus' '-mthumb' '-D' 'THUMB_PRESENT' '-mno-thumb-interwork' '-D' 'THUMB_NO_INTERWORKING' '-mno-unaligned-access' '-v' '-L./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/ld' '-mfloat-abi=soft' '-mlibarch=armv6s-m' '-march=armv6s-m' '-dumpdir' '.build/qp_test_default.elf.'
 |  h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/collect2.exe -plugin h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/liblto_plugin.dll -plugin-opt=h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/lto-wrapper.exe -plugin-opt=-fresolution=H:\QMK_MSYS\tmp\ccHG8A3N.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lg_nano -plugin-opt=-pass-through=-lc_nano -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc_nano -X -o .build/qp_test_default.elf -L./lib/chibios-contrib/os/common/startup/ARMCMx/compilers/GCC/ld -L./platforms/chibios/boards/common/ld -L./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/ld -Lh:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/thumb/v6-m/nofp -Lh:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib/thumb/v6-m/nofp -Lh:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0 -Lh:/qmk_msys/mingw64/bin/../lib/gcc -Lh:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib --wrap=__aeabi_idiv --wrap=__aeabi_idivmod --wrap=__aeabi_ldivmod --wrap=__aeabi_uidiv --wrap=__aeabi_uidivmod --wrap=__aeabi_uldivmod --wrap=__aeabi_lmul .build/obj_qp_test_default/lcd/lcd_helper.o .build/obj_qp_test_default/gfx/fonts/noto.qff.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0002.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0005.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0009.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0011.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0013.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0015.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0023.qgf.o .build/obj_qp_test_default/keyboards/qp_test/qp_test.o .build/obj_qp_test_default/.build/obj_qp_test_default/src/default_keyboard.o .build/obj_qp_test_default/quantum/keymap_introspection.o .build/obj_qp_test_default/quantum/quantum.o .build/obj_qp_test_default/quantum/bitwise.o .build/obj_qp_test_default/quantum/led.o .build/obj_qp_test_default/quantum/action.o .build/obj_qp_test_default/quantum/action_layer.o .build/obj_qp_test_default/quantum/action_tapping.o .build/obj_qp_test_default/quantum/action_util.o .build/obj_qp_test_default/quantum/eeconfig.o .build/obj_qp_test_default/quantum/keyboard.o .build/obj_qp_test_default/quantum/keymap_common.o .build/obj_qp_test_default/quantum/keycode_config.o .build/obj_qp_test_default/quantum/sync_timer.o .build/obj_qp_test_default/quantum/logging/debug.o .build/obj_qp_test_default/quantum/logging/sendchar.o .build/obj_qp_test_default/quantum/logging/print.o .build/obj_qp_test_default/quantum/matrix_common.o .build/obj_qp_test_default/quantum/matrix.o .build/obj_qp_test_default/quantum/debounce/sym_defer_g.o .build/obj_qp_test_default/quantum/main.o .build/obj_qp_test_default/printf.o .build/obj_qp_test_default/quantum/unicode/utf8.o .build/obj_qp_test_default/quantum/color.o .build/obj_qp_test_default/quantum/painter/qp.o .build/obj_qp_test_default/quantum/painter/qp_internal.o .build/obj_qp_test_default/quantum/painter/qp_stream.o .build/obj_qp_test_default/quantum/painter/qgf.o .build/obj_qp_test_default/quantum/painter/qff.o .build/obj_qp_test_default/quantum/painter/qp_draw_core.o .build/obj_qp_test_default/quantum/painter/qp_draw_codec.o .build/obj_qp_test_default/quantum/painter/qp_draw_circle.o .build/obj_qp_test_default/quantum/painter/qp_draw_ellipse.o .build/obj_qp_test_default/quantum/painter/qp_draw_image.o .build/obj_qp_test_default/quantum/painter/qp_draw_text.o .build/obj_qp_test_default/drivers/painter/tft_panel/qp_tft_panel.o .build/obj_qp_test_default/drivers/painter/st77xx/qp_st7735.o .build/obj_qp_test_default/quantum/painter/qp_comms.o .build/obj_qp_test_default/drivers/painter/comms/qp_comms_spi.o .build/obj_qp_test_default/eeprom_driver.o .build/obj_qp_test_default/eeprom_wear_leveling.o .build/obj_qp_test_default/wear_leveling.o .build/obj_qp_test_default/wear_leveling_rp2040_flash.o .build/obj_qp_test_default/quantum/rgb_matrix/rgb_matrix.o .build/obj_qp_test_default/quantum/rgb_matrix/rgb_matrix_drivers.o .build/obj_qp_test_default/quantum/process_keycode/process_rgb.o .build/obj_qp_test_default/quantum/led_tables.o .build/obj_qp_test_default/qmk_fnv_type_validation.o .build/obj_qp_test_default/hash_32a.o .build/obj_qp_test_default/hash_64a.o .build/obj_qp_test_default/lib/lib8tion/lib8tion.o .build/obj_qp_test_default/ws2812_vendor.o .build/obj_qp_test_default/quantum/bootmagic/bootmagic.o .build/obj_qp_test_default/quantum/deferred_exec.o .build/obj_qp_test_default/quantum/process_keycode/process_grave_esc.o .build/obj_qp_test_default/quantum/process_keycode/process_magic.o .build/obj_qp_test_default/quantum/mousekey.o .build/obj_qp_test_default/quantum/send_string/send_string.o .build/obj_qp_test_default/quantum/process_keycode/process_space_cadet.o .build/obj_qp_test_default/protocol/host.o .build/obj_qp_test_default/protocol/report.o .build/obj_qp_test_default/protocol/usb_device_state.o .build/obj_qp_test_default/protocol/usb_util.o .build/obj_qp_test_default/platforms/suspend.o .build/obj_qp_test_default/platforms/synchronization_util.o .build/obj_qp_test_default/platforms/timer.o .build/obj_qp_test_default/platforms/chibios/hardware_id.o .build/obj_qp_test_default/platforms/chibios/platform.o .build/obj_qp_test_default/platforms/chibios/suspend.o .build/obj_qp_test_default/platforms/chibios/timer.o .build/obj_qp_test_default/platforms/chibios/bootloaders/rp2040.o .build/obj_qp_test_default/spi_master.a .build/obj_qp_test_default/./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/crt0_v6m.o .build/obj_qp_test_default/./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/vectors.o .build/obj_qp_test_default/./lib/chibios/os/common/ports/ARMv6-M-RP2/compilers/GCC/chcoreasm.o .build/obj_qp_test_default/protocol/chibios/usb_main.o .build/obj_qp_test_default/protocol/chibios/chibios.o .build/obj_qp_test_default/usb_descriptor.o .build/obj_qp_test_default/protocol/chibios/usb_driver.o .build/obj_qp_test_default/protocol/chibios/usb_endpoints.o .build/obj_qp_test_default/protocol/chibios/usb_report_handling.o .build/obj_qp_test_default/protocol/chibios/usb_util.o .build/obj_qp_test_default/lib/chibios/os/common/startup/ARMCMx/compilers/GCC/crt1.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chsys.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chrfcu.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chdebug.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chtrace.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chvt.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chschd.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chinstances.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chthreads.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chtm.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chstats.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chregistry.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chsem.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chmtx.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chcond.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chevents.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chmsg.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chdynamic.o .build/obj_qp_test_default/lib/chibios/os/common/ports/ARMv6-M-RP2/chcore.o .build/obj_qp_test_default/lib/chibios/os/hal/osal/rt-nil/osal.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chmboxes.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chmemcore.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chmemheaps.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chmempools.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chpipes.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chobjcaches.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chdelegates.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chfactory.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_st.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_buffers.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_queues.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_flash.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_mmcsd.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_adc.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_can.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_crypto.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_dac.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_efl.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_gpt.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_i2c.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_i2s.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_icu.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_mac.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_mmc_spi.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_pal.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_pwm.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_rtc.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_sdc.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_serial.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_serial_usb.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_sio.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_spi.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_trng.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_uart.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_usb.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_wdg.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_wspi.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/common/ARMCMx/nvic.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/RP2040/rp_isr.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/RP2040/hal_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/DMAv1/rp_dma.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/GPIOv1/hal_pal_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/SPIv1/hal_spi_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/TIMERv1/hal_st_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/UARTv1/hal_sio_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/RTCv1/hal_rtc_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/WDGv1/hal_wdg_lld.o .build/obj_qp_test_default/lib/chibios-contrib/os/hal/ports/RP/LLD/I2Cv1/hal_i2c_lld.o .build/obj_qp_test_default/lib/chibios-contrib/os/hal/ports/RP/LLD/PWMv1/hal_pwm_lld.o .build/obj_qp_test_default/lib/chibios-contrib/os/hal/ports/RP/LLD/ADCv1/hal_adc_lld.o .build/obj_qp_test_default/lib/chibios-contrib/os/hal/ports/RP/LLD/USBDv1/hal_usb_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/boards/RP_PICO_RP2040/board.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/chprintf.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/chscanf.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/memstreams.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/nullstreams.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/bufstreams.o .build/obj_qp_test_default/lib/chibios/os/various/syscalls.o .build/obj_qp_test_default/platforms/chibios/syscall-fallbacks.o .build/obj_qp_test_default/platforms/chibios/wait.o .build/obj_qp_test_default/platforms/chibios/synchronization_util.o .build/obj_qp_test_default/platforms/chibios/interrupt_handlers.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_clocks/clocks.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_pll/pll.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_pio/pio.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_timer/timer.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_flash/flash.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_gpio/gpio.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_claim/claim.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_watchdog/watchdog.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_xosc/xosc.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/pico_bootrom/bootrom.o .build/obj_qp_test_default/platforms/chibios/vendors/RP/stage2_bootloaders.o .build/obj_qp_test_default/platforms/chibios/vendors/RP/pico_sdk_shims.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/pico_divider/divider.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/pico_int64_ops/pico_int64_ops_aeabi.o --gc-sections --no-warn-rwx-segments --defsym=__process_stack_size__=0x800 --defsym=__main_stack_size__=0x400 --no-wchar-size-warning -Map=.build/qp_test_default.map --cref -lm --defsym FLASH_LEN=16m --start-group -lgcc -lg_nano -lc_nano --end-group --start-group -lgcc -lc_nano --end-group -T ./platforms/chibios/boards/common/ld/RP2040_FLASH_TIMECRIT.ld
 | COLLECT_GCC_OPTIONS='-D' 'THUMB_PRESENT' '-D' 'THUMB_NO_INTERWORKING' '-D' 'PICO_NO_FPGA_CHECK' '-D' 'NDEBUG' '-fomit-frame-pointer' '-ffunction-sections' '-fdata-sections' '-fshort-wchar' '-fno-builtin-printf' '-ggdb' '-Os' '-Wall' '-Wstrict-prototypes' '-Werror' '-std=gnu11' '-fcommon' '-o' '.build/qp_test_default.elf' '-L./lib/chibios-contrib/os/common/startup/ARMCMx/compilers/GCC/ld' '-T' './platforms/chibios/boards/common/ld/RP2040_FLASH_TIMECRIT.ld' '-L./platforms/chibios/boards/common/ld' '-nostartfiles' '-specs=nano.specs' '-mcpu=cortex-m0plus' '-mthumb' '-D' 'THUMB_PRESENT' '-mno-thumb-interwork' '-D' 'THUMB_NO_INTERWORKING' '-mno-unaligned-access' '-v' '-L./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/ld' '-mfloat-abi=soft' '-mlibarch=armv6s-m' '-march=armv6s-m' '-dumpdir' '.build/qp_test_default.elf.'
 | 
Creating UF2 file for deployment: .build/qp_test_default.uf2                                        [32;01m[OK][0m
Copying qp_test_default.uf2 to qmk_firmware folder                                                  [32;01m[OK][0m
0139dd971ba59f3d03699d2c49f43077 *.build/qp_test_default.uf2

チェックサムの値は以下です。

0139dd971ba59f3d03699d2c49f43077

解決法 その2 のチェックサム

リンカスクリプト関係のログだけ抜粋して示します。

Linking: .build/qp_test_default.elf                                                                 [33;01m[WARNINGS][0m
 | 
 | Using built-in specs.
 | Reading specs from h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib/nano.specs
 | rename spec link to nano_link
 | rename spec link_gcc_c_sequence to nano_link_gcc_c_sequence
 | rename spec cpp_unique_options to nano_cpp_unique_options
 | COLLECT_GCC=H:\QMK_MSYS\mingw64\bin\arm-none-eabi-gcc.exe
 | COLLECT_LTO_WRAPPER=h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/lto-wrapper.exe
 | Target: arm-none-eabi
 | Configured with: ../configure --build=x86_64-w64-mingw32 --host=x86_64-w64-mingw32 --prefix=/mingw64 --target=arm-none-eabi --with-native-system-header-dir=/mingw64/include --libexecdir=/mingw64/lib --enable-languages=c,c++ --enable-plugins --disable-decimal-float --disable-libffi --disable-libgomp --disable-libmudflap --disable-libquadmath --disable-libssp --disable-libstdcxx-pch --disable-nls --disable-shared --disable-threads --disable-tls --disable-libada --with-gnu-as --with-gnu-ld --with-system-zlib --with-newlib --with-headers=/mingw64/arm-none-eabi/include --with-python-dir=share/gcc-arm-none-eabi --with-gmp --with-mpfr --with-mpc --with-isl --with-libelf --enable-gnu-indirect_function --with-multilib-list=rmprofile --with-host-libstdcxx='-static-libgcc -Wl,-Bstatic,-lstdc++,-Bdynamic -lm' --enable-linker-plugin-flags='LDFLAGS=-static-libstdc++\ -static-libgcc\ -pipe\ -Wl,--stack,12582912' LDFLAGS='-pipe -Wl,--disable-dynamicbase'
 | Thread model: single
 | Supported LTO compression algorithms: zlib zstd
 | gcc version 12.2.0 (GCC) 
 | COMPILER_PATH=h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/;h:/qmk_msys/mingw64/bin/../lib/gcc/;h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/bin/
 | LIBRARY_PATH=h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/thumb/v6-m/nofp/;h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib/thumb/v6-m/nofp/;h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/;h:/qmk_msys/mingw64/bin/../lib/gcc/;h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib/
 | COLLECT_GCC_OPTIONS='-D' 'THUMB_PRESENT' '-D' 'THUMB_NO_INTERWORKING' '-D' 'PICO_NO_FPGA_CHECK' '-D' 'NDEBUG' '-fomit-frame-pointer' '-ffunction-sections' '-fdata-sections' '-fshort-wchar' '-fno-builtin-printf' '-ggdb' '-Os' '-Wall' '-Wstrict-prototypes' '-Werror' '-std=gnu11' '-fcommon' '-o' '.build/qp_test_default.elf' '-L./lib/chibios-contrib/os/common/startup/ARMCMx/compilers/GCC/ld' '-T' 'keyboards/qp_test/ld/RP2040_FLASH_16MB.ld' '-Lkeyboards/qp_test/ld' '-nostartfiles' '-specs=nano.specs' '-mcpu=cortex-m0plus' '-mthumb' '-D' 'THUMB_PRESENT' '-mno-thumb-interwork' '-D' 'THUMB_NO_INTERWORKING' '-mno-unaligned-access' '-v' '-L./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/ld' '-mfloat-abi=soft' '-mlibarch=armv6s-m' '-march=armv6s-m' '-dumpdir' '.build/qp_test_default.elf.'
 |  h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/collect2.exe -plugin h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/liblto_plugin.dll -plugin-opt=h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/lto-wrapper.exe -plugin-opt=-fresolution=H:\QMK_MSYS\tmp\ccsCZDv8.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lg_nano -plugin-opt=-pass-through=-lc_nano -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc_nano -X -o .build/qp_test_default.elf -L./lib/chibios-contrib/os/common/startup/ARMCMx/compilers/GCC/ld -Lkeyboards/qp_test/ld -L./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/ld -Lh:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/thumb/v6-m/nofp -Lh:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib/thumb/v6-m/nofp -Lh:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0 -Lh:/qmk_msys/mingw64/bin/../lib/gcc -Lh:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/lib --wrap=__aeabi_idiv --wrap=__aeabi_idivmod --wrap=__aeabi_ldivmod --wrap=__aeabi_uidiv --wrap=__aeabi_uidivmod --wrap=__aeabi_uldivmod --wrap=__aeabi_lmul .build/obj_qp_test_default/lcd/lcd_helper.o .build/obj_qp_test_default/gfx/fonts/noto.qff.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0002.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0005.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0009.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0011.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0013.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0015.qgf.o .build/obj_qp_test_default/gfx/images/rabbit_hole_0023.qgf.o .build/obj_qp_test_default/keyboards/qp_test/qp_test.o .build/obj_qp_test_default/.build/obj_qp_test_default/src/default_keyboard.o .build/obj_qp_test_default/quantum/keymap_introspection.o .build/obj_qp_test_default/quantum/quantum.o .build/obj_qp_test_default/quantum/bitwise.o .build/obj_qp_test_default/quantum/led.o .build/obj_qp_test_default/quantum/action.o .build/obj_qp_test_default/quantum/action_layer.o .build/obj_qp_test_default/quantum/action_tapping.o .build/obj_qp_test_default/quantum/action_util.o .build/obj_qp_test_default/quantum/eeconfig.o .build/obj_qp_test_default/quantum/keyboard.o .build/obj_qp_test_default/quantum/keymap_common.o .build/obj_qp_test_default/quantum/keycode_config.o .build/obj_qp_test_default/quantum/sync_timer.o .build/obj_qp_test_default/quantum/logging/debug.o .build/obj_qp_test_default/quantum/logging/sendchar.o .build/obj_qp_test_default/quantum/logging/print.o .build/obj_qp_test_default/quantum/matrix_common.o .build/obj_qp_test_default/quantum/matrix.o .build/obj_qp_test_default/quantum/debounce/sym_defer_g.o .build/obj_qp_test_default/quantum/main.o .build/obj_qp_test_default/printf.o .build/obj_qp_test_default/quantum/unicode/utf8.o .build/obj_qp_test_default/quantum/color.o .build/obj_qp_test_default/quantum/painter/qp.o .build/obj_qp_test_default/quantum/painter/qp_internal.o .build/obj_qp_test_default/quantum/painter/qp_stream.o .build/obj_qp_test_default/quantum/painter/qgf.o .build/obj_qp_test_default/quantum/painter/qff.o .build/obj_qp_test_default/quantum/painter/qp_draw_core.o .build/obj_qp_test_default/quantum/painter/qp_draw_codec.o .build/obj_qp_test_default/quantum/painter/qp_draw_circle.o .build/obj_qp_test_default/quantum/painter/qp_draw_ellipse.o .build/obj_qp_test_default/quantum/painter/qp_draw_image.o .build/obj_qp_test_default/quantum/painter/qp_draw_text.o .build/obj_qp_test_default/drivers/painter/tft_panel/qp_tft_panel.o .build/obj_qp_test_default/drivers/painter/st77xx/qp_st7735.o .build/obj_qp_test_default/quantum/painter/qp_comms.o .build/obj_qp_test_default/drivers/painter/comms/qp_comms_spi.o .build/obj_qp_test_default/eeprom_driver.o .build/obj_qp_test_default/eeprom_wear_leveling.o .build/obj_qp_test_default/wear_leveling.o .build/obj_qp_test_default/wear_leveling_rp2040_flash.o .build/obj_qp_test_default/quantum/rgb_matrix/rgb_matrix.o .build/obj_qp_test_default/quantum/rgb_matrix/rgb_matrix_drivers.o .build/obj_qp_test_default/quantum/process_keycode/process_rgb.o .build/obj_qp_test_default/quantum/led_tables.o .build/obj_qp_test_default/qmk_fnv_type_validation.o .build/obj_qp_test_default/hash_32a.o .build/obj_qp_test_default/hash_64a.o .build/obj_qp_test_default/lib/lib8tion/lib8tion.o .build/obj_qp_test_default/ws2812_vendor.o .build/obj_qp_test_default/quantum/bootmagic/bootmagic.o .build/obj_qp_test_default/quantum/deferred_exec.o .build/obj_qp_test_default/quantum/process_keycode/process_grave_esc.o .build/obj_qp_test_default/quantum/process_keycode/process_magic.o .build/obj_qp_test_default/quantum/mousekey.o .build/obj_qp_test_default/quantum/send_string/send_string.o .build/obj_qp_test_default/quantum/process_keycode/process_space_cadet.o .build/obj_qp_test_default/protocol/host.o .build/obj_qp_test_default/protocol/report.o .build/obj_qp_test_default/protocol/usb_device_state.o .build/obj_qp_test_default/protocol/usb_util.o .build/obj_qp_test_default/platforms/suspend.o .build/obj_qp_test_default/platforms/synchronization_util.o .build/obj_qp_test_default/platforms/timer.o .build/obj_qp_test_default/platforms/chibios/hardware_id.o .build/obj_qp_test_default/platforms/chibios/platform.o .build/obj_qp_test_default/platforms/chibios/suspend.o .build/obj_qp_test_default/platforms/chibios/timer.o .build/obj_qp_test_default/platforms/chibios/bootloaders/rp2040.o .build/obj_qp_test_default/spi_master.a .build/obj_qp_test_default/./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/crt0_v6m.o .build/obj_qp_test_default/./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/vectors.o .build/obj_qp_test_default/./lib/chibios/os/common/ports/ARMv6-M-RP2/compilers/GCC/chcoreasm.o .build/obj_qp_test_default/protocol/chibios/usb_main.o .build/obj_qp_test_default/protocol/chibios/chibios.o .build/obj_qp_test_default/usb_descriptor.o .build/obj_qp_test_default/protocol/chibios/usb_driver.o .build/obj_qp_test_default/protocol/chibios/usb_endpoints.o .build/obj_qp_test_default/protocol/chibios/usb_report_handling.o .build/obj_qp_test_default/protocol/chibios/usb_util.o .build/obj_qp_test_default/lib/chibios/os/common/startup/ARMCMx/compilers/GCC/crt1.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chsys.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chrfcu.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chdebug.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chtrace.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chvt.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chschd.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chinstances.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chthreads.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chtm.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chstats.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chregistry.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chsem.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chmtx.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chcond.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chevents.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chmsg.o .build/obj_qp_test_default/lib/chibios/os/rt/src/chdynamic.o .build/obj_qp_test_default/lib/chibios/os/common/ports/ARMv6-M-RP2/chcore.o .build/obj_qp_test_default/lib/chibios/os/hal/osal/rt-nil/osal.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chmboxes.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chmemcore.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chmemheaps.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chmempools.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chpipes.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chobjcaches.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chdelegates.o .build/obj_qp_test_default/lib/chibios/os/oslib/src/chfactory.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_st.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_buffers.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_queues.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_flash.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_mmcsd.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_adc.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_can.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_crypto.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_dac.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_efl.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_gpt.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_i2c.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_i2s.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_icu.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_mac.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_mmc_spi.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_pal.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_pwm.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_rtc.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_sdc.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_serial.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_serial_usb.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_sio.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_spi.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_trng.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_uart.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_usb.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_wdg.o .build/obj_qp_test_default/lib/chibios/os/hal/src/hal_wspi.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/common/ARMCMx/nvic.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/RP2040/rp_isr.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/RP2040/hal_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/DMAv1/rp_dma.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/GPIOv1/hal_pal_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/SPIv1/hal_spi_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/TIMERv1/hal_st_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/UARTv1/hal_sio_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/RTCv1/hal_rtc_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/ports/RP/LLD/WDGv1/hal_wdg_lld.o .build/obj_qp_test_default/lib/chibios-contrib/os/hal/ports/RP/LLD/I2Cv1/hal_i2c_lld.o .build/obj_qp_test_default/lib/chibios-contrib/os/hal/ports/RP/LLD/PWMv1/hal_pwm_lld.o .build/obj_qp_test_default/lib/chibios-contrib/os/hal/ports/RP/LLD/ADCv1/hal_adc_lld.o .build/obj_qp_test_default/lib/chibios-contrib/os/hal/ports/RP/LLD/USBDv1/hal_usb_lld.o .build/obj_qp_test_default/lib/chibios/os/hal/boards/RP_PICO_RP2040/board.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/chprintf.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/chscanf.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/memstreams.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/nullstreams.o .build/obj_qp_test_default/lib/chibios/os/hal/lib/streams/bufstreams.o .build/obj_qp_test_default/lib/chibios/os/various/syscalls.o .build/obj_qp_test_default/platforms/chibios/syscall-fallbacks.o .build/obj_qp_test_default/platforms/chibios/wait.o .build/obj_qp_test_default/platforms/chibios/synchronization_util.o .build/obj_qp_test_default/platforms/chibios/interrupt_handlers.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_clocks/clocks.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_pll/pll.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_pio/pio.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_timer/timer.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_flash/flash.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_gpio/gpio.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_claim/claim.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_watchdog/watchdog.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/hardware_xosc/xosc.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/pico_bootrom/bootrom.o .build/obj_qp_test_default/platforms/chibios/vendors/RP/stage2_bootloaders.o .build/obj_qp_test_default/platforms/chibios/vendors/RP/pico_sdk_shims.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/pico_divider/divider.o .build/obj_qp_test_default/./lib/pico-sdk/src/rp2_common/pico_int64_ops/pico_int64_ops_aeabi.o --gc-sections --no-warn-rwx-segments --defsym=__process_stack_size__=0x800 --defsym=__main_stack_size__=0x400 --no-wchar-size-warning -Map=.build/qp_test_default.map --cref -lm --start-group -lgcc -lg_nano -lc_nano --end-group --start-group -lgcc -lc_nano --end-group -T keyboards/qp_test/ld/RP2040_FLASH_16MB.ld
 | COLLECT_GCC_OPTIONS='-D' 'THUMB_PRESENT' '-D' 'THUMB_NO_INTERWORKING' '-D' 'PICO_NO_FPGA_CHECK' '-D' 'NDEBUG' '-fomit-frame-pointer' '-ffunction-sections' '-fdata-sections' '-fshort-wchar' '-fno-builtin-printf' '-ggdb' '-Os' '-Wall' '-Wstrict-prototypes' '-Werror' '-std=gnu11' '-fcommon' '-o' '.build/qp_test_default.elf' '-L./lib/chibios-contrib/os/common/startup/ARMCMx/compilers/GCC/ld' '-T' 'keyboards/qp_test/ld/RP2040_FLASH_16MB.ld' '-Lkeyboards/qp_test/ld' '-nostartfiles' '-specs=nano.specs' '-mcpu=cortex-m0plus' '-mthumb' '-D' 'THUMB_PRESENT' '-mno-thumb-interwork' '-D' 'THUMB_NO_INTERWORKING' '-mno-unaligned-access' '-v' '-L./lib/chibios/os/common/startup/ARMCMx/compilers/GCC/ld' '-mfloat-abi=soft' '-mlibarch=armv6s-m' '-march=armv6s-m' '-dumpdir' '.build/qp_test_default.elf.'
 | 
Creating UF2 file for deployment: .build/qp_test_default.uf2                                        [32;01m[OK][0m
Copying qp_test_default.uf2 to qmk_firmware folder                                                  [32;01m[OK][0m
0139dd971ba59f3d03699d2c49f43077 *.build/qp_test_default.uf2

チェックサムの値は以下です。

0139dd971ba59f3d03699d2c49f43077

比較

解決法 その1

0139dd971ba59f3d03699d2c49f43077

解決法 その2

0139dd971ba59f3d03699d2c49f43077

どちらも同じ、つまり、解決法 その1その2 でビルド成果物に変化がないことがわかりました。

ちなみに、 解決法 その3チェックサムも同一でした。

0139dd971ba59f3d03699d2c49f43077

Tips

ビルドの詳細を表示する

make VERBOSE=true <keyboard>:<keymap>

ld コマンドの詳細を表示する

make VERBOSE_LD_CMD=yes <keyboard>:<keymap>

画面がログで埋まるので、 > log などでファイルにリダイレクトした方がいい。(検索性も上がる)

make コマンドで有効な引数など一覧

docs.qmk.fm

おわりに

無事に 2MB の壁を越えてファームウェアがビルドできるようになりました。
これで Quantum Painter でアニメーション GIF を表示しまくれます。やったぜ!

そもそも #define PICO_FLASH_SIZE_BYTES (16 * 1024 * 1024) とかしてるんだから、それ見ていい感じにしてくれたらいいのになぁとか思ったり……

謝辞

今回の問題解決にあたり、 Self-Made Keyboard in Japan のたくさんの方々に助言をいただきました。
ここに、感謝を申し上げます。

QMK では 2MB を超えるファームは作れない?

はじめに

QMK で遊んでいたところ、 2MB を超えるファームウェアはビルドできないっぽい現象にぶち当たりました。

何をどうすれば解決できるのか全然わからないので、有識者に教えて欲しいです……

背景

Quantum Painter

最近、 QMK でフルカラー液晶に画像やら表示できる機能、 Quantum Painter というものがあることを知りました。

docs.qmk.fm

アニメーション GIF やらを液晶に表示して無限ループさせたり楽しい機能です。

Quantum Painter では PNG や GIF を QMK Graphics Format(以下、 QGF)というフォーマットに変換して、ソースコードに含めてコンパイル、ビルドを行います。
QGF に変換するコマンドがあり、 <src_file_name>.qgf.h, <src_file_name>.qgf.c というファイルが生成され、そいつを突っ込んでコンパイルします。

できた <src_file_name>.qgf.h/c 自体はバイナリを HEX の配列で書いてるようなものなのでバカでかいですが、コンパイルすればデコードされるので、思ったよりも小さくなります。

ここでは本題ではないので、「そういう機能があるのね」くらいの認識で大丈夫です。

問題発生

Quantum Painter でアニメーション GIF を表示させて遊んでいたところ、とある問題が発生しました。

タイトルの通り、「2MB を超えるファームウェアをビルドできない」という問題です。

普通にファームウェアを作っていれば、 2MB なんてそうそう到達しません。
しかし、アニメーション GIF を表示しようとするとそうでもありません。
画像やらアニメーション GIF なんてただでさえ容量デカいのに、複数突っ込めばそりゃファームウェアも肥大化します。

そこで救世主となるのが RP2040 です。

RP2040 は最大で 16MB のフラッシュまで対応しています。
というわけで、「フラッシュサイズを 16MB に拡張すりゃええやろ!」と config.h に以下を追加してコンパイル

#define PICO_FLASH_SIZE_BYTES (16 * 1024 * 1024)

……が……ダメ!

Linking: .build/qp_test_default.elf                                                                 [ERRORS]
 |
 | lto-wrapper.exe: warning: using serial compilation of 2 LTRANS jobs
 | lto-wrapper.exe: note: see the '-flto' option documentation for more information
 | h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/bin/ld.exe: .build/qp_test_default.elf section `.rodata' will not fit in region `flash1'
 | h:/qmk_msys/mingw64/bin/../lib/gcc/arm-none-eabi/12.2.0/../../../../arm-none-eabi/bin/ld.exe: region `flash1' overflowed by 611892 bytes  | collect2.exe: error: ld returned 1 exit status
 |
make: *** [builddefs/common_rules.mk:280: .build/qp_test_default.elf] エラー 1

<keyboard>.elf を作る段になってこんなこと言われて失敗しました。

解決方法を探してみる

対策 その1

とりあえずエラーでググってみると以下のサイトを見つけました。

forums.raspberrypi.com

要約すると、「最大 2MB の決め打ちになってるから、そいつを編集すればいけるよ」みたいな感じですかね。

スレッドにある通り、 qmk_firmware/.build/<keyboard>.map の中身を確認してみると

.flash_begin    0x10000000        0x0
                0x10000000                        __flash_binary_start = .
.
.
.flash_end      0x10295634        0x0
                0x10295634                        __flash_binary_end = .

となっています。

0x10295634(271144500) - 0x10000000(268435456) = 2709044 で、 2MB(2097152) - 2709044 = -611892 と、エラーの通り 611892 bytes オーバーフローしているのがわかります。

で、解決策というのが、 pico-sdk/src/rp2_common/pico_standard_link/memmap_default.ld を編集するというものでした。
QMK の場合は qmk_firmware/lib/pico-sdk/src/rp2_common/pico_standard_link/memmap_default.ld と解釈しました。

編集したのがこちら。

MEMORY
{
    FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 16384k /* 2048k から変更 */
    RAM(rwx) : ORIGIN =  0x20000000, LENGTH = 256k
    SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
    SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
}

というわけでコンパイル

……が……ダメ!

全く同じエラーを吐いて失敗しました……

対策 その2

改めて QMK のドキュメントを読むと、以下のセクションを見つけました。

docs.qmk.fm

どうやら、デフォルトでは SPI フラッシュのブートローダとして W25Q080 用の物が使われるらしいです。(SPI フラッシュが W25Q080 コンパチのブートローダを持って動いてると解釈する感じ?)

で、 AT25SF128A に設定することもできるようでした。

AT25SF128A は 128Mbit = 16MB の容量を持つ SPI フラッシュらしいので、もしかしたらこれに設定してみたらあるいは……?
ということで config.h に以下を追加してコンパイル

#define RP2040_FLASH_AT25SF128A
#define PICO_FLASH_SIZE_BYTES (16 * 1024 * 1024)

……が……ダメ!

変わりませんでした。

そもそも W25Q080 が 8Mbit = 1MB の容量なので、まあ、関係なかったっすね……

結論

結局、 2MB の壁を超える方法が全然わかりませんでした……

「16MB のフラッシュ積んで贅沢に使ってやるぜ~~~~!!!!」とか思っても、 QMK が 2MB 以上のファームウェアを作ってくれないなら宝の持ち腐れです……
16MB まるまる使えるなら、アニメーション GIF 突っ込みまくって遊べるのに……

おわりに

はい、全然解決してません。

何をどうすればいいのか全然わからないので、有識者の方~~、どうか助言をお願いします~~ 🙏🙏🙏

Lofree Wizard の 3D モデル作った

はじめに

前回はこちら。

koktoh.hatenablog.com

公開場所

github.com

ライセンス

CC BY-NC-SA で公開しています。

使用する際の注意事項はこちらもご確認ください。

koktoh.hatenablog.com

内容

いつもの。

STEP と、 KiCad 用の WRL ファイルが入っています。

プリセットモデル

見た目はこんな感じ

パーツモデル

はい、パーツモデルもあります。

  • bottom.step
  • shrunken_spring.step
  • spring.step
  • stem_wizard.step
  • top.step

ピンとかは Choc V2 の使いまわしなので、新規はありません。

おわりに

というわけで Lofree Wizard でした。
Ghost と Phantom は持ってないので、手に入れたら作るかもしれません。

【開発者向け】 TRRS ジャックの配線ってどうすればいいの?

はじめに

左右分割キーボードを設計するときに、左右間の通信に多く採用されているのが TRRS ケーブル(およびジャック)です
しかし、 TRRS ジャックへの配線については、あまり知識が共有されず、感覚的 / 経験的に行われているような気がします

この記事では、私が「どのような考え方のもと、どう配線しているか」をまとめました

なお、一個人の考えなので、「絶対にこの配線にしろ」と強制したり、「この配線以外はクソ」とか言うつもりはないので、考え方の一種、参考程度に捉えてください

結論

さっさと実際の配線を示してしまいましょう

理由とかを読むのが面倒なら、とりあえずこの配線を真似するだけでいいです

I2CSDA / SCL は入れ替えてもいいです

TRRS とは?

左右分割キーボードで使われる TRRS ケーブル / ジャックは、「Φ3.5mm 4極オーディオケーブル / プラグ / ジャック」とか呼ばれていると思います

左が一般的な「Φ3.5mm 3極オーディオプラグ(TRS プラグ)」、右が TRRS プラグです
端子(極)が1つ増えてますね

つまり、 TRS では3本、 TRRS では4本の信号線が繋がっているということです

世の中には極数がもっと多いものもあります(昔買った Walkman に付いてたノイズキャンセリング付きイヤホンは5極とかだった気がする)

配線の理由

では、最初に示した配線について、その理由を説明しましょう

電源系とデータ系をどう割り振るか

QMK では、左右間通信に電源系(VCC, GND)とデータ系(DATASDA, SCL)で3線か4線での接続が必要です
この3線、4線の割り振りを TRRS プラグ / ジャックの構造から考えていきます

一部省略していますが、 TRRS ジャックのピンはこんな風になっています

シンボルのラベルと各ピンの対応は以下です

シンボルのラベル ピンの色
A 黄色
B
C 水色
D マゼンタ

本来は、ピン A は挿入口の入口付近まで端子が伸びていて、プラグの根元の端子と接触します(モデルでは省略)

ここに TRRS プラグを挿入するとこうなります(手前側のピン A は省略)

プラグとジャックの端子が正しく接触しています(省略された手前側で A とプラグの黄色の端子が接触している)

では、間違えて TRS プラグを挿入するとどうなるでしょう

ピン AB が、プラグの同じ端子と接触してショートしてしまいます
つまり、ピン AB にそれぞれ VCC, GND を割り当てていた場合、 MCU を破壊してしまう可能性があります

逆に、左右間通信に3線で行う Serial を使っていた場合、ピン C を通信線にして、 B を未接続にしておけば TRS ケーブルでも使える可能性が生まれます

このことから、電源系は一番端の A, D に接続し、データ系は C, BC を優先)に接続するようにしています

電源系をどう割り振るか

VCCGNDAD のどちらに接続するか」

これは好みのような気がしますが、個人的な見解から、 AGNDDVCC を接続するようにしています

この世に存在するすべての TRRS プラグを確認したわけではありませんが、根元の端子がそのままプラグ全体と一体化しているプラグが見受けられます

図のように、全体が黄色の端子という状態です

この場合、「VCC を露出させるよりも GND が露出している状態の方が好ましいのではないか」という考えで、プラグの根元に接触する AGND としています
電子回路のセオリーとしてどちらであるべきなのかは調べていないので、異論はありそうですが……(場合によるので一概に言えないとかもありそう)

活線挿抜と TR(R)S の問題点

TR(R)S を使用する左右分割キーボードで話題になるのが「活線挿抜」です

活線挿抜とは、「通電している状態でケーブルを抜き挿しすること」です

基本的に、左右分割キーボードで TR(R)S ケーブルの活線挿抜は避けるべき です

TR(R)S は、端子が直列に並んでいるという構造上、「挿入中に容易にショートし得る」という問題点があります
図はプラグ挿入中の例ですが、プラグの紫の端子を通して BC がショートしています(逆に、ジャックの端子によってプラグの端子同士がショートする場合もある)
もしこのピンが VCCGND に繋がっていて、なおかつ通電状態だった場合、 MCU を破壊してしまう可能性があります

私の配線方法であればそのような可能性は低いかもしれませんが、「活線挿抜に絶対的な耐性がある」と言い切る自信はありません

安心して活線挿抜をしたいならば、 USB のように、端子が並列に並んでいるものを採用するべきでしょう

おわりに

以上、 TRRS ジャックの配線について、個人的な考察に基づく根拠とともに説明しました

冒頭でも述べたように、個人的な意見なので、「絶対この配線にしろ」と強弁するつもりはありません

ただ、設計の際に「こういう考え方もあるのか」と参考にしてもらえれば幸いです