外国のYouTuberの動画と、そこからのリンク先のページが参考になります。

外国のYouTuberの動画
Use Raspberry Pi 5 GPIO – Push button input – LED output – ChatGTP for code – GPIOD [PART 2 OF 2]

リンク先のページ
GPIO Programming: Exploring the libgpiod Library

 

以下、素人の私が書く事ですので、何処かに誤りがあるかも知れません。私本人は以下のプログラムが動くので、取り敢えずこれで良しと思っていますが。それを承知の上で参考にして下さい。

 

ラズパイ5のGPIOはgpiodで使うみたい。ラズパイ4まではpigpioでLチカ出来ていた。pigpioを使用したソースコードでもラズパイ5でコンパイルは成功する。だけど、そのソフトを走らせると不動作で「ラズパイでは無いようです」みたいに言われてしまう。

↓ この画像は、ラズパイ5にgpiod等をインストールして、/dev/gpiochip4を利用してLチカしている所です。

 

↓ C言語でGPIOを使用したい時に必要。

$ sudo apt install gpiod libgpiod2 libgpiod-dev

 

↓ chronyc sources

↑ PPSはこのラズパイ5。pi4やzero2にpi2は同じLAN内のラズパイで、どれもNTPサーバーです。PPSのラズパイ5、pi4、zero2の3つのラズパイは各々に繋がれたGPSの1PPSに同期中。pi2はLAN内のNTPサーバーを参照しています。

↓ 3つのLEDをchronyc sourcesの結果に応じて点灯させるプログラムのソースコード。

// Raspberry Pi 5
// pi@pi5:~ $ g++ -Wall -o chronyCheck chronyCheck.cpp -l gpiod

#include <err.h>
#include <cstring>
#include <iostream>
#include <gpiod.h>
#include <unistd.h>
#include <stdio.h>

#define BUF 256
#define H 1
#define L 0
#define PPS 10
#define NTP 9
#define NG 11

using namespace std;

enum kekka{
    pps_hantei = 10,
    ntp_hantei,
    ng_hantei
};

int mainLoop(void);

int main(void)
{
    if(daemon(0, 0) == 0) { // デーモン関数
        mainLoop();
    } else {
        cout << "error" << endl;
    }
    return 0;
}

int mainLoop(void)
{
    FILE *fp;
    char *ret;
    char str[512], *ptr;
    const char* cmdline = "chronyc sources";
    
    enum kekka hantei;
    
    gpiod_chip *chip;
    gpiod_line *line_PPS, *line_NTP, *line_NG;
    //For Raspberry Pi 5 use gpiochip4 (For Raspberry Pi 4 use gpiochip0)
    const char *chipname = "gpiochip4";
        
    // Open GPIO chip
    chip = gpiod_chip_open_by_name(chipname);

    // Open GPIO lines
    line_PPS = gpiod_chip_get_line(chip, PPS);
    line_NTP = gpiod_chip_get_line(chip, NTP);
    line_NG = gpiod_chip_get_line(chip, NG);

    // Open LED lines for output
    gpiod_line_request_output(line_PPS, "chronyCheck", 0);
    gpiod_line_request_output(line_NTP, "chronyCheck", 0);
    gpiod_line_request_output(line_NG, "chronyCheck", 0);
    
   
    // 無限ループ
    while (1) {
        
        // コマンドの chronyc sources 実行
        if ((fp=popen(cmdline,"r")) == NULL) {
            err(EXIT_FAILURE, "%s", cmdline);
        }
        
        // chronyc sources 実行結果を1行づつチェックしながら
        // 最後の行まで繰り返す。その1行毎に応じて i の値を足し算する
        // 但し、377を最優先して検索行に * と 177 や 277 等が有っても同期判定しない
        do {
            ret = fgets(str, BUF, fp); // 1行読み取り
            ptr = strstr(str, " 377 "); //  377 を検索(377前後のスペースも含む)
            if (ptr != NULL) {
                ptr = strstr(str, "#*"); // #* を検索
                if (ptr != NULL) {       // 検索行に #* と 377 があった時だけ1PPS同期判定とする
                    hantei = pps_hantei;
                    // cout << "Synchronizing to 1PPS & 377" << endl;
                    ret = NULL; // ここに来たらdo-whileから抜けて1行読み取りを終了する
                } else {
                    ptr = strstr(str, "^*");
                    if (ptr != NULL) {
                        hantei = ntp_hantei;
                        // cout << "Synchronizing to NTP" << endl;
                        ret = NULL; // ここに来たらdo-whileから抜けて1行読み取りを終了する
                    } else {
                        ptr = strstr(str, "^+");
                        if (ptr != NULL) {
                            hantei = ntp_hantei;
                            // cout << "Synchronizing to NTP" << endl;
                            ret = NULL; // ここに来たらdo-whileから抜けて1行読み取りを終了する
                        } else {
                            ptr = strstr(str, "^-");
                            if (ptr != NULL) {
                                hantei = ntp_hantei;
                                // cout << "Synchronizing to NTP" << endl;
                                ret = NULL; // ここに来たらdo-whileから抜けて1行読み取りを終了する
                            } else {
                                hantei = ng_hantei; // ここに来た場合は、1PPSにもNTPにも同期せず
                                // cout << "NG" << endl;
                            }
                        }
                    }
                }
            } else {
                hantei = ng_hantei; // 377が無い場合1PPSにもNTPにも同期せずとする
                // cout << "NG" << endl;
            }
        } while (ret != NULL);
    
        // 全LEDを消灯
        gpiod_line_set_value(line_PPS, L);
        gpiod_line_set_value(line_NTP, L);
        gpiod_line_set_value(line_NG, L);
    
        // hanteiに応じてLEDを点灯
        switch (hantei) {
            case pps_hantei:
                gpiod_line_set_value(line_PPS, H);
                sleep(70); // 1PPS同期判定時は70秒休憩
                break;
            case ntp_hantei:
                gpiod_line_set_value(line_NTP, H);
                sleep(16); // NTP同期判定時は16秒休憩
                break;
            case ng_hantei:
                gpiod_line_set_value(line_NG, H);
                sleep(4); // NG判定時は4秒休憩
                break;
            default: // ここに来るはず無いので意味合いとしては異常表示
                gpiod_line_set_value(line_PPS, H);
                gpiod_line_set_value(line_NTP, H);
                gpiod_line_set_value(line_NG, H);   
        }

        pclose(fp);
        
    }

    // 以下は無限ループ外なので不要?
    gpiod_line_release(line_PPS);
    gpiod_line_release(line_NTP);
    gpiod_line_release(line_NG);
    gpiod_chip_close(chip);
    
    return 0;
}

↑ デーモン関数はOS起動と共に起動して常に作動する為の物。それにはユニットファイル作成等も必要だけど。ここには記載していません。

コンパイル

$ g++ -Wall -o chronyCheck chronyCheck.cpp -l gpiod

OLEDのクリア方法を代えてパラパラを無くしました。

環境はこちらの別サイトに習い整えました。
Raspberry Pi: OLED-Display 128×64 mit Python ansteuern (I2C)

 

ここでは、上記環境下での画面クリアの違いを動画で見比べます。

動画前半(改善前)→ oled.cls()
動画後半(改善後)→ draw.rectangle((0, 0, 127, 63), outline=0, fill=0)

 

↓ 動画前半がパラパラした(瞬きする様な)表示になってしまっています。後半からはパラパラせず普通に見やすい表示器です。

 

↓ 動画後半のPythonコードで、無限ループする描画に関した部分です。

#oled.cls()
draw.rectangle((0, 0, 127, 63), outline=0, fill=0)
# ↑ oled.cls()の代わりに画面全体を黒色の四角で塗りつぶす。

draw.text((0, 0), data1, fill=1)
draw.text((0, 17), f'{data2:>6.1f}', font=DejaVuSansMono14, fill=1)
draw.text((0, 34), f'{data3:>6.1f}', font=DejaVuSansMono14, fill=1)
draw.text((0, 51), f'{press:>6.1f}', font=DejaVuSansMono14, fill=1)
    
draw.text((55, 17), "C", font=FreeSerif14, fill=1)
draw.text((55, 34), "%", font=FreeSerif14, fill=1)
draw.text((55, 51), "hPa", font=FreeSerif14, fill=1)

oled.display()

time.sleep(1)

oled.cls()の代わりにdraw.rectangleで画面全体を黒色の四角で塗りつぶし、それから必要な描画設定をして最後にoled.display()で表示させます。

この記事は「 OLED-Displayと等幅フォント 」からの続きです。

 

文字を反転させてハイライト表示にして、〇〇である事を強調するようにしてみました。

反転文字と書きましたが「反転したように見える」が正しいかも。

 

↓ Ambient(ambidata.io)へデータ送信した時だけに、OLED-Displayの四角内を反転させます。四角内の数値の単位は分でありカウントダウンします。表示の更新は1分毎ですが、送信は5分毎にしています。

↓ データ送信しない表示更新時は反転させません。

 

私が凄く参考にしているサイトです。
Raspberry Pi: OLED-Display 128×64 mit Python ansteuern (I2C)

 

↑ このサイトのソースコード “Beispiel 3” を参考にしています。↓

↑ A、B、Cの四角を描くソースコード(Python)です。↓

#!/usr/bin/env python
# coding=utf-8

# Bibliotheken importieren
from lib_oled96 import ssd1306
from smbus import SMBus
from PIL import ImageFont

# Display einrichten
i2cbus = SMBus(1)       # 0 = Raspberry Pi 1, 1 = Raspberry Pi > 1
oled = ssd1306(i2cbus)

# Ein paar Abkürzungen, um den Code zu entschlacken
draw = oled.canvas
DejaVuSerifBold14 = ImageFont.truetype('DejaVuSerif-Bold.ttf', 14)

# Display zum Start löschen
oled.cls()
oled.display()

# Formen zeichnen
draw.rectangle((80, 10, 127, 63), outline=1, fill=0) # A
draw.rectangle((10, 20, 100, 50), outline=1, fill=1) # B
draw.rectangle((0, 0, 20, 30), outline=1, fill=0)    # C

draw.text((110, 20), "A", font=DejaVuSerifBold14, fill=1)
draw.text((50, 25), "B", font=DejaVuSerifBold14, fill=0)
draw.text((4, 5), "C", font=DejaVuSerifBold14, fill=1)

oled.display()

outline や fill の値を1又は0にする事で、縁取りの線や塗りつぶしの色が白や黒になるようです。

ただ、outline を0した場合などは、四角の重ね位置により意図しない描画になる事がありました。

 

使用OLED-Displayは秋月電子通商の製品で通販コードP-12031です。

この記事は「 OLED-Display 128×64とPython 」からの続きです。

 

右寄せに嬉しいフォントが足元にありました。

フォントを FreeSans.ttf から DejaVuSansMono.ttf に変えました。

 

その足元とは、ラズパイの中です。Raspberry Pi OS Liteなのですが最初から入っていたのでしょうか? 等幅フォントで右寄せに良い感じのフォントです。

↓ その等幅フォントで表示させると温度、湿度、気圧の数値を右寄せに、小数点を揃える事が出来ました。それに、見やすいフォントなのでちょっと満足。因みに、日時のフォントは別のフォントです。

 

↓ こちらも右寄せなのですが、使用した FreeSans.ttf が等幅フォントでは無いせいか小数点の位置が不揃いです。気圧値がズレています。当初は、この状態で気持ち悪かったのですが、解決してスッキリです。

 

その等幅フォントは、DejaVuSansMono.ttf

/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf

↓ ラズパイの中をfindコマンドで

$ sudo find / -name "*.ttf"

この検索に複数ヒットして、その中にありました。

 

↓ FreeSans.ttf から DejaVuSansMono.ttf に変えた所のコード(Python)は

#FreeSans14 = ImageFont.truetype('FreeSans.ttf', 14)
DejaVuSansMono14 = ImageFont.truetype('DejaVuSansMono.ttf', 14)
oled.cls()

draw.text((10, 0), data1, fill=1) #日時
draw.text((10, 17), f'{data2:>6.1f}', font=DejaVuSansMono14, fill=1)#温度
draw.text((10, 34), f'{data3:>6.1f}', font=DejaVuSansMono14, fill=1)#湿度
draw.text((10, 51), f'{press:>6.1f}', font=DejaVuSansMono14, fill=1)#気圧
    
draw.text((65, 17), "C", font=DejaVuSansMono14, fill=1)
draw.text((65, 34), "%", font=DejaVuSansMono14, fill=1)
draw.text((65, 51), "hPa", font=DejaVuSansMono14, fill=1)

oled.display()

 

この記事の続きは「 OLED-Displayと反転文字 」です。

長い時間と過労(長時間のGoogle検索)を経て、表示させたい値をSSD1306使用0.96インチ128×64ドット有機ELディスプレイ(以下OLEDディスプレイ)に表示させる事が出来ました。

↑ Raspberry Pi 4のI2Cで各センサーから得た値をOLEDディスプレイに表示させている。データは5分毎にAmbient(ambidata.io)へ送り蓄積させています。↓

 

OLEDの表示に関して、凄く参考になったサイト

Raspberry Pi: OLED-Display 128×64 mit Python ansteuern (I2C)

 

↑ 凄く参考になったサイト(横文字でドイツ語?のサイトなので、私はコードを眺めるだけですが)の″Beispiel2″のコードを参考にしてフォントサイズを変えてみると。↓

↑ 日時のフォントサイズはデフォルト?で、温度、湿度、気圧のフォントサイズを14にしてみました。

 

フォントサイズに関するコード(Python)の一部です。

from lib_oled96 import ssd1306
from smbus import SMBus
from PIL import ImageFont
FreeSans14 = ImageFont.truetype('FreeSans.ttf', 14)
oled.cls()

draw.text((10, 0), data1, fill=1) # 日時
draw.text((10, 20), f'{data2:>8.1f}', font=FreeSans14, fill=1) # 温度値
draw.text((10, 35), f'{data3:>8.1f}', font=FreeSans14, fill=1) # 湿度値

draw.text((60, 20), "C", font=FreeSans14, fill=1) # 温度の単位
draw.text((60, 35), "%", font=FreeSans14, fill=1) # 湿度の単位

oled.display()

温度値と湿度値の表示は、桁数指定と右寄せをしています。気圧値の表示も右寄せをさせてますが、現時点で1000hPa未満の5桁(小数点を含む)にならないので確認出来ておらず恥ずかしいので気圧のコードは載せていません。

 

この記事の続きは「 OLED-Displayと等幅フォント 」です。