# ==============================================================================
# 【App A】 気象GPV 解析・キャッシュエンジン
# VERSION: 97.0 (最新プロセス管理 + ガイダンス/大気解析 統合版)
# ==============================================================================
import sys, os, glob, re, subprocess
from datetime import datetime
os.environ[‘QT_API’] = ‘pyqt6’
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QListWidget, QFileDialog, QMessageBox,
QSystemTrayIcon, QMenu)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QSettings
from PyQt6.QtGui import QIcon, QPixmap, QPainter, QColor, QAction
try:
import cfgrib
except ImportError:
sys.exit(1)
APP_DIR = os.getcwd()
DEFAULT_OUTPUT_DIR = os.path.join(APP_DIR, “gpv_cache_npz”)
CURRENT_OUTPUT_DIR = DEFAULT_OUTPUT_DIR
WGRIB2_EXE = os.path.join(APP_DIR, “wgrib2_data”, “wgrib2.exe”)
os.makedirs(CURRENT_OUTPUT_DIR, exist_ok=True)
def write_syslog(msg):
log_file = os.path.join(CURRENT_OUTPUT_DIR, “system_log.txt”)
try:
with open(log_file, “a”, encoding=”utf-8″) as f:
f.write(f”[{datetime.now().strftime(‘%H:%M:%S’)}] {msg}\n”)
except Exception: pass
# ==============================================================================
# バックグラウンド処理用ワーカーコード (動的生成)
# ※ cfgribのメモリリークを完全に防ぐため、別プロセスで実行して破棄する構造
# ==============================================================================
WORKER_SCRIPT = os.path.join(APP_DIR, “_gpv_worker.py”)
WORKER_CODE = “””
import sys, traceback
def main():
try:
import os, gc, subprocess, warnings
import numpy as np
import cfgrib
warnings.filterwarnings(“ignore”)
os.environ[“ECCODES_MAX_VALUES”] = “5000000”
# Windows用 DLLパス設定
if sys.platform == “win32”:
conda_dir = os.path.dirname(sys.executable)
dll_paths = [os.path.join(conda_dir, “Library”, “bin”), os.path.join(conda_dir, “bin”)]
for p in dll_paths:
if os.path.exists(p):
os.environ[“PATH”] = f”{p};{os.environ.get(‘PATH’, ”)}”
try: os.add_dll_directory(p)
except Exception: pass
def calculate_vorticity(u, v, lon, lat):
try:
R = 6371000.0; LON, LAT = np.meshgrid(lon, lat) if lon.ndim == 1 else (lon, lat)
rad_lat = np.deg2rad(LAT); rad_lon = np.deg2rad(LON)
dy = R * np.gradient(rad_lat, axis=0); dx = R * np.cos(rad_lat) * np.gradient(rad_lon, axis=1)
dx[dx == 0] = 1e-10; dy[dy == 0] = 1e-10
return ((np.gradient(v, axis=1) / dx) – (np.gradient(u, axis=0) / dy)) * 1e5
except Exception: return np.zeros_like(u)
model = sys.argv[1]
mode = sys.argv[2] # GPV / ANAL / GUID
cache = sys.argv[3]
init = sys.argv[4]
target_fts_str = sys.argv[5]
f1 = sys.argv[6]
f2 = sys.argv[7]
wgrib2_path = sys.argv[8]
target_fts = [int(x) for x in target_fts_str.split(‘,’)]
d_all = {ft: {} for ft in target_fts}
creationflags = 0x08000000 if sys.platform == “win32” else 0
for t_ft in target_fts:
d = d_all[t_ft]
def slice_with_wgrib2(fin, ft_val, suffix):
if fin == “NONE”: return “NONE”
fout = os.path.join(cache, f”temp_{model}_{mode}_{init}_{ft_val}_{suffix}.bin”)
# モード別の正規表現マッチ
if mode == “ANAL”:
match_str = “:(anl):”
elif ft_val == 0:
match_str = “:(anl|0 hour [^:]+|[0-9]+\\\\-0 hour [^:]+):”
else:
match_str = f”:({ft_val} hour [^:]+|[0-9]+\\\\-{ft_val} hour [^:]+):”
cmd = [wgrib2_path, fin, “-match”, match_str, “-grib”, fout]
try:
subprocess.run(cmd, creationflags=creationflags, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if os.path.exists(fout) and os.path.getsize(fout) > 0: return fout
except Exception: pass
return “NONE”
f1_mini = slice_with_wgrib2(f1, t_ft, “1”)
f2_mini = slice_with_wgrib2(f2, t_ft, “2”)
def process_file(filepath, is_pall):
if filepath == “NONE”: return
try:
dss = cfgrib.open_datasets(filepath, backend_kwargs={‘indexpath’: ”})
for ds in dss:
lon = ds.longitude.values if hasattr(ds, ‘longitude’) else None
lat = ds.latitude.values if hasattr(ds, ‘latitude’) else None
for v in ds.data_vars:
da = ds[v]
is_ens = ‘number’ in da.dims
# 座標系の保存
if mode in [“GPV”, “ANAL”]:
if not is_pall and ‘lon_surf’ not in d and lon is not None:
d[‘lon_surf’] = lon; d[‘lat_surf’] = lat; d[‘lon’] = lon; d[‘lat’] = lat
elif is_pall and ‘lon_pall’ not in d and lon is not None:
d[‘lon_pall’] = lon; d[‘lat_pall’] = lat
elif mode == “GUID”:
if ‘lon’ not in d and lon is not None:
d[‘lon’] = lon; d[‘lat’] = lat
da_step = da.isel(step=0) if ‘step’ in da.dims else da
if is_ens and ‘number’ in da_step.coords: da_step = da_step.isel(number=0)
sName = str(da.attrs.get(‘GRIB_shortName’, v)).lower()
attrs_str = str(da.attrs).lower()
disc = da.attrs.get(‘GRIB_discipline’, -1)
cat = da.attrs.get(‘GRIB_parameterCategory’, -1)
num = da.attrs.get(‘GRIB_parameterNumber’, -1)
# ========== GPV / ANAL モードの解析 ==========
if mode in [“GPV”, “ANAL”]:
if not is_pall:
val = da_step.values.copy()
while val.ndim > 2: val = val[0]
if sName in [‘prmsl’, ‘msl’, ‘mslet’]: d[‘slp’] = val / 100.0 if np.nanmax(val) > 2000 else val
elif sName in [‘pres’, ‘sp’] or ‘pressure’ in attrs_str:
if ‘slp’ not in d: d[‘pres’] = val / 100.0 if np.nanmax(val) > 2000 else val
elif sName in [’10u’, ‘u’, ‘u10’] or (‘u-component’ in attrs_str and ’10’ in attrs_str): d[‘u10’] = val
elif sName in [’10v’, ‘v’, ‘v10’] or (‘v-component’ in attrs_str and ’10’ in attrs_str): d[‘v10’] = val
elif sName in [‘2t’, ‘t’, ‘t2m’, ‘temp’] or (‘temperature’ in attrs_str and ‘2’ in attrs_str):
d[‘t2m’] = val – 273.15 if np.nanmax(val) > 150 else val
elif sName in [‘2r’, ‘r’, ‘rh2m’, ‘rh’] or (‘humidity’ in attrs_str): d[‘rh2m’] = val
elif sName in [‘tcc’, ‘hcc’, ‘mcc’, ‘lcc’]: d[sName] = val
elif sName in [‘tp’, ‘apcp’, ‘pr’, ‘precip’] or ‘precip’ in attrs_str or ‘accum’ in attrs_str:
if ‘precip’ not in d: d[‘precip’] = np.nan_to_num(val, nan=0.0)
else:
levels = []
if ‘isobaricInhPa’ in da_step.coords: levels = np.atleast_1d(da_step.isobaricInhPa.values)
elif ‘level’ in da_step.coords: levels = np.atleast_1d(da_step.level.values)
else: levels = np.atleast_1d([da.attrs.get(‘GRIB_level’, 0)])
for l_idx, lvl in enumerate(levels):
lvl = int(lvl)
if lvl in [300, 500, 600, 700, 850, 925, 950, 975]:
if len(levels) > 1:
dim_n = ‘isobaricInhPa’ if ‘isobaricInhPa’ in da_step.coords else ‘level’
val_l = da_step.isel(**{dim_n: l_idx}).values.copy()
else:
val_l = da_step.values.copy()
while val_l.ndim > 2: val_l = val_l[0]
if sName in [‘t’, ‘temp’] or ‘temperature’ in attrs_str: d[f’t{lvl}’] = val_l – 273.15 if np.nanmax(val_l) > 150 else val_l
elif sName in [‘u’, ‘u-component’]: d[f’u{lvl}’] = val_l
elif sName in [‘v’, ‘v-component’]: d[f’v{lvl}’] = val_l
elif sName in [‘r’, ‘rh’, ‘humidity’]: d[f’r{lvl}’] = val_l
elif sName in [‘w’, ‘v-velocity’, ‘dz’]: d[f’w{lvl}’] = val_l
elif sName in [‘gh’, ‘z’, ‘geopotential’]: d[f’gh{lvl}’] = val_l
# ========== ガイダンス(GUID) モードの解析 ==========
elif mode == “GUID”:
val = da_step.values.copy()
while val.ndim > 2: val = val[0]
if sName != ‘unknown’: d[sName] = val
else: d[f”var_{disc}_{cat}_{num}”] = val
if sName in [‘2t’, ‘t’, ‘t2m’, ‘tmp’, ‘temp’] or (disc == 0 and cat == 0 and num == 0) or ‘temperature’ in attrs_str: d[‘t2m’] = val
elif sName in [‘2r’, ‘r’, ‘rh2m’, ‘rh’] or (disc == 0 and cat == 1 and num == 1) or ‘humidity’ in attrs_str: d[‘rh2m’] = val
elif sName in [’10u’, ‘u’, ‘u10’, ‘ugrd’] or (disc == 0 and cat == 2 and num == 2): d[‘u10’] = val
elif sName in [’10v’, ‘v’, ‘v10’, ‘vgrd’] or (disc == 0 and cat == 2 and num == 3): d[‘v10’] = val
elif sName in [‘tp’, ‘apcp’, ‘pr’, ‘precip’] or (disc == 0 and cat == 1 and num in [8, 52]) or ‘precip’ in attrs_str:
if ‘precip’ not in d: d[‘precip’] = val
elif sName in [‘weasd’, ‘snod’, ‘snow’] or (disc == 0 and cat == 1 and num in [11, 13, 29, 60]) or ‘snow’ in attrs_str: d[‘snow’] = val
elif sName in [‘wea’, ‘nswrs’, ‘nswrv’, ‘weather’] or (disc == 0 and cat == 19 and num == 192) or ‘weather’ in attrs_str: d[‘wea’] = val
elif sName in [‘thund’, ‘lig’, ‘ltng’, ‘thunder’, ‘prstm’] or (disc == 0 and cat == 19 and num == 193): d[‘thund’] = val
elif sName in [‘tmax’, ‘max t’] or (disc == 0 and cat == 0 and num == 4): d[‘tmax’] = val
elif sName in [‘tmin’, ‘min t’] or (disc == 0 and cat == 0 and num == 5): d[‘tmin’] = val
for ds in dss: ds.close()
except Exception: pass
finally: gc.collect()
process_file(f1_mini, False)
process_file(f2_mini, True)
try:
if f1_mini != “NONE” and os.path.exists(f1_mini): os.remove(f1_mini)
if f2_mini != “NONE” and os.path.exists(f2_mini): os.remove(f2_mini)
except Exception: pass
# ========== 各FTごとの派生パラメータ計算 ==========
for t_ft in target_fts:
d = d_all[t_ft]
if not d: continue
if mode in [“GPV”, “ANAL”]:
for lvl in [300, 500, 600, 700, 850, 925, 950, 975]:
tc = d.get(f’t{lvl}’); rh = d.get(f’r{lvl}’)
if tc is not None and rh is not None:
rh_c = np.clip(rh, 0.1, 100)
e = 6.112 * np.exp((17.67*tc)/(tc+243.5)) * (rh_c/100.0)
td = (243.5*np.log(e/6.112))/(17.67-np.log(e/6.112))
d[f’tddep{lvl}’] = tc – td
tk = tc + 273.15; theta = tk*(1000.0/lvl)**0.2854; w = 0.622*e/(lvl-e)
d[f’ep{lvl}’] = theta * np.exp((2.5e6*w)/(1004.0*tk))
u500 = d.get(‘u500’); v500 = d.get(‘v500’)
if u500 is not None and v500 is not None and ‘lon_pall’ in d:
d[‘vort500’] = calculate_vorticity(u500, v500, d[‘lon_pall’], d[‘lat_pall’])
pfx = “GUID_” if mode == “GUID” else “”
out_file = os.path.join(cache, f”{model}_{pfx}{init}_FT{t_ft:02d}.npz”)
np.savez_compressed(out_file, **d)
print(f”SUCCESS:{t_ft}”, flush=True)
except BaseException as e:
print(f”CRITICAL_ERROR: {traceback.format_exc()}”, flush=True)
sys.exit(1)
if __name__ == ‘__main__’: main()
“””
try:
with open(WORKER_SCRIPT, “w”, encoding=”utf-8″) as f:
f.write(WORKER_CODE)
except Exception: pass
def create_lightning_icon():
pixmap = QPixmap(64, 64)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
font = painter.font()
font.setPixelSize(50)
painter.setFont(font)
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, “⚡”)
painter.end()
return QIcon(pixmap)
# ==============================================================================
# データスキャン・ジョブ管理スレッド
# ==============================================================================
class DataParserThread(QThread):
log_signal = pyqtSignal(str)
progress_signal = pyqtSignal(int, str)
finished_signal = pyqtSignal(bool)
def __init__(self, folder_paths, output_dir, last_reported):
super().__init__()
self.folder_paths = [path for path in folder_paths if path and os.path.exists(path)]
self.output_dir = output_dir
self.last_reported = last_reported
self.abort = False
def run(self):
if not self.folder_paths:
self.finished_signal.emit(False); return
if not os.path.exists(WGRIB2_EXE):
self.log_signal.emit(f”⚠️ wgrib2が見つかりません。”); self.finished_signal.emit(False); return
has_new_data = False
scan_started = False
reported_new_file = False
creationflags = 0x08000000 if sys.platform == “win32” else 0
conda_dir = os.path.dirname(sys.executable)
lib_bin = os.path.join(conda_dir, “Library”, “bin”)
env = os.environ.copy()
env[“PATH”] = f”{lib_bin};{env.get(‘PATH’, ”)}”
env[“PYTHONIOENCODING”] = “utf-8”
for folder_path in self.folder_paths:
if self.abort: break
msm_files_all = glob.glob(os.path.join(folder_path, ‘**’, ‘*MSM*GPV*.bin’), recursive=True)
gsm_files_all = glob.glob(os.path.join(folder_path, ‘**’, ‘*GSM*GPV*.bin’), recursive=True)
gsm_guid = glob.glob(os.path.join(folder_path, ‘**’, ‘*GSM_GUID*Toorg*.bin’), recursive=True)
msm_guid = glob.glob(os.path.join(folder_path, ‘**’, ‘*MSM_GUID*Toorg*.bin’), recursive=True)
meps_gpv = glob.glob(os.path.join(folder_path, ‘**’, ‘*MEPS_GPV*.bin’), recursive=True)
anal_files_all = glob.glob(os.path.join(folder_path, ‘**’, ‘*ANAL_grib2*.bin’), recursive=True)
def get_latest_files(file_list):
if not file_list: return [], “UNKNOWN”
times = set(re.search(r’_(\d{14})_’, os.path.basename(f)).group(1) for f in file_list if re.search(r’_(\d{14})_’, f))
if not times: return file_list, “UNKNOWN”
latest_time = max(times)
return [f for f in file_list if latest_time in f], latest_time
msm_files, msm_init = get_latest_files(msm_files_all)
gsm_files, gsm_init = get_latest_files(gsm_files_all)
gg_files, gg_init = get_latest_files(gsm_guid)
mg_files, mg_init = get_latest_files(msm_guid)
meps_files, meps_init = get_latest_files(meps_gpv)
anal_files, anal_init = get_latest_files(anal_files_all)
phases = [
(‘GSM’, gsm_files, gsm_init, list(range(0, 265, 3)), “GPV”),
(‘MSM’, msm_files, msm_init, list(range(0, 80, 1)), “GPV”),
(‘GSM’, gg_files, gg_init, list(range(0, 265, 3)), “GUID”),
(‘MSM’, mg_files, mg_init, list(range(0, 80, 1)), “GUID”),
(‘MEPS’, meps_files, meps_init, list(range(0, 40, 3)), “GPV”),
(‘ANAL’, anal_files, anal_init, [0], “ANAL”)
]
for model_name, files, init_time_str, ft_list, mode in phases:
if self.abort or not files or init_time_str == “UNKNOWN”: continue
if self.last_reported.get(f”{model_name}_{mode}”) != init_time_str:
self.log_signal.emit(f”✅ {model_name} {mode} 最新データ発見: {init_time_str}”)
self.last_reported[f”{model_name}_{mode}”] = init_time_str
reported_new_file = True
tasks = []
for ft in ft_list:
pfx = “GUID_” if mode == “GUID” else “”
npz_file = os.path.join(self.output_dir, f”{model_name}_{pfx}{init_time_str}_FT{ft:02d}.npz”)
if not os.path.exists(npz_file): tasks.append(ft)
if not tasks: continue
if not scan_started:
self.log_signal.emit(“🔍 新規データを抽出中…”)
scan_started = True; has_new_data = True
proc_name = mode
file_groups = {}
for ft in tasks:
if mode == “GPV”:
f1 = self._get_file_for_ft(files, “Lsurf”, ft) or “NONE”
f2 = self._get_file_for_ft(files, “P-all” if any(“P-all” in x for x in files) else “L-pall”, ft) or “NONE”
elif mode == “ANAL”:
f1 = files[0]; f2 = “NONE”
else: # GUID
f1 = self._get_file_for_ft(files, “”, ft) or files[0]
f2 = “NONE”
if f1 == “NONE” and f2 == “NONE”: continue
key = (f1, f2)
if key not in file_groups: file_groups[key] = []
file_groups[key].append(ft)
self.log_signal.emit(f”🚀 {model_name} {proc_name} 解析開始 (対象: {len(tasks)}件)…”)
total_extracted = 0
for (f1, f2), fts in file_groups.items():
if self.abort: break
for i in range(0, len(fts), 5):
if self.abort: break
chunk_fts = fts[i:i + 5]
ft_str = “,”.join(map(str, chunk_fts))
cmd = [sys.executable, WORKER_SCRIPT, model_name, mode, self.output_dir, init_time_str, ft_str, f1, f2, WGRIB2_EXE]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding=’utf-8′, errors=’replace’, creationflags=creationflags, env=env)
for line in iter(process.stdout.readline, ”):
line = line.strip()
if not line: continue
if line.startswith(“SUCCESS:”):
ft_success = line.split(“:”)[1]
total_extracted += 1
self.progress_signal.emit(int((total_extracted / len(tasks)) * 100), f”⚡ 解析中 {model_name}_{mode} FT={ft_success}h…”)
self.log_signal.emit(f”⚡ 抽出完了: {model_name} {proc_name} FT={ft_success}h”)
elif “CRITICAL_ERROR:” in line or “Traceback” in line or “Error:” in line or “File ” in line:
self.log_signal.emit(f”⚠️ {line}”)
process.stdout.close()
process.wait()
if has_new_data or reported_new_file:
self.log_signal.emit(“✅ スキャン・抽出完了”)
self.finished_signal.emit(has_new_data)
def _get_file_for_ft(self, files_list, keyword, ft):
# ファイルの結合状態を判定
is_gsm = any(“GSM_GPV” in f for f in files_list)
if is_gsm:
for f in files_list:
if keyword and keyword not in f: continue
m = re.search(r’FD(\d{2})(\d{2})-(\d{2})(\d{2})’, os.path.basename(f))
if m and int(m.group(1))*24 + int(m.group(2)) <= ft <= int(m.group(3))*24 + int(m.group(4)): return f
return next((f for f in files_list if (not keyword or keyword in f)), None)
for f in files_list:
if not keyword or keyword in f:
m = re.search(r’FH(\d{2})-(\d{2})’, os.path.basename(f))
if m and int(m.group(1)) <= ft <= int(m.group(2)): return f
elif not m: return f
return None
# ==============================================================================
# GUI & タスクトレイ 管理
# ==============================================================================
class EngineStatusWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle(“GPV 解析エンジン (Ver 97.0 – 統合版)”)
self.setFixedSize(650, 580)
self.setStyleSheet(“background-color: #2b2b2b; color: #e0e0e0; font-family: ‘MS Gothic’;”)
layout = QVBoxLayout(self)
self.status_label = QLabel(“状態: 起動中…”)
self.status_label.setStyleSheet(“font-size: 14pt; font-weight: bold; color: #00fa9a;”)
layout.addWidget(self.status_label)
control_layout = QHBoxLayout()
self.pause_btn = QPushButton(“⏸ 一時停止”)
self.resume_btn = QPushButton(“▶ 手動強制スキャン”)
self.stop_btn = QPushButton(“⏹ 停止”)
self.clear_btn = QPushButton(“🧹 キャッシュ全削除”)
for btn in [self.pause_btn, self.resume_btn, self.stop_btn]:
btn.setStyleSheet(“background-color: #555; padding: 4px; font-weight: bold;”)
control_layout.addWidget(btn)
self.clear_btn.setStyleSheet(“background-color: #b22222; padding: 4px; font-weight: bold; color: white;”)
control_layout.addWidget(self.clear_btn)
layout.addLayout(control_layout)
out_layout = QHBoxLayout()
self.out_folder_btn = QPushButton(“💾 出力先フォルダ(共有先)を設定”)
self.out_folder_btn.setStyleSheet(“background-color: #3498DB; padding: 4px; font-weight: bold; color: white;”)
out_layout.addWidget(self.out_folder_btn)
self.out_folder_label = QLabel(“未設定”)
self.out_folder_label.setStyleSheet(“background: #444; padding: 4px; border: 1px solid #777;”)
out_layout.addWidget(self.out_folder_label, stretch=1)
layout.addLayout(out_layout)
self.log_list = QListWidget()
layout.addWidget(self.log_list)
self.copy_btn = QPushButton(“📋 ログをコピー”)
layout.addWidget(self.copy_btn)
folder_layout = QHBoxLayout()
self.folder_btns = [QPushButton(f”📁 読込元フォルダ {i+1}”) for i in range(3)]
for b in self.folder_btns:
folder_layout.addWidget(b)
layout.addLayout(folder_layout)
self.exit_btn = QPushButton(“システムを完全に終了”)
self.exit_btn.setStyleSheet(“background-color: #8b0000; color: white; font-weight: bold; padding: 6px;”)
layout.addWidget(self.exit_btn)
def log(self, message):
self.log_list.addItem(f”[{datetime.now().strftime(‘%H:%M:%S’)}] {message}”)
self.log_list.scrollToBottom()
write_syslog(message)
def closeEvent(self, event):
event.ignore()
self.hide()
if hasattr(self, ‘tray_msg_callback’):
self.tray_msg_callback()
class TrayApp(QApplication):
def __init__(self, sys_argv):
super().__init__(sys_argv)
self.setQuitOnLastWindowClosed(False)
self.settings = QSettings(“SapporoWeatherApp”, “GPVEngine”)
self.monitor_folders = [self.settings.value(f”watch_dir_{i+1}”, “”) for i in range(3)]
global CURRENT_OUTPUT_DIR
self.output_dir = self.settings.value(“output_dir”, DEFAULT_OUTPUT_DIR)
CURRENT_OUTPUT_DIR = self.output_dir
os.makedirs(self.output_dir, exist_ok=True)
self.last_reported = {}
self.window = EngineStatusWindow()
self.window.tray_msg_callback = self.show_tray_message
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(create_lightning_icon())
self.tray_icon.setToolTip(“GPV抽出エンジン (稼働中)”)
tray_menu = QMenu()
show_action = QAction(“ウィンドウを表示”, self)
show_action.triggered.connect(self.restore_window)
quit_action = QAction(“完全に終了する”, self)
quit_action.triggered.connect(self.quit)
tray_menu.addAction(show_action)
tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.activated.connect(self.tray_icon_activated)
self.tray_icon.show()
with open(os.path.join(self.output_dir, “system_log.txt”), “w”, encoding=”utf-8″) as f: pass
self.window.log(“===============================”)
self.window.log(“システム起動: Ver 97.0 (大気解析・ガイダンス対応)”)
self.window.log(f”wgrib2 パス確認: {‘✅ 認識完了’ if os.path.exists(WGRIB2_EXE) else ‘⚠️ 未発見’}”)
self.window.log(f”現在の出力先: {self.output_dir}”)
self.window.log(“===============================”)
self.window.out_folder_label.setText(self.output_dir)
self.window.out_folder_btn.clicked.connect(self.select_output_folder)
folders_set = False
for i in range(3):
self.window.folder_btns[i].clicked.connect(lambda checked, idx=i: self.select_folder(idx))
self.update_folder_btn_text(i)
if self.monitor_folders[i]: folders_set = True
if not folders_set:
self.window.status_label.setText(“状態: 読込元フォルダ未設定”)
self.window.copy_btn.clicked.connect(self.copy_logs)
self.window.pause_btn.clicked.connect(self.pause_parsing)
self.window.resume_btn.clicked.connect(self.force_scan)
self.window.stop_btn.clicked.connect(self.stop_parsing)
self.window.clear_btn.clicked.connect(self.clear_cache)
self.window.exit_btn.clicked.connect(self.quit)
self.timer = QTimer()
self.timer.timeout.connect(self.run_parser)
self.timer.start(10000)
self.is_processing = False
self.run_parser()
self.window.show()
def tray_icon_activated(self, reason):
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
self.restore_window()
def restore_window(self):
self.window.showNormal()
self.window.activateWindow()
def show_tray_message(self):
self.tray_icon.showMessage(
“エンジン稼働中”,
“ウィンドウを閉じましたが、裏で抽出を続けています。終了する場合は右クリックから「完全に終了する」を選んでください。”,
QIcon(self.tray_icon.icon()),
3000
)
def select_output_folder(self):
global CURRENT_OUTPUT_DIR
folder = QFileDialog.getExistingDirectory(self.window, “出力先フォルダ(共有)を選択”, self.output_dir)
if folder:
self.output_dir = folder
CURRENT_OUTPUT_DIR = folder
self.settings.setValue(“output_dir”, folder)
self.window.out_folder_label.setText(folder)
self.window.log(f”💾 出力先フォルダを更新しました: {folder}”)
self.run_parser()
def clear_cache(self):
reply = QMessageBox.question(self.window, “確認”, “現在の出力先のデータをすべて削除し再抽出しますか?”,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
self.timer.stop()
self.window.log(“🗑️ キャッシュデータのお掃除を開始します…”)
for f in glob.glob(os.path.join(self.output_dir, “*.npz”)) + glob.glob(os.path.join(self.output_dir, “*.json”)) + glob.glob(os.path.join(self.output_dir, “*.bin”)):
try: os.remove(f)
except Exception: pass
for d in self.monitor_folders:
if d:
for idx_f in glob.glob(os.path.join(d, ‘**’, ‘*.idx’), recursive=True):
try: os.remove(idx_f)
except Exception: pass
self.last_reported = {}
self.window.log(f”✨ データを削除しました。再抽出を開始します!”)
self.run_parser()
self.timer.start(10000)
def select_folder(self, index):
folder = QFileDialog.getExistingDirectory(self.window, f”読込元フォルダ {index+1} を選択”)
if folder:
self.monitor_folders[index] = folder
self.settings.setValue(f”watch_dir_{index+1}”, folder)
self.update_folder_btn_text(index)
self.window.log(f”フォルダ{index+1}を更新: {folder}”)
self.run_parser()
def update_folder_btn_text(self, i):
f = self.monitor_folders[i]
self.window.folder_btns[i].setText(f”📁 {os.path.basename(f)}” if f else f”📁 未設定 {i+1}”)
def copy_logs(self):
QApplication.clipboard().setText(“\n”.join([self.window.log_list.item(i).text() for i in range(self.window.log_list.count())]))
def pause_parsing(self):
self.timer.stop(); self.window.log(“一時停止しました”)
def force_scan(self):
self.window.log(“手動強制スキャンを開始します…”); self.timer.start(10000); self.run_parser()
def stop_parsing(self):
self.timer.stop(); self.window.log(“停止しました”)
def run_parser(self):
if self.is_processing: return
if not any(self.monitor_folders):
self.window.status_label.setText(“状態: 読込元フォルダ未設定”)
return
self.is_processing = True
self.worker = DataParserThread(self.monitor_folders, self.output_dir, self.last_reported)
self.worker.log_signal.connect(self.window.log)
self.worker.progress_signal.connect(lambda v, t: self.window.status_label.setText(f”状態: {t}”))
self.worker.finished_signal.connect(self.on_parse_finished)
self.worker.start()
def on_parse_finished(self, has_new_data):
self.is_processing = False
self.window.status_label.setText(“状態: 待機中 (自動スキャン継続中)”)
if __name__ == ‘__main__’:
app = TrayApp(sys.argv)
sys.exit(app.exec())


コメント