0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

文字閲覧スマートグラス(仮)までの道のり1 #備忘録

0
Last updated at Posted at 2026-04-16

下記の内容は2026年2月時点でのライブラリや公開ページによるものであり、バージョンの更新やライブラリの変更については手持ちのAIに聞いてください。
特にESP32へのフォント/dataアップロード周りはIDE1.xと2.xでプラグイン周りが変わってるので間違えると沼ります。

手持ちのarduino uno3 CH06 tft7735

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SoftwareSerial.h>

// ---- TFT pins (UNO) ----
#define TFT_CS   10
#define TFT_DC    9
#define TFT_RST   8

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

// ---- HC-06 SoftwareSerial pins ----
// HC-06 TXD -> D2 (RX)
// HC-06 RXD <- D3 (TX)  ※UNO→HC-06は分圧推奨
SoftwareSerial bt(2, 3); // RX, TX  ※HC-06 TX->D2, HC-06 RX<-D3(分圧)
#define BT_RX 2
#define BT_TX 3

// ---- text layout ----
const uint8_t TEXT_SIZE = 2;      // 1が無難。大きくしたいなら2
const uint16_t FG = ST77XX_WHITE;
const uint16_t BG = ST77XX_BLACK;

int16_t cursorX = 0;
int16_t cursorY = 0;
int16_t lineH;

void newLine() {
  cursorX = 0;
  cursorY += lineH;
  if (cursorY + lineH > tft.height()) {
    tft.fillScreen(BG);
    cursorY = 0;
  }
  tft.setCursor(cursorX, cursorY);
}

void setup() {
  Serial.begin(115200);
  bt.begin(9600);

  tft.initR(INITR_MINI160x80_PLUGIN);// ここはあなたの個体に合わせて
  tft.setRotation(0);
  uint8_t madctl = 0x08;               // まず0x00から
  madctl ^= 0x40;                      // MX をトグル(左右反転)
  tft.sendCommand(0x36, &madctl, 1);   // MADCTL
  
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  tft.setTextSize(1);
  tft.setCursor(0,0);
  tft.println("BT ready");
}
static char line[128];
static uint8_t idx = 0;

void loop() {
  while (bt.available()) {
    char c = bt.read();
    if (c == '\r') continue;

    if (c == '\n') {
      line[idx] = 0;
      // 行を描画(ここで初めてTFTを触る)
      tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
      tft.println(line);
      idx = 0;
      return;
    }

    if (idx < sizeof(line)-1) {
      line[idx++] = c;
    } else {
      // 行が長すぎたら強制フラッシュ
      line[idx] = 0;
      tft.println(line);
      idx = 0;
    }
  }
}

esp32とtft7789(graphictestの改変)

最初に入れておく
https://github.com/wilmsn/Arduino-ST7789-Library

/***************************************************
  This is a library for the ST7789 IPS SPI display.

  Written by Ananev Ilya.
 ****************************************************/

#include <Adafruit_GFX.h>    // Core graphics library by Adafruit
#include <Arduino_ST7789.h> // Hardware-specific library for ST7789 (with or without CS pin)
#include <SPI.h>
#include "BluetoothSerial.h"
BluetoothSerial BT;
static char line[256];
static uint16_t idx = 0;

#define TFT_DC    19
#define TFT_RST   5 
#define TFT_CS    10 // only for displays with CS pin
#define TFT_MOSI  23   // for hardware SPI data pin (all of available pins)
#define TFT_SCLK  18   // for hardware SPI sclk pin (all of available pins)

Arduino_ST7789 tft = Arduino_ST7789(TFT_DC, TFT_RST, TFT_MOSI, TFT_SCLK); //for display without CS pin

float p = 3.1415926;

void setup(void) {
  Serial.begin(9600);
  Serial.print("Hello! ST7789 TFT Test");

  tft.init(240, 240);   // initialize a ST7789 chip, 240x240 pixels

  Serial.println("Initialized");

  uint16_t time = millis();
  tft.fillScreen(BLACK);
  time = millis() - time;

  Serial.println(time, DEC);
  delay(500);

  tft.setTextWrap(true);
  tft.setTextColor(WHITE, BLACK);
  tft.setTextSize(2);
  tft.setCursor(0, 0);
  tft.println("BT ready");

  // 追加:Bluetooth SPP
  BT.begin("ESP32-ST7789");
}

void loop() {
  while (BT.available()) {
    char c = (char)BT.read();
    if (c == '\r') continue;

    if (c == '\f') {                 // クリア
      tft.fillScreen(BLACK);
      tft.setCursor(0,0);
      idx = 0;
      continue;
    }

    if (c == '\n') {                 // 1行確定
      line[idx] = 0;
      tft.println(line);
      idx = 0;

      // 末尾に行ったらまとめて消す(ここだけ)
      if (tft.getCursorY() > tft.height() - 16) {
        tft.fillScreen(BLACK);
        tft.setCursor(0,0);
      }
      continue;
    }

    if (idx < sizeof(line)-1) line[idx++] = c;
  }
}

MVIMG_20260220_011857.jpg

参考先

esp32 file system uploader (IDE2.X)

pluginに
https://github.com/earlephilhower/arduino-littlefs-upload/releases
image.png
image.png
ctrl shit P
image.png

image.png
フォントをESP32に焼けたので良し

skechbook/プロジェクト/data
ここにフォントfntを配置
misaki.hをideにインストールしてなかったのでインストール
https://github.com/mgo-tec/ESP32_SPIFFS_MisakiFNT
https://github.com/mgo-tec/ESP32_SPIFFS_UTF8toSJIS
(zipをそのままライブラリに)

フォントアップロード確認

#include "FS.h"
#include "LittleFS.h"

void dump(const char* path){
  File f = LittleFS.open("/MSKG13.FNT","r");
  Serial.printf("%s open=%s size=%d\n", path, f?"OK":"NG", f?(int)f.size():-1);
  if(f){
    for(int i=0;i<16 && f.available(); i++){
      Serial.printf("%02X ", (uint8_t)f.read());
    }
    Serial.println();
    f.close();
  }
}

void setup(){
  Serial.begin(115200);
  delay(300);
  if(!LittleFS.begin(true)){
    Serial.println("LittleFS mount failed");
    return;
  }
  Serial.println("LittleFS mounted");
  dump("/Utf8Sjis.tbl");
  dump("/mgotec48.FNT");
  dump("/MSKG13.FNT");
}
void loop(){}

size=0でなければ完了、あとシリアルモニタとか別のIDEウインドウがあるとアップロード失敗する(10敗)
必ずスケッチフォルダ/dataにフォントは格納する。

以下を参照

日本語フォント導入

#include <Arduino.h>
#include <SPI.h>
#include <FS.h>
#include <LittleFS.h>

#include <Arduino_ST7789.h>
#include "BluetoothSerial.h"

// mgo-tec(あなたがLittleFS化した版を使う想定)
#include "ESP32_LittleFS_MisakiFNT.h"
#include "ESP32_LittleFS_UTF8toSJIS.h"

// ====== ファイル名(LittleFS直下) ======
static const char* UTF8SJIS_FILE    = "/Utf8Sjis.tbl";
static const char* MISAKI_HALF_FILE = "/mgotec48.FNT";
static const char* MISAKI_ZEN_FILE  = "/MSKG13.FNT";

// ====== ST7789配線(あなたの現状に合わせた) ======
#define TFT_DC    19
#define TFT_RST   5
#define TFT_MOSI  23
#define TFT_SCLK  18

Arduino_ST7789 tft(TFT_DC, TFT_RST, TFT_MOSI, TFT_SCLK);

// ====== BT ======
BluetoothSerial BT;

// ====== Misaki ======
ESP32_LittleFS_MisakiFNT MFR;

// ====== 画面/文字設定 ======
static const int TFT_W = 240;
static const int TFT_H = 240;

static const uint16_t FG = WHITE;
static const uint16_t BG = BLACK;

static const int SCALE = 2;              // 1:8x8, 2:16x16
static const int GLYPH_W = 8 * SCALE;
static const int GLYPH_H = 8 * SCALE;
static const int LINE_CHARS = TFT_W / GLYPH_W;   // SCALE=2なら15

static int curX = 0;
static int curY = 0;

// フォント描画バッファ(グローバル固定。ここが肝)
static uint8_t font_buf[LINE_CHARS][8];

// BT受信用(1行バッファ)
static char lineBuf[256];
static uint16_t idx = 0;

// ====== UTF-8 文字数で最大maxCharsまでのバイト長を返す ======
static size_t utf8_prefix_bytes(const char* s, size_t maxChars) {
  size_t bytes = 0;
  size_t chars = 0;
  while (s[bytes] && chars < maxChars) {
    uint8_t b = (uint8_t)s[bytes];
    size_t adv = 1;
    if ((b & 0x80) == 0x00) adv = 1;           // 0xxxxxxx
    else if ((b & 0xE0) == 0xC0) adv = 2;      // 110xxxxx
    else if ((b & 0xF0) == 0xE0) adv = 3;      // 1110xxxx
    else if ((b & 0xF8) == 0xF0) adv = 4;      // 11110xxx(絵文字等)
    else adv = 1;                               // 壊れたUTF-8は1扱い

    // 途中で切れないようにする
    for (size_t i = 1; i < adv; i++) {
      if (!s[bytes + i]) return bytes;
    }
    bytes += adv;
    chars++;
  }
  return bytes;
}

// ====== 8x8ビットマップをSCALE倍して描画 ======
static inline void drawGlyph8x8Scaled(int x, int y, const uint8_t g8[8]) {
  for (int row = 0; row < 8; row++) {
    uint8_t bits = g8[row];
    for (int col = 0; col < 8; col++) {
      bool on = (bits >> (7 - col)) & 1;
      uint16_t c = on ? FG : BG;
      for (int dy = 0; dy < SCALE; dy++) {
        for (int dx = 0; dx < SCALE; dx++) {
          tft.drawPixel(x + col * SCALE + dx, y + row * SCALE + dy, c);
        }
      }
    }
  }
}

static inline void clearScreen() {
  tft.fillScreen(BG);
  curX = 0;
  curY = 0;
}

static inline void newLine() {
  curX = 0;
  curY += GLYPH_H;
  if (curY + GLYPH_H > TFT_H) {
    clearScreen();
  }
}

// ====== 1行描画:UTF-8文字列 → 美咲フォント → ST7789 ======
static void drawUTF8Line(const char* utf8) {
  // ここで「最大15文字」に切ってからライブラリに渡す(破壊対策)
  char tmp[256];
  strncpy(tmp, utf8, sizeof(tmp) - 1);
  tmp[sizeof(tmp) - 1] = 0;

  size_t n = utf8_prefix_bytes(tmp, LINE_CHARS);
  tmp[n] = 0;

  // ライブラリ変換(LINE_CHARSぶんをfont_bufへ)
  // ※ ここがあなたのライブラリ関数名に合ってる前提
  MFR.StrDirect_MisakiFNT_readALL(tmp, font_buf);

  // 描画
  for (int i = 0; i < LINE_CHARS; i++) {
    if (curX + GLYPH_W > TFT_W) break;
    drawGlyph8x8Scaled(curX, curY, font_buf[i]);
    curX += GLYPH_W;
  }
  newLine();
}

// ====== ファイル存在/サイズ確認 ======
static void dumpFile(const char* path) {
  File f = LittleFS.open(path, "r");
  Serial.printf("%s open=%s size=%d\n", path, f ? "OK" : "NG", f ? (int)f.size() : -1);
  if (f) f.close();
}

void setup() {
  Serial.begin(115200);
  delay(300);

  // TFT init
  tft.init(TFT_W, TFT_H);
  clearScreen();

  // LittleFS(ここは1回だけ)
  bool ok = LittleFS.begin(false);
  if (!ok) {
    Serial.println("LittleFS mount NG -> format");
    LittleFS.format();
    ok = LittleFS.begin(false);
  }
  Serial.printf("LittleFS.begin = %s\n", ok ? "OK" : "NG");
  if (!ok) {
    Serial.println("STOP");
    while (1) delay(1000);
  }

  dumpFile(UTF8SJIS_FILE);
  dumpFile(MISAKI_HALF_FILE);
  dumpFile(MISAKI_ZEN_FILE);

  // Misaki init(あなたがLittleFS化したライブラリならOK)
  // ここでライブラリがLittleFS.begin()を呼ばないようにしてある前提
  MFR.LittleFS_Misaki_Init3F(UTF8SJIS_FILE, MISAKI_HALF_FILE, MISAKI_ZEN_FILE);

  // BT
  BT.begin("ESP32-ST7789-JP");
  BT.println("BT ready");
  BT.println("Send lines with \\n. \\f=clear, !=draw test, ?=tx test.");

  drawUTF8Line("BT ready");
}

void loop() {
  while (BT.available()) {
    char c = (char)BT.read();
    if (c == '\r') continue;

    // command: clear
    if (c == '\f') {
      clearScreen();
      idx = 0;
      BT.println("[CLR]");
      continue;
    }
    // command: tx test
    if (c == '?') {
      BT.println("TEST_TX: こんにちは / Hello / 123");
      continue;
    }
    // command: draw test
    if (c == '!') {
      const char* s = "TEST_DRAW: こんにちは / Hello / 123";
      drawUTF8Line(s);
      BT.println(s);
      continue;
    }

    // line end
    if (c == '\n') {
      lineBuf[idx] = 0;
      drawUTF8Line(lineBuf);
      idx = 0;
      continue;
    }

    // buffer
    if (idx < sizeof(lineBuf) - 1) {
      lineBuf[idx++] = c;
    } else {
      // overflow: 強制確定して流す(落ちない方を優先)
      lineBuf[sizeof(lineBuf) - 1] = 0;
      drawUTF8Line(lineBuf);
      idx = 0;
    }
  }
}

ここまでやってなんだけどrasberrypi Picoとかのほうがライブラリ回り簡単でいい気がする。
→ESP32でwebページ型で試す。

GPT5.2log(トラブル履歴)

了解。ここまでの往復を「やったこと → 出た問題 → 切り分け → 修正」の流れで、ログっぽく残すね(時系列)。

最初の目的
スマホ→Bluetoothでテキストを飛ばして、ESP32 + ST7789(240x240) に表示。最終的にAR的な“文字だけ表示”用。

第1段階:UNO + HC-06 + ST7735(仮の最小系)
やったこと
・AndroidのBluetooth terminal からHC-06へ送信、UNOがSoftwareSerialで受けて、Adafruit_ST7735で表示。
出た問題
・鏡文字(左右反転)が起きたり戻ったり。
・画面ノイズや半分死ぬ/残像っぽい。
・Serialで送れる文字列に上限がある。
・日本語表示はそのままだと無理(ASCII想定、フォント/エンコーディング問題)。
切り分け/対処
・MADCTL(0x36)でMX/MYなどをトグルして鏡文字は解消できた。
・ノイズは配線/初期化/リフレッシュ絡みの疑い、最終的にはESP32へ移行。

第2段階:ESP32 + ST7789へ移植
やったこと
・ESP32はBluetooth内蔵なのでHC-06を捨ててBluetoothSerialへ。
・ST7789はCSなし基板で、Adafruit_ST7789のコンストラクタがCS必須寄りで詰まり、別ライブラリ(Arduino_ST7789系)へ。
出た問題
・最初は何も映らない(配線/ドライバ/初期化パラメータ不一致)。
・一度映ってから、受信文字送信はできるがノイズが出る/位置ズレ疑い。
切り分け/対処
・動いたgraphicstest構成を土台にして、BT受信→printlnで表示する方向に寄せた。

第3段階:日本語表示(美咲フォント + 変換テーブル)
狙い
・UTF-8の日本語を受けて、Shift_JIS変換テーブル(Utf8Sjis.tbl)経由で美咲フォント(FNT)を引き、8x8ビットマップとして描画。

やったこと
・mgo-tec系の「SPIFFS + 美咲フォント」手法をESP32環境に持ち込み。
・最初はヘッダが無い、関数名が違う、LittleFSが宣言されてない等でコンパイルが崩れる。
出た問題(コンパイル)
・ESP32_SPIFFS_MisakiFNT.h: No such file(ライブラリ導入不足)
・UTF8_to_SJIS_1Char などメンバ不一致(記事の版と手元ライブラリのAPI差)
・LittleFS was not declared(FS種別の混線)
対処
・ライブラリ/ヘッダを正しく入れ、API差は「手元の実装に合わせる」方針へ。
・ファイルシステムは一旦「LittleFSで統一」に寄せる判断。

第4段階:ファイルシステム地獄(SPIFFS/LittleFS/SDの混線)
観測ログ(典型)
・E ... Corrupted dir pair / mount failed (-84)(LittleFSが壊れてる)
・SPIFFS: mount failed -10025(SPIFFSも呼ばれて失敗してる=誰かがSPIFFS.beginしてる)
・なのに LittleFS mounted が出る(戻り値チェック無しの嘘ログ)
切り分けで分かったこと
・スケッチ側だけじゃなく、ライブラリ側が勝手に SPIFFS.begin(true) を呼んでいた。
・ログの card initialized. はSDじゃなくSPIFFS初期化ログとして書かれてて紛らわしい。
・一時期 Detected size(4096k) smaller than ... header(8192k) も出て、Flash size/Partition前提がズレたバイナリを書いて起動すら落ちた(この時は土台が崩壊)。
修正

Flash size/Partitionを実フラッシュ(4MB)に合わせる(8MBヘッダ問題を消す)

「成功してるのにmountedと表示」問題を潰すため、LittleFS.begin() の戻り値を必ずチェックしてログを正直にする

最重要:ライブラリ内のSPIFFS依存を排除
 ・#include をやめて LittleFS.h に変更
 ・SPIFFS.begin/open/exists... を全部 LittleFS... に置換
結果
・起動ログが LittleFS initialized. になり、Utf8Sjis.tbl / mgotec48.FNT / MSKG13.FNT が正常に open & size取得できるようになった。

第5段階:実行時クラッシュ(heap_caps_free outside heap)
観測ログ
・ファイル読み込み成功の後、数秒〜十数秒で
 assert failed: heap_caps_free ... free() target pointer is outside heap areas
原因推定
・ほぼメモリ破壊(バッファオーバーラン/二重free/壊れたポインタ)
・状況的に「受信した1行が長すぎる」→「フォント変換/描画バッファが固定長想定」→踏み抜いて後でfreeで爆発、が一番整合。
修正
・BT受信1行を、表示可能文字数に制限してから描画処理へ渡す。
・240x240で16x16相当表示(SCALE=2)なら1行15文字なので、まず「15文字まで」にクリップ。
・UTF-8はバイト数が可変なので、UTF-8文字境界で切る(途中バイトで切って文字化け/破壊しない)。
結果
・「15文字までなら成功」で安定動作を確認。

最終状態(現時点の到達点)
・ESP32 + ST7789(240x240) で、BluetoothSerial経由のテキスト受信→表示が動作。
・美咲フォント一式(Utf8Sjis.tbl / mgotec48.FNT / MSKG13.FNT)は LittleFS から読み出せる。
・ライブラリはSPIFFS依存を全撤去してLittleFSに統一。
・クラッシュは「入力行が長すぎて内部の固定長バッファを踏む」だったので、1行15文字制限で回避して安定。

補足として残しておくと良い“地雷”
・「LittleFS begin(true) なのに Corrupted dir pair が出る」みたいな矛盾は、“別コンポーネントが先に触ってログを吐いてる/二重初期化/戻り値無視ログ”で起きる。まずログを正直にする(戻り値チェック)と切り分けが速い。
・SPIFFS/LittleFSを混ぜると、どっちの領域をどのpartitionで使ってるか不明になって壊れやすい。今回は「ライブラリ側が勝手にSPIFFS.begin」してたのが根っこ。
・Flash size/partitionがズレると、起動時にassertで死ぬ(4MB実機に8MBヘッダを書かない)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?