15 KiB
15 KiB
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
// 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
迴圈邏輯
// 遍历 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)
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)
// 單一控制器壓縮
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)
// 原始資料: 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 類別使用
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 檔案的程式碼
// 從 reply[15] 開始,寫入 512 bytes
fileOut.write(reply, 15, 512);
所以 .bin 檔案內容是:
- 每筆 512 bytes
- 包含 header 資訊
解析方式
1. 解析 Header
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 解析範例
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('<I', record[0:4])[0]
event_id = struct.unpack('<H', record[4:6])[0]
message = record[6:].decode('ascii', errors='replace').rstrip('\x00')
records.append({
'timestamp': timestamp,
'event_id': event_id,
'message': message
})
return records
# 使用
with open('EventPage.bin', 'rb') as f:
data = f.read()
events = parse_event_log(data)
for e in events:
print(f"{e['event_id']}: {e['message']}")
4. 用 Hexdump 查看
# 查看前 512 bytes
xxd -l 512 EventPage.bin
# 輸出範例:
00000000: 5001 0000 0000 0000 0000 5001 ff ff 0000 0000 P...........P....
00000010: 0000 0001 0000 0001 0000 0000 0000 0000 ................
00000020: 3031 3233 3435 3637 3839 3031 3233 3435 0123456789012345
00000030: 3637 3839 0000 0000 0000 0000 0000 0000 6789............
...
TelMesPages.log 特殊處理
Page 0x64 有特殊處理:
- 原始:40 bytes/record
- 輸出:去除 null 字元 (0x00)
// 寫入時的處理
for(int j=0; j < logs[i].length()/40; j++) {
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); // 加上換行
}
Rust 解析範例
use std::fs::File;
use std::io::Read;
#[derive(Debug)]
struct LogHeader {
status: u8,
page_code: u8,
log_count: u32,
start_block: u32,
total_blocks: u32,
}
fn parse_bin_header(block: &[u8; 512]) -> 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;
}
}