-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathfw_git_dialog.py
More file actions
410 lines (362 loc) · 16.1 KB
/
Copy pathfw_git_dialog.py
File metadata and controls
410 lines (362 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
"""
GitHub release list → select → download/extract dialog.
Emits firmware_ready(bin_path, filesize) signal on completion, then auto-closes.
fw_type_list (optional): WIZ550처럼 같은 장치가 여러 repo를 갖는 경우
[{"label": "SR", "family": {...}, "device_spec": {...}}, ...]
제공 시 "Type:" 콤보가 Release 위에 표시되고, 선택 변경 시 릴리스 목록 갱신.
"""
import os
from PyQt5.QtCore import QThread, pyqtSignal, QTimer
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QComboBox, QPushButton, QProgressBar,
QFileDialog, QMessageBox, QFrame, QLineEdit,
)
class ReportUnsupportedThread(QThread):
"""
미지원 장치 이슈 보고를 백그라운드로 돌린다.
검색·등록이 수 초 걸릴 수 있어 GUI 를 붙잡지 않기 위함.
done 은 fw_issue_reporter.report_unsupported() 의 결과 dict 를 그대로 전달한다.
"""
done = pyqtSignal(dict)
def __init__(self, reporter, device_name, fw_version=""):
super().__init__()
self._reporter = reporter
self._device_name = device_name
self._fw_version = fw_version
def run(self):
try:
self.done.emit(
self._reporter.report_unsupported(self._device_name, self._fw_version)
)
except Exception as e: # 보고 실패가 툴 동작을 막으면 안 됨
self.done.emit({"action": "error", "url": "", "message": str(e)})
class _FetchReleasesThread(QThread):
done = pyqtSignal(list)
error = pyqtSignal(str)
def __init__(self, fetcher, family):
super().__init__()
self._fetcher = fetcher
self._family = family
def run(self):
try:
# family 의 source_type 에 따라 GitHub 릴리즈 / docs 페이지로 분기
self.done.emit(self._fetcher.get_releases_for(self._family))
except Exception as e:
self.error.emit(str(e))
class _DownloadThread(QThread):
done = pyqtSignal(str, int) # (bin_path, filesize)
error = pyqtSignal(str)
def __init__(self, fetcher, asset, dest_dir, extract_file):
super().__init__()
self._fetcher = fetcher
self._asset = asset
self._dest_dir = dest_dir
self._extract_file = extract_file
def run(self):
try:
path, size = self._fetcher.download_and_extract(
self._asset, self._dest_dir, self._extract_file
)
self.done.emit(path, size)
except Exception as e:
self.error.emit(str(e))
class FWGitDialog(QDialog):
firmware_ready = pyqtSignal(str, int) # (bin_path, filesize)
def __init__(self, parent, device_name, family, device_spec, fetcher, dl_path,
fw_type_list=None, wiz550_config=None, image_validator=None):
"""
fw_type_list: [{"label": str, "family": dict, "device_spec": dict}, ...]
None 또는 빈 리스트면 타입 선택 행 미표시.
wiz550_config: {"target_ip": str, "target_mac": str, "localip_addr": str, "pw_setting": str}
제공 시 다운로드 완료 후 TFTP 업로드를 이 창 안에서 자동 진행.
"""
super().__init__(parent)
self._device_name = device_name
self._family = family
self._device_spec = device_spec
self._fetcher = fetcher
self._dl_path = dl_path
self._fw_type_list = fw_type_list or []
self._wiz550_config = wiz550_config
# WIZ550 은 이 창 안에서 바로 TFTP 업로드하므로 firmware_ready 를 타지 않는다.
# 그 경로도 같은 이미지 검증을 받도록 호출자가 검증 함수를 넘겨준다.
self._image_validator = image_validator
self._releases = []
self._current_asset = None
self._fetch_thread = None
self._dl_thread = None
self._upload_thread = None
self._tmp_bin_path = None
self.setWindowTitle("FW from Git")
self.setFixedWidth(540)
self.setModal(True)
self._build_ui()
self._start_fetch()
# ── UI ───────────────────────────────────────────────────────────
def _build_ui(self):
root = QVBoxLayout(self)
root.setSpacing(8)
root.setContentsMargins(12, 12, 12, 12)
grid = QGridLayout()
grid.setColumnStretch(1, 1)
row = 0
# Device / repository info
grid.addWidget(QLabel("Device:"), row, 0)
self._lbl_device = QLabel()
self._lbl_device.setWordWrap(True)
self._refresh_device_label()
grid.addWidget(self._lbl_device, row, 1)
row += 1
# Type 콤보 (fw_type_list 제공 시에만)
if self._fw_type_list:
grid.addWidget(QLabel("Type:"), row, 0)
self._combo_type = QComboBox()
for entry in self._fw_type_list:
self._combo_type.addItem(entry["label"])
self._combo_type.currentIndexChanged.connect(self._on_type_changed)
grid.addWidget(self._combo_type, row, 1)
row += 1
# Release selection row
grid.addWidget(QLabel("Release:"), row, 0)
rel_row = QHBoxLayout()
self._combo = QComboBox()
self._combo.setMinimumWidth(260)
self._combo.currentIndexChanged.connect(self._update_asset_label)
rel_row.addWidget(self._combo, 1)
self._btn_refresh = QPushButton("Refresh")
self._btn_refresh.setFixedWidth(80)
self._btn_refresh.clicked.connect(self._start_fetch)
rel_row.addWidget(self._btn_refresh)
grid.addLayout(rel_row, row, 1)
row += 1
# Asset row
grid.addWidget(QLabel("Asset:"), row, 0)
self._lbl_asset = QLabel("—")
self._lbl_asset.setWordWrap(True)
grid.addWidget(self._lbl_asset, row, 1)
row += 1
# Password 행 (WIZ550 업로드 모드에서만 표시)
if self._wiz550_config is not None:
grid.addWidget(QLabel("Password:"), row, 0)
self._edit_pw = QLineEdit()
self._edit_pw.setEchoMode(QLineEdit.Password)
self._edit_pw.setPlaceholderText("Optional")
if self._wiz550_config.get('pw_setting'):
self._edit_pw.setText(self._wiz550_config['pw_setting'])
grid.addWidget(self._edit_pw, row, 1)
root.addLayout(grid)
# Separator
sep1 = QFrame()
sep1.setFrameShape(QFrame.HLine)
sep1.setFrameShadow(QFrame.Sunken)
root.addWidget(sep1)
# Download path row
path_row = QHBoxLayout()
path_row.addWidget(QLabel("Save Path:"))
self._lbl_dlpath = QLabel(self._dl_path)
self._lbl_dlpath.setWordWrap(True)
path_row.addWidget(self._lbl_dlpath, 1)
btn_change = QPushButton("Browse...")
btn_change.setFixedWidth(70)
btn_change.clicked.connect(self._change_dl_path)
path_row.addWidget(btn_change)
root.addLayout(path_row)
# Separator
sep2 = QFrame()
sep2.setFrameShape(QFrame.HLine)
sep2.setFrameShadow(QFrame.Sunken)
root.addWidget(sep2)
# Progress bar (hidden by default)
self._pgbar = QProgressBar()
self._pgbar.setRange(0, 0) # indeterminate
self._pgbar.setVisible(False)
root.addWidget(self._pgbar)
# Bottom button row
btn_row = QHBoxLayout()
btn_row.addStretch()
self._btn_dl = QPushButton("Download")
self._btn_dl.setEnabled(False)
self._btn_dl.setDefault(True)
self._btn_dl.clicked.connect(self._on_download)
btn_row.addWidget(self._btn_dl)
btn_cancel = QPushButton("Cancel")
btn_cancel.clicked.connect(self.reject)
btn_row.addWidget(btn_cancel)
root.addLayout(btn_row)
def _refresh_device_label(self):
self._lbl_device.setText(
f"<b>{self._device_name}</b> "
f"<span style='color:gray'>({self._family['repo']})</span>"
)
# ── Type 변경 ────────────────────────────────────────────────────
def _on_type_changed(self, index):
if index < 0 or index >= len(self._fw_type_list):
return
entry = self._fw_type_list[index]
self._family = entry["family"]
self._device_spec = entry["device_spec"]
self._refresh_device_label()
self._start_fetch()
# ── Fetch releases ───────────────────────────────────────────────
def _start_fetch(self):
self._set_busy(True)
self._combo.clear()
self._lbl_asset.setText("Fetching releases...")
self._lbl_asset.setStyleSheet("")
self._releases = []
self._current_asset = None
self._fetch_thread = _FetchReleasesThread(self._fetcher, self._family)
self._fetch_thread.done.connect(self._on_releases_fetched)
self._fetch_thread.error.connect(self._on_fetch_error)
self._fetch_thread.start()
def _on_releases_fetched(self, releases):
self._releases = releases
self._combo.blockSignals(True)
self._combo.clear()
for r in releases:
date = r.get("published_at", "")[:10]
self._combo.addItem(f"{r['tag_name']} ({date})")
self._combo.blockSignals(False)
self._set_busy(False)
if releases:
self._update_asset_label(0)
else:
self._lbl_asset.setText("No releases found.")
def _on_fetch_error(self, msg):
self._set_busy(False)
self._lbl_asset.setText("Fetch failed")
self._lbl_asset.setStyleSheet("color: red;")
QMessageBox.critical(self, "Error", f"Failed to fetch releases:\n{msg}")
def _update_asset_label(self, index):
if not self._releases or index < 0 or index >= len(self._releases):
self._current_asset = None
self._btn_dl.setEnabled(False)
return
release = self._releases[index]
asset = self._fetcher.find_asset(release, self._device_spec, self._family)
self._current_asset = asset
if asset:
extract = self._device_spec.get("extract_file") or "(direct use)"
self._lbl_asset.setText(f"{asset['name']} → {extract}")
self._lbl_asset.setStyleSheet("")
self._btn_dl.setEnabled(True)
else:
self._lbl_asset.setText("No matching asset for this release.")
self._lbl_asset.setStyleSheet("color: red;")
self._btn_dl.setEnabled(False)
# ── Download ─────────────────────────────────────────────────────
def _on_download(self):
self._set_busy(True)
extract_file = self._device_spec.get("extract_file")
self._dl_thread = _DownloadThread(
self._fetcher,
self._current_asset,
self._dl_path,
extract_file,
)
self._dl_thread.done.connect(self._on_download_done)
self._dl_thread.error.connect(self._on_download_error)
self._dl_thread.start()
def _on_download_done(self, path, size):
self._tmp_bin_path = path
if self._wiz550_config is not None:
if self._image_validator is not None and not self._image_validator(path):
self._set_busy(False)
self._cleanup_tmp()
self._lbl_asset.setText("Firmware image check failed.")
self._lbl_asset.setStyleSheet("color: red;")
return
self._start_wiz550_upload(path)
else:
self._set_busy(False)
self.firmware_ready.emit(path, size)
self.accept()
def _on_download_error(self, msg):
self._set_busy(False)
self._cleanup_tmp()
QMessageBox.critical(self, "Error", f"Download failed:\n{msg}")
# ── WIZ550 인라인 TFTP 업로드 ─────────────────────────────────────
def _start_wiz550_upload(self, fw_path):
from WIZ550FWUploadThread import WIZ550FWUploadThread
from wiz550_fw_dialog import _best_server_ip, _warn_no_nic
cfg = self._wiz550_config
self._lbl_asset.setText("Starting TFTP upload...")
self._lbl_asset.setStyleSheet("color: gray;")
password = self._edit_pw.text() if hasattr(self, '_edit_pw') else ''
server_ip, all_ips = _best_server_ip(cfg['target_ip'])
if not server_ip:
if not _warn_no_nic(self, cfg['target_ip'], all_ips):
self._set_busy(False)
return
server_ip = ""
self._upload_thread = WIZ550FWUploadThread(
mode='auto',
fw_path=fw_path,
target_ip=cfg['target_ip'],
target_mac=cfg['target_mac'],
server_ip=server_ip,
server_port=69,
password=password,
iface_ip=cfg['localip_addr'],
)
self._upload_thread.progress.connect(self._on_upload_progress)
self._upload_thread.finished.connect(self._on_upload_finished)
self._upload_thread.error.connect(self._on_upload_error)
self._upload_thread.start()
def _on_upload_progress(self, msg: str):
self._lbl_asset.setText(msg)
self._lbl_asset.setStyleSheet("color: gray;")
def _on_upload_finished(self, success: bool):
self._set_busy(False)
self._cleanup_tmp()
if success:
self._lbl_asset.setText("Upload complete!")
self._lbl_asset.setStyleSheet("color: green;")
QTimer.singleShot(1500, self.accept)
else:
current = self._lbl_asset.text()
if current in {"Starting TFTP upload...", "Sending firmware upload request...",
"Waiting for device response..."}:
self._lbl_asset.setText(
"Upload timed out — no response from device. "
"Check connection and retry."
)
self._lbl_asset.setStyleSheet("color: red;")
def _on_upload_error(self, msg: str):
self._set_busy(False)
self._cleanup_tmp()
self._lbl_asset.setText(f"Upload error: {msg}")
self._lbl_asset.setStyleSheet("color: red;")
def _cleanup_tmp(self):
if self._tmp_bin_path and os.path.isfile(self._tmp_bin_path):
try:
os.remove(self._tmp_bin_path)
except OSError:
pass
self._tmp_bin_path = None
# ── Save path ────────────────────────────────────────────────────
def _change_dl_path(self):
path = QFileDialog.getExistingDirectory(
self, "Select Download Directory", self._dl_path
)
if path:
self._dl_path = path
self._lbl_dlpath.setText(path)
# ── 닫기 안전 정리 ────────────────────────────────────────────────
def closeEvent(self, event):
if self._upload_thread is not None and self._upload_thread.isRunning():
self._upload_thread.stop()
self._upload_thread.wait(3000)
event.accept()
# ── Helpers ──────────────────────────────────────────────────────
def _set_busy(self, busy: bool):
self._pgbar.setVisible(busy)
self._btn_refresh.setEnabled(not busy)
self._combo.setEnabled(not busy)
if self._fw_type_list:
self._combo_type.setEnabled(not busy)
if not busy:
self._btn_dl.setEnabled(self._current_asset is not None)
else:
self._btn_dl.setEnabled(False)