virt_flyのブログ

virt_flyのブログ

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

OLEDで表示する速度計と高度計↑Raspberry Pi Picoと2枚のOLEDを使って、FlightGearの飛行中のデータを反映する速度計、2針の高度計を実現
 

2枚のOLEDで計器を2つに

 

■2系統あるI2Cを使って2枚のOLEDに描画


この間、フライトシミュレーターのFlightGearで飛行中のデータを、Wi-FiやUSB接続でRaspberry Pi Picoに送り、OLED画面にミニPFD(Primary Flight Display、水平儀・速度・高度)を描かせてきました。今回は、このOLEDを2枚使って、ミニPFDとなんちゃってHSI(Horizontal Situation Indicator、方位指示器)をこしらえてみたいと思います。

OLEDを2枚使うとなれば、その方法は概ね次の2つでしょう。

一つは、今使っている0.96インチのOLEDは2つのI2Cアドレス(0x3C、0x3D)を持つことができるので、アドレスを2つに分けて並列接続する方法。もう一つは、PicoにはI2Cが2系統あり、これを利用する方法です。

前者の場合は、OLED基板裏面のジャンパパッド部分の抵抗を付け替えることでアドレスを切り替えるのですが、工作のハードルが少々高く、今回は見送って後者を採用しました。

I2Cを2系統使うということは、1枚目のOLEDのSDAピンをGP0、SCLをGP1に、2枚目のOLEDのSDAをGP2、SCLをGP3に接続します。この辺りのプログラムの書き方は、実際を後に示しますので確認してください。

 

いじるプログラムは、基本的にPico側パイソンプログラム(main.py)。必要に応じて使うFlightGearデータを書き込むxmlファイル(fgdata.xml)です。パソコン側のパイソンプログラムやFlightGearの起動オプションはこれまで通りで、変更はありません。

 

楽するために、計器のプログラムの作成には生成AI利用しました。とはいっても、次に述べるように楽ができるかどうかは、生成AIの使い手次第です。

 

■生成AIはロジック偏重

Raspberry Pi Pico OLEDでFlightGearデータ表示

↑改良したミニPFDと単なる方位コンパスのなんちゃってHSI

 

前回のブログ「生成AIを御する」にて触れたことですが、生成AIも使い手次第。ときには迷路に誘い込まれ、動いていたプログラムを復旧するのに四苦八苦することもあります。

今回も特にひどかったのが、流れる文字列を画面の特定箇所でマスクを使い非表示にし、その上に別な文字列を固定表示させようとしたときに起こりました。

マスクが少しずれていて、文字列が一部重なりあって読みにくくなっていたのですが、生成AIは文字列の間隔を詰めてズレの範囲に収めようと試みたり、マスクの幅を広げるのはいいが逆の方向に広げるものだから余計なところが非表示になってしまうというお粗末さ。

こうした場合、マスクをズレた分動かし、必要ならサイズを変更する、というのが普通じゃないかと思うのですが。まさに、居酒屋で客が騒いでうるさい時、BGMの音を大きくしてかき消そうとするようなもの。ますますやかましく事態は悪化するばかりです。本来的には、騒がしい客に少し静かにするように注意するとか、自国ファーストで平和外交を疎んじる今であれば、四の五の言わずに相手の顔面に一発お見舞いし黙らせる、というところでしょう。

我慢できなくなって指摘するまで生成AIも気が付かない。ロジックを重視しがちと生成AI自ら弁解していました。これまで動いていたプログラムを借りて少し変えたいとき、うまくいかなければ変えた部分を疑うべきなのに、生成AIはこれまで動いていたプログラムをいじりだそうとすることがしょっちゅうです。

 

今回も、ミニPFDと方位コンパスだけのなんちゃってHSIを2枚のOLEDで実現した後、該当箇所を取り換えて速度計と高度計を作ろうとしたときのことです。うまく動かなかったら早速変えたところと関係のない箇所を変えようとしだしました。いままでちゃんと動いていたところを変更して、動かなくなったらどうするの。万事がこれですから、要注意です。他にも右と左、上下もでしょうか、よく間違うようです。迷路に落ち込まない、迷路から早く抜け出すためには、間違っている、おかしいと感じたら早目はやめに指摘して、軌道修正してやることが大事です。

 

■PFDの改良、なんちゃってHSI・簡素な速度計・2針式高度計の作成

OLEDに表示された速度計と方位計

↑ヌルヌル目盛の動くミニPFD、東西南北しか目盛りのないHSI

 

今回のミニPFDでは、水平儀の左右にある速度計と高度計の上下する目盛部分を、ほんのわずかですが上下の数字に隙間をつくり棒線を入れ、四角い枠線の中は実際の値を表示させるなど本物に少しでも似せようと改善しました。あまりに液晶画面が小さいもので、 その努力が実ったとは言えません。

 

HSIといってもただの方位コンパスにすぎませんが、方位を示す角度の数字を加えると、液晶画面の狭さから数字同士が重なってどうにもみっともないものにしかならず、結局東西南北の方位を示すだけの見栄えのしないものになりました。

 

軽飛行機風の速度計と高度計の作成では、方位コンパスの失敗に学び、円形の目盛の外側に数字を配置することにしました。字同士の重なりはなくせましたが、目盛りの円形が少々小さくなり、その中に速度や高度の実際値を表示させるようにしたものですから、針が見えにくくなってしまいました。なお高度計は2針式で、長身が100ft刻み、短針が1000ft刻みを表します。

 

やはり0.96インチのOLEDでは画面が狭すぎますが、成功するかどうかのわからないリアル計器パネルを作るのもどうかと思うし、そのために金をかけるわけにもいきません。それでも、Aliexpressで5個920円(現在790円)という1個200円を切る値段で販売されていたものですから、ついポチってしまいました。

 

とはいえ、4計器や6パック(速度計、姿勢指示器、高度計、旋回計、方位計、昇降計)の実現は、ハードの改造が伴うのでハードルが高そう。とりあえず、ここまでできたことを良しとすべきかも知れません。

 

【参考】

 

速度計と高度計を例に、2枚のOLEDを使ったプログラムを掲載します。

 

使い方は「FlightGearのデータを外部表示する(4)」までのブログ記事をご覧ください。

 

・fgdata.xml

 

<?xml version="1.0"?>
<PropertyList>
 <generic>
  <output>
   <line_separator>\n</line_separator>
   <var_separator>,</var_separator>
   <chunk>
     <name>airspeed</name>
     <node>/velocities/airspeed-kt</node>
   </chunk>
   <chunk>
     <name>altitude</name>
     <node>/position/altitude-ft</node>
   </chunk>
  </output>
 </generic>
</PropertyList>

 

このxmlファイルは、FlightGearのデータファルダ内のProtocolサブフォルダに置きます。

 

・main.py

 

from machine import Pin, I2C
import ssd1306
import sys
import math

# ========= I2C =========
i2c0 = I2C(0, scl=Pin(1), sda=Pin(0))
i2c1 = I2C(1, scl=Pin(3), sda=Pin(2))

oled1 = ssd1306.SSD1306_I2C(128, 64, i2c0)
oled2 = ssd1306.SSD1306_I2C(128, 64, i2c1)

speed = 0
alt = 0

# ========= 右寄せ =========
def draw_right(oled, text, y, right_edge):
  x = right_edge - len(text) * 8
  oled.text(text, x, y)

# ========= USB =========
def read_usb():
  global speed, alt
  try:
    line = sys.stdin.readline()
    if line:
      s = line.strip().split(",")
      if len(s) >= 2:
        speed = float(s[0])
        alt = float(s[1])
  except:
    pass

# ========= 速度計 =========
def draw_speed(oled, speed):
  cx, cy = 64, 32
   r = 22

  oled.fill(0)

  # 外円
  oled.ellipse(cx, cy, r, r, 1)

   # ===== 円内タイトル =====
  oled.text("SPD", cx-12, cy-r+10)

  # ===== 目盛り =====
  for v in range(0, 181, 20):
    if v == 180:
      continue

    ang = math.radians(-90 + (v / 180) * 360)

    x1 = int(cx + (r-2) * math.cos(ang))
    y1 = int(cy + (r-2) * math.sin(ang))
    x2 = int(cx + r * math.cos(ang))
    y2 = int(cy + r * math.sin(ang))
    oled.line(x1, y1, x2, y2, 1)

    xt = int(cx + (r+6) * math.cos(ang))
    yt = int(cy + (r+6) * math.sin(ang))

    if 90 < v < 180:
      xt -= 14
    elif v < 90:
      xt += 4

    oled.text(str(v), xt-6, yt-4)

  # ===== 針 =====
  ang = math.radians(-90 + (speed / 180) * 360)
  x = int(cx + (r-5) * math.cos(ang))
  y = int(cy + (r-5) * math.sin(ang))
  oled.line(cx, cy, x, y, 1)

  # ===== 円内 実測値(右寄せ)=====
  draw_right(oled, str(int(speed)), cy + r - 16, cx + r - 10)

  oled.show()

# ========= 高度計 =========
def draw_alt(oled, alt):
  cx, cy = 64, 32
  r = 22

  oled.fill(0)

  # 外円
  oled.ellipse(cx, cy, r, r, 1)

  # ===== 円内タイトル =====
  oled.text("ALT", cx-12, cy-r+10)

  # ===== 目盛り =====
  for v in range(0, 10):
    ang = math.radians(-90 + v * 36)

    x1 = int(cx + (r-2) * math.cos(ang))
    y1 = int(cy + (r-2) * math.sin(ang))
    x2 = int(cx + r * math.cos(ang))
    y2 = int(cy + r * math.sin(ang))
    oled.line(x1, y1, x2, y2, 1)

    xt = int(cx + (r+6) * math.cos(ang))
    yt = int(cy + (r+6) * math.sin(ang))
    oled.text(str(v), xt-4, yt-4)

  # ===== 長針(100ft)=====
  ang1 = math.radians(-90 + (alt % 1000) / 1000 * 360)
  x1 = int(cx + (r-4) * math.cos(ang1))
  y1 = int(cy + (r-4) * math.sin(ang1))
  oled.line(cx, cy, x1, y1, 1)

  # ===== 短針(1000ft)=====
  ang2 = math.radians(-90 + (alt % 10000) / 10000 * 360)
  x2 = int(cx + (r-10) * math.cos(ang2))
  y2 = int(cy + (r-10) * math.sin(ang2))
  oled.line(cx, cy, x2, y2, 1)

  # ===== 円内 実測値(右寄せ)=====
  draw_right(oled, "{:5d}".format(int(alt)), cy + r - 16, cx + r - 4)

  oled.show()

# ========= LOOP =========
while True:
  read_usb()
  draw_speed(oled1, speed)
  draw_alt(oled2, alt)

ここに示したプログラムはChatGPTの助けを借りて作成しています。

 

このMicroPythonプログラム(main.py)は、Pico Wに書き込みます。

 

・FlightGearの起動オプション・fg-pc_bridge.py・start_fg.bat

 

FlightGearの起動オプションやfg-pc_bridge.py、start_fg.batについては割愛します。
 

 

《関連》

 

 

 

 

 

Raspberry Pi Picoで生成AIにドライバを書かせる

 

Pico用ST7789ドライバをつくる

 

■Pico用のST7789ドライバがない?

 

Raspberry Pi Pico Wに2.0インチのTFTディスプレイをつなごうとしたら、ST7789のドライバがないんですね。これまでも、そんなことがあったし、以前はあったのにドライバがThonnyのパッケージ管理に出てこなくなったということも。

 

Pico 2が販売されるようになったので、すっかり過去の物扱いだとかマイナーなんてことでもなさそうだし、なにかよくわかりません。

 

最初は、パッケージ管理にそれらしいものがでてきたのでインストールしたものの動かず、生成AIに尋ねたらCircuitpython用のドライバだと言われてしまい、次に教えられたレポジトリに行ったらビルドが必要ものしかみつからない。結局、ドライバの全文を示せと言ってやってようやく手に入れたかと思えば動かない。驚いたのは、動かないと言ったら、生成AIの奴が生意気にも環境にあったドライバを書いてやると言うんです。

 

■生成AIの迷走

 

まあ、それで書かせてみたのですが、たった一言”Hello”と表示させるのにすったもんだ。複数の生成AIを使ったもののさっぱりだめで、「ST7789じゃない他のドライバだ」とあちこち引っ張り回され堂々めぐりを繰り返されたり、気が付けば途中で勝手にピン配置を変えられたり、あげくRaspberry Pi Zeroで表示できたと伝えているのに、ハードが壊れているとのたまう。最後には7ピンのこのタイプはZero用で8ピンのArduino用を購入せよとまで言われました。

 

ちなみに、これはと思う製品が見つかればチェックしてやるので販売サイトのURLを知らせろというので教えたら、これはダメだというのです。実はPicoで動かせた実例の中で示された購入先の物だったので、そのことを伝えたら態度がコロッと変わり、動くと言い出す始末。

 

Pico ST7789ドライバで表示できた画面

↑7ピンの2.0インチTFT液晶に図や文字が表示できた

 

ほぼ諦めかけていましたが、試行錯誤の中で一瞬色塗りができたり、文字が表示されたこともあったので、全くダメというのも一概に言えない気がしてしつこく試した結果、4日目にして描画、文字表示に成功しました。諦めておれば、それまでの3日間を無駄にするところでした。やればできる子じゃないの。

 

■ドライバが書けたのは

 

成功したのは、まず第1に目的と使用するものの特徴、他で成功した例などを的確に示すことを心掛けたとき、色塗りに成功したのです。その後、過大な要求(航空機のPFDを一気に作ること)をしたことで、またまた迷走することになりましたが。

 

第2は、迷走に入る前の時点に立ち戻って、そこから再挑戦したこと。少しつまづきはあったもののあっさりと目的の描画と文字表示ができるようになったのです。

 

細かな教訓も多々ありますが、なによりも大事なのは、使い手の的確な指示、迷走したらチャラにして迷走前に戻って再チャレンジ、また無駄に時間を費やさないためには、迷走しそうな気配があれば早めに指摘し、発想を変えることを促すことであると感じます。

 

■使い手がアホなら生成AIもアホになる

 

生成AIの迷走を悪し様にあげつらいましたが、実のところはどんなに優れた道具も、それを生かすも殺すも使い手次第、ということの典型です。使い手がアホなら生成AIもアホになるわけで、責任は使い手にあることを、今回よく思い知らされました。

 

・的確な指示

プログラミングに生成AIは使えるなと思う反面、気を付けたいこともあります。いつまでも文句を言うこともなく、結論まで付き合ってくれることはありがたい生成AIなのですが、一歩間違うと延々とおかしな方向に突き進むことがあります。都度都度、的確な指示を出してやることが大事であり、最初から的確、明瞭な指示を出しておくことです。。

 

・早目の指示

タイパのご時勢です。無駄に時間を浪費しないためにも、明らかに間違っていることはもちろんですが、勘であっても何か違うような気がするときは早目に指摘してやった方がよいでしょう。一気に解決に向かうことが、度々ありました。

 

・生成AIの誤りを正す

生成AIも勘違いします。ピン配置を間違えていたり、描画の際の左右を間違えるということもあります。こちらの要望すらたまに忘れたりということもありましたから、遠慮せずに指摘してやらないといつまでもなおらないことになります。

 

・時々プログラム全文を書かせる

生成AIは、プログラムの修正点だけを示してくることがありますが、挿入個所の誤りやインデントの乱れを起こす原因になります。プログラムの全文を書かせることも大事です。

 

・異なる生成AIを使うのも手

使い手の指示の出し方に負うところ大というのが持論ですが、上手くいかないときは別な生成AIに質問するのも手だと思います。得手不得手もあるのか、一気に解決に至ることも少なくありません。

 

・あきらめないこと、疑問に感じること

生成AIを使って成功するには、諦めないことも重要です。時間制限などの制約がないわけではありませんが、文句も言わず(高ビーなところはある)、解決、あるいは結論の出るまでつきあってくれるのですから。そのためにも、疑問を大事にすべきと思います。生成AIの性急な結論に惑わされず、本当に壊れているのだろうかとか、ある程度成功しているのだからできないのは発想の転換が必要ということではないかとか、実際に成功しているといいながら片方でこれにはできないと矛盾したことを言っていないか、など。素人なりに頑張ってみたいと思います。

 

生成AIの進歩は急速で、ブログに書くのが追い付かないくらいのスピードです。描画機能が日々充実していくのに驚いた半面、的確なプロンプトを書くのに難儀したこともありましたが、逆に余計なお節介も困りもの。最近はそれも減り、結構アドバイスが的を射たものになってきたように思います。生成AIの進歩が楽しみですね。

 

PicoとTFTディスプレイを接続した実験

↑ようやくPico用に最低限の機能を備えたst7789ドライバができた

 

 

【作成したドライバst7789.pyの全文】

 

from machine import Pin import time

BLACK=0x0000
WHITE=0xFFFF
RED=0xF800
GREEN=0x07E0
BLUE=0x001F
YELLOW=0xFFE0
CYAN=0x07FF
MAGENTA=0xF81F


class ST7789:

  def __init__(self,spi,width,height,reset,dc,cs):

    self.spi=spi
    self.width=width
    self.height=height
    self.reset=reset
    self.dc=dc
    self.cs=cs

    self.cs.init(Pin.OUT,value=1)
    self.dc.init(Pin.OUT,value=0)
    self.reset.init(Pin.OUT,value=1)

  def cmd(self,c):

    self.cs(0)
    self.dc(0)
    self.spi.write(bytearray([c]))
    self.cs(1)

  def data(self,d):

    self.cs(0)
    self.dc(1)
    self.spi.write(bytearray([d]))
    self.cs(1)

  def reset_display(self):

    self.reset(0)
    time.sleep_ms(50)
    self.reset(1)
    time.sleep_ms(50)

  def init(self):

    self.reset_display()

    self.cmd(0x11)
    time.sleep_ms(120)

    self.cmd(0x36)
    self.data(0x00)

    self.cmd(0x3A)
    self.data(0x55)

    self.cmd(0x21)

    self.cmd(0x29)
    time.sleep_ms(20)

  def window(self,x0,y0,x1,y1):

    self.cmd(0x2A)
    self.data(x0>>8)
    self.data(x0&255)
    self.data(x1>>8)
    self.data(x1&255)

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

    self.cmd(0x2C)

  def pixel(self,x,y,color):

    self.window(x,y,x,y)

    hi=color>>8
    lo=color&255

    self.cs(0)
    self.dc(1)
    self.spi.write(bytearray([hi,lo]))
    self.cs(1)

  def fill(self,color):

    self.window(0,0,self.width-1,self.height-1)

    hi=color>>8
    lo=color&255

    buf = bytearray(512)

    for i in range(0,512,2):
      buf[i]=hi
      buf[i+1]=lo

    self.cs(0)
    self.dc(1)

    pixels=self.width*self.height

    for _ in range(pixels//256):
      self.spi.write(buf)

    self.cs(1)

  def fill_rect(self,x,y,w,h,color):

    self.window(x,y,x+w-1,y+h-1)

    hi=color>>8
    lo=color&255

    buf=bytearray(w*2)

    for i in range(0,w*2,2):
      buf[i]=hi
      buf[i+1]=lo

    self.cs(0)
    self.dc(1)

    for _ in range(h):
      self.spi.write(buf)

    self.cs(1)

  def hline(self,x,y,w,color):
    self.fill_rect(x,y,w,1,color)

  def vline(self,x,y,h,color):
    self.fill_rect(x,y,1,h,color)

  def rect(self,x,y,w,h,color):

    self.hline(x,y,w,color)
    self.hline(x,y+h-1,w,color)
    self.vline(x,y,h,color)
    self.vline(x+w-1,y,h,color)

  def line(self,x0,y0,x1,y1,color):

    dx=abs(x1-x0)
    dy=abs(y1-y0)

    sx=1 if x0 <x1 else -1
    sy=1 if y0 <y1 else -1

    err=dx-dy

    while True:

      self.pixel(x0,y0,color)

      if x0==x1 and y0==y1:
        reak

      e2=2*err

      if e2>-dy:
        err-=dy
        x0+=sx

      if e2<dx:
         err+=dx
        y0+=sy


# ---------- フォント ----------

  font = {

"A":[0x18,0x24,0x42,0x7E,0x42,0x42,0x42,0],
"B":[0x7C,0x42,0x42,0x7C,0x42,0x42,0x7C,0],
"C":[0x3C,0x42,0x40,0x40,0x40,0x42,0x3C,0],
"D":[0x78,0x44,0x42,0x42,0x42,0x44,0x78,0],
"E":[0x7E,0x40,0x40,0x7C,0x40,0x40,0x7E,0],
"F":[0x7E,0x40,0x40,0x7C,0x40,0x40,0x40,0],
"G":[0x3C,0x42,0x40,0x4E,0x42,0x42,0x3C,0],
"H":[0x42,0x42,0x42,0x7E,0x42,0x42,0x42,0],
"I":[0x3C,0x08,0x08,0x08,0x08,0x08,0x3C,0],
"J":[0x1E,0x04,0x04,0x04,0x44,0x44,0x38,0],
"K":[0x42,0x44,0x48,0x70,0x48,0x44,0x42,0],
"L":[0x40,0x40,0x40,0x40,0x40,0x40,0x7E,0],
"M":[0x42,0x66,0x5A,0x5A,0x42,0x42,0x42,0],
"N":[0x42,0x62,0x52,0x4A,0x46,0x42,0x42,0],
"O":[0x3C,0x42,0x42,0x42,0x42,0x42,0x3C,0],
"P":[0x7C,0x42,0x42,0x7C,0x40,0x40,0x40,0],
"Q":[0x3C,0x42,0x42,0x42,0x4A,0x44,0x3A,0],
"R":[0x7C,0x42,0x42,0x7C,0x48,0x44,0x42,0],
"S":[0x3C,0x42,0x40,0x3C,0x02,0x42,0x3C,0],
"T":[0x7F,0x08,0x08,0x08,0x08,0x08,0x08,0],
"U":[0x42,0x42,0x42,0x42,0x42,0x42,0x3C,0],
"V":[0x42,0x42,0x42,0x42,0x42,0x24,0x18,0],
"W":[0x42,0x42,0x42,0x5A,0x5A,0x66,0x42,0],
"X":[0x42,0x42,0x24,0x18,0x24,0x42,0x42,0],
"Y":[0x42,0x42,0x24,0x18,0x08,0x08,0x08,0],
"Z":[0x7E,0x02,0x04,0x18,0x20,0x40,0x7E,0],

"0":[0x3C,0x46,0x4A,0x52,0x62,0x46,0x3C,0],
"1":[0x08,0x18,0x28,0x08,0x08,0x08,0x3E,0],
"2":[0x3C,0x42,0x02,0x0C,0x30,0x40,0x7E,0],
"3":[0x3C,0x42,0x02,0x1C,0x02,0x42,0x3C,0],
"4":[0x0C,0x14,0x24,0x44,0x7E,0x04,0x04,0],
"5":[0x7E,0x40,0x7C,0x02,0x02,0x42,0x3C,0],
"6":[0x1C,0x20,0x40,0x7C,0x42,0x42,0x3C,0],
"7":[0x7E,0x02,0x04,0x08,0x10,0x20,0x20,0],
"8":[0x3C,0x42,0x42,0x3C,0x42,0x42,0x3C,0],
"9":[0x3C,0x42,0x42,0x3E,0x02,0x04,0x38,0]

  }

  def char(self,x,y,ch,color):

    if ch not in self.font:
      return

    data=self.font[ch]

    for row in range(8):

      line=data[row]

      for col in range(8):

        if line&(1<<(7-col)):
          self.pixel(x+col,y+row,color)

  def text(self,x,y,string,color):

    for i,ch in enumerate(string):
      self.char(x+i*8,y,ch,color)

 

ST7789 driver for Raspberry Pi Pico
Written with assistance of ChatGPT
Public Domain / MIT License

 

 

《関連》

Raspberry Pi PicoとOLEDでPFD表示

↑代り映えしない絵柄です せめてRaspberry Pi PicoにかえてRP2040₋Zeroを使用

 

USB接続、0.96インチOLED

USB接続にはバッチファイルがあるとよい

 

■USB接続が通常―Wi-Fi接続との違い

 

Wi-Fi接続は楽で良いのですが、リアルなコクピットを作る場合は有線接続が通常かと思われますので、パソコンにRaspberry Pi PicoをUSB接続してOLED表示を試すことにしました。

 

USBはシリアル通信ですが、FlightGearは対応していませんから、パソコンでチェンジするPC-ブリッジプログラムが間に1枚かます必要があります。

 

【構成の比較】

 

Wi-Fi接続 USB接続
FlightGear
  ↓ UDP
PCブリッジ(Python)
  ↓ USB
Pico
  ↓
OLED
FlightGear
  ↓ UDP
Wi-Fi
  ↓
Rasopberry Pi Pico W
  ↓ I2C
OLED

 

■必要なものと手順

 

Pythonの利用が前提です。Raspberry Pi PicoはMicroPythonの使用が前提です。

 

【ハード】

 

・Raspberry Pi Pico、同互換機…マイクロコントローラーのRaspberry Pi Pico,ならびに互換機RP2400₋Zeroを使用

 

・OLEDディスプレイ…手持ちの0.96インチ4ピンI2C対応 ドライバSSD1306またはSSD1315

 

・他…工作用にブレッドボード、ジャンパー線など

 

OLEDとPico Wとの配線は次の通り

 

OLED Pico W
VCC 3.3V(Pin 36)
GND GND(Pin 38など)
SCL GP1(Pin 2)
SDA GP0(Pin 1)

 

RP2040-ZeroでOLED表示

↑FlightGearが動くPCとUSB接続したRP2040₋ZeroでミニPFD

 

【ソフト】

 

・fgdata.xml

 

プロトコルを記述したこのfgdata.xmlファイルは、前回と同じもの。詳しくは、前回を参照のこと。このファイルを、FlightGearのデータファルダ内のProtocolサブフォルダに置く(C:/Users/virt_fly/FlightGear/Downloads/fgdata_2024_1/Protcol/fgdata.xml)のも同様です。


・main.py

 

Picoにインストールするmain.pyです。

 

import sys from machine import Pin, I2C
import ssd1306
import time
import math

# ---------- OLED ----------

WIDTH = 128
HEIGHT = 64

i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)
oled = ssd1306.SSD1306_I2C(WIDTH, HEIGHT, i2c)

speed = 0
alt = 0
heading = 0
pitch = 0
roll = 0

# ---------- 速度テープ ----------

def draw_speed_tape():

  center = int(speed/10)*10

  for i in range(-3,4):

    v = center + i*10
    y = 32 - i*8

    if 10 < y < 56:
      oled.text(str(v),0,y,1)

  oled.text(">",24,32,1)
  oled.text("SPD",8,56,1)

# ---------- 高度テープ ----------

def draw_alt_tape():

  center = int(alt/10)*10

  for i in range(-3,4):

    v = center + i*10
    y = 32 - i*8

    if 10 < y < 56:
      oled.text(str(v),96,y,1)

  oled.text("<",88,32,1)
  oled.text("ALT",88,56,1)

# ---------- 方位テープ ----------

def draw_heading():

  center = int(heading/10)*10

  for i in range(-6,7):

     h = (center + i*10) % 360
     x = 64 + i*10

     if 5 < x < 120:

       if h % 30 == 0:
         oled.text(str(h), x-6, 0, 1)

       else:
         oled.pixel(x,6,1)

  oled.text("^",62,10,1)
  oled.text("HDG",54,16,1)

# ---------- 水平儀 ----------

def draw_horizon():

  cx = 64
  cy = 36

  r = math.radians(-roll)

  length = 80

  x1 = int(cx - length*math.cos(r))
  y1 = int(cy - length*math.sin(r) + pitch)

  x2 = int(cx + length*math.cos(r))
  y2 = int(cy + length*math.sin(r) + pitch)

  oled.line(x1,y1,x2,y2,1)

  # 機体マーク
  oled.line(cx-6,cy,cx+6,cy,1)

# ---------- メインループ ----------

while True:

  try:

    line = sys.stdin.readline().strip()

    parts = line.split(",")

    speed = float(parts[0])
    alt = float(parts[1])
    heading = float(parts[2])
    pitch = float(parts[3])
    roll = float(parts[4])

  except:
    pass

  oled.fill(0)

  draw_heading()
  draw_speed_tape()
  draw_alt_tape()
  draw_horizon()

  oled.show()


・FlightGearの起動オプション

 

(起動はGUIに書き込んでOK)
 

--generic=socket,out,10,127.0.0.1,5501,udp,fgdata

 

・fg-pc_bridge.py

 

FlightGear側Pythonプログラムであるfg-pc_bridge.pyは、 C:/Users/virt_fly/desktopに置くのも楽です。なお、CMD(コマンドプロンプト)から起動してもよいのですが、FlightGearの起動と同時に自動起動するバッチファイルを用意した方が楽でしょう。次項に記します。

 

import socket
import serial

# PicoのCOMポート
ser = serial.Serial("COM10",115200)

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)

  ser.write(data)

 

※注意 PicoのCOMポートとfg-pc_bridge.pyで指定するCOMポートが一致しているかあらかじめ確認が大事です。Picoやその互換機によりCOMポートは異なるため、違っていればポート番号を書き改めることが必要です。

 
・start_fg.bat

 

バッチファイルです。C:/Users/virt_fly/desktopに置きます。このバッチファイルをダブルクリックすれば、FlightGearの起動と同時に先のパイソンプログラムfg-pc_bridge.pyも自動起動します。

 

@echo off
echo Checking FlightGear bridge...

tasklist | find /i "python.exe" | find /i "fg-pc_bridge.py" >nul

if %errorlevel%==0 (
  echo Bridge already running
) else (
  echo Starting bridge
  start /min python C:\Users\virt_fly\desktop\fg-pc_bridge.py
)

echo Starting FlightGear...

"C:\Program Files\FlightGear 2024.1\bin\fgfs.exe" ^
--fg-root="C:/Users/virt_fly/FlightGear/Downloads/fgdata_2024_1" ^
--fg-aircraft="C:/Users/virt_fly/FlightGear/Custom Aircraft" ^
--aircraft=F1M2 ^
--airport=RJBB ^
--runway=24L ^
--generic=socket,out,30,127.0.0.1,5501,udp,fgdata

 

 

※ここで示したプログラムはCopilotとChatGPTの助けを借りて作成しています。

 

 

《関連》