-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
373 lines (309 loc) · 14.5 KB
/
main.py
File metadata and controls
373 lines (309 loc) · 14.5 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
import configparser
import logging
import sys
import time
import json
import subprocess # 用於 PowerShell 指令
from pathlib import Path
from datetime import datetime
import requests
# --- Environment Detection (GUI vs CLI) ---
USE_GUI = False
if sys.platform == 'win32':
try:
import tkinter as tk
from tkinter import messagebox
USE_GUI = True
except ImportError:
USE_GUI = False
else:
USE_GUI = False
def get_executable_version() -> str:
"""Reads the version from the executable's properties."""
try:
from win32api import GetFileVersionInfo, LOWORD, HIWORD
info = GetFileVersionInfo(sys.executable, "\\")
ms = info['FileVersionMS']
ls = info['FileVersionLS']
return f"{HIWORD(ms)}.{LOWORD(ms)}.{HIWORD(ls)}.{LOWORD(ls)}"
except Exception:
return f"Unknown"
# --- Path Handling for PyInstaller ---
def get_resource_path(relative_path: str) -> Path:
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
base_path = Path(sys.executable).parent
else:
base_path = Path(__file__).parent
return base_path / relative_path
# --- Logger Setup ---
def setup_logging(log_file_path: str):
logging.basicConfig(
level=logging.DEBUG,
format='[%(asctime)s] [%(levelname)s] %(message)s',
handlers=[logging.FileHandler(log_file_path, mode='w', encoding='utf-8'),
logging.StreamHandler(sys.stdout)]
)
# --- Configuration Handling ---
def load_config():
config_path = get_resource_path('config.ini')
if not config_path.is_file():
logging.error(f"Configuration file '{config_path}' not found.")
return None
config = configparser.ConfigParser()
config.read(config_path, encoding='utf-8')
try:
settings = {
'mes_server': config.get('Global', 'MES_Server').strip('"\' '),
'mes_api': config.get('Global', 'MES_API').strip('"\' '),
'mb_sn_path': config.get('Global', 'MB_SN_PATH').strip('"\' '),
'retry_count': config.getint('Global', 'RETRY_COUNT', fallback=3),
'retry_delay': config.getint('Global', 'RETRY_DELAY_SECONDS', fallback=5),
'template_path': config.get('Global', 'TEMPLATE_PATH', fallback='mes_template.txt').strip('"\' '),
'output_path': config.get('Global', 'OUTPUT_PATH', fallback='MES.txt').strip('"\' '),
'raw_output_path': config.get('Global', 'RAW_OUTPUT_PATH', fallback='MES_Raw.json').strip('"\' '),
'log_path': config.get('Global', 'LOG_PATH', fallback='./log/').strip('"\' '),
'request_timeout': config.getint('Global', 'REQUEST_TIMEOUT_SECONDS', fallback=10)
}
logging.info("Configuration loaded successfully.")
return settings
except configparser.NoOptionError as e:
logging.error(f"Missing required option in config file: {e}")
return None
# --- Serial Number Handling (PowerShell Priority) ---
def get_mb_sn(file_path: str) -> str | None:
"""
優先嘗試透過 PowerShell 獲取 BaseBoard SN。
如果失敗或為空,則降級使用讀取設定檔的方式。
"""
# --- 優先嘗試:使用 PowerShell 獲取 BaseBoard SN ---
try:
logging.info("Attempting to get SN via PowerShell (Win32_BaseBoard)...")
cmd = ["powershell", "-Command", "(Get-WmiObject Win32_BaseBoard).SerialNumber"]
startupinfo = None
if sys.platform == 'win32':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=5,
startupinfo=startupinfo
)
if result.returncode == 0:
p_sn = result.stdout.strip()
if p_sn:
logging.info(f"Successfully read SN via PowerShell: {p_sn}")
return p_sn
else:
logging.warning("PowerShell returned an empty SN.")
else:
logging.warning(f"PowerShell command failed with return code: {result.returncode}")
except Exception as e:
logging.warning(f"Failed to get SN via PowerShell: {e}")
# --- 備案機制:讀取 SN.ini ---
logging.info(f"Falling back to reading SN from file: {file_path}")
sn_path = Path(file_path)
if not sn_path.is_file():
logging.error(f"SN file '{file_path}' not found.")
return None
try:
sn = sn_path.read_text().strip()
if not sn:
logging.error(f"SN file '{file_path}' is empty.")
return None
logging.info(f"Successfully read SN from file: {sn}")
return sn
except Exception as e:
logging.error(f"Error reading SN file '{file_path}': {e}")
return None
# --- Template Processing Logic ---
def process_mes_template(template_path: Path, mes_data: dict) -> list[str]:
lines = []
if template_path.is_file():
try:
with open(template_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
except Exception as e:
logging.error(f"Error reading template file: {e}")
else:
logging.warning(f"Template file '{template_path}' not found. Generating default key-value list.")
remaining_keys = list(mes_data.keys())
new_content = []
now = datetime.now()
timestamp_str = now.strftime("%Y-%m-%d %H:%M:%S") + f".{now.strftime('%f')[:2]}"
new_content.append(f"{timestamp_str}\n")
for line in lines:
line_stripped = line.strip()
if not line.endswith('\n'):
line += '\n'
if "##" in line_stripped and ("=" in line_stripped or ":" in line_stripped):
sep = "=" if "=" in line_stripped else ":"
try:
start_idx = line.find("##")
sep_idx = line.find(sep, start_idx)
if sep_idx > start_idx:
key_in_template = line[start_idx+2:sep_idx].strip()
if key_in_template in mes_data:
value = mes_data[key_in_template]
prefix = line[:sep_idx+1]
new_line = f"{prefix}{value}\n"
new_content.append(new_line)
if key_in_template in remaining_keys:
remaining_keys.remove(key_in_template)
continue
except Exception as e:
logging.warning(f"Failed to parse template line '{line_stripped}': {e}")
new_content.append(line)
if remaining_keys:
new_keys_content = []
for key in remaining_keys:
new_keys_content.append(f"##{key}={mes_data[key]}\n")
if new_content and new_content[-1].strip() == "##":
last_line = new_content.pop()
new_content.extend(new_keys_content)
new_content.append(last_line)
else:
new_content.extend(new_keys_content)
return new_content
# --- Error Handling ---
def show_error_and_exit(message: str):
logging.error(f"Displaying error and exiting: {message}")
if USE_GUI:
try:
root = tk.Tk()
root.withdraw()
messagebox.showerror("Connection Failed", message)
except Exception as e:
print(f"\n[ERROR] {message}\n", file=sys.stderr)
else:
print(f"\n[ERROR] {message}\n", file=sys.stderr)
sys.exit(1)
# --- Main Application Logic ---
def main():
version = get_executable_version()
logging.info(f"--- MES Tool Version: {version} ---")
if not USE_GUI:
logging.info("Running in CLI mode (No GUI).")
else:
logging.info("Running in GUI mode.")
config = load_config()
if not config:
show_error_and_exit("Failed to load configuration.")
# 1. Get MB SN (PowerShell -> File)
mb_sn = get_mb_sn(config['mb_sn_path'])
if not mb_sn:
show_error_and_exit("Failed to load SN configuration (PowerShell & File both failed).")
api_url = f"{config['mes_server'].rstrip('/')}/{config['mes_api'].lstrip('/')}{mb_sn}"
logging.info(f"Preparing to connect to MES API: {api_url}")
response = None
mes_data_content = None
# 變數初始化
system_sn = None
mes_station = None
# 2. Connect to MES
for attempt in range(config['retry_count']):
try:
logging.info(f"Connection attempt {attempt + 1}/{config['retry_count']}...")
response = requests.get(api_url, timeout=config['request_timeout'])
logging.debug(f"Response Status: {response.status_code}")
if response.status_code == 200:
logging.info("Successfully retrieved information from MES (HTTP 200 OK).")
try:
resp_json = response.json()
is_successful = resp_json.get('success')
if is_successful is True:
logging.info(f"MES business logic success ('success': {is_successful}).")
data_dict = resp_json.get('data', {})
# [Logic 1] Check System SN
system_sn = data_dict.get('systemSN')
if not system_sn:
show_error_and_exit("MES response missing 'systemSN' or value is empty.")
logging.info(f"Retrieved System SN: {system_sn}")
# [Logic 2] Check MES Station (NEW)
mes_station = data_dict.get('mesStation')
if not mes_station:
show_error_and_exit("MES response missing 'mesStation' or value is empty.")
logging.info(f"Retrieved MES Station: {mes_station}")
template_path = get_resource_path(config['template_path'])
logging.info(f"Processing template: {template_path}")
mes_data_content = process_mes_template(template_path, data_dict)
break
else:
error_message = resp_json.get('message', 'No message provided.')
logging.warning(f"MES business logic failed. Message: {error_message}")
response = None
except requests.exceptions.JSONDecodeError:
logging.error("Failed to parse MES response as JSON.")
response = None
else:
logging.warning(f"Connection failed, status code: {response.status_code}.")
response = None
except requests.exceptions.RequestException as e:
logging.error(f"An HTTP Request exception occurred: {e}")
response = None
if response is None and attempt < config['retry_count'] - 1:
logging.info(f"Retrying in {config['retry_delay']} seconds...")
time.sleep(config['retry_delay'])
if mes_data_content is None or response is None:
show_error_and_exit(f"Could not connect to MES system or retrieve valid data.\nURL: {api_url}")
# 4-1. Generate PROCESSED file (MES.txt)
output_file_path = get_resource_path(config['output_path'])
try:
output_file_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_file_path, 'w', encoding='utf-8') as f:
f.writelines(mes_data_content)
logging.info(f"Successfully wrote Processed MES information to '{output_file_path}'.")
except IOError as e:
logging.error(f"Failed to write to file '{output_file_path}': {e}")
show_error_and_exit(f"Could not write to output file '{output_file_path}'.")
# 4-2. Generate SN file (sn.txt)
sn_output_path = output_file_path.parent / 'sn.txt'
try:
with open(sn_output_path, 'w', encoding='utf-8') as f:
f.write(str(system_sn))
logging.info(f"Successfully wrote System SN to '{sn_output_path}'.")
except IOError as e:
logging.error(f"Failed to write to SN file '{sn_output_path}': {e}")
show_error_and_exit(f"Could not write to SN file '{sn_output_path}'.")
# 4-3. Generate Station file (station.txt) (NEW)
station_output_path = output_file_path.parent / 'station.txt'
try:
with open(station_output_path, 'w', encoding='utf-8') as f:
f.write(str(mes_station))
logging.info(f"Successfully wrote Station to '{station_output_path}'.")
except IOError as e:
logging.error(f"Failed to write to Station file '{station_output_path}': {e}")
show_error_and_exit(f"Could not write to Station file '{station_output_path}'.")
# 4-4. Generate RAW JSON file
raw_output_path = get_resource_path(config['raw_output_path'])
try:
raw_output_path.parent.mkdir(parents=True, exist_ok=True)
with open(raw_output_path, 'w', encoding='utf-8') as f:
try:
json.dump(response.json(), f, ensure_ascii=False, indent=4)
except:
f.write(response.text)
logging.info(f"Successfully wrote Raw JSON information to '{raw_output_path}'.")
except IOError as e:
logging.error(f"Failed to write to raw file '{raw_output_path}': {e}")
logging.info("Tool execution finished.")
sys.exit(0)
if __name__ == '__main__':
config_file_path = get_resource_path('config.ini')
temp_config = configparser.ConfigParser()
temp_config.read(config_file_path, encoding='utf-8')
log_path = temp_config.get('Global', 'LOG_PATH', fallback='./log/').strip('"\' ')
log_dir = get_resource_path(log_path)
try:
log_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
print(f"Warning: Could not create log directory {log_dir}: {e}")
timestamp = time.strftime("%Y%m%d_%H%M%S")
log_file = log_dir / f"debug_{timestamp}.log"
setup_logging(str(log_file))
try:
main()
except KeyboardInterrupt:
logging.warning("Program interrupted by user. Exiting.")
sys.exit(130)