503 lines
15 KiB
Markdown
503 lines
15 KiB
Markdown
# 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('<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 查看
|
||
|
||
```bash
|
||
# 查看前 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)
|
||
|
||
```java
|
||
// 寫入時的處理
|
||
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 解析範例
|
||
|
||
```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;
|
||
}
|
||
}
|
||
```
|