UNO R4 WiFi 版 OpenWeather
UNO R4 WiFi 版 OpenWeather

はじめに

Bodmer/OpenWeather を Arduino UNO R4 WiFi でも動作するようにマイグレーションした ArduinoOpenWeather を紹介する3部作の最終回です。

  1. RTC、NTP、HTTP、TLS、ルートCA証明書の例題
  2. 大きなJSONデータを解析するArduinoJson活用法
  3. アイコン画像をUNO R4のFlash容量内に収める方法(今回)
Bodmer さんのお天気アイコン
Bodmer さんのお天気アイコン

オリジナル版のお天気アイコンは、Flash 上に構築されたファイルシステムにアップロードした1画素24ビットの .bmp ファイルを SPIFFS(現在は非推奨)で読み出し描画していますが、当然、UNO R4 では容量オーバーです。

そこで今回は、如何にコンパクトな画像フォーマットで同等の機能を実現するかがお題となります。

白羽の矢 を立てたのが、ESP32 だけでなく UNO R4 でも動作するグラフィックライブラリ moononournation/Arduino_GFX です。ESP32 では描画速度こそ lovyan03/LovyanGFX や Bodmer/TFT_espi に譲りますが、UNO R4 では Adafruit-GFX-Library よりずっと高速で、かつ少ないメモリのマイクロコントローラーでも動作するように作られた、私のお気に入りのライブラリの1つです。

Arduino_GFX で扱えるビットマップデータ

Arduino_GFX における「ビットマップ」とは、いわゆる Windows の .bmp 形式 ではなく、画素の情報が配列として並んだ単純なデータ列を指します。色深度 に応じた描画メソッドのうち、主なものを以下に紹介します。

  • RGB888 カラー
    draw24bitRGBBitmap() は、RGB888 のビットマップデータを RGB565 に変換して描画します。

  • RGB565 カラー
    draw16bitRGBBitmap() は、RGB565 のビットマップデータを描画します。またその派生として、透過色が指定可能な draw16bitRGBBitmapWithTranColor‎()、1画素1ビットの切り抜き用マスク画像が指定可能な draw16bitRGBBitmapWithMask‎() があります。画像形式ごとのエンディアンの違い に対応するため、名前に Be の入った ビッグエンディアン用メソッド もあります。

  • インデックスカラー
    drawIndexedBitmap‎() は、RGB565 のカラーパレットを参照するビットマップデータを描画します。データサイズは RGB565 カラーの 12\frac{1}{2} + カラーパレット(最大 512 バイト)になります。

  • グレースケール
    drawGrayscaleBitmap() は、1画素1バイトのグレースケール画像を描画します。データサイズは RGB565 カラーのちょうど 12\frac{1}{2} になります。RGB565 に丸められるため 256 階調分の濃淡は再現できません。

  • モノクローム
    drawBitmap() は、1画素1ビットのビットマップデータを描画します。データサイズは RGB565 のおよそ 116\frac{1}{16} です。描画色や背景色の指定も可能です。drawXBitmap() では、GIMP で .xbm ファイル にエクスポートした C ソースコードの利用が可能です。

  • GFXfont
    drawChar() は、GFXfont で定義される1画素1ビットのビットマップデータを描画します。アイコンフォントを組み込めば、画像の代わりになります。

実例

スプラッシュ画像(インデックスカラー画像)

インデックスカラー化に適した例
インデックスカラー化に適した例

今回のアプリでは、起動時の初期化中に OpenWeather のロゴ画像 を表示しています。ご覧の通り色数が少なく、RG565 カラーや JPEG よりインデックスカラーが適しています。

JPEG の利用について

画像サイズが大きい場合、JPEG の利用も考えられます。以下の2つのライブラリが有名どころでしょう。

前者はおよそ 20KB の RAM を要求し、シンプルな adafruit_gfx_demo ですら UNO R4 ではメモリ不足でコンパイルが通りません。

また後者は Flash_Jpg_GFX を参考にしましたが、グローバル変数 TJpgDec が常に 3.5KB の作業用メモリをキープし、もれなく SD もリンクされるため、今回のアプリでは JSON データの解析時にメモリ不足に陥ってしまいました。

今回のように一時的に表示するだけの場合は、変数はローカル化し、不要になった作業用メモリは解放されるよう TJpgDec を直接組み込むのが良いと思います。

ちなみに JPEG 画像の Flash 埋め込み用データを作成するには、ファイルの中身を 16 進数でダンプしてくれる File to hexadecimal converter が便利です。

組み込み向けに LCD 用の画像変換ツールは幾つもありますが、インデックスカラーは見つからず、Arduino_GFX と親和性の良い、独自の変換プロセスを作らざるを得ませんでした 😉

インデックスカラー化したビットマップデータの作成方法

GIMP:インデックスカラー変換
GIMP:インデックスカラー変換

画像のインデックスカラー化には GIMP を使います。

まずはメニューから「画像 → モード → インデックス…」を選択します。

色数が多い場合には、ディザリングに誤差拡散系を指定した方が良いようです。詳しくは GIMP のマニュアル 「インデックスカラーに変換」ダイアログ を参照して下さい。

GIMP:ヘッダーファイルとして出力
GIMP:拡張子を .h にして出力

次に「ファイル → 名前を付けてエクスポート…」を選択し、「画像のエクスポート」ダイアログで「ファイル形式の選択」に「C ソースコードヘッダー」(拡張子は .h)を選択します。

さらにエクスポートした .h ファイルのカラーパレットを rgbconvert.py で RGB888 から RGB565 に書き換えます。

rgbconvert.py の使用方法

バージョン 2.7.16 以上の Python で、コマンドラインから以下を実行します。

% python rgbconvert.py input.h > output.h
Windows な人向けの PowerShell スクリプト

rgbconvert.py は Mac/Linux を想定し、以下を要件に ChatGPT に作らせました。

対象 変換内容
入力ファイル GIMP でエクスポートした C ソースコードのヘッダーファイル
変換結果 標準出力に表示
width / height 変数宣言を #define 文に変換
HEADER_PIXEL マクロ マクロ定義のブロックを削除
header_data_cmap RGB888の各色をRGB565に変換し、uint16_t 配列に変更
header_data 画素データを16進数表記に変換し、uint8_t 配列に変更
変数名 入力ファイル名から拡張子を除き、変数名に使用可能な文字列に置換
ヘッダー全体 入力ファイル名に基づくインクルードガードを追加
エラー処理 標準エラー出力に適切なエラーメッセージを表示

Windows 10/11 用に PowerShell で実行可能な rgbconvert.ps1 も作らせましたが、私の Win 環境は Mac 上の Win 7 と古いので、動作は未検証です。

実行は、以下のように プロファイルは読み込まず、ExecutionPolicy を一時的に無視せよ とのことですが、組織のグループポリシーによってはエラーになるそうです。

powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\rgbconvert.ps1" ".\input.h" ".\output.h"

実行は自己責任でお願いします 🙇‍♂️

変換前 (input.h)

GIMP でエクスポートした .h の中身はおよそ次のようになっています。

/*  GIMP header image file format (INDEXED): input.h  */

static unsigned int width = 180;
static unsigned int height = 80;

/*  Call this macro repeatedly.  After each use, the pixel data can be extracted  */

#define HEADER_PIXEL(data,pixel) {\
pixel[0] = header_data_cmap[(unsigned char)data[0]][0]; \
pixel[1] = header_data_cmap[(unsigned char)data[0]][1]; \
pixel[2] = header_data_cmap[(unsigned char)data[0]][2]; \
data ++; }

static unsigned char header_data_cmap[256][3] = {
  { 58, 57, 60},
  ...
  {255,255,255}
  };
static unsigned char header_data[] = {
  254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,
  ...
  254,254,254,254
  };
変換後 (output.h)

変換後は、RGB888 のカラーパレットが RGB565 に変換され、変数名も入力ファイル名を基に置換されます。

#ifndef _INPUT_H_
#define _INPUT_H_

/*  GIMP header image file format (INDEXED): input.h  */

#define INPUT_WIDTH 180
#define INPUT_HEIGHT 80

static const uint16_t input_cmap[256] = {
  0x2966, 0x31A6, 0x39C7, 0x39C7, 0x39C7, 0x39E7, 0x39E7, 0x39E8,
  ...
  0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
};
static const uint8_t input[] = {
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  ...
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE
};
#endif
描画方法

変換後の .h をインクルードし、drawIndexedBitmap‎() に変換結果を与えます。

gfx->drawIndexedBitmap(
    0, 0,                               // 描画開始位置
    const_cast<uint8_t  *>(input),      // ビットマップデータ
    const_cast<uint16_t *>(input_map),  // カラーパレット
    INPUT_WIDTH, INPUT_HEIGHT           // 画像のサイズ
);

月齢画像(グレースケール画像)

グレイスケール化に適した例
グレイスケール化に適した例

皆さんは、月の満ち欠けって興味ありますか? 私は大いにアリなので是非とも移植したい項目でした 🎑

ということで、月齢画像は NASA の Moon Phases Loop から切り出しました。

File to C style array converter
File to C style array converter

Flash 埋め込み用ビットマップデータの作成には、ブラウザ上で動作する軽量な File to C style array converter が私のお気に入りです。

ほぼそのまま使える C ソースコードが生成できます。

月齢の計算アルゴリズムについて

Google で 月齢の計算アルゴリズム を検索すると AI の解説を含めて様々出てきます。そこで Bodmer 氏が採用した 月齢計算プログラム との精度を比較してみました。「正解」としたのは以下の2つです。

結果、簡易計算や近似計算では数日のズレが出るのに対し、Bodmer 氏のプログラムでは大体 ±0.5 日未満のズレに収まっていました。このプログラムの出展を見つけるのに苦労しましたが、インターネットアーカイブに残っていました。詳しいロジックは分かりませんが、ざっと眺めてみると、地球から見た太陽と月との位置関係を元に幾何学的に計算しているように思います(定かではありませんが… 🙄)。

ということで、これをそのまま使わせてもらうことにしました。

ちなみに後日 Auduino ライブラリの signetica/MoonPhase を見つけましたが、「正解」とのズレは未検証です。

お天気アイコン(GFX フォント画像)

Arduino_GFX のフォント ArduinoFreeFontFile は、Adafruit の GFX フォント を踏襲しています。

カスタムなフォントを作るには、TrueType フォントを変換するプログラム fontconvert か、オンラインツールの truetype2gfx が利用できます。前者は生成するアスキーコードの範囲が指定でき、後者は 0x20 〜 0x7E までのフォントが定義された .h ファイルをゲット出来ます。

FontForge でアイコンを定義
FontForge でアイコンを定義
truetype2gfx で変換
truetype2gfx で変換
fontconvert のコンパイル方法

fontconvert.c のコンパイルには FreeType が必要です。

基本的には、Download から最新版のパッケージをダウンロード&展開し、docs/INSTALL から各 OS 環境ごとのコンパイル&インストール方法に従いますが、私は Google に聞いた「各OS環境におけるfreetypeのインストール方法」が簡単で上手くいきました 😁

FontForge のアウトライン編集
FontForge のアウトライン編集

私は SVG は全くのシロウトなので、ベースの TrueType フォント を FontForge に読み込み、「アウトラインウィンドウ」の GUI 上で編集を行いました。

Google font のアイコンフォント から SVG ファイルをダウンロードし、同ウィンドウから「ファイル(F) → 取り込み(I)…」も可能です(位置の合わせ込みが必要ですが…)。

FontForge の日本語ドキュメント が有志により整備されているので、一読推奨です 😅

アイコンのカラー化

アイコンフォントの多色刷り
アイコンフォントの多色刷り

フォントの描画は基本的に単色ですが、オリジナルのカラー化された GUI に近づけるため、複数のパーツに分けたフォントを同じ位置に異なる色で重ね打ちする「多色刷り」を実装してみました。

興味があれば gfx.h の ココ と gfx.cpp の ココ や ココ を読み解いてみて下さい 😎

参考情報

フォント関連
お天気アイコンの例

おわりに − ヒープメモリ残量の見積もり

本記事のメインテーマは Flash サイズでしたが、今後「お天気アイコンをタッチすると、時刻ごとの天候や気温のグラフを表示する機能」が追加できそうか、UNO R4 WiFi でヒープメモリの残量を見積もってみました。

見積もり方法は前回記事「3. deserializeJson() が使えるヒープメモリを確保する」の通りです。前提となるソフトウェアのバージョンは以下となります。

パッケージ/ライブラリ バージョン
ArduinoCore-renesas 1.6.0
ArduinoJson 7.4.3
GFX Library for Arduino 1.6.9
NTPClient 3.2.1
RTC ArduinoCore-renesas 1.6.0
SPI ArduinoCore-renesas 1.6.0
WiFiS3 ArduinoCore-renesas 1.6.0
Wire ArduinoCore-renesas 1.6.0

まずはコンパイル結果ですが、静的状態では RAM 容量にまだまだ余裕があります。

Sketch uses 209684 bytes (79%) of program storage space. Maximum is 262144 bytes.
Global variables use 10408 bytes (31%) of dynamic memory, leaving 22360 bytes for local variables. Maximum is 32768 bytes.

マップファイルから求めたヒープの残りサイズもコンパイル結果に近い値です。

項目 アドレス/サイズ
__HeapLimit 0x0000000020007b00
__HeapBase 0x00000000200028c0
__HeapLimit - __HeapBase 0x5240 (21056)

次に動的状態で調べるため、setup() の初期化終了時点と loop() の末尾に次のコードを仕掛け、ヒープメモリの使用状況を表示させました。

#include <stdio.h>
#include <malloc.h>

void check_heap_size(void) {
  struct mallinfo mi = mallinfo();
  printf("total space allocated from system (arena): %lu\n", mi.arena);
}

ということで、ヒープメモリ残量の見積もり結果は以下の通りとなりました。

計測ポイント arena のサイズ (__HeapLimit - __HeapBase) - arena
setup() の初期化終了時点 1484 21056 - 1484 = 19572 (19.0KB)
loop() の末尾 17588 21056 - 17588 = 3468 (3.0KB)

う〜ん、残り 3KB でどれだけできるか分かりませんが、そのうちチャレンジしてみます 🍁