[Raspberry Pi] GPIOの学習を兼ねて色々盛って作ってみた

【読むのにかかる時間: 5 分】

Last Updated on 2026/02/05 by りょう

昨年末頃から引き続き自分の中で盛り上がっているラズパイ弄り…年明け早々にロボットカーを作ったことで更に拍車が掛かってきた模様。

ロボットカーでは搭載されたセンサーやサーボなどを操作・制御して様々な動きをさせることが出来る。
これはこれで楽しいし、未だに全ての機能を使いこなせているわけでもないけれど、次は自分が考えた操作・制御をハードとソフトの両方から作り上げていく形で試したい。
と考えて、ふと欲しいと思った機能を持った(盛った)ラズパイ四号機を組み立てることにした。
ちなみに、一号機は色々使い回して今は電子工作用になっているラズパイ3B+、二号機はタブレット(RasPad3.0…4B)、三号機はロボットカー(PiCar-X…4B)になる。

希望機能
・バッテリーバックアップのRTC
・UPSとバッテリー
・CPU温度に応じて回転ON/OFFと回転速度を可変するクーリングファン
・OLEDで各種情報表示(IPアドレス、CPU温度とファンの稼動状況、バッテリー残量、他)
・シャットダウンボタン
・電源スイッチ
そして、出来るだけコンパクトな金属製ケースに収めたい。

希望機能から先ずは主要モジュール(ボード、HAT)を選定し、ダンボールなどでモックアップを作って大まかな容積を出し、更に各モジュール間の接続などを検討した上で、全てが無理なく収まるケースを調査。
並行して、外付けのスイッチ(電源用スライドスイッチ、操作用タクトスイッチ)やGPIO端子の引き出し、I2Cの延長と分岐(RTC、OLED)などを検討。

或る程度構想が固まった時点で必要な部材と工具を洗い出して発注…最近はAmazonを始めとして中国製の安価な部材や工具を良く見掛けるが、以前に比べると品質が大いに向上していて価格も安いので、こうした趣味の物作りには非常に嬉しい。
まぁ、中にはハズレも有ったりするけれど、流石に使い物にならないような物はもはや無く、ちょっとした不具合も安いので大目に見られるレベル、小物(ネジやスタンドオフなど)は元の量が豊富なので殆ど問題無し。

外観(斜め前方から)
外観(斜め後方から)

構想から約一ヶ月。
ハード(ハンダ付け、ケーブル作り、板金加工までも)、ソフト(情報収集・表示・制御用スクリプト)など、時々トラブルも遭ったけれど、かなり楽しんで進めることが出来た。
当初想定していた機能は全て実装出来、操作性もまぁまぁ、サイズは幅95×高さ50×奥行き65mmと結構コンパクトにまとめることが出来た。

使用部材

部材一式(ネジやスタンドオフ類を除く)

・左列上から:金属製ケース、ラズパイ4B、純銅製ヒートシンク
・中列上から:クーリングファンHAT、UPSモジュール、LiPoバッテリー(3.7V/4,000mAh)
・右列上から:RTC(DS3231、CR1220)、OLED(0.91インチ)、自作ケーブル類

金属製ケース

元々は背の高い冷却システム(大型ヒートシンク&クーリングファン)の搭載を想定したケースで高さ方向に結構余裕があり、今回の用途にピッタリ。
また、フットプリントはほぼラズパイサイズだけど、GPIOコネクタ側が広めになっていて、外付けスイッチ類を内蔵するのに良さそう。
尚、ラズパイ固定用のダボ(雌ネジ受け)は二箇所(写真下側)にしかなく、他方(写真上側)は各コネクタの出っ張りをケース側面に引っ掛けて支える構造になっている。
このままではラズパイに重ねて装着する各種モジュールやバッテリーの重さでコネクタへ大きな負担が掛かるため、ラズパイの下側に短い(5mm)スタンドオフを装着して支え、ケースには両面テープで固定することにした。
上からケースの蓋で押し付ける感じになるため、強固に固定する必要は無い。

純銅製ヒートシンク

一般的な貼り付けタイプのヒートシンクは意外と高さ(厚さ)があるため、上に重ねるモジュールとの干渉の恐れが有るし、干渉を回避しようと間隔を空けるとケース内に収まらない。
このヒートシンクは熱伝導性が良い純銅製で、薄い(2mm)ため他モジュールと干渉し難く、間隔を最小限に出来る。
四ヶ所をネジ留めするのでしっかり固定出来るのも良し。
ちなみに、ロボットカーでも使用しており、剥き出しなこともあってかラズパイ4Bでもファン無しで全く問題無し。

クーリングファンHAT

ラズパイのGPIOコネクタに装着してCPU温度によって回転を制御するタイプのHATは幾つか出ているが、これはGPIOコネクタを自分でハンダ付けするタイプのため、このような拡張基板を追加するのが容易。
(ファンのドライブ回路…トランジスタ、ダイオード、抵抗…も自分でハンダ付けする必要がある。)
また、基板の一角にユニバーサルエリアが有り、自作の回路(今回はI2Cの延長と分岐)を実装出来る。
尚、オリジナルのファンは大きくて(40mm角×厚さ10mm)、上に重ねる他モジュールと干渉するため、以前KiWiSDRに使った小型の物(30mm角×厚さ7mm)に交換した。

ファンのPWM制御にはGPIO18が使用されるが、UPSボードもシャットダウン信号の通知用に同じくGPIO18を使用するため、ファン側をGPIO19に変更している。
(UPSボード側でも無改造で変更出来るが、シリアル信号(二本)と合わせてシンプルに横並べしたかった。)
変更は、基板上に明記されたカット位置でパターンをカットして、それぞれ用意されたスルーホールを使ってストラップ(上の写真でファンコネクタの手前を通っている青色の配線)をするだけ。

RTCとOLEDを装着

ユニバーサルエリアへのI2C各信号はラズパイ(+3.3V、GND)とファンHAT内(SDA、SCL)から供給し、RTCはピンヘッダに直挿し、OLEDはケーブルで接続。

拡張基板

ラズパイ用ブレッドボードから切り出して作成した拡張基板には、各種スイッチとGPIOコネクタから引き出したピンヘッダ(使用するGPIO、+5V、+3.3V、GND)を装着している。
スイッチは左から、電源、シャットダウン(長押しでシャットダウン)、OLED表示(押すと一定時間表示)。

UPSボード

3.7VのLiPoバッテリーを接続し、外部給電(USB-C)されている時は機器への給電とバッテリーへの充電、外部給電が途切れるとバッテリーから機器へ給電し、その切替時は一切瞬断無し。
機器への給電はUSB-A(二系統)の他に、直接ラズパイのGPIOコネクタ(+5V、GND)経由で行う方法があり、今回は給電性能が高く、ケーブルの引き回しがコンパクトになる後者を採用。
シリアル通信でバッテリー残量/外部給電有無/出力電圧などの情報を取得することが出来る。
他には、UPSボード自身にも電源スイッチ(ボード右端)が搭載されているが、外部スイッチ用コネクタ(ボード右端)も用意されている。(自照LED用端子も有り。)
尚、シリアル通信/シャットダウン通知用と給電用のコネクタ(ボード中程に有る横に並んだ3pinと2pinの物)は装着されていないので(ピンヘッダ付属)、手持ちのパーツを使用した。

今回、各コネクタ付きケーブルは全てシリコン電線を使用。
軟らかくて取り回しがし易く、それでいて強く、耐熱性も良い。
電流量が多い(+5V、3A以上)給電部分は22AWG、その他は28AWGを使い分けている。
基板部分の配線(I2C)にはポリウレタン線を使用。

ハード組み上げ

ケースへの組み込み
ラズパイのインタフェース(給電用USB-Cは使わないためカバーを装着)
各種スイッチとOLED

各部材を下から積み上げる感じでケースに固定していくが、窮屈ではないが結構込み入っているため、まるでパズルのよう。
事前にケーブルの引き回しを試行錯誤したおかげでちょうど良い感じの長さになったと思う。
バッテリーの上に載っているのは、ケースとバッテリーの隙間を埋めるための熱伝導シート。
両面テープだと粘着力が強くて剥がすのが大変だけど、これは程良い粘着力だし、バッテリーの熱をケースに伝えて放熱にもなる。
ちなみに、UPSへの給電用USB-C端子と各種スイッチ類はそのままではケース側面に蔽われてしまうため、元々の開口部(OLEDが覗いている部分)を一部上に切り拡げた。
UPSに実装されている電源スイッチを使用せず外付けスイッチを付けたのも、同じくケース(蓋部分の側面)に蔽われてしまい、更にそちらは開口部の拡張が難しかったため。

ソフト(スクリプト)作り

これで一応ラズパイとしては普通に使えるけれど、予定機能を実現するためにはソフトの作成が必要。
ラズパイのプログラミングも今回の目標なので、早速Pythonでスクリプトを作ることにした。
ちなみに、Pythonに触れたのはほぼ初めてだったりする。
プログラミング自体が随分久しぶりだけど(ロボットカーではブロックを並べる仕組みなのでちょっと雰囲気が違う)、自分が作ったハード/ソフトが自分の思う通りに動くのはやはり感動する。
そこに至るまでの試行錯誤も面白い。

今回作成したスクリプトはシステムの起動時に自動実行し、そのまま常駐する。
⇒「/etc/rc.local」ファイルの末尾(「exit 0」の上)に「python3 “スクリプトファイル名” &」を追記して再起動。

実装機能:
・スクリプト起動時に(システムの)起動完了を通知する。
・一定間隔(現在約1分)でCPU温度を監視し、規定値(40℃)を超えたらファンが回転する。
温度上昇に応じて回転数を可変(温度上昇⇒回転数上昇)し、上限値(70℃)を超えたらフル回転する。
・一定間隔(現在約1分)でバッテリー残量を監視し、規定値(30%)を下回ったら警告メッセージを表示、下限値(5%)を下回ったらメッセージを表示してシャットダウンを実行。
 実はUPSボード自身にもバッテリー残量監視機能が有るが、「残量が無くなるとシャットダウン通知をする」(GPIO経由で通知し、ソフトで監視⇒シャットダウン処理)という仕様なので、明確な残量を指定して警告とシャットダウンを個別に行いたく自分で組むことにした次第。
・OLED表示ボタンが押されたら情報(IPアドレス、CPU温度、ファン回転ON/OFF、バッテリー残量、外部給電可否)を取得し、一定時間(約10秒)表示する。
・シャットダウンボタンが長押しされたらメッセージを表示してシャットダウンを実行。

あと、ラズパイの生存(正常動作中=非フリーズ状態)確認をどうしようかと思案していたが、通常は正常・異常(フリーズ)関係無く通電されていれば点灯しっぱなしのPWR LEDを、正常時は点滅・異常時は点灯したまま(heartbeat)にソフト設定だけで行えることを知り、早速適用。
スクリプトでシャットダウンした場合も、このPWR LEDの点滅が停まれば電源をOFFに出来る。
⇒「/boot/config.txt」ファイルに「dtparam=pwr_led_trigger=heartbeat」を追記して再起動。

スクリプト

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import RPi.GPIO as GPIO
import time
import subprocess
import re
import netifaces as ni
from datetime import datetime
import board
from PIL import Image, ImageDraw, ImageFont
import adafruit_ssd1306
import os

# UPSモジュールのライブラリを使用
from upspackv2 import *
import tkinter as tk
import serial

# 定数定義
FANCONTROL_PIN      = 19 # 冷却ファンのPWM制御用GPIO端子
FANCONTROL_INTERVAL = 60 # 冷却ファンの制御間隔(秒)
OLEDDISP_BUTTON_PIN = 23 # OLEDの表示トリガー用GPIO端子
SHUTDOWN_BUTTON_PIN = 24 # シャットダウンの起動トリガー用GPIO端子

def main():
    # グローバル変数定義
    global G_date
    global G_temp
    global G_temp_num
    global G_fan
    global G_fanspd
    global G_ip_adrs
    global G_font1
    global G_font2
    global G_oled
    global G_UPS_version
    global G_UPS_vin
    global G_UPS_batcap
    global G_UPS_batcap_int
    global G_UPS_vout

    # ワーニング出力無効化
    GPIO.setwarnings(False)
    # BCM指定
    GPIO.setmode(GPIO.BCM)
    # BCMの19番ピンを出力に設定
    GPIO.setup(FANCONTROL_PIN, GPIO.OUT)
    # GPIO19の出力を50Hz(3000rpm)に設定
    pi = GPIO.PWM(FANCONTROL_PIN, 50)
    # デューティ比0で出力開始
    pi.start(0)
    
    # BCMの23番ピンと24番ピンを入力に設定
    GPIO.setup(OLEDDISP_BUTTON_PIN, GPIO.IN, GPIO.PUD_UP) 
    GPIO.setup(SHUTDOWN_BUTTON_PIN, GPIO.IN, GPIO.PUD_UP) 
    # OLEDDISPボタンのcallback登録(GIO.FALLING:立下りエッジ検出、bouncetime:1000ms)
    GPIO.add_event_detect(OLEDDISP_BUTTON_PIN, GPIO.FALLING, callback=oleddisp_callback, bouncetime=1000)
    # SHUTDOWNボタンのcallback登録(GIO.FALLING:立下りエッジ検出、bouncetime:1000ms)
    GPIO.add_event_detect(SHUTDOWN_BUTTON_PIN, GPIO.FALLING, callback=shutdown_callback, bouncetime=1000)

    # 初期値設定        
    G_temp = "41'C"                  # CPU温度
    G_temp_num = 41                  # CPU温度
    G_fan = 'ON'                     # ファン回転ON
    G_fanspd = 41                    # デューティ比
    pi.ChangeDutyCycle(G_fanspd)     # 出力のデューティ比を設定
    prev_fancontrol = datetime.now() # 前回の冷却ファン制御日時

    # UPSからの情報取得
    UPS_test = UPS2("/dev/ttyAMA0")
    G_UPS_version, G_UPS_vin, G_UPS_batcap, G_UPS_vout = UPS_test.decode_uart()
    G_UPS_batcap_int = int(G_UPS_batcap)

    # OLED設定
    i2c = board.I2C()
    G_oled = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c, addr=0x3c)
    # 画面消去
    G_oled.fill(0)
    G_oled.show()
    # 画面初期化
    image = Image.new("1", (G_oled.width, G_oled.height))
    draw = ImageDraw.Draw(image)
    # フォント設定
    G_font1 = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 10)
    G_font2 = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 14)

    # 起動完了表示
    # 表示イメージ作成
    disp_str1 = "BOOT COMPLETE"
    draw.text((0, 0),  disp_str1, font=G_font2, fill=255)
    # 画面表示
    G_oled.image(image)
    G_oled.show()
    # 消灯まで10秒待つ
    time.sleep(10)
    # 画面消去
    G_oled.fill(0)
    G_oled.show()

    try:
        while(True):
            # 現在時刻取得
            cur_time = datetime.now()
            # 前回の制御時刻から制御時間間隔を過ぎている場合は電源監視とファン制御を実行
            if (cur_time - prev_fancontrol).total_seconds() >= FANCONTROL_INTERVAL:
                prev_fancontrol = cur_time

                # 電源監視
                # UPSからの情報取得
                UPS_test = UPS2("/dev/ttyAMA0")
                G_UPS_version, G_UPS_vin, G_UPS_batcap, G_UPS_vout = UPS_test.decode_uart()
                G_UPS_batcap_int = int(G_UPS_batcap)
                # バッテリー残量が30%未満でLowBattery通知
                if G_UPS_batcap_int < 30:
                    # 画面初期化
                    image = Image.new("1", (G_oled.width, G_oled.height))
                    draw = ImageDraw.Draw(image)
                    # 表示イメージ作成
                    disp_str1 = "LOW BATTERY"
                    draw.text((0, 0),  disp_str1, font=G_font2, fill=255)
                    # 画面表示
                    G_oled.image(image)
                    G_oled.show()
                    # バッテリー残量が5%未満でシャットダウン実行
                    if G_UPS_batcap_int < 5:
                         # 画面初期化
                        disp_str2 = "SHUTDOWN!!!"
                        draw.text((0, 16),  disp_str2, font=G_font2, fill=255)
                        # 画面表示
                        G_oled.image(image)
                        G_oled.show()
                        # OSコマンドでシャットダウン実行
                        os.system("sudo shutdown -h now")
                        sys.exit()
                else:
                    # 画面消去
                    G_oled.fill(0)
                    G_oled.show()

                # ファン制御
                # CPU温度取得
                G_temp = run_shell_command("vcgencmd measure_temp")
                # ファンの駆動設定
                # 温度情報の数字部分を抽出
                G_temp_num = re.sub("'C", "",  G_temp)
                # 抽出した温度情報を数値化
                G_temp_num = float(G_temp_num)
                # 温度に応じて回転有無と速度を設定
                if G_temp_num > 40 and G_temp_num < 69:
                    G_fan = 'ON'
                    G_fanspd = int(G_temp_num)
                elif G_temp_num >= 69:
                    G_fan = 'ON'
                    G_fanspd = 100
                else:
                    G_fan = 'OFF'
                    G_fanspd = 0
                #CPU温度に応じて出力のデューティ比を変更
                pi.ChangeDutyCycle(G_fanspd)
                # 現在時刻取得
                G_date    = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
                print("[TEST] {}, temp:{}, fan:{}, fanspd:{}, batcap:{}%, ExtVin:{}".format(G_date, G_temp, G_fan, G_fanspd, G_UPS_batcap_int, G_UPS_vin))

            time.sleep(1)


    # Keyboard入力があれば終わり
    except KeyboardInterrupt:
        print("break")
        GPIO.cleanup()


# GPIO23のボタンが押されたら各種情報をOLEDに表示する
def oleddisp_callback(channel):
    # グローバル変数定義
    global G_temp
    global G_temp_num
    global G_fan
    global G_fanspd
    global G_ip_adrs
    global G_font1
    global G_oled
    global G_UPS_version
    global G_UPS_vin
    global G_UPS_batcap_int
    global G_UPS_vout

    # IPアドレス取得
    G_ip_adrs = ni.ifaddresses('wlan0')[ni.AF_INET][0]['addr']  # wlan0のIPアドレス

    # 画面初期化
    image = Image.new("1", (G_oled.width, G_oled.height))
    draw = ImageDraw.Draw(image)
    # 表示イメージ作成
    disp_str1 = "IP-adrs:{}".format(G_ip_adrs)
    disp_str2 = "Temp:{}ºC Fan:{}".format(G_temp_num, G_fan)
    disp_str3 = "Batt:{}% ExtVin:{}".format(G_UPS_batcap_int, G_UPS_vin)
    draw.text((0, 0),  disp_str1, font=G_font1, fill=255)
    draw.text((0, 11), disp_str2, font=G_font1, fill=255)
    draw.text((0, 22), disp_str3, font=G_font1, fill=255)
    # 画面表示
    G_oled.image(image)
    G_oled.show()
    # 消灯まで10秒待つ
    time.sleep(10)
    # 画面消去
    G_oled.fill(0)
    G_oled.show()


# GPIO24のボタンが長押しされたらシャットダウンする
def shutdown_callback(channel):
    # グローバル変数定義
    global G_font2
    global G_oled

    print("button pushed %s"%channel)
    sw_count = 0
    while True:
        sw_status = GPIO.input(channel)
        if sw_status == 0:
            sw_count += 1
            if sw_count >= 50:
                # 画面初期化
                image = Image.new("1", (G_oled.width, G_oled.height))
                draw = ImageDraw.Draw(image)
                # 表示イメージ作成
                disp_str1 = "SHUTDOWN!!!"
                draw.text((0, 0),  disp_str1, font=G_font2, fill=255)
                # 画面表示
                G_oled.image(image)
                G_oled.show()
                # OSコマンドでシャットダウン実行
                os.system("sudo shutdown -h now")
                sys.exit()            
                break
            else:
                print("short push {}".format(sw_count))
            time.sleep(0.01)
        else:
            break



# シェルコマンドを実行する関数
def run_shell_command(command_str):
    proc = subprocess.run(command_str, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
    result = proc.stdout.split("=")
    return result[1].replace('\n', '')


if __name__ == "__main__":
    main()

素人の手習い且つ随分久しぶりのコーディングなので無駄が多そう…。

起動完了
バッテリー残量警告
バッテリー残量不足⇒シャットダウン
手動シャットダウン
各種情報表示(外部給電可)
各種情報表示(外部給電不可)

新調した工具

今回新調した工具類(左から):
・ハンドニブラ(ニブリングツール)…ケース(1mm厚の鉄板)の開口部拡張用
・精密圧着ペンチ…コネクタの作成用
・ナットドライバ(3/16インチ)…M2.5スタンドオフの締緩用
・彫刻刀(三角、1.5mm)…プリントパターンのカット用

いずれも以前から欲しかった物で今回思い切って購入。
やはり工具が揃っていると作業が捗る…特に精密圧着ペンチはコレがあるおかげでケーブル作りが全く苦にならなかった。
余談だが、ナットドライバはAmazonで二本セットが単品(一本)より五割ほど安かった。

トラブル

ラズパイでハードを弄るのが初めてならPythonを使うのも初めて…当然全てが順調だったわけではなくて、ハード/ソフトで結構あちこち引っ掛かった。
トラブルシューティングは原因を突き止めて解決策を見出した時の達成感と爽快感があるので、さほど苦にならない(特に趣味だとね)。

  • RTCが認識出来なくなった
    最初は問題無く認識して時刻操作も正常に行えていたが、OLEDを追加すると認識が不安定になり、その内完全に認識しなくなってしまった。
    ⇒最初は普通に使えていたので初期不良ではなく、その後OLEDを追加する際に何度か外して付け替えていたので、もしかすると挿し間違えて逆電圧でも掛けてしまったのかもしれない。
  • タクトスイッチでチャタリングと誤認識が起こる
    一度押すと何度も押されたかのように認識したり、スイッチや接続しているGPIO端子を触れただけで押されたと認識してしまう。
    ⇒該当するGPIO端子のプルアップ設定を忘れるという実に単純なミス。
  • ラズパイの動作不安定
    ラズパイとUPSをシリアル接続しているとシリアル通信をしていなくてもラズパイが非常に不安定(極端に遅くなる、フリーズする)になってしまう。
    ⇒ラズパイの【Raspberry Piの設定】で【インターフェイス】の「シリアルポート」と「シリアルコンソール」の両方が有効になっていたため、試しに「シリアルコンソール」を無効にしたら解決したが、いずれちゃんと原因を調べよう。(試しにやって結果オーライでは成果にならない。)

さて

まだ初歩の初歩・基礎の基礎だけどラズパイのハードとソフト弄りのスタートを切った。
こうして組み上げた四号機の活用についてはまだスタート前。
さて、何に使おうか。
せっかくのバッテリー稼働とステータス表示があるので、屋外での単体使用に良さそう。
例えば…アマ無線のFT8運用とかね。

後日談①(Wi-FIのアクセスポートモード/クライアントモード同時対応)

屋外での単体使用・アマ無線のFT8運用…となると是非とも欲しい機能がある。
それは、Wi-Fiの二方向同時使用。
屋外&単体使用であればノートPCやタブレット、スマートホンで操作(VNC、SSH)することになり、その際にWi-Fiルータ等が必要だが、別持ちするのは嵩張るので避けたい。
また、FT8運用時には同時にラズパイからインターネットへのアクセスも行いたい。
インターネットアクセスは必須ではないが、交信直後にeQSLへのログのアップロード(⇒カード送付)を自動実行したり、PSKreporterへリアルタイムで情報提供する際には欠かせない。
それにインターネットアクセスが出来れば、NTPと同期してより正確な時刻補正が出来る。
USB接続のGPSレシーバーやIC-705内蔵のGPSを使えば、NTPが使えなくても正確な時刻補正が出来るけれど、内蔵RTCやNTPとの切替で挙動不審を起こしやすいという事例もあることから見送り。
現在は、NTPが利用出来る時はNTPがマスター/NTPが利用出来ない時はRTCがマスターになってシステムの時計を補正する構成にしている。(NTPがマスターの時はRTCの補正も行う。)

まずはラズパイをWi-Fiのアクセスポイントにすべく情報収集していると、アクセスポイントモード(AP、親機として他の端末を接続)とクライアントモード(STA、子機として他のAPに接続)の両方を、ラズパイ内蔵のWi-Fi機能だけで同時に実現する方法を発見。
早速導入…ラズパイをiPhone 8に接続(インターネット共有)してインターネットアクセスしつつ、ラズパイをアクセスポイントとして接続したiPad mini 5から操作(VNC、SSH)する環境が構築出来た。
もちろんiPhone 8の他に自宅のWi-Fiルータ(アクセスポイント)での使用もOK。
現在はiPhone8が最優先になっているため、自宅のWi-Fiルータに接続していてもiPhone8のインターネット共有を有効にして暫く経つと自動で切り替わっている。

ただ、クライアントモードでIPアドレスやDNSアドレスなどの設定を「自宅のWi-Fiルータ接続時は固定/iPhoneのインターネット共有接続時は未固定」と接続SSID毎に切り替える動作が未だ実現していない。
自宅ではVNCやSSHでの利便性を考えて設定を固定したいが、固定にするとiPhoneのインターネット共有で接続出来ているのにインターネットアクセスが出来ないという状態になってしまうため、現在はやむを得ず常に未固定にしている。
尚、アクセスポイントモードではIPアドレス固定で問題無い。

覚え書き(ヘッドレス&VNC操作時の低レスポンス回避)

作製した四号機はディスプレイなどを繋がないヘッドレスで、VNC経由での表示・操作がメイン。
暫く使ってみたところ、Chromium(標準でインストールされているWebブラウザ)が非常に重く、表示までに時間がかなり掛かる、スクロールバーを使ったスクロール操作に全く追従出来ない(というか殆ど動かない)、スクロールバーをクリック(タップ)して直接移動しても表示されるまでにかなり時間が掛かる。
また、文字入力でも一文字入力するのに秒単位で掛かるほどで、Googleなどの認証コード入力がタイムアウトになる始末。
ラズパイ4BでVNC経由の操作が遅いという記事は幾つか見掛けたが、同じラズパイ4Bを使っているRasPad 3.0では全く問題無く、内蔵ディスプレイはもちろんVNC経由でもスクロールバー操作でもスムーズに追従し、直接移動では即座に表示され、文字入力も問題無し。

Webブラウザ自体の問題だろうと考えて、軽いと評判のブラウザを試してみたものの、多少軽減はされるが軽さは全く感じられず。
しばらく悩んで、ふと気付いた…同じVNC経由でもRasPad 3.0はHDMI接続のディスプレイ内蔵、四号機はディスプレイ接続無し。
もしかしてVNC経由だけではなくヘッドレスも何か関係するのかも。
試しに四号機にHDMI接続のディスプレイを繋いだところ、VNC経由でもWebブラウザが不満無く使用出来た。

早速、ヘッドレス且つVNC操作時の低レスポンスを回避する方法を適用したところ、驚くほど改善された。
回避策自体は以前から知っていたが、VNC操作全般(マウス、キーボード)が遅くなるという事例であり、自分の場合はChromium(を含むWebブラウザ)以外では特に遅さを感じていなかったので、別の問題だと思い込んでいた。
実際に操作全般や他のアプリでは適用前から特に高速化した感じは無し。
ただ、JTDXのLagが明らかに減ったという嬉しい副効果は有ったかな。
それによってデコード率やコールバック率が上がったという実感は無いけれども。

ヘッドレス且つVNC操作時の低レスポンス回避方法(概略)

/boot/config.txt 変更(該当部分のみ抜粋)
#hdmi_force_hotplug=1 ⇒ hdmi_force_hotplug=1
hdmi_group=1 ⇒ hdmi_group=2
hdmi_mode=1 ⇒ hdmi_mode=32
dtoverlay=vc4-kms-v3d ⇒ #dtoverlay=vc4-kms-v3d

② デスクトップ解像度変更
・【メニューアイコン】⇒【設定】⇒【Raspberry Piの設定】
・【ディスプレイ】⇒【ヘッドレス解像度】⇒【1280×1024】
※外部ディスプレイを使用する場合は以下の設定も行う。
・【ディスプレイ】⇒【解像度を設定】⇒【DMT Mode 35 1280×1024 60Hz 5:4】

③ 再起動

後日談②(iPhoneのインターネット共有を介してiPad miniと接続)

iPhone 8のインターネット共有でインターネット接続をしつつ、併せて、iPad mini 5でVNC/SSH接続して操作したい…ということでWi-FiのAPとSTAを同時に運用する構成にしたが、ラズパイとiPad mini 5の両方をiPhone 8のインターネット共有に接続すれば、ラズパイとiPad mini 5の間で直接操作(VNC、SSH)が可能になる。
ラズパイのIPアドレスはその都度変わってしまうが、内蔵したOLEDにIPアドレスを表示出来るので、そのIPアドレス宛に接続すれば良い。
この構成であれば、iPad mini 5自体もインターネットアクセスが可能になる。

ようこそ!コメントをどうぞ♪