Day After Day
tsurezure naru mamani...
ANOTHER DECADE

from 2022 when it's begining after/with CORONA Virus.

初めてのPython 備忘録

5月
12
2023
Back
Alt+HOME


TLMForwarderが一応ランニングテストを始められるところまで完成したので、ここ数ヶ月四苦八苦したポイントを残しておこうと思います。正しいかどうかは不明ですが取り敢えず課題解決記録として・・・
ChatGPTにかなりお世話になりました。有り難う!!

TLMForwarerとは・・・
衛星から送られてくるテレメトリー(診断データ)を、状態分析しているデータベースへ提供するためのアプリです。アマチュア無線で遊びながら、少しは貢献出来ればと自分なりに作ってみました。(もっと高機能なアプリは沢山有ります。)


転送したデータの分析結果はこちら(転送したデータ量もコールサインをクリックすると出ます。)
SatNOGS Dashboard Telemetry/GreenCube

今回GUIのフォームとしてtkinterを初めて勉強しましたが、殆どのオブジェクトに於いてその設置にplaceを使用しています。 ドット単位で座標指定できるのできっちり作れるのですが、Windowサイズを変更するなどに追随できずスケーラブルではありません。 次の機会には、packを勉強しようと思います。

TreeViewに於いて背景を黒地にする


# スタイル定義
self.style = ttk.Style()
self.style.theme_use('default')				# テーマを指定 clam / alt / default / classic
self.style.configure("Treeview.Heading", font=("Cascadia mono", 11, 'bold'),)
self.style.configure("Treeview", font=("Cascadia mono",10, 'normal'), fieldbackground='black',)	
							# 上のテーマ指定をしないとfieldbackgroundが無視される

#Treeviewの定義
self.tree_teigi = ttk.Treeview(
	frame_teigi,
	columns=(1, 2, 3, 4, 5),
	show='headings',
	style='Treeview'
)

Treeviewの文字色・背景色の定義


self.tree.tag_configure("fg_white", background='black', foreground='white')	# 文字色・背景色の組合せに名前を付ける
self.tree.insert('', tk.END, values=data, tags='fg_black')			# その名前を tags= で指定してdataに色を着ける

Treeviewとは行と列でスプレッドシートの様にデータを記述出来る機能です。 各列は幅をドット単位で設定でき、合計が表の幅となり、表の高さは全体で設定します。

背景画像の上に文字だけを(レベルが透明と同様)表示


# 画像と文字を表示するためのフォント指定
font_teigi = ImageFont.truetype("arial.ttf", 12)

# frame_teigi用の画像を読み込む
frame_teigi = Image.open("image.png")

# テキスト指定
label_text = "Sample Text"

# frame_teigiにテキスト(label_test)を書き込む
draw = ImageDraw.Draw(frame_teigi)
draw.text((座標x, 座標y), label_text, font=font_teigi, fill=(255, 255, 255, 255))
		
# テキストラベルと背景を透明にする
label_teigi = Image.new('RGBA', (150, 50), (0, 0, 0, 0))
draw = ImageDraw.Draw(label1)
draw.text((0, 0), label_teigi, font=font_teigi, fill=(255, 255, 255, 255))

# 透明なlabelをfreme_teigiに貼り付ける
frame_teigi.paste(label_teigi, (座標x, 座標y), mask=label_teigi)

# 画像を保存する
frame_teigi.save("result.png")

frame_teigiに読み込んだ画像(image.png)にlabel_textで指定した文字列を、背景を透明にした上で貼り付けます。 これをそのまま表示するのではなく、result.png として保存し、改めて表示ルーティンに入ります。

キャンバスに画像を表示する(文字を重ねる時は前項より続く)


# 画像表示用キャンバス
self.canvas = tk.Canvas(self, relief=tk.FLAT, width=幅(px), height=高さ(px), bg='white')
self.canvas.place(x=座標x, y=座標y)

# キャンバスのセンターを取得
self.update()
canvas_width = self.canvas.winfo_width()			# キャンバスの横サイズを取得
canvas_height = self.canvas.winfo_height()			#    〃  縦サイズを取得

# 初期画面に画像を貼り付け
self.img = ImageTk.PhotoImage(file="result.png")		# 画像作成で出来上がった result.pngをimgに定義
self.canvas.create_image(
	canvas_width / 2,					# 画像の縦横センターを指定
	canvas_height /2,					# キャンバスサイズの半分
	image=self.img						# イメージとして self.imgを指定
)

JPEGについては最近では対応しているのですがもう一工夫必要なようです。

動く時計の表示


# frameへ時計を表示(Local/UTC切替)
def check_time(frame, dt):
	now = datetime.now()
	if frame.var.get() == 1:						# ラジオボタンでlocal表示が選択されている時
		dt.set(now.strftime('%Y-%m-%d %H:%M:%S')) 			# リアルタイム時刻表示
		timestamp = now.strftime('%H:%M:%S')				# データ添付用テキスト
	else:
		dt.set(now.astimezone(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'))
		timestamp = now.astimezone(timezone.utc).strftime('%H:%M:%S')
	frame.after(1000, lambda:check_time(frame, dt))
	return timestamp							# データ添付用としてその時点の時刻を返す

# 時計ラベルのテキストデータとしてdtを指定
dt = tk.StringVar()
label_dt=tk.Label(menu_bar, textvariable=dt, font=('文字フォント', 10, 'bold'))
label_dt.place(x=座標x, y=座標y)
menu_bar.after(1000, lambda:chk_time(menu_bar, dt))				# 時刻取得の再帰呼び出し

縦スクロールバーの設置


# スクロールバー
self.vscrollbar = ttk.Scrollbar(
	frame_teigi,				# 設置するフレームを指定
	orient='vertical',				# 縦横の指定
	command=self.tree_teigi.yview	 # 関連づけるオブジェクトを指定
)

# スクロールバーの配置
self.vscrollbar.place(x=座標x, y=座標y, height=高さpx)
self.tree_teigi.configure(yscrollcommand=self.vscrollbar.set)


メインループとは別にスレッドを立てる


import tkinter as tk
class Main(tk.Tk):
	def	__init__(self, *args, **kwargs):
		tk.Tk.__init__(self, *args, **kwargs)

		def process(data)
			処理を記述
			
		self.thread = Sub(process)
		self.thread.start()


────────────────────────────────────
import threading
class Sub():
	def __init__(self, callback):
		super().__init__()

		# データをメインスレッドに転送
		self.callback = callback

		# データ受信専用スレッドの定義
		self.thread = threading.Thread(target=self.run)
		self.stop_event = threading.Event()

	# サブスレッドの開始
	def start(self):							# メインスレッドから開始操作
		self.thread.start()

	# サブスレッドの停止フラグ発生
	def stop(self):								# メインスレッドから停止操作
		self.stop_event.set()

	# スレッド開始とともに自動的に実行される
	def run(self):
		処理

		# コールバック関数を呼び出し、データを渡す
		self.callback(data)

log出力の定義


# ログ出力定義
logging.basicConfig(
	level=logging.DEBUG, filename="LOG.log", format="log_title: %(message)s"
)
		
	# ログを出力
	logging.debug(data)

TCP接続


from socket import socket, AF_INET, SOCK_STREAM

# ソケットを生成
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((IPADDR, PORT))

connectに関しては try: except: で対象の稼働状況による接続不可を処理する方が良い。

UDPによるデータ転送


import requests
from socket import socket, AF_INET, SOCK_DGRAM

# UDPソケットを作成します
sock = socket(AF_INET, SOCK_DGRAM)

# 転送データをサーバーに転送します
res = requests.post(転送先URL, data=転送データ, timeout = int(秒数))	# 秒数だけ返信を待つ
res_status = res.raise_for_status()					# 返信(HTTPコード)が 200番台以外の時例外を起こす

# 例外処理でなかったら(stat_code == 200番台)
if res_status == None: 				
	stat_code = str(res.status_code)

# ソケットを閉じる
sock.close()	

requests ですがこの一言でTLS1.2のハンドシェイクや転送自体を全部やってくれます。何と言うことでしょう。 当初C言語で書こうとしていたけれど、OpenSSLを入れて、このrequestsライブラリー自体を書こうとしていたことになるのでは・・

タイトルバーにアイコンを表示


iconファイルを添付して表示させると、配布の時そのファイルも同一フォルダに存在している必要が有るため、 ソースの中に画像を書き込むと言う方法を採る。(PowerShellのコマンドを使う)

PS X:\py3> certutil -encode title_icon.gif title_icon.txt

タイトルアイコンの GIF ファイルを作り、それをテキストファイルにエンコードする。

-----BEGIN CERTIFICATE-----
R0lGODlhWAJYAvcAAAAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr
/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCq
mQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMA
MzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV
/zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPV

		======	中略  ======					びっくりするほど長い

al4knjSHXCxYTitIlmmRc4YZE8B3NO0JmWgHKUVRPG4El0SVkvARRlQ1b9BHjPaF
emc1VypREQ/iKvMmfWoFfuL2IKkiESWilACEb0ACUeOyOBIUYZvlgATXP/XCSH/n
UJSUoePolEApWYiqUBfqOIvnQjCUMJdznABYaiUEccGZj/LYmllJU3hIJ71ZUy+k
gee5MilHMrp1Ms01pT02pgtxerSWnr72iI8pe1NXbKLYdF20e852H8MndVKnIsL6
pVBmRk1RVkEBNW61Z+hRdluBLPPWPvkkhBHifNfRrV9jbt+mVhESEAA7
-----END CERTIFICATE-----

この内容(BEGIN と END の行は省く)を Pythonファイル(icon.py)にコピーして次の様にする

import tkinter as tk

#############################
#   Title ICON Image
#############################
icon_data = """R0lGODlhWAJYAvcAAAAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr
/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCq
mQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMA
MzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV
/zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPV

		======	中略  ======

al4knjSHXCxYTitIlmmRc4YZE8B3NO0JmWgHKUVRPG4El0SVkvARRlQ1b9BHjPaF
emc1VypREQ/iKvMmfWoFfuL2IKkiESWilACEb0ACUeOyOBIUYZvlgATXP/XCSH/n
UJSUoePolEApWYiqUBfqOIvnQjCUMJdznABYaiUEccGZj/LYmllJU3hIJ71ZUy+k
gee5MilHMrp1Ms01pT02pgtxerSWnr72iI8pe1NXbKLYdF20e852H8MndVKnIsL6
pVBmRk1RVkEBNW61Z+hRdluBLPPWPvkkhBHifNfRrV9jbt+mVhESEAA7"""

def get_image():
    return tk.PhotoImage(data=icon_data)

main.py 側でこのファイルをインポートしてタイトルアイコンを作成する

import icon   # iconファイルの名前

title_icon = icon.get_image()
self.iconphoto(False, title_icon)

コンパイルして一つのEXEファイルにする


pyinstaller main.py --onefile --clean --noconsole --icon=app_icon.ico --name=APPLICATION_NAME

--icon は作成した ico ファイルをアプリケーションのデスクトップ・アイコンにする。 また、--name を指定しないと main.exe と言うアプリになる。

ドロップダウンメニュー初期値の表示


variable = tk.StringVar(value='初期値')	# 選択した衛星名を保持する変数

# コンボボックスの表示
combo = ttk.Combobox(self, width=幅(文字数), height=高さ(px), values=somelist, textvariable=variable)

通常はtk.StringVar()と記述するが、それに規定値を持たせている。 なおこの初期値を変更選択した時、変更値をファイル保存し次回の初期値にすると、良く使用するものを自動的に表示できる。

combo.set(list[0]) 等の説明が多いが、私のやり方がまずいのか期待どおりの動作をしなかった。

プログラムの一部分を外部モジュールとし、機能を差し替える


# 機能名から同名の外部モジュールを検索
filepath = './modules/' + 機能名 + '.py'
is_file = os.path.isfile(filepath)			ファイルが実存するか確認

# 見つかった時はそのモジュールをインポートする
if is_file:
	module = 'modules.' + str(機能名)
	ext_functions = importlib.import_module(module)		# import フォルダ名.ファイル名(拡張子無し) の書式に基づく参照

# 対応モジュールが無かった時のメッセージ(GUIへ表示)
else:
	エラーメッセージなど

# 外部モジュールへdataを持ってタスクを渡し、結果をstringで受ける
string = ext_functions.make_string(data)

外部モジュール(アプリ本体のインストールフォルダ/modules/機能名.py)
def make_string(data):
	string = dataを元にした処理を記述
	return string

現在、どちらもソースレベルでは実働しているが、本体のみコンパイル(EXE化)して、機能.py を編集可能(ソースのまま)ではうまく読んでくれない。

アプリ内部モジュール化(クラスのインスタンスを動的に生成)


前項の外部モジュールについては中々うまく行かないので、取り敢えずアプリ内部で各衛星の個別モジュールをクラス化して、 アプリ内で衛星を選択した時に、動的にクラスが切り替わる様にする。

# 選択された衛星のNoradIDを取得する
id = self.combo_selected()	# comboboxのEventで起動する関数内で衛星のidを拾う

# 衛星別クラス名を作成
class_name = 'ID' + str(id)			# class ID9999() のような形式でクラスを作っておく

# クラス名を表す変数を定義
class_to_call = globals()[class_name]

# 該当クラスのインスタンスを生成
instance = class_to_call()

# 衛星別クラスを呼び出し、返り値を取得
res = instance.do_something(data)

別Threadで受信を待つ様なケースで本体が終了できない場合


def __init__(self, callback):

	super().__init__()

	# データをメインスレッドに転送
	self.callback = callback

	# データ受信専用スレッドの定義
	self.thread = threading.Thread(target=self.run)
	self.stop_event = threading.Event()				# 終了したい時セットする

	# サブスレッドの開始
	def start(self):						# メインスレッドから開始操作
		self.thread.start()

	# サブスレッドの停止フラグ発生
	def stop(self):							# メインスレッドから停止操作
		self.stop_event.set()

	# スレッド開始とともに自動的に実行される
	def run(self):

		# ソケットを生成
		sock = socket(AF_INET, SOCK_STREAM)

		# 接続時、モデムが起動していない時のエラー処理を加える
		try:
			sock.connect((IPADDR, PORT))

			# 接続完了した時の表示

		except:
			# 接続できなかった時のメッセージ(GUIへ表示)

		sock.settimeout(1)					# 終了に関するミソ ------(1)

		# データ受信
		while True:

			# stop_eventがセットされたら切断する
			if self.stop_event.is_set():
				break
			
			try:						# 受信が有ったら取得し
				# パケットを受信			
				recvbuf = sock.recv(BUFSIZE)

			except OSError: 	  			# 無かったら1秒待って続ける(ずっと待っていない)(2)
				continue

			# 受信データがなければ切断
			if not recvbuf:
				break

			# 受信したデータを渡す
			self.callback(recvbuf)


初期の状況:終了ボタンを押してもすぐ終了せず、次の受信があると同時に終了していた

そこでChatGPTに教えを請うと:受信待ちにタイマを入れ(1秒)これを過ぎるとwhileを回すと言うアドバイスを貰う。 詰まり、受信が有ったのと同じ(無いけど)状況を作れば終了すると言うことである。

当初 exceptの部分が異なって居り次の様なエラーが発生

except socket.timeout:
TypeError: catching classes that do not inherit from BaseException is not allowed

再度ChatGPTによると、これはsocket.timeoutがBaseExceptionを継承していないため発生しているので socket.timeout の代わりに OSError に変更して使用する様提案が有った。

結果はあっという間に正しく終了した。ただ、素人考えであるが今まで次の受信が有るまでCPUも休んでいたところが1秒毎の負担が増えているのではと思っている。

終了時に無駄なファイルを削除する。(ログの空ファイル)


# その他のファイルでサイズがゼロならば削除
logging.shutdown()
if os.path.exists(filename) and os.path.getsize(filename) == 0:
	os.remove(filename) 

loggingを使ったログファイルは、アプリケーションを終了すると自動的にクローズされる。従って close(fp) のような締め作業は不要である。 しかし、今回の場合アプリケーション終了前に、そのログファイルを内容が空なら削除したいという例である。

logging.shutdown()は一回実行すると、ログファイルが複数あっても同時にクローズする。これで空ファイルを削除して整理できる

ディレクトリーパスの末尾に"/"が付いているか!?


dir = "C:/folder/"
last_char = dir[-1]			# インデックス
print(last_char)  # "/"	
dir = "C:/folder/"
last_char = dir[-1:]			# slice
print(last_char)  # "/"
dir = "C:/folder/"
last_char = str(dir[len(dir)-1])	# str() + len()
print(last_char)  # "/"


Back
Alt+HOME