techemo(てけも)のブログ

30代リーマンの副業ブログ

【買ってよかった中国輸入品】ボタン付き0.96インチOLEDディスプレイ(SSD1306)~特徴は?使い道は?~

📝 投稿: 🔄 更新:

307記事目(中国輸入品シリーズ)

 

こんばんは。いつも読んでいただきありがとうございます。

趣味で中国から面白そう・便利そうな商品を輸入して使ってみています。techemoです。

今日は最近購入したボタン付き0.96インチOLEDディスプレイ(SSD1306)の紹介です。

ボタン付き0.96インチOLEDディスプレイ(SSD1306)

AHT10温湿度センサーでデータ取得はできたけど「数値を表示したいなぁ」と思っていたところ、

このディスプレイを見つけました。しかもボタン付きで操作もできるのに300円台!驚きです!

 

特徴は

・高コントラストOLEDで見やすい

自発光なので暗い場所でもくっきり表示!LCDと違ってバックライト不要

電源を切ると真っ黒になるから消費電力も少ない

 

・4つのボタンで操作可能

単なる表示器じゃなくて、メニュー操作や設定変更もできます

上下左右やOK/キャンセルなど、自由にプログラム可能

 

・コンパクトで実用的(0.96インチ)

約2.4cmの小さな画面だけど128×64ピクセルで意外と情報量多い!

日本語も表示できます(フォントデータ次第)

 

・I2C通信で配線簡単

VCC、GND、SDA、SCLの4本だけ!AHT10やBMP280と同じバスに繋げられます

 

・とにかく安い(300円台)

ボタン付きでこの価格!普通の0.96インチOLEDより少し高いだけ

 

 

LCD vs OLED 表示性能比較

OLED (SSD1306)

自発光 ✓

コントラスト: 超高

視野角: 180°

ボタン: 4個付き

消費電力: 低

価格: 300円台

VS
一般的なLCD

バックライト必要

コントラスト: 中

視野角: 狭い

ボタン: 別途必要 ✗

消費電力: 高め

価格: 200円~

暗所での視認性と操作性を重視するならOLED一択!

技術仕様

ディスプレイ仕様:

サイズ: 0.96インチ(約24mm×12mm表示エリア)

解像度: 128×64ピクセル

表示色: ホワイト/ブルー(製品により異なる)

コントローラー: SSD1306

 

電気的仕様:

動作電圧: 3.3V~5V

通信方式: I2C(アドレス:0x3C or 0x3D)

消費電力: 約20mA(全点灯時)

 

ボタン仕様:

ボタン数: 4個(タクトスイッチ)

接続: GPIOピンに直接接続可能

 

配線方法(ESP32の場合)

◆ ディスプレイ接続

VCC → 3.3V(電源)🔴
GND → GND(グランド)⚫
SDA → GPIO21(データ)🔵
SCL → GPIO22(クロック)🟡

◆ ボタン接続

BTN1 → GPIO13(ボタン1)⚪
BTN2 → GPIO14(ボタン2)⚪
BTN3 → GPIO27(ボタン3)⚪
BTN4 → GPIO26(ボタン4)⚪

使い道は

私は主にセンサーデータの表示とメニュー操作に使っています

AHT10で取得した温湿度を表示して、ボタンで画面切り替えや設定変更ができるようにしました

 

 

他にもいろんな使い道が想定されます

・IoTデバイスの表示部

温湿度計、気圧計、CO2センサーなどの測定値表示

グラフ表示で変化の傾向も一目瞭然

 

・小型測定器の製作

電圧計、電流計、周波数カウンターなど

ボタンでレンジ切り替えも可能

 

スマートホーム制御パネル

照明のON/OFF、エアコンの温度設定

各部屋の状態表示と操作

 

・ポータブルゲーム機

シンプルなレトロゲームなら十分動作

テトリスやスネークゲームなど

 

🎮 ボタン操作例(メニューシステム)

📊 メインメニュー
├─ ▶ センサー表示
│   ├─ 温度/湿度
│   ├─ 気圧
│   └─ グラフ表示
├─ ⚙️ 設定
│   ├─ アラート設定
│   ├─ 表示設定
│   └─ 通信設定
├─ 📁 データログ
└─ ℹ️ システム情報

ボタン割り当て:
🔺 上ボタン:カーソル上移動
🔻 下ボタン:カーソル下移動
⭕ OKボタン:決定/実行
❌ 戻るボタン:キャンセル/戻る

プログラミング

Arduino用のライブラリがたくさんあるので、初心者でも簡単に使えます

 

人気のライブラリ:

・Adafruit SSD1306ライブラリ

最も人気で情報も豊富。ボタン付きの本品でも問題なく動作

 

・U8g2ライブラリ

多機能で日本語フォントも使える。メモリ使用量は多め

 

・SSD1306Asciiライブラリ

軽量でメモリ消費が少ない。文字表示メイン向け

 

I2Cアドレスは通常0x3Cですが、製品によっては0x3Dの場合もあります

I2Cスキャナーで確認してから使うのがおすすめです

 

ボタンの処理:

チャタリング防止のためにdebounce処理を入れるのがポイント

長押し検出も実装すれば、より多機能な操作が可能になります

使い方は

①配線

ESP32の場合(上記の配線図参照)

ディスプレイ部:

3.3V⇒VCC、GND⇒GND、GPIO21⇒SDA、GPIO22⇒SCL

ボタン部:

各ボタンをGPIOピンに接続(プルアップ抵抗は内蔵を使用)

 

②ライブラリのインストール

ArduinoIDEのツール>ライブラリ管理

ライブラリマネージャーの検索窓に"SSD1306"と入力し

"Adafruit SSD1306"と"Adafruit GFX Library"をインストール

 

③コードの作成

スケッチ例から"ssd1306_128x64_i2c"を開いて

I2Cアドレスを確認(0x3Cか0x3D)して書き込み

 

④ボタン処理の追加

digitalRead()でボタン状態を読み取り

メニュー操作や画面切り替えを実装

 

センサーと組み合わせれば、立派な測定器が作れますよ!

techemo.hatenablog.com

🚀 サンプルコード(ESP32-C3 環境監視システム) クリックで展開
/*
- ESP32-C3 環境監視システム(リッチUI版)- 修正版
- 対応センサー: AHT10 (温湿度)
- 対応ディスプレイ: SSD1315 OLED 128x64
- 機能: アニメーション、アイコン、グラフ、トレンド表示
  */

#include 
#include 
#include 
#include 
#include 

// ESP32-C3専用ピン定義
#define SDA_PIN 8
#define SCL_PIN 10

// ボタンピン設定
#define BTN_UP 4
#define BTN_DOWN 5
#define BTN_SELECT 6
#define BTN_BACK 7

// ディスプレイ設定
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define DISPLAY_I2C_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// センサー設定
Adafruit_AHTX0 aht;

// アイコン定義(8x8ピクセル)
const unsigned char ICON_TEMP[] PROGMEM = {
0x18, 0x24, 0x24, 0x24, 0x24, 0x5A, 0x7E, 0x3C
};

const unsigned char ICON_HUMIDITY[] PROGMEM = {
0x18, 0x3C, 0x7E, 0x7E, 0xFF, 0xFF, 0x7E, 0x3C
};

const unsigned char ICON_ALARM[] PROGMEM = {
0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x00, 0x18, 0x18
};

const unsigned char ICON_SETTINGS[] PROGMEM = {
0x0E, 0x1F, 0x3F, 0x7C, 0x3E, 0xFC, 0xF8, 0x70
};

const unsigned char ICON_UP_ARROW[] PROGMEM = {
0x08, 0x1C, 0x3E, 0x7F, 0x08, 0x08, 0x08, 0x08
};

const unsigned char ICON_DOWN_ARROW[] PROGMEM = {
0x08, 0x08, 0x08, 0x08, 0x7F, 0x3E, 0x1C, 0x08
};

// グラフデータ履歴
#define GRAPH_POINTS 32
float tempHistory[GRAPH_POINTS];
float humidHistory[GRAPH_POINTS];
int graphIndex = 0;

// システム変数
float temperature = 0.0;
float humidity = 0.0;
float lastTemperature = 0.0;
float lastHumidity = 0.0;
int currentMenu = 0;
int currentSetting = 0;
float tempAlarmHigh = 30.0;
float tempAlarmLow = 10.0;
float humidityAlarmHigh = 80.0;
float humidityAlarmLow = 30.0;
bool alarmEnabled = true;
bool alarmTriggered = false;

// アニメーション変数
unsigned long animationTimer = 0;
int animationFrame = 0;
float displayTemp = 0.0;
float displayHumid = 0.0;
int scrollOffset = 0;
bool scrollDirection = true;

// トレンド追跡
enum Trend { TREND_STABLE, TREND_RISING, TREND_FALLING };
Trend tempTrend = TREND_STABLE;
Trend humidTrend = TREND_STABLE;

// UIテーマ
struct Theme {
bool darkMode;
int transitionSpeed;
bool animationsEnabled;
} theme = {true, 50, true};

// ボタン状態管理
bool lastBtnState[4] = {HIGH, HIGH, HIGH, HIGH};
unsigned long lastDebounceTime[4] = {0, 0, 0, 0};
const unsigned long debounceDelay = 50;
bool displayNeedsUpdate = false;

// 関数の前方宣言
void initializeGraphData();
void updateGraphData();
void calculateTrends();
void animateValues();
void drawBatteryIcon(int x, int y, int level);
void drawWiFiIcon(int x, int y, bool connected);
void drawProgressBar(int x, int y, int width, int height, float value, float max);
void drawTrendArrow(int x, int y, Trend trend);
void drawAnimatedIcon(int x, int y, const unsigned char* icon);
void drawMainScreenRich();
void drawSettingsScreenRich();
void drawGraphScreenRich();
void drawHistoryScreenRich();
void drawSystemScreenRich();
void drawSplashScreen();
void drawAlarmNotification();

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

Serial.println("ESP32-C3 Environment Monitor - Rich UI v4.0");

// ピン初期化
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_SELECT, INPUT_PULLUP);
pinMode(BTN_BACK, INPUT_PULLUP);

// I2C初期化
Wire.begin(SDA_PIN, SCL_PIN);

// ディスプレイ初期化
if(!display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_I2C_ADDRESS)) {
Serial.println("Display not found!");
}

// スプラッシュスクリーン表示
drawSplashScreen();
delay(2000);

// センサー初期化
if (!aht.begin()) {
Serial.println("AHT10 init failed!");
}

// グラフデータ初期化
initializeGraphData();

Serial.println("Setup complete!");
}

void loop() {
static unsigned long lastSensorRead = 0;
static unsigned long lastGraphUpdate = 0;

// センサー読み取り(3秒ごと)
if (millis() - lastSensorRead > 3000) {
sensors_event_t humidity_event, temp_event;
aht.getEvent(&humidity_event, &temp_event);

lastTemperature = temperature;
lastHumidity = humidity;
temperature = temp_event.temperature;
humidity = humidity_event.relative_humidity;

calculateTrends();

// アラームチェック
if (alarmEnabled) {
  alarmTriggered = (temperature > tempAlarmHigh || temperature < tempAlarmLow || 
                   humidity > humidityAlarmHigh || humidity < humidityAlarmLow);
}

lastSensorRead = millis();
}

// グラフデータ更新(30秒ごと)
if (millis() - lastGraphUpdate > 30000) {
updateGraphData();
lastGraphUpdate = millis();
}

// アニメーション処理
if (theme.animationsEnabled) {
animateValues();
} else {
displayTemp = temperature;
displayHumid = humidity;
}

// ボタン処理
handleButtons();

// 画面更新
if (displayNeedsUpdate || millis() - animationTimer > 100) {
updateDisplay();
animationTimer = millis();
animationFrame++;
displayNeedsUpdate = false;
}

delay(10);
}

void initializeGraphData() {
for (int i = 0; i < GRAPH_POINTS; i++) {
tempHistory[i] = 20.0;
humidHistory[i] = 50.0;
}
}

void updateGraphData() {
// 履歴を左にシフト
for (int i = 0; i < GRAPH_POINTS - 1; i++) {
tempHistory[i] = tempHistory[i + 1];
humidHistory[i] = humidHistory[i + 1];
}

// 新しいデータを追加
tempHistory[GRAPH_POINTS - 1] = temperature;
humidHistory[GRAPH_POINTS - 1] = humidity;
}

void calculateTrends() {
float tempDiff = temperature - lastTemperature;
float humidDiff = humidity - lastHumidity;

if (abs(tempDiff) < 0.5) tempTrend = TREND_STABLE;
else if (tempDiff > 0) tempTrend = TREND_RISING;
else tempTrend = TREND_FALLING;

if (abs(humidDiff) < 2.0) humidTrend = TREND_STABLE;
else if (humidDiff > 0) humidTrend = TREND_RISING;
else humidTrend = TREND_FALLING;
}

void animateValues() {
// スムーズな値の遷移
float tempStep = (temperature - displayTemp) / 10.0;
float humidStep = (humidity - displayHumid) / 10.0;

if (abs(temperature - displayTemp) > 0.1) {
displayTemp += tempStep;
} else {
displayTemp = temperature;
}

if (abs(humidity - displayHumid) > 0.5) {
displayHumid += humidStep;
} else {
displayHumid = humidity;
}
}

void drawBatteryIcon(int x, int y, int level) {
// バッテリー外枠
display.drawRect(x, y, 12, 7, SSD1306_WHITE);
display.drawPixel(x + 12, y + 2, SSD1306_WHITE);
display.drawPixel(x + 12, y + 3, SSD1306_WHITE);
display.drawPixel(x + 12, y + 4, SSD1306_WHITE);

// バッテリーレベル
int bars = map(level, 0, 100, 0, 10);
display.fillRect(x + 1, y + 1, bars, 5, SSD1306_WHITE);
}

void drawWiFiIcon(int x, int y, bool connected) {
if (connected) {
display.drawPixel(x + 3, y + 4, SSD1306_WHITE);
display.drawLine(x + 2, y + 3, x + 4, y + 3, SSD1306_WHITE);
display.drawLine(x + 1, y + 2, x + 5, y + 2, SSD1306_WHITE);
display.drawLine(x, y + 1, x + 6, y + 1, SSD1306_WHITE);
} else {
display.drawLine(x, y, x + 6, y + 6, SSD1306_WHITE);
display.drawLine(x + 6, y, x, y + 6, SSD1306_WHITE);
}
}

void drawProgressBar(int x, int y, int width, int height, float value, float max) {
display.drawRect(x, y, width, height, SSD1306_WHITE);
int fillWidth = (int)((value / max) * (width - 2));
fillWidth = constrain(fillWidth, 0, width - 2);
display.fillRect(x + 1, y + 1, fillWidth, height - 2, SSD1306_WHITE);
}

void drawTrendArrow(int x, int y, Trend trend) {
switch (trend) {
case TREND_RISING:
display.drawLine(x, y + 4, x + 4, y, SSD1306_WHITE);
display.drawLine(x + 4, y, x + 4, y + 2, SSD1306_WHITE);
display.drawLine(x + 4, y, x + 2, y, SSD1306_WHITE);
break;
case TREND_FALLING:
display.drawLine(x, y, x + 4, y + 4, SSD1306_WHITE);
display.drawLine(x + 4, y + 4, x + 4, y + 2, SSD1306_WHITE);
display.drawLine(x + 4, y + 4, x + 2, y + 4, SSD1306_WHITE);
break;
case TREND_STABLE:
display.drawLine(x, y + 2, x + 4, y + 2, SSD1306_WHITE);
break;
}
}

void drawAnimatedIcon(int x, int y, const unsigned char* icon) {
// アニメーション効果でアイコンを描画
if (animationFrame % 10 < 5) {
display.drawBitmap(x, y, icon, 8, 8, SSD1306_WHITE);
} else {
display.drawBitmap(x, y - 1, icon, 8, 8, SSD1306_WHITE);
}
}

void drawSplashScreen() {
display.clearDisplay();

// ロゴアニメーション
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 10);
display.print("ESP32-C3");

display.setTextSize(1);
display.setCursor(25, 30);
display.print("Environment");
display.setCursor(30, 40);
display.print("Monitor");

// プログレスバー
for (int i = 0; i <= 100; i += 10) {
drawProgressBar(14, 50, 100, 8, i, 100);
display.display();
delay(100);
}
}

void drawAlarmNotification() {
// アラーム通知の枠
if (animationFrame % 10 < 5) {
display.drawRect(10, 15, 108, 34, SSD1306_WHITE);
display.drawRect(11, 16, 106, 32, SSD1306_WHITE);
}

// アラームアイコン
drawAnimatedIcon(20, 25, ICON_ALARM);

// アラームテキスト
display.setTextSize(1);
display.setCursor(35, 25);
display.print("ALARM!");
display.setCursor(30, 35);

if (temperature > tempAlarmHigh || temperature < tempAlarmLow) {
display.printf("Temp: %.1fC", temperature);
} else if (humidity > humidityAlarmHigh || humidity < humidityAlarmLow) {
display.printf("Humid: %.0f%%", humidity);
}
}

void drawMainScreenRich() {
// ステータスバー
display.drawLine(0, 9, 128, 9, SSD1306_WHITE);

// タイトル
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Monitor");

// 時刻(右揃え、分まで表示)- 修正版
char timeString[8];
int hours = (millis() / 3600000) % 24;
int minutes = (millis() / 60000) % 60;
sprintf(timeString, "%02d:%02d", hours, minutes);

// 文字幅を計算して右揃え
int timeWidth = strlen(timeString) * 6; // 6ピクセル/文字
display.setCursor(128 - timeWidth, 0);
display.print(timeString);

// メインコンテンツエリア - レイアウト調整
// 温度セクション
display.drawBitmap(2, 15, ICON_TEMP, 8, 8, SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(12, 15);
display.printf("%.1fC", displayTemp);
drawTrendArrow(45, 15, tempTrend);

// 温度プログレスバー
drawProgressBar(12, 25, 45, 3, displayTemp + 40, 125);

// 湿度セクション
display.drawBitmap(2, 32, ICON_HUMIDITY, 8, 8, SSD1306_WHITE);
display.setCursor(12, 32);
display.printf("%.0f%%", displayHumid);
drawTrendArrow(45, 32, humidTrend);

// 湿度プログレスバー
drawProgressBar(12, 42, 45, 3, displayHumid, 100);

// 右側ステータスパネル - サイズ調整
display.drawRect(64, 12, 62, 36, SSD1306_WHITE);
display.setCursor(66, 14);
display.print("Status");

if (alarmTriggered) {
// アラーム点滅
if (animationFrame % 10 < 5) {
  display.fillRect(66, 24, 58, 10, SSD1306_WHITE);
  display.setTextColor(SSD1306_BLACK);
  display.setCursor(75, 26);
  display.print("ALARM");
  display.setTextColor(SSD1306_WHITE);
} else {
  display.setCursor(75, 26);
  display.print("ALARM");
}
} else {
display.setCursor(86, 26);
display.print("OK");
}

// バッテリーアイコン(サイズ調整)
drawBatteryIcon(68, 36, 75);

// WiFiアイコン(位置調整)
drawWiFiIcon(100, 36, false);

// 操作ガイド(下部に配置)
display.setCursor(0, 56);
display.print("#:Menu  *:Graph");
}

void drawSettingsScreenRich() {
// ヘッダー
display.setTextSize(1);
display.drawBitmap(2, 1, ICON_SETTINGS, 8, 8, SSD1306_WHITE);
display.setCursor(12, 1);
display.print("Settings");
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);

const char* settings[] = {"Temp High", "Temp Low", "Humid High", "Humid Low"};
float* values[] = {&tempAlarmHigh, &tempAlarmLow, &humidityAlarmHigh, &humidityAlarmLow};
const char* units[] = {"C", "C", "%", "%"};

for (int i = 0; i < 4; i++) {
int yPos = 12 + i * 10; // 行間隔を調整

// 選択カーソル(アニメーション付き)
if (i == currentSetting) {
  if (animationFrame % 10 < 5) {
    display.fillRect(0, yPos, 128, 9, SSD1306_WHITE);
    display.setTextColor(SSD1306_BLACK);
  } else {
    display.drawRect(0, yPos, 128, 9, SSD1306_WHITE);
  }
}

display.setCursor(2, yPos + 1);
display.print(settings[i]);

// 値表示(位置調整)
display.setCursor(60, yPos + 1);
display.printf("%.0f%s", *values[i], units[i]);

// ミニプログレスバー(サイズ調整)
float maxVal = (i < 2) ? 85.0 : 100.0;
float minVal = (i < 2) ? -40.0 : 0.0;
drawProgressBar(85, yPos + 1, 40, 6, *values[i] - minVal, maxVal - minVal);

if (i == currentSetting && animationFrame % 10 < 5) {
  display.setTextColor(SSD1306_WHITE);
}
}

// アラーム状態(位置調整)
display.setCursor(0, 52);
display.print("Alarm: ");
if (alarmEnabled) {
display.drawBitmap(40, 51, ICON_ALARM, 8, 8, SSD1306_WHITE);
display.setCursor(50, 52);
display.print("ON");
} else {
display.print("OFF");
}

// 操作ガイド
display.setCursor(0, 56);
display.print("UP/DN  *:Back");
}

void drawGraphScreenRich() {
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Live Graph");
display.drawLine(0, 9, 128, 9, SSD1306_WHITE);

// グラフエリア
int graphX = 5;
int graphY = 12;
int graphW = 118;
int graphH = 32;

// グラフ枠
display.drawRect(graphX, graphY, graphW, graphH, SSD1306_WHITE);

// グリッドライン
for (int i = 1; i < 4; i++) {
int y = graphY + (graphH * i / 4);
for (int x = graphX + 5; x < graphX + graphW - 5; x += 4) {
  display.drawPixel(x, y, SSD1306_WHITE);
}
}

// データプロット
for (int i = 0; i < GRAPH_POINTS - 1; i++) {
int x1 = graphX + 2 + (i * (graphW - 4) / GRAPH_POINTS);
int x2 = graphX + 2 + ((i + 1) * (graphW - 4) / GRAPH_POINTS);

// 温度グラフ
int y1 = graphY + graphH - 2 - map(tempHistory[i], 0, 50, 0, graphH - 4);
int y2 = graphY + graphH - 2 - map(tempHistory[i + 1], 0, 50, 0, graphH - 4);
display.drawLine(x1, y1, x2, y2, SSD1306_WHITE);

// 湿度グラフ(点線)
if (i % 2 == 0) {
  y1 = graphY + graphH - 2 - map(humidHistory[i], 0, 100, 0, graphH - 4);
  y2 = graphY + graphH - 2 - map(humidHistory[i + 1], 0, 100, 0, graphH - 4);
  display.drawPixel(x1, y1, SSD1306_WHITE);
  display.drawPixel(x2, y2, SSD1306_WHITE);
}
}

// 凡例(位置調整)
display.setCursor(5, 47);
display.print("Temp:");
display.drawLine(30, 49, 40, 49, SSD1306_WHITE);

display.setCursor(45, 47);
display.print("Humid:");
for (int x = 75; x < 85; x += 2) {
display.drawPixel(x, 49, SSD1306_WHITE);
}

// 現在値(位置調整)
display.setCursor(5, 56);
display.printf("%.1fC %.0f%%", temperature, humidity);
}

void drawHistoryScreenRich() {
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Data History");
display.drawLine(0, 9, 128, 9, SSD1306_WHITE);

// 統計情報ボックス(サイズ調整)
display.drawRect(2, 12, 58, 38, SSD1306_WHITE);
display.setCursor(4, 14);
display.print("Temperature");

// 最大/最小/平均を計算
float maxTemp = tempHistory[0];
float minTemp = tempHistory[0];
float avgTemp = 0;

for (int i = 0; i < GRAPH_POINTS; i++) {
if (tempHistory[i] > maxTemp) maxTemp = tempHistory[i];
if (tempHistory[i] < minTemp) minTemp = tempHistory[i];
avgTemp += tempHistory[i];
}
avgTemp /= GRAPH_POINTS;

display.setCursor(4, 23);
display.printf("Max:%.1f", maxTemp);
display.setCursor(4, 32);
display.printf("Min:%.1f", minTemp);
display.setCursor(4, 41);
display.printf("Avg:%.1f", avgTemp);

// 湿度統計(サイズ調整)
display.drawRect(68, 12, 58, 38, SSD1306_WHITE);
display.setCursor(70, 14);
display.print("Humidity");

float maxHumid = humidHistory[0];
float minHumid = humidHistory[0];
float avgHumid = 0;

for (int i = 0; i < GRAPH_POINTS; i++) {
if (humidHistory[i] > maxHumid) maxHumid = humidHistory[i];
if (humidHistory[i] < minHumid) minHumid = humidHistory[i];
avgHumid += humidHistory[i];
}
avgHumid /= GRAPH_POINTS;

display.setCursor(70, 23);
display.printf("Max:%.0f", maxHumid);
display.setCursor(70, 32);
display.printf("Min:%.0f", minHumid);
display.setCursor(70, 41);
display.printf("Avg:%.0f", avgHumid);

// フッター(位置調整)
display.setCursor(0, 56);
display.printf("Samples:%d *:Back", GRAPH_POINTS);
}

void drawSystemScreenRich() {
display.setTextSize(1);
display.setCursor(0, 0);
display.print("System Info");
display.drawLine(0, 9, 128, 9, SSD1306_WHITE);

// システム情報パネル(サイズ調整)
display.drawRect(2, 12, 124, 38, SSD1306_WHITE);

// CPU情報
display.setCursor(4, 14);
display.print("ESP32-C3 RISC-V");
display.setCursor(4, 23);
display.printf("Clock: %dMHz", getCpuFrequencyMhz());

// メモリ情報とバー
display.setCursor(4, 32);
display.printf("RAM: %dK/%dK", ESP.getFreeHeap() / 1024, ESP.getHeapSize() / 1024);

int memUsage = 100 - (ESP.getFreeHeap() * 100 / ESP.getHeapSize());
drawProgressBar(4, 41, 116, 6, memUsage, 100);

// 稼働時間
unsigned long uptime = millis();
int hours = uptime / 3600000;
int minutes = (uptime / 60000) % 60;

display.setCursor(0, 56);
display.printf("Uptime: %02d:%02d", hours, minutes);

// リフレッシュインジケータ(アニメーション)
if (animationFrame % 20 < 10) {
display.fillCircle(120, 58, 2, SSD1306_WHITE);
}
}

void updateDisplay() {
display.clearDisplay();

// アラーム通知を最優先で表示
if (alarmTriggered && animationFrame % 20 < 10 && currentMenu == 0) {
drawAlarmNotification();
} else {
switch (currentMenu) {
case 0:
drawMainScreenRich();
break;
case 1:
drawSettingsScreenRich();
break;
case 2:
drawGraphScreenRich();
break;
case 3:
drawHistoryScreenRich();
break;
case 4:
drawSystemScreenRich();
break;
default:
currentMenu = 0;
drawMainScreenRich();
break;
}
}

display.display();
}

void handleButtons() {
static bool lastButtonStates[4] = {true, true, true, true};
static unsigned long lastButtonChange[4] = {0, 0, 0, 0};

int pins[] = {BTN_UP, BTN_DOWN, BTN_SELECT, BTN_BACK};

for (int i = 0; i < 4; i++) {
bool currentState = digitalRead(pins[i]);

if (currentState != lastButtonStates[i]) {
  lastButtonChange[i] = millis();
  
  if (currentState == LOW) {
    switch(i) {
      case 0: handleUpButton(); break;
      case 1: handleDownButton(); break;
      case 2: handleSelectButton(); break;
      case 3: handleBackButton(); break;
    }
    displayNeedsUpdate = true;
  }
  lastButtonStates[i] = currentState;
}
}
}

void handleUpButton() {
if (currentMenu == 1) {
adjustSetting(true);
}
}

void handleDownButton() {
if (currentMenu == 1) {
adjustSetting(false);
}
}

void handleSelectButton() {
if (currentMenu == 0) {
currentMenu = 1;
currentSetting = 0;
} else if (currentMenu == 1) {
currentSetting = (currentSetting + 1) % 4;
} else if (currentMenu == 2) {
currentMenu = 3;
} else if (currentMenu == 3) {
currentMenu = 4;
} else {
currentMenu = 0;
}
}

void handleBackButton() {
if (currentMenu == 0) {
currentMenu = 2;
} else if (currentMenu == 1) {
// 設定保存アニメーション
display.clearDisplay();
display.setCursor(40, 28);
display.print("Saving...");
display.display();
delay(500);
currentMenu = 0;
} else {
currentMenu = 0;
}
}

void adjustSetting(bool increase) {
float step = 1.0;

switch (currentSetting) {
case 0:
tempAlarmHigh += increase ? step : -step;
tempAlarmHigh = constrain(tempAlarmHigh, -40, 85);
break;
case 1:
tempAlarmLow += increase ? step : -step;
tempAlarmLow = constrain(tempAlarmLow, -40, 85);
break;
case 2:
humidityAlarmHigh += increase ? step * 5 : -step * 5;
humidityAlarmHigh = constrain(humidityAlarmHigh, 0, 100);
break;
case 3:
humidityAlarmLow += increase ? step * 5 : -step * 5;
humidityAlarmLow = constrain(humidityAlarmLow, 0, 100);
break;
}
}
            

📁 コードの特徴

  • ESP32-C3専用のピン配置に対応
  • アニメーション効果とリッチなUI
  • グラフ表示機能付き
  • アラーム機能とトレンド分析
  • 4つのボタンによる直感的な操作

まとめ

0.96インチOLEDディスプレイ(SSD1306)は表示と操作が一体化した便利なモジュールです

 

単なるディスプレイと違って4つのボタン付きなので、

メニュー操作や設定変更まで実装できるのが最大の魅力です

 

OLEDならではのコントラスト表示は暗い場所でも見やすく、

消費電力も少ないので電池駆動のプロジェクトにも最適です

 

AHT10やBMP280などのセンサーと組み合わせれば、

立派な測定器やIoTデバイス総額1000円以下で作れちゃいます!

 

今日も最後までお付き合いいただきありがとうございました。

私が買ってよかった商品はネットショップで販売できればと思っています。

似たようなものはAmazonでも購入できます。