LoginSignup
6
1

More than 3 years have passed since last update.

RaspberryPiに刺したOLEDにブラウザ画面を描画する

Last updated at Posted at 2020-12-08

やること

Raspberry Pi の上で Chromium(Chromium Embedded Framework)を動かし、ブラウザ画面を OLED に描画します。

構成・方針

Chromium を windowless モードで動かし、画面データを抜き取って、OLED display に直接投げます。
X や wayland は介さないです。

「 Chromium を windowless モードで動かし、画面データを抜き取る」が本記事のポイントです。パフォーマンスは落ちますが、これが出来ると大抵の事はお手軽にできるようになります。本記事では、この辺りを重点的に解説しているつもりです。

Chromium を全てソースコードからビルドしても良いのですが、空き容量や PC スペックが足りない方も多いと思います。ここでは、ブラウザを組み込むための Chromium Embedded Framework (以降 CEF と省略します) と、コアの部分がビルド済みになっているパッケージを利用します。

環境

クロスビルドは行わず、RaspberryPi 上でビルドします。

  • RaspberryPi4 Raspberry Pi OS (buster)
  • ubuntu 20.04 LTS

今回使用したビルド済みバージョンは、以下のバージョンのものです。

cef_binary_87.1.11+g8bb7705+chromium-87.0.4280.66

Chromium Embedded Framework を試す

入手

CEF の入手方法は、 repository(bitbucket) の wiki に手順が掲載されています。
https://bitbucket.org/chromiumembedded/cef/wiki/MasterBuildQuickStart.md
が、掲載されているのは、Chromium のソースコードからフルビルドを行う方法です。

This guide is NOT intended for:

Those seeking a prebuilt binary distribution for use in third-party apps. Go here instead.

を見ていくと、以下のサイトにたどり着くと思います。
https://cef-builds.spotifycdn.com/index.html

RaspberryPi 上で動作させるパッケージが欲しい場合は、 Linux ARMStandard Distribution をダウンロードします1

一旦手元で試したい…という場合は、 Linux 64-bitStandard Distribution をダウンロードします。手元の環境が Linux で無い場合は、VirtualBox 等仮想環境を導入します。Windows/Mac は、本記事では触れません。

ビルド

cmakeを使います2。コマンドも一般的なC++プロジェクトと大差ありません。

sudo apt install cmake ninja-build

サンプルアプリは、cefclient, cefsimple 等複数入っています。 cefsimple をベースに色々手を加えていくので、 cefsimple をビルドすることにします。

mkdir -p out
cd out
cmake .. -GNinja
ninja cefsimple

生成物を実行してみて、Google が出たら ok です。
GUIが無い環境では起動できません。

# bash
./tests/cefsimple/Release/cefsimple

...

ちなみに、デフォルトではリリース版がビルドされます。
スレッドやnullptrを実行時にチェックするデバッグ版をビルドするには、cmakeコマンドを以下のようにします。

cmake .. -GNinja -DCMAKE_BUILD_TYPE=Debug
ninja cefsimple
./tests/cefsimple/Debug/cefsimple

arm版のバイナリパッケージを使う場合は、PROJECT_ARCH マクロを追加します。これをしないと、-msse2のような x86-64 アーキテクチャ固有のコンパイルオプションを付けられて、コンパイルが失敗します。

cmake .. -GNinja -DPROJECT_ARCH=arm

cefsimpleの各ファイルの解説

ざっくり説明します

  • cefsimple_linux.cc
    • main関数があります。CefAppを継承したSimpleAppの起動等を行っています。
  • simple_app.cc
    • SimpleAppの実装が含まれています。主にウィンドウを起動する実装が含まれています。
    • ウィンドウの制御を行うクラス(開いた時や閉じた時の挙動を記述するSimpleWindowDelegate、新しくウィンドウを開く必要が起きた時の挙動を記述するSimpleBrowserViewDelegate)の定義・実装があります。
  • simple_handler.cc
    • ブラウザの画面やページのハンドラを定義・実装しています。
    • SimpleHandler は、CefClient と複数の Cef*Handler を継承します。
    • CefClient は複数の Cef*Handler を管理するためのインターフェースです。
    • 例えば、CefDisplayHandler を実装するときは、CefClient::GetDisplayHandler() をオーバーライドした、CefDisplayHandler を継承したクラスのインスタンスの参照を返すメソッドを作成します。SimpleHandler の実装例では、CefClientCefDisplayHandler を多重継承しているので、return this; しています。

windowless モードで起動・画面データ取り出し

windowless モードで起動

windowless モード(headlessとも呼ばれていました)で起動するよう、コードを書き換えてみます。これにより、window なしでブラウザが起動します。

方法は簡単で、ブラウザの設定(CefSettings)とブラウザウィンドウ(CefWindowInfo)に windowless_rendering_enabled というフラグがあるので、これらに true を指定するだけです。

cefsimple_linux.cc
@@ -59,6 +59,7 @@ int main(int argc, char* argv[]) {

   // Specify CEF global settings here.
   CefSettings settings;
+  settings.windowless_rendering_enabled = true;

   if (command_line->HasSwitch("enable-chrome-runtime")) {
     // Enable experimental Chrome runtime. See issue #2969 for details.
simple_handler.cc
@@ -123,6 +123,7 @@ void SimpleApp::OnContextInitialized() {
   } else {
     // Information used when creating the native window.
     CefWindowInfo window_info;
+    window_info.windowless_rendering_enabled = true;

 #if defined(OS_WIN)
     // On Windows we need to specify certain flags that will be passed to

実行してみると、画面が表示されなくなりました。windowlessモードは効いているかもしれませんが、画面が無くなったので、これでは正しく動いているかどうか確認が取れません…。

ところで、上記の windowless モードにする方法ですが、公式ドキュメントを見つけられませんでした。
cefclient の実装例を読むか、include 以下のヘッダファイル群を探し当てることになります3

windowless モードで画面取得

実装の詳細は、Wikiに記載されています。windowless_rendering_enabled については何故か書かれていません。
https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md
Off-Screen Rendering の項目です。

windowless の状態で、どうやって操作するべきか?について書かれています。

The CefRenderHandler::OnPaint() method will be called to provide invalid regions and the updated pixel buffer. The cefclient application draws the buffer using OpenGL but your application can use whatever technique you prefer.

CefRenderHandler::OnPaint で描画時に画素データを受け取れるようなので、やってみます。

追記する先は、CefClient と複数の Cef*Handler を多重継承している SimpleHandler です。箇条書きで書くと、以下のステップになります。

  • SimpleHandler に抽象クラス CefRenderHandler を継承して実装する。
  • CefClient::GetRenderHandler() をオーバーライドしたメソッドを作成する4

SimpleHandler に抽象クラス CefRenderHandler を継承して実装する。

継承を追加

simple_handler.h
class SimpleHandler : public CefClient,
                      public CefDisplayHandler,
                      public CefLifeSpanHandler,
                      public CefLoadHandler,
                      public CefRenderHandler, {   // << add

追加したクラスのメソッドを定義

simple_handler.h
  // CefRenderHandler methods:
  void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) OVERRIDE;
  void OnPaint(CefRefPtr<CefBrowser> browser, PaintElementType type,
               const RectList &dirtyRects, const void *buffer, int width,
               int height) OVERRIDE;

追加したメソッドの実装を書きます。今は特に実装しませんが、LOG(INFO) で、ログを書き出せるので、書き出すようにします。

simple_handler.cc
void SimpleHandler::GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) {
  rect.width = 640;
  rect.height = 480;
}

void SimpleHandler::OnPaint(CefRefPtr<CefBrowser> browser,
                            PaintElementType type, const RectList &dirtyRects,
                            const void *buffer, int width, int height) {
  LOG(INFO) << "onpaint!";
}

CefClient::GetRenderHandler() をオーバーライドしたメソッドを作成する。

CefRenderHandler を実装したクラスのインスタンスは自身なので、this を返します。

simple_handler.h
  virtual CefRefPtr<CefRenderHandler> GetRenderHandler() OVERRIDE { return this; }

実行結果

[1116/013706.478396:INFO:simple_handler.cc(151)] onpaint!
[1116/013706.492529:INFO:simple_handler.cc(151)] onpaint!
[1116/013706.567228:INFO:simple_handler.cc(151)] onpaint!
[1116/013706.567908:INFO:simple_handler.cc(151)] onpaint!
[1116/013706.592335:INFO:simple_handler.cc(151)] onpaint!

描画したがっているのは分かりました。

OnPaint から画面データを取り出す

状況が分からないので、受け取ったデータをファイルか何かに書き出してみます。

include/cef_render_handler.h によると、

  // Called when an element should be painted. Pixel values passed to this
  // method are scaled relative to view coordinates based on the value of
  // CefScreenInfo.device_scale_factor returned from GetScreenInfo. |type|
  // indicates whether the element is the view or the popup widget. |buffer|
  // contains the pixel data for the whole image. |dirtyRects| contains the set
  // of rectangles in pixel coordinates that need to be repainted. |buffer| will
  // be |width|*|height|*4 bytes in size and represents a BGRA image with an
  // upper-left origin. This method is only called when
  // CefWindowInfo::shared_texture_enabled is set to false.

buffer|width|*|height|*4 bytes で、BGRA の順で画素データが並べられているようです。

適当に以下のように記述して出力してみます。

#include <fstream>

void SimpleHandler::OnPaint(CefRefPtr<CefBrowser> browser,
                            PaintElementType type, const RectList &dirtyRects,
                            const void *buffer, int width, int height) {
  static int counter = 0;
  std::string filename = std::to_string(counter++) + ".bgra";
  LOG(INFO) << "onpaint: " << filename << " (" << width << "x" << height << ")";
  std::ofstream o(filename, std::ios_base::out);
  o.write((char *)buffer, width * height * 4);
  o.close();
  // note: BAD な実装です。
}

画像ファイルが出力されたと思います。画像データを確認するには、 image magick で png に変換するのが楽です。

$ convert -depth 8 -size 640x480 bgra:./20.bgra out.png

画面が得られました。カーソルの点滅が原因で、画像を出力し続けていたんですね。

OnPaint から「別スレッドで」画面データを取り出す

先ほど書いたコードは誤りがあります。ファイルの書き込み等の重い処理をUIスレッドで行うべきではないからです。

Chromium を含む多くの GUI アプリケーションは、複数のスレッドで動作します。特に、UI スレッド(メインスレッドとも呼ばれることがある)は、ユーザ入力を受ける処理を行うので、このスレッドに負荷がかかると、操作が利かなくなる等、不幸なことが起こりえます。

UI 以外のスレッドについては、以下に記載されています。
https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-threads

自前でスレッドを作成、管理する場合は、CefThreadCefThread#GetTaskRunner を使います。

CEF には FILE スレッドがあります。このスレッドに処理を投函する (CefPostTask) ことにします5

#include <fstream>
#include <vector>

#include "include/base/cef_bind.h"
#include "include/wrapper/cef_closure_task.h"

namespace {
void WriteData(std::string filename, std::vector<char> buffer, int length) {
  // debug ビルド時 FILE スレッド以外から呼ばれたら SIGABRT する
  // release ビルド時はコンパイルしない
  DCHECK(CefCurrentlyOn(TID_FILE));

  std::ofstream o(filename, std::ios_base::out);
  o.write(buffer.data(), length);
  o.close();
}
}

void SimpleHandler::OnPaint(CefRefPtr<CefBrowser> browser,
                            PaintElementType type, const RectList &dirtyRects,
                            const void *buffer, int width, int height) {
  static int counter = 0;
  DCHECK(CefCurrentlyOn(TID_UI));

  std::string filename = std::to_string(counter++) + ".bgra";
  int length = width * height * 4;

  std::vector<char> copied_buffer(length);
  std::copy((const char*)buffer, (const char*)buffer + length, copied_buffer.begin());

  LOG(INFO) << "onpaint: " << filename << " (" << width << "x" << height << ")";
  CefPostTask(TID_FILE, base::Bind(&WriteData, filename, std::move(copied_buffer), width*height*4));
}
  • WriteData はファイルに書き込むだけの関数です。
    • SimpleHandler のインスタンスの参照は要らないので、メソッドにはしていません。
    • 他のファイルから参照しないので、無名名前空間で囲みます
  • OnPaint で渡される *buffer を非同期関数の引数に渡すと危険です。非同期関数が実行される頃には delete されているかどうか分からない為です。バッファをコピーしておき、データごと渡すようにします。
  • CefPostTask の後に copied_buffer は使わないので、std::move します。std::move しないと、引数に渡すためのデータのコピーが発生するため無駄です。

取り出した画像データを OLED に描画する

ここから先は OLED 依存なので短めに書きます。
以下の SPI 通信で描画出来る 128x128 の OLED を使いました。

128x128, General 1.5inch RGB OLED display Module, 16-bit high color

https://www.waveshare.com/1.5inch-rgb-oled-module.htm

OLED にとにかく描画する

wiki にサンプルコードがあるので、これで動作確認しました。ライセンスはMIT(ただしフォントは3条項BSD)です。

  • 圧縮ファイルを展開すると、RasberryPi/c ディレクトリがあると思います。このディレクトリを使います
  • makefile を開き、 WiringPi を使うよう変更します。デフォルトでは bcm2835.h を使うようになっていますが、手元の動作確認では SIGSEGV で死にました。恐らくバージョン違い。
  • OLED から RPi0 への接続は、VCC -> 5V, GND -> GND, DIN -> SPI0 MOSI, CLK ->SPI0 SCLK, CS -> GPIO24, DC -> GPIO25, RST -> GPIO8 です。これらはサンプルコードから読み取れます。
  • RPi0 のピン配置は https://pinout.xyz/pinout/spi を参照しました。

make でビルドして、得られた実行ファイルを実行すると、以下のようにデモが動きました。

サンプルコードを外部ライブラリ化する

手を抜きます。main 関数周辺の実装と、makefile だけを書き換えて外部ライブラリ化させてみます。

main.c を以下のように書き換えてみます。(RGBOLED_displaySampleBitmap はテスト用に残しておきました)

#include "OLED_Driver.h"
#include "OLED_GFX.h"

void RGBOLED_init() {
  DEV_ModuleInit();
  Device_Init();
}

void RGBOLED_displayBitmap(void* data, uint16_t size) {
  uint16_t i = 0;
  if (size > 128*128*2)
    size = 128*128*2;
  RAM_Address();
  Write_Command(0x5C);

  for( ; i < 128*128*2; i+=2)  {
    color_byte[0] = ((uint8_t*)data)[i];
    color_byte[1] = ((uint8_t*)data)[i+1];
    Write_Data(color_byte[0]);
    Write_Data(color_byte[1]);
  }
}

void RGBOLED_displaySampleBitmap(void) {
  uint16_t i = 0;
  RAM_Address();
  Write_Command(0x5C);

  for( ; i < 128*128*2; i+=2)  {
    color_byte[0] = gImage_bmp2[i];
    color_byte[1] = gImage_bmp2[i+1];
    Write_Data(color_byte[0]);
    Write_Data(color_byte[1]);
  }
}

makefile は以下のように書き換えました。主に shared library を作るように引数の変更と、出力ファイル名の変更です。

# TARGET = main
TARGET = oled.so

### 省略 ###

# ${TARGET}:${OBJ_O}
#   $(CC) $(CFLAGS) $(OBJ_O) -o $@ $(LIB)

${TARGET}:${OBJ_O}
    $(CC) -shared -Wl,-soname,$(TARGET) $(CFLAGS) $(OBJ_O) -o $@ $(LIB)

make すると、oled.so という名前の共有ライブラリが得られます。

作成した shared の動作確認

CEF とは別に、共有ライブラリを実行時に読み込んで関数を呼び出すコードを書いてみます。C++ではなく、C言語です。

共有ライブラリを実行時に開くには、dlfcn の関数を使います。
また、ライブラリ dl をリンクする必要があります

#include <dlfcn.h>
#define NULL 0

void *handle;
void (*RGBOLED_init)(void);
void (*RGBOLED_displaySampleBitmap)(void);

int main() {
  handle = dlopen("./oled.so", RTLD_LAZY);
  if (handle == NULL) return 1;
  RGBOLED_init = (void (*)(void))dlsym(handle, "RGBOLED_init");
  RGBOLED_displaySampleBitmap = (void (*)(void))dlsym(handle, "RGBOLED_displaySampleBitmap");
  if (RGBOLED_init == NULL) return 1;
  if (RGBOLED_displaySampleBitmap == NULL) return 1;
  RGBOLED_init();
  RGBOLED_displaySampleBitmap();
  dlclose(handle);
  return 0;
}

上のソースコードのファイル名を test.c とすると、gccを使ったビルドコマンドは、以下のようになります。

gcc test.c -ldl -o test

oled.so を生成された実行ファイルと同じディレクトリの下にコピーします。
生成された実行ファイル test を実行すると、サンプルコードに含まれていた風景画が OLED に映し出されます。

CEF に oled のモジュールを組み込む

「作成した shared の動作確認」で書いた実装をCEFのプロジェクトに組み込みます。

OLED を制御するためのクラスを作る

C++ のクラスで再実装します。
インスタンスは唯一で十分なので、シングルトンパターンで実装します。wikipediaを参考にしています。

以下は定義部分のヘッダファイル oled_driver.h です。

#ifndef OLED_DRIVER_H_
#define OLED_DRIVER_H_

class OLEDDriver {
 public:
  static OLEDDriver& getInstance();
  void initialize();
  void displayBitmap(void* data, int width, int height);

 private:
  OLEDDriver();
  ~OLEDDriver();
  OLEDDriver& operator=(const OLEDDriver&) = delete;
  OLEDDriver(OLEDDriver&&) = delete;
  OLEDDriver& operator=(OLEDDriver&&) = delete;
};

#endif  // OLED_DRIVER_H_

次に実装部分のコードです。ファイル名は oled_driver.cc としました。
OLEDとブラウザ画面の画素データの間で色順序やビット幅が異なるので、変換実装を書いています。

#include "tests/cefsimple/oled_driver.h"

#include <dlfcn.h>
#include <cstdint>

#include "include/base/cef_logging.h"

namespace {
void *handle = nullptr;
void (*RGBOLED_init)(void);
void (*RGBOLED_displayBitmap)(void*, uint16_t);
}

OLEDDriver::OLEDDriver() {
  handle = dlopen("./oled.so", RTLD_LAZY);
  if (handle == nullptr) {
    LOG(ERROR) << __FUNCTION__ << " dlopen failed";
    return;
  }
  RGBOLED_init = (void (*)(void))dlsym(handle, "RGBOLED_init");
  RGBOLED_displayBitmap = (void (*)(void*, uint16_t))dlsym(handle, "RGBOLED_displayBitmap");
  if (RGBOLED_init == nullptr) {
    LOG(ERROR) << __FUNCTION__ << " dlsym RGBOLED_init failed";
    dlclose(handle);
    handle = nullptr;
    return;
  }
  if (RGBOLED_displayBitmap == nullptr) {
    LOG(ERROR) << __FUNCTION__ << " dlsym RGBOLED_displayBitmap failed";
    dlclose(handle);
    handle = nullptr;
    return;
  }
}

OLEDDriver::~OLEDDriver() {
  if (handle == nullptr)
    return;
  dlclose(handle);
  handle = nullptr;
}

OLEDDriver& OLEDDriver::getInstance() {
  static OLEDDriver instance;
  return instance;
}

void OLEDDriver::initialize() {
  if (handle == nullptr)
    return;
  RGBOLED_init();
}

void OLEDDriver::displayBitmap(void* data, int width, int height) {
  if (handle == nullptr)
    return;

  // 対象のOLEDは15ビットカラー(に設定されている)で、渡される画素データはalpha値含む32ビットカラー
  // 変換する必要がある。
  uint16_t buffer[128 * 128];
  std::fill(buffer, buffer + 128 * 128, (uint16_t)0xFFFF);
  for (int y = 0; y < std::min(height, 128); ++y) {
    for (int x = 0; x < std::min(width, 128); ++x) {
      uint32_t color32 = ((uint32_t *)data)[y * width + x];
      uint16_t b = ((color32)&0xFF) >> 3;         // 8bit -> 5bit
      uint16_t g = ((color32 >> 8) & 0xFF) >> 3;  // 8bit -> 5bit
      uint16_t r = ((color32 >> 16) & 0xFF) >> 3; // 8bit -> 5bit
      // blue 5bit, red 5bit, green 5bit
      buffer[y * 128 + x] = (b << 10) | (r << 5) | g;
    }
  }
  RGBOLED_displayBitmap(buffer, 128 * 128 * 2);
}

作成したクラスを組み込む

SimpleHandler への追加

simple_handler.cc に書いた、ファイルに画像を書き出す実装を、OLED に書き出す実装に置き換えます。
全く重要では無いですが、解像度を渡す引数を増やしています。

void WriteData(std::string filename, std::vector<char> buffer, int length, int width, int height) {
  // std::ofstream o(filename, std::ios_base::out);
  // o.write(buffer.data(), length);
  // o.close();
  OLEDDriver::getInstance().displayBitmap((void*)buffer.data(), width, height);
  LOG(INFO) << "wrote: " << filename;
}

void SimpleHandler::OnPaint(CefRefPtr<CefBrowser> browser,
                            PaintElementType type, const RectList &dirtyRects,
                            const void *buffer, int width, int height) {
  // ... 省略 ...
  CefPostTask(TID_FILE, base::Bind(&WriteData, filename, std::move(copied_buffer), width*height*4, width, height));
}

また、OLED の画面解像度は 128x128 なので、SimpleHandler::GetViewRect もその画面解像度を返すようにします。

void SimpleHandler::GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) {
  rect.width = 128;
  rect.height = 128;
}

CMakefile へ追加

以下のように、追加するファイルと、shared library の動的ロードをするためのライブラリ dl の追加が必要です。

diff --git a/tests/cefsimple/CMakeLists.txt b/tests/cefsimple/CMakeLists.txt
index 1cc6477..30e3293 100644
--- a/tests/cefsimple/CMakeLists.txt
+++ b/tests/cefsimple/CMakeLists.txt
@@ -8,6 +8,8 @@

 # cefsimple sources.
 set(CEFSIMPLE_SRCS
+  oled_driver.cc
+  oled_driver.h
   simple_app.cc
   simple_app.h
   simple_handler.cc
@@ -89,7 +91,7 @@ if(OS_LINUX)
   add_executable(${CEF_TARGET} ${CEFSIMPLE_SRCS})
   SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_TARGET})
   add_dependencies(${CEF_TARGET} libcef_dll_wrapper)
-  target_link_libraries(${CEF_TARGET} libcef_lib libcef_dll_wrapper ${CEF_STANDARD_LIBS})
+  target_link_libraries(${CEF_TARGET} libcef_lib libcef_dll_wrapper ${CEF_STANDARD_LIBS} dl)

   # Set rpath so that libraries can be placed next to the executable.
   set_target_properties(${CEF_TARGET} PROPERTIES INSTALL_RPATH "$ORIGIN"

動作確認

wikipedia を表示させてみました。top画と同じ画像です。
画面が小さくて何かに使うには厳しいですかね…

何故wikipediaなのかというと、googleの場合、以下のようになってしまうからです。これは面白くない

out.png

残課題

  • 描画が遅い
    • サンプルコードをそのまま突っ込んでいるので、無駄は多いです
    • それでもSPI通信がボトルネックになっていて、遅いです
    • アニメーションを含む画面の場合は、描画が追い付かなくなると思います。フレームレートを設定するCEFのAPIを使ったり、詰まっていたら描画処理を捨てたりする実装が必要です
  • x window system / GUI からの起動が必要
    • 外せるはず
      • include 以下のディレクトリに #define CEF_X11 1 を定義している箇所があるので、0にする
      • CMakefiles.txt から x11 関連を削除する
  • raspberrypi zero で動かせてない
    • Illegal Instruction と出て動かせませんでした。chromiumソースコードからのフルビルドが必要かも

終わりに

ACCESS Advent Calendar 2020 の 8 日目の記事でした。最後まで目を通していただきありがとうございます。
ここまでボリュームを増やすつもりは無かったのですが、画面を取り出すところで止めておけばよかったですかね…


  1. RPi3/4であれば64bitでも問題ないはずですが…試していないです 

  2. chromium全体をビルドする時は cmake では無く、gn を使います。 

  3. 例えば、ディレクトリ内を windowless で検索すると、見つかります。 

  4. CefClientは、複数の Handler 系を管理するクラス。CefClient::GetRenderHandler() は、「ハンドリングが必要なら、CefRenderHandlerのインスタンスの参照を返す」メソッドです。 

  5. OLEDへの書き込みは非常に低速なので、FILE スレッドも使わず、自前でスレッドを準備した方が良さそうです。 

6
1
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
6
1