virt_flyのブログ -4ページ目

virt_flyのブログ

フライトシミュレーターソフトのFlightGearで仮想飛行を楽しむブログです。

Pico Wで実現したニキシー管風単管時計

↑Pico Wに置き換えが成功したニキシー管風単管時計

 

1.3インチCS無しカラーIPS液晶

Raspberry Pi Pico Wで、クセつよ2インチTFT液晶を用いてニキシー管風時計がまがりなりにも実現できたのに気をよくして、1.3インチCS無しカラーIPS液晶でも実現に挑みました。

 

もともとRaspberry Pi Zero Wで作製したニキシー管風時計に用いたディスプレイは、この1.3インチ液晶(st7789)でした。ただ、工作物に組み込まれていて、ばらすことになっては面倒なので、先に2インチTFT液晶を試したものでした。

 

 

 

■このケースでも2回に1回の電源の入れ直しが必要

 

1.3インチ用に作成したpythonプログラムは、一発で時計を動かすことができました。しかし、後になって気付いたのですが、なんと2インチTFT液晶(GMT020-02)同様、2回に1回は電源を入れなおさないと時計が動き出さないのです。原因はわかりませんが、こうなると、液晶側でなくPico W側が怪しくなってきます。AliExpressにて安価で購入したものなので、純正品でなく互換品の可能性が大であることが関係しているのかもしれません。

 

■画像の反転、回転も可

 

次いで問題となったのは、Raspberry Pi Zero W用に作成していたニキシー管風時計では、当該液晶を工作の都合で逆さまに固定することにしていたため、画像が倒立状態になってしまったことです。当初は、どこかでミスったのでしょう。上下反転や左右反転でも画像が表示がされずノイズが入ったりして、MADCTLが効かないと思い込んだのですが、ソフトウエアで反転はできたものの、描画に時間がかかって瞬時に加増転換できず時計としては使い物にならないことから、今一度MADCTLを試したところ反転、回転ができることがわかりました。なお、上下の位置のズレが生じていましたが、位置調整で解消が可能なものでした。

 

180度回転で倒立は解消。また左右反転により、ペッパーズ・ゴーストの仕組みを応用した疑似透過ディスプレイによるホログラム時計も簡単に実現できました。

 

Pico Wで実現したニキシー管風単管時計

↑2インチTFT液晶(手目)、1.3インチIPS液晶(奥)に表示をさせたニキシー管風単管時計

 

■Pico Wに置き換えが成功したニキシー管風単管時計

 

2回に1回の時計の起動の問題はあるものの、Pi Zero W用にニキシー管ぽく工作した単管時計がPico Wで動かすことが出来ました。置き換え成功です。なお、同じmain.pyで1.3インチと2インチの両方の液晶で時計が動きます。両者で表示位置に若干の違い、すなわち上下に多少ズレがありますが、液晶画面をはみだすものではありません。

 

以下にコードを示します。液晶用のドライバーは不要です。ピン接続はコードを見てください。画像ファイルは、0~9の数字を表すものと時刻表示の区切りをわかりやすくするためのブランク画像の計11枚を使用しています。

 

【ニキシー管風単管時計のPico用main.py(画像180度回転)】

 

from machine import Pin, SPI
import network, ntptime, time

SSID = "Wi-Fiネットワーク名"
PASSWORD = "パスワード"

BLACK = 0x0000

spi = SPI(
  0,
  baudrate=10000000,
  polarity=1,
  phase=1,
  sck=Pin(18),
  mosi=Pin(19),
  miso=None
)

dc = Pin(21, Pin.OUT)
rst = Pin(20, Pin.OUT)
cs = Pin(17, Pin.OUT)

# ---- SPI立ち上がり安定化 ----
def spi_warmup():
  cs(0); dc(1)
  spi.write(b"\x00")
  cs(1)
  time.sleep_ms(2)

def cmd(c):
  cs(0); dc(0)
  spi.write(bytearray([c]))
  cs(1)

def data(d):
  cs(0); dc(1)
  spi.write(bytearray([d]))
  cs(1)

def reset_display():
  rst(0); time.sleep_ms(80)
  rst(1); time.sleep_ms(150)

def init():
  cmd(0x11); time.sleep_ms(120)
  cmd(0x36); data(0xC0)
  cmd(0x3A); data(0x55)
  cmd(0x21)
  cmd(0x29); time.sleep_ms(20)

def init_stable():
  spi_warmup()
  reset_display()
  time.sleep_ms(60)
  init()
  time.sleep_ms(60)
  init()

def window(x0, y0, x1, y1):
  cmd(0x2A)
  data(x0 >> 8); data(x0 & 255)
  data(x1 >> 8); data(x1 & 255)

  cmd(0x2B)
  data(y0 >> 8); data(y0 & 255)
  data(y1 >> 8); data(y1 & 255)

  cmd(0x2C)

def fill(color):
  window(0, 0, 239, 319)
  hi = color >> 8
  lo = color & 255
  buf = bytearray(512)
  for i in range(0, 512, 2):
    buf[i] = hi
    buf[i+1] = lo
  pixels = 240 * 320
  cs(0); dc(1)
  for _ in range(pixels // 256):
    spi.write(buf)
  cs(1)

def draw_raw_135x240(filename, x, y):
  w = 135
  h = 240
  window(x, y, x + w - 1, y + h - 1)
  cs(0); dc(1)
  with open(filename, "rb") as f:
    while True:
      chunk = f.read(512)
      if not chunk:
        break
      spi.write(chunk)
  cs(1)

# ===== Wi-Fi / NTP =====
def connect_wifi():
  wlan = network.WLAN(network.STA_IF)
  wlan.active(True)
  wlan.config(pm=0xa11140)
  wlan.connect(SSID, PASSWORD)
  print("Connecting Wi-Fi...")
  while not wlan.isconnected():
    time.sleep(0.1)
  print("Connected:", wlan.ifconfig())

def sync_time():
  ntptime.host = "ntp.nict.jp"
  for _ in range(5):
    try:
      ntptime.settime()
      print("NTP synced")
      return
    except:
      time.sleep(1)
  print("NTP failed")

JST = 9 * 3600

def get_digits():
  t = time.localtime(time.time() + JST)
  h, m, s = t[3], t[4], t[5]
  return {
    "H10": h // 10,
    "H1": h % 10,
    "M10": m // 10,
    "M1": m % 10,
    "S10": s // 10,
    "S1": s % 10
  }

center_x = (240 - 135) // 2
center_y = (320 - 240) // 2 + 36

def show_digit(d):
  if d is None:
    draw_raw_135x240("digit_blank.raw", center_x, center_y)
  else:
    draw_raw_135x240(f"digit{d}.raw", center_x, center_y)

sequence = [
  ("H10", 0.7),
  ("H1", 0.7),
  ("BLANK", 0.3),
  ("M10", 0.7),
  ("M1", 0.7),
  ("BLANK", 0.3),
  ("S10", 0.7),
  ("S1", 0.7),
  ("BLANK", 0.8),
]

def main():
  print("INIT...")
  init_stable()
  fill(BLACK)
  draw_raw_135x240("digit0.raw", center_x, center_y)
  print("INIT DONE")

  connect_wifi()
  sync_time()

  while True:
    digits = get_digits()
    for kind, dur in sequence:
      if kind == "BLANK":
        show_digit(None)
      else:
        show_digit(digits[kind])
      time.sleep(dur)
main()

このコードはChatGPT、Copilotの助けを借りて作成しました

 

Raspberry Pi Pico WとTFTディスプレイ

↑Raspberry Pi Pico Wと2インチTFTディスプレイでニキシー管風時計をつくる

クセの強い2インチTFTディスプレイGMT020-02

 

またニキシー管風時計の話かよ、しつこいな、とお思いのことでしょうが、今回はRaspberry Pi Zero WをPico Wに置き換えようというもの。Zero Wは安くても3000円ほどし、Pico Wの方は互換品かも知れませんが半額の1500円台のものがありますから、画像表示で時刻を示すくらいのことにZero Wは使うのはいかにももったいない。思えば、Zero Wでもかつては1300円ほどだったんですが…。

 

■専用ドライバーの設定を踏襲が必須


無印のPicoでなくてPico Wの方を使用したのは、Wi-Fi機能を使って労せず時刻を取得できるから。リアルタイムクロック(RTC)は持っていないし、時刻設定の仕組みを設けるのは少々面倒ですからね。


今回使用したディスプレイは、クセの強い2インチTFT SPIディスプレイのGMT020-02です。Raspberry Pi Zero Wにて、ペッパーズ・ゴーストの仕組みを応用した疑似透過ディスプレイに使いました。Picoでの使用歴は、先の当ブログの「生成AIを御する」にてGMT020-02専用のドライバーを生成AIに作成させた記事で取り上げています。
 

GMT020-02 2インチTFT SPIディスプレイ

↑クセつよのGMT020-02の裏面

 

 

クセつよと言ったように、ST7789Vだとか言われるGMT020-02ですが、かなり特殊なもので今回も結局先に生成AIに作成させた専用ドライバーの設定以外では動かせるものはありませんでした。なお、ドライバーがどれだったかわからなくなっては困るので、今回はドライバーは使わず、main.pyのコード中に設定を取り込みました。

 

■画像はRaw形式、135x240に軽量化が必須


おかげで、何とか画像は表示できたものの、問題は画像ファイルのサイズ。この2インチTFTディスプレイはフルで240x320の画像ファイルが使えますが、そのままのサイズで0~9の数字をあらわす画像ファイルを用意しても、Picoの容量をオーバーしてしまいます。画像を軽量のRAW形式に、サイズも135x240に変換する必要がありました。RAW形式への変換は、生成AIにPythonプログラムを書かせて実行しました。

 

■なぜか2回に1回は電源の入れ直しが必須

 

面倒はその後にまだ控えていました。電源をONにしても1回では時計表示にいたらず、電源を入れなおす必要があったのです。

 

RST の保持時間、初期化順序、SPI の再初期化、コマンド順の調整、遅延の追加、RST 2回、MADCTL の変更、画面クリアの順序変更、電源投入後の待ち時間追加、などとソフトでできそうなことはさまざま行ってみたが改善は見られず、発想を変えて初期化に失敗したら自動リブート(ウォッチドッグタイマーWDTを使う)もだめ、RUNピンを制御してfail_rebootも効果はなく、結局ハードが悪いと結論付けるしかありませんでした。

 

Pico Wへの置き換えにはもう一つの目的がありました。電源のON/OFFだけで時計を立ち上げ、終了させることができるということです。ところが、2回に1回しか時計が動き出さない点では、不都合と言わざるをえません。それでも、電源を入れなおすだけ、それも音声操作でもう一度電源をONにするように言えば済むことですから、少しばかりは我慢すべきかも。

 

■画面が大きいだけにきれい

 

ニキシー管風時計 Raspberry Pi Pico W 2インチTFT

↑画像サイズは135x240と小さいが、結構鮮明できれい

 

時計の起動にまつわる不都合とともに、液晶画面の周囲の白い枠がニキシー管風時計に不似合いなのが少々不満で、黒く塗りつぶしてやりたいところですが、1.3インチの液晶なんかに比べれば画面が大きくて、画像ファイルが135x240にもかかわらず、鮮明なところがきれいで好印象です。

Raspberry Pi Pico 4計器表示

↑基本4計器の同時表示が実現(画像を差し替えました 思えば長時間露光すれば同期を気にせず写真が撮れます)

 

できた!T字配列


●せめて基本4計器の同時表示を実現したくて

 

航空機の重要な計器の配置の姿からBASIC T(T亊配列)という言葉があります。真ん中に姿勢指示器、その左右に速度計と高度計、下に方位計の4つの計器からなります。

 

この間、フライトシミュレーターのFlightGearの飛行中のデータを、Raspberry Pi Picoを使って加工し、OLED画面に表示させようと取り組んできました。前回は、2つのOLEDにミニPFDと方位コンパスだけのHSI、あるいは速度計と高度計を表示させるところまで到達しました。ここまでくると、やはりT字配列の4つの計器くらいは、4つのOLEDを使って表示できるようにしたいものです。

 

●模索の末にPicoの2個使い

 

4つの計器の実現のためにOLEDを4枚使用するとなると、Picoの2つのI2CにOLED2つをぶら下げる形になるのが一般的かと思われます。

 

1つのI2CにOLEDを2枚つなげるには、0.96インチOLEDの2つのアドレスを切り替えて使う方法がありますが、前回もふれたようにハードの改造が必要になります。抵抗を付け替えるだけのこととはいえ、小さな部品をハンダ付けするのは、素人にはハードを壊すリスクがあります。

 

かわって、1つのI2Cを分岐させるI2C分配チップのTCA9548Aを使う方法もあるようですが、手元になくて購入の必要があるということで見送り。残すはI2Cをソフトで増やす方法を試すのみとなったものの、実現には至りませんでした。
 

ここまでくれば破れかぶれ、1個のPicoで2枚のOLEDが使えたのだから、同時に2個なら都合4枚のOLEDが使え、手っ取り早いと思うじゃないですか。もちろん、Pico2つで1つは受信専用、1つは表示専用として使うほどに、負荷を分散させて安定をはかるくらい繊細なPicoのことですから、Pico2つをパソコンにつなぐ際にUSBハブが必要になるとなれば、不安定さが気にもなります。どうもパソコンのUSBポートが経年劣化してき、不安定であることを思えばなおさらです。おそらく非推奨を覚悟していたのですが、ChatGPTに聞けばできるというので挑戦することにしました。


Raspberry Pi Picoで4計器表示

↑安物のブレッドボードのせいでジャンパー線だらけ。見てくれは悪いですが、私のFlightGearに新たな楽しみ方が一つ増えました

 

●方法

 

Pico用Micropythonコードは、従前のものがそのまま使用可能です。ただし、扱うFlightGearデータの数の違い、すなわち速度や高度だけからピッチやロール機首方位までなどの数の違いから起きるエラーは、Pico側のコードで対処でき、その1行であるif len(s) >= 〇:の〇部分の数字を、一番多いデータ数に変えるだけで回避ができます。したがって、扱うデータを指定するxmlファイルも複数設ける必要はなく、一つで済ませられるのも助かります。

 

実質変更が必要なのは、パソコン側PCブリッジのコードでUDPを2か所に送るように設定(PicoのCOMポートに注意)、FlightGear側も起動オプションでUDPを2つ出すようにするだけ。ピン配置もいじらなくてよく、これなら楽です。

 

・パソコン側PCブリッジの改良

 

import socket
import serial

ser1 = serial.Serial("COM3",115200)  ←COMポートは例 Picoのcomポートに合わせます
ser2 = serial.Serial("COM4",115200)  ←COMポートは例 もう一つのPicoのcomポートに合わせます

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1",5501))

print("Bridge start")

while True:
    data, addr = sock.recvfrom(1024)

    ser1.write(data)
    ser2.write(data)

 

・FlightGear起動オプション

 

 --generic=socket,out,30,127.0.0.1,5501,udp,fgdata
 --generic=socket,out,30,127.0.0.1,5502,udp,fgdata

 

ここでfgdataは/Protocolフォルダに置いた扱うデータ指定用のxmlファイル名

 

●私のFlightGearの楽しみ方が1つ増えました

 

無事、速度計、水平儀、高度計、方位コンパスの4つを、それぞれ4つのOLEDに表示させることができました。各計器ともFlightGaerの画面表示に遅れることなく順調に動いています。さすがに0.96インチでは画面があまりに小さくて、目盛の文字に比べても計器が小さく、見づらいものになっていて、もう少しサイズの大きいディスプレイが欲しいところです。

 

安物のブレッドボードを使ったせいで、直接ジャンパー線でつながないと動かない憂目にあい、写真のように配線がうざいことになっています。せめてパネルで覆うことぐらいはしたらよいのですが。なお、目的がコクピットの実体化ではなかったので、今回の連載もこれくらいでおしまいとします。

 

もし、自分のコクピットを作ろうというのであれば、やはり計器は最低6つ、そしてスロットルレバーくらいは欲しいですね。