# Dump Controller Log 實現分析 ## 流程架構 ``` ┌─────────────────────────────────────────────────────────────┐ │ frmMain.jmDumpEventLog_actionPerformed() │ │ (根據型號選擇使用 dlgDumpLog 或 dlgDumpLog2) │ └──────────────────────────┬──────────────────────────────────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ dlgDumpLog │ │ dlgDumpLog2 │ │ (舊型號) │ │ (新型號) │ └──────────────┘ └──────────────┘ │ │ └───────────┬───────────────────┘ ▼ ┌──────────────────────────┐ │ dumpEventLog() │ │ TCP 連線 Port 8922 │ └───────────┬──────────────┘ ▼ ┌──────────────────────────┐ │ OpCode: 0x01 │ │ SubOp: 0x03 │ │ Page: 0x60 ~ 0x64 │ └───────────┬──────────────┘ ▼ ┌──────────────────────────┐ │ 寫入 ./Log 目錄 │ │ 壓縮為 ZIP 檔 │ └──────────────────────────┘ ``` ## 通訊協定 ### Request ``` Head: [0x01, 0x17, 0x00, 0x00, 0x00] (23 bytes) Data: [0x02, 0x01, SN(16 bytes), CDB(5 bytes)] ``` ### CDB 結構 | Offset | 值 | 說明 | |--------|-----|------| | 0 | 0x01 | OpCode - GET_PAGE | | 1 | 0x03 | SubOp | | 2 | 0x60-0x64 | Page Code (Log Type) | | 3 | LSB | Block Index | | 4 | MSB | Block Index | ### Log Types | Page | 說明 | |------|------| | 0x60 | System Log | | 0x61 | Event Log | | 0x62 | I/O Error Log | | 0x63 | Dump Log | | 0x64 | Reserved | ## 實現細節 ### dlgDumpLog.java ```java // 1. 連線到控制器 theSocket.connect(new InetSocketAddress(PageDataDB.GuiSrvIpAddr, Port), 10000); // 2. 發送请求 (OpCode 0x01, SubOp 0x03) Cdb[0] = 0x01; // GET_PAGE Cdb[1] = 0x03; // SubOp Cdb[2] = 0x60; // Log type (0x60-0x64) Cdb[3] = BlockIndexLSB; Cdb[4] = BlockIndexMSB; // 3. 解析回應 arg[0] = reply[9]; // Status (0x50 = Success) arg[1] = reply[12]; // Page code arg[2] = startBlock; arg[3] = totalBlocks; // 4. 寫入檔案 (./Log 目錄) File file = getFile((byte)arg[1]); fileOut.write(reply, 15, 512); // 寫入 512 bytes // 5. 壓縮成 ZIP compressLog(); // 打包成 Log_YYYYMMDD_HHMMSS.zip ``` ### 迴圈邏輯 ```java // 遍历 5 種 log 類型 for (byte i = 0x60; i <= 0x64; i++) { // 取得該類型的 log 總量 arg1 = dumpEventLog(Cdb); // 遍历每個 block for (int j = arg1[2]; j < arg1[3]; j++) { Cdb[3] = (byte)(j & 0xFF); // LSB Cdb[4] = (byte)((j >> 8) & 0xFF); // MSB arg2 = dumpEventLog(Cdb); // 寫入單個 block fileOut.write(reply, 15, 512); } } ``` ## 輸出檔案 - **目錄**: `./Log/` - **格式**: `Log_YYYYMMDD_HHMMSS.zip` - **內容**: 5 個 log 類型的原始資料 (各 512 bytes/block) ## 關鍵類 | 類別 | 檔案 | 功能 | |------|------|------| | `frmMain` | frmMain.java:4136 | 入口,選擇 dialog 版本 | | `dlgDumpLog` | dlgDumpLog.java | 舊型號使用 (單一控制器) | | `dlgDumpLog2` | dlgDumpLog2.java | 新型號使用 (支援多控制器) | | `SendCmd` | InBandAPI/SendCmd.java | TCP 通訊底層 | ## Code Flow ### 1. Menu 點擊 ``` frmMain.jmDumpControllerLog (MenuItem) → frmMain.jmDumpEventLog_actionPerformed() → 檢查型號選擇 dlgDumpLog 或 dlgDumpLog2 ``` ### 2. Dump 執行 ``` dlgDumpLog.run() → for (0x60 ~ 0x64) { dumpEventLog() 取得總量 for (每個 block) { dumpEventLog() 取得資料 寫入檔案 } } → compressLog() 壓縮 → dispose() ``` ### 3. Socket 通訊 ``` dumpEventLog(Cdb) → connect(Port 8922) → send(Head + SN + CDB) → read(reply) → parse status & data → close() ``` --- ## ZIP 壓縮處理細節 ### 輸出檔案 **dlgDumpLog.java (舊型號)** ``` ./Log/{SN}_ControllerLog_{YYYY}_{M}_{D}_{H}_{m}_{s}.zip ``` **dlgDumpLog2.java (新型號)** ``` Primary: ./Log/Primary_{SN}_ControllerLog.zip Secondary: ./Log/Secondary_{SN}_ControllerLog.zip 合併: ./Log/{PrimarySN}_{SecondarySN}_ControllerLog_{YYYY}_{M}_{D}_{H}_{m}_{s}.zip ``` ### 壓縮前的暫存檔 | Page Code | 檔案名稱 | 說明 | |-----------|----------|------| | 0x60 | EventPage.bin | Event Log | | 0x61 | DebugPage.bin | Debug Log | | 0x62 | DiskMetadataPage.bin | Disk Metadata | | 0x63 | SpecialLogPage.bin | Special Log | | 0x64 | TelMesPages.log | Telemetry Messages | ### compressLog() 流程 (dlgDumpLog.java) ```java public void compressLog() { File[] logs = new File[5]; for(int i=0; i<=4; i++) { logs[i] = getFile((byte)(i+0x60)); // 取得 5 個暫存檔 } // 建立 ZIP 檔案 Calendar rightNow = Calendar.getInstance(); String filename = "./Log/" + PageDataDB.sSN.trim() + "_ControllerLog_" + rightNow.get(Calendar.YEAR) + "_" + (rightNow.get(Calendar.MONTH)+1) + "_" + rightNow.get(Calendar.DAY_OF_MONTH) + "_" + rightNow.get(Calendar.HOUR) + "_" + rightNow.get(Calendar.MINUTE) + "_" + rightNow.get(Calendar.SECOND) + ".zip"; FileOutputStream fout = new FileOutputStream(filename); ZipOutputStream zout = new ZipOutputStream(fout); zout.setLevel(5); // 壓縮等級 0-9 // 逐一壓縮每個檔案 for (byte i = 0; i <= 4; i++) { if(logs[i].exists()) { ZipEntry ze = new ZipEntry(logs[i].getName()); FileInputStream fin = new FileInputStream(logs[i]); zout.putNextEntry(ze); // 特殊處理 Page 0x64 (TelMesPages.log) if(i != 0x4) { // 一般檔案:直接複製 for(int c = fin.read(); c != -1; c = fin.read()) { zout.write(c); } } else { // TelMesPages.log: 每 40 bytes 為一筆記錄 // 移除空字元 (0x00),只保留有效資料 byte data; boolean skipNull = false; for(int j=0; j < logs[i].length()/40; j++) { skipNull = false; for(int k=0; k<40; k++) { data = (byte)fin.read(); if(data == 0x0a || skipNull == true) { skipNull = true; } else { zout.write(data); } } zout.write(0x0a); // 加上換行符 } } zout.closeEntry(); fin.close(); logs[i].delete(); // 刪除暫存檔 } } zout.close(); } ``` ### compressLog() 流程 (dlgDumpLog2.java) ```java // 單一控制器壓縮 public void compressLog(String sSN) { String prefix = sSN.equals(PageDataDB.sSN) ? "Primary" : "Secondary"; String filename = "./Log/" + prefix + "_" + sSN.trim() + "_ControllerLog.zip"; // ... 同上邏輯 } // 雙控制器合併壓縮 public void compressLog2() { File[] logs = new File[2]; logs[0] = new File(sMaterFileNamePatch); // Primary logs[1] = new File(sSlaveFileNamePatch); // Secondary // 合併成單一 ZIP,內含兩個 ZIP 檔 String filename = "./Log/" + PageDataDB.sSN.trim() + "_" + PageDataDB.sSlaveSN.trim() + "_ControllerLog_{timestamp}.zip"; // ... } ``` ### ZIP 結構 ``` Archive: {SN}_ControllerLog_2024_3_29_10_30_45.zip ├── EventPage.bin (512 bytes/block) ├── DebugPage.bin (512 bytes/block) ├── DiskMetadataPage.bin (512 bytes/block) ├── SpecialLogPage.bin (512 bytes/block) └── TelMesPages.log (40 bytes/record, 去除 null) ``` ### 特殊處理:TelMesPages.log (Page 0x64) ```java // 原始資料: 40 bytes/record,包含 null (0x00) 字元 // 處理邏輯: // 1. 讀取每 40 bytes // 2. 跳過開頭的 null 字元 // 3. 遇到換行符 (0x0a) 後,後續全部視為有效資料 // 4. 輸出時每筆記錄加上換行符 // Example: // Input: [0x00, 0x00, 0x00, 0x31, 0x32, ..., 0x00, 0x00] // Output: "12...data\n" ``` ### Java 類別使用 ```java import java.util.zip.ZipOutputStream; import java.util.zip.ZipEntry; import java.util.Calendar; // 關鍵類別 ZipOutputStream zout = new ZipOutputStream(fout); zout.setLevel(5); // 壓縮等級 (0=儲存, 9=最大壓縮) ZipEntry ze = new ZipEntry(filename); zout.putNextEntry(ze); zout.write(byte[] data); zout.closeEntry(); zout.close(); ``` --- ## .bin 檔案解析 ### 原始資料格式 控制器回傳的資料包含 **Header + Data**: ``` ┌─────────────────────────────────────────────┐ │ Controller Response │ ├─────────┬───────────────────────────────────┤ │ Offset │ Description │ ├─────────┼───────────────────────────────────┤ │ 0-2 │ Total Length (2 bytes) │ │ 3 │ (continuation?) │ │ 4-8 │ Unknown │ │ 9 │ Status (0x50 = Success) │ │ 10-11 │ Unknown │ │ 12 │ Page Code (0x60-0x64) │ │ 13-14 │ 0xFF 0xFF (flag) │ │ 15-18 │ Log Count (4 bytes) │ │ 19-22 │ Start Block (4 bytes) │ │ 23-26 │ Total Blocks (4 bytes) │ ├─────────┴───────────────────────────────────┤ │ 15-511 │ Actual Data (496 bytes) │ └─────────┴───────────────────────────────────┘ ``` ### 寫入 .bin 檔案的程式碼 ```java // 從 reply[15] 開始,寫入 512 bytes fileOut.write(reply, 15, 512); ``` 所以 .bin 檔案內容是: - **每筆 512 bytes** - **包含 header 資訊** ### 解析方式 #### 1. 解析 Header ```python def parse_bin_header(block): # block 是 512 bytes status = block[9] # 0x50 = Success page_code = block[12] # 0x60-0x64 log_count = int.from_bytes(block[15:19], 'little') start_block = int.from_bytes(block[19:23], 'little') total_blocks = int.from_bytes(block[23:27], 'little') data = block[15:] # 實際資料 return { 'status': status, 'page_code': page_code, 'log_count': log_count, 'start_block': start_block, 'total_blocks': total_blocks, 'data': data } ``` #### 2. 解析各類型 Log | Page Code | 類型 | 資料格式 | |-----------|------|----------| | 0x60 | EventPage.bin | 32 bytes/record | | 0x61 | DebugPage.bin | 32 bytes/record | | 0x62 | DiskMetadataPage.bin | 32 bytes/record | | 0x63 | SpecialLogPage.bin | 48 bytes/record | | 0x64 | TelMesPages.log | 40 bytes/record (特殊處理) | #### 3. Python 解析範例 ```python import struct import datetime def parse_event_log(data): """解析 Event Page (0x60)""" records = [] for i in range(0, len(data), 32): if i + 32 > len(data): break record = data[i:i+32] # 解析 timestamp (假設格式) timestamp = struct.unpack(' LogHeader { LogHeader { status: block[9], page_code: block[12], log_count: u32::from_le_bytes([block[15], block[16], block[17], block[18]]), start_block: u32::from_le_bytes([block[19], block[20], block[21], block[22]]), total_blocks: u32::from_le_bytes([block[23], block[24], block[25], block[26]]), } } fn main() { let mut file = File::open("EventPage.bin").unwrap(); let mut buffer = [0u8; 512]; let mut block_num = 0; while file.read(&mut buffer).unwrap() > 0 { let header = parse_bin_header(&buffer); println!("Block {}: status={:02x}, page={:02x}", block_num, header.status, header.page_code); block_num += 1; } } ```