docs: 更新 Memory Viewer 设计文档,新增双击编辑和写入机制

This commit is contained in:
xiahb 2026-07-22 16:29:48 +08:00
parent 427d83c9bb
commit e0274c3cb9
4 changed files with 582 additions and 11 deletions

View File

@ -0,0 +1,391 @@
# Memory Viewer 内存视图设计文档
## 1. 概述
Memory Viewer 是一个嵌入式面板视图,为用户提供实时内存数据查看、导出、导入和填充功能。作为 VS Code 面板容器中的视图(与问题、输出、终端同级),通过 DAP (Debug Adapter Protocol) 自定义请求与 DSS 调试引擎通信。
### 1.1 核心能力
| 功能 | 说明 |
|------|------|
| 内存读取 | 通过 DAP `readMemory` 从调试目标读取指定地址范围的内存数据 |
| 数据展示 | 支持十六进制、有符号/无符号整数、单精度/双精度浮点多种格式 |
| 滚动加载 | 向下滚动自动追加数据,向上滚动自动回溯历史数据 |
| 内存导出 | 支持 TI HEX / 整数文本 / 长整数文本 / 浮点文本 / COFF / 原始二进制 六种格式 |
| 内存导入 | 支持自动检测格式COFF / TI-TXT / Intel HEX和原始二进制导入 |
| 内存填充 | 用指定值填充连续内存区域 |
| 双击编辑 | 双击值单元格可修改内存数据Enter 确认写入Esc 取消 |
| 自动刷新 | 可配置间隔的定时自动刷新 |
## 2. 架构
### 2.1 组件关系
```
┌─────────────────────────────────────────────────────────────┐
│ VS Code 面板容器 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Memory Viewer (WebviewView) │ │
│ │ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌───────┐ │ │
│ │ │ 工具栏 │ │ 数据表格 │ │ 状态栏 │ │ 对话框 │ │ │
│ │ │(HTML/JS) │ │(渲染引擎) │ │ │ │ │ │ │
│ │ └────┬────┘ └────┬─────┘ └────┬─────┘ └───┬───┘ │ │
│ │ │ │ │ │ │ │
│ │ └────────────┴─────────────┴─────────────┘ │ │
│ │ postMessage │ │
│ └───────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┴───────────────────────────────┐ │
│ │ MemoryViewerProvider (Extension) │ │
│ │ - 消息路由与业务逻辑 │ │
│ │ - 文件对话框交互 │ │
│ │ - 自动刷新定时器管理 │ │
│ └───────────────────────┬───────────────────────────────┘ │
│ │ customRequest │
│ ┌───────────────────────┴───────────────────────────────┐ │
│ │ DSS Bridge (Adapter) │ │
│ │ - DAP 协议封装 │ │
│ │ - stdin/stdout JSON 通信 │ │
│ └───────────────────────┬───────────────────────────────┘ │
│ │ stdin/stdout │
│ ┌───────────────────────┴───────────────────────────────┐ │
│ │ dss-wrapper.js (Rhino) │ │
│ │ - DSS API 调用 │ │
│ │ - 中文路径处理 │ │
│ │ - UTF-8 编码转换 │ │
│ └───────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┴───────────────────────────────┐ │
│ │ DSS Debug Engine (Java) │ │
│ │ - Memory.readData / writeData │ │
│ │ - Memory.saveData / saveRaw │ │
│ │ - Memory.load / loadRaw │ │
│ │ - Memory.fill │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 数据流
#### 读取流程
```
Webview.scroll/go → postMessage('read') → Provider._read()
→ session.customRequest('readMemory') → Bridge → DSS
→ DSS readData() → 返回 base64 → 解码为 byte[]
→ postMessage('data') → Webview.render()
```
#### 导出流程
```
Webview.btnExport → showSaveDialog() → postMessage('export')
→ Provider._export() → session.customRequest('exportMemory')
→ Bridge → DSS saveData()/saveRaw()
→ [中文路径: 写入临时文件 → renameTo/复制到目标路径]
```
#### 导入流程
```
Webview.btnImport → showOpenDialog() → postMessage('import')
→ Provider._import() → session.customRequest('importMemory')
→ Bridge → DSS load()/loadRaw()
→ [中文路径: 复制到临时文件 → DSS 从临时文件加载 → 删除临时文件]
```
#### 写入流程(双击编辑)
```
Webview commit() → postMessage('write', { address, hexData, callbackId })
→ Provider._write(address, hexData, callbackId)
→ Buffer.from(hexData, "hex") → base64 编码
→ session.customRequest('writeMemory', { memoryReference, data: base64 })
→ adapter.writeMemoryRequest() → bridge.writeMemory()
→ DSS writeMemory() → writeDataArray() → 逐字节写入
→ postMessage('writeResult', { callbackId, success }) → Webview 刷新
```
## 3. 通信协议
### 3.1 Webview ↔ ExtensionpostMessage
#### Webview → Extension
| 命令 | 参数 | 说明 |
|------|------|------|
| `read` | `address, count, prepend?, append?` | 读取内存 |
| `autoRefresh` | `enabled, interval` | 设置自动刷新 |
| `export` | `address, length, format` | 导出内存 |
| `import` | `address, format` | 导入内存 |
| `fill` | `address, length, value` | 填充内存 |
| `write` | `address, hexData, callbackId` | 写入单个值(双击编辑) |
#### Extension → Webview
| 命令 | 参数 | 说明 |
|------|------|------|
| `data` | `address, bytes, prepend?, append?` | 内存数据返回 |
| `error` | `text` | 错误信息 |
| `info` | `text` | 成功信息 |
| `loading` | `text` | 操作进行中(带脉冲动画) |
| `sessionEnded` | — | 调试会话结束,禁用按钮 |
| `sessionActive` | — | 调试会话启动,启用按钮 |
| `autoRead` | — | 触发自动读取 |
| `autoRefreshTick` | — | 自动刷新定时器触发 |
| `writeResult` | `callbackId, success, error?` | 写入结果返回 |
### 3.2 Extension ↔ DSSDAP Custom Request
| 请求名 | 参数 | 返回 |
|--------|------|------|
| `readMemory` | `memoryReference, count` | `{ data: base64 }` |
| `writeMemory` | `memoryReference, data (base64)` | `{ bytesWritten }` |
| `exportMemory` | `address, length, format, filePath` | `{ success, message/error }` |
| `importMemory` | `address, filePath, format` | `{ success, message/error }` |
| `fillMemory` | `address, length, value` | `{ success, message/error }` |
## 4. 导出格式
| 格式标识 | 名称 | DSS API | IOFormat | 文件扩展名 |
|----------|------|---------|----------|-----------|
| `hex` | TI HEX (TI-DAT) | `saveData` | `IOFormat.HEX (1)` | .txt |
| `int` | 整数文本 | `saveData` | `IOFormat.INT (2)` | .txt |
| `long` | 长整数文本 | `saveData` | `IOFormat.LONG (3)` | .txt |
| `float` | 浮点文本 | `saveData` | `IOFormat.FLOAT (4)` | .txt |
| `coff` | COFF | `saveData` | `IOFormat.COFF (5)` | .out |
| `raw` | 原始二进制 | `saveRaw` | — | .bin |
### 4.1 saveRaw 参数说明
```
saveRaw(nPage, nAddress, sFilename, nLength, nTypeSize, bByteSwap)
```
- **nLength**: 单位是 target memory wordsC66xx 32 位 = 4 字节),前端传字节数需除以 4
- **nTypeSize = 32**: 按 32 位字处理字节序C66xx 大端 → 主机小端自动转换)
- **bByteSwap = false**: 启用自动字节序转换
## 5. 导入格式
### 5.1 格式检测策略(优先级从高到低)
1. 用户显式选择 `raw` 格式 → 使用 `loadRaw` 加载
2. 文件扩展名为 `.bin` / `.dat` / `.raw` → 使用 `loadRaw` 加载
3. 其他格式 → 使用 `load()` 自动检测(根据文件头识别 COFF / TI-TXT / Intel HEX
### 5.2 loadRaw 参数说明
```
loadRaw(nPage, nAddress, sFilename, nTypeSize, bByteSwap)
```
- **nTypeSize = 32**: 与 saveRaw 的 nTypeSize=32 匹配,确保字节序一致
- **文件大小**: DSS 自动确定,无需手动指定长度
## 6. 中文路径处理
### 6.1 问题根因
中文 Windows 下DSS 的 Rhino 引擎和 Java 文件 I/O 存在两层编码问题:
| 层级 | 问题 | 影响 |
|------|------|------|
| stdin/stdout 通信 | `InputStreamReader(System.in)` 默认使用 GBK 编码 | JSON 中的中文路径乱码 |
| 文件 I/O | `saveData`/`load` 内部用 GBK 编码文件名 | 创建的文件名乱码,或找不到文件 |
### 6.2 解决方案
#### stdin/stdout 编码修复
```javascript
// stdin: 指定 UTF-8 编码
var reader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
// stdout: 用 _writeUtf8() 直接写 UTF-8 字节
function _writeUtf8(msg) {
var bytes = new java.lang.String(str).getBytes('UTF-8');
System.out['write(byte[])'](bytes);
System.out['write(int)'](0x0A);
System.out.flush();
}
```
#### 文件 I/O 中文路径 workaround
**导出**: 先写入 ASCII 临时文件 → `File.renameTo` 或手动复制到目标路径
**导入**: 先复制源文件到 ASCII 临时文件 → DSS API 从临时文件加载 → 删除临时文件
```javascript
// 检测是否需要 workaround
var needsWorkaround = /[^\x00-\x7F]/.test(filePath);
if (needsWorkaround) {
var tempFile = new java.io.File(tempDir, "dss_export_" + nanoTime + ".tmp");
// ... DSS 写入临时文件 ...
// 然后移动到目标路径
tempFile.renameTo(targetFile); // 或手动复制
}
```
## 7. 前端交互设计
### 7.1 双击编辑机制
#### 交互流程
```
用户双击 c-val 单元格
→ 创建 input 输入框,显示当前格式下的原始值
→ 用户修改值,按 Enter 或失焦触发 commit()
→ commit() 处理:
1. committed 标志防止 Enter 和 blur 双重触发
2. 根据当前格式hex/uint/int/float/double将输入值转换为字节数组
3. NaN 检查:十六进制输入无效时回退并提示
4. hex 格式特殊处理字节序反转hex 显示时已反转,编辑时需再反转回来)
5. 构造 hexData 字符串
6. 通过 postMessage('write') 发送写入请求
→ 按 Esc 取消编辑,恢复原始文本
```
#### 值格式转换规则
| 格式 | 输入示例 | 转换逻辑 | 字节序 |
|------|---------|---------|--------|
| hex | `000003E8` | 直接解析为字节 | 显示时反转,写入时再反转 |
| uint | `1000` | parseInt → 逐字节提取 | 小端序 |
| int | `-123` | parseInt → 补码转换 → 逐字节提取 | 小端序 |
| float | `3.14` | Float32Array 编码4 字节) | 小端序 |
| double | `3.14159265` | Float64Array 编码8 字节) | 小端序 |
#### 数据流
```
commit() → valueToHexBytes(value, format, sizeBytes)
→ hexStr = hex字节拼接hex 格式需反转字节序)
→ postMessage('write', { address, hexData, callbackId })
→ Provider._write()
→ Buffer.from(hexData, "hex") → base64 编码
→ session.customRequest('writeMemory', { memoryReference, data: base64 })
→ adapter.writeMemoryRequest() → bridge.writeMemory()
→ DSS writeMemory() → writeDataArray() → 逐字节写入
→ 返回 writeResult { success, callbackId }
→ Webview 更新状态栏 + autoRead 刷新
```
### 7.2 callbackId 机制
双击编辑使用 `callbackId` 匹配异步写入结果:
```javascript
var writeCallbackId = 0;
var writeCallbacks = {}; // { [cbId]: { addr, td, origBytes, newHex } }
// commit() 时:
var cbId = ++writeCallbackId;
writeCallbacks[cbId] = { addr, td, origBytes, newHex };
V.postMessage({ command: 'write', address, hexData, callbackId: cbId });
// 收到 writeResult 时:
var cb = writeCallbacks[m.callbackId];
if (cb) {
if (m.success) { st.textContent = '写入成功: ' + cb.addr; }
else { st.textContent = '写入失败: ' + m.error; cb.td.textContent = cb.origBytes; }
delete writeCallbacks[m.callbackId];
}
```
### 7.3 按钮状态管理
| 状态 | 读取/翻页 | 导出/导入/填充 |
|------|----------|---------------|
| 无调试会话 | 禁用 | 禁用 |
| 有调试会话 | 启用 | 启用 |
| 操作进行中 | 启用 | 禁用 |
### 7.4 状态栏反馈
| 状态 | 样式 | 文本示例 |
|------|------|----------|
| 就绪 | 默认 | `就绪` |
| 加载中 | 默认 | `加载中...` |
| 导出中 | 脉冲动画 | `正在导出...` |
| 导入中 | 脉冲动画 | `正在导入...` |
| 提交修改中 | 脉冲动画 | `正在提交修改...` |
| 成功 | 绿色 | `导出成功: test.bin` |
| 写入成功 | 绿色 | `写入成功: 0x0C000000` |
| 错误 | 红色 | `导入失败: ...` |
| 写入失败 | 红色 | `写入失败: ...` |
| 会话结束 | 红色 | `会话结束` |
### 7.5 滚动加载机制
- **向下滚动**: 当 `scrollTop + clientHeight >= scrollHeight - 2*ROW_H` 时,请求追加下一个 CHUNK4096 字节)
- **向上滚动**: 当 `scrollTop <= 2*ROW_H``curAddr > 0` 时,请求回溯上一个 CHUNK
- **防抖**: `busy``scrollBusy` 标志防止并发请求
## 8. writeMemory 写入机制
### 8.1 调用链路
```
Webview commit()
→ postMessage('write', { address, hexData, callbackId })
→ Provider._write(address, hexData, callbackId)
→ Buffer.from(hexData, "hex") → base64 编码
→ session.customRequest('writeMemory', { memoryReference: address, data: base64 })
→ adapter.writeMemoryRequest() → bridge.writeMemory(address, hexData)
→ DSS writeMemory() → 将 hexData 字符串解析为 byteArray
→ writeDataArray() 逐字节调用 writeData(page, addr, value, 8)
→ 返回 { success, bytesWritten }
→ Provider postMessage('writeResult', { callbackId, success, error? })
```
### 8.2 writeDataArray 实现
```javascript
function writeDataArray(page, address, byteArr, typeSize) {
for (var i = 0; i < byteArr.length; i++) {
session.memory.writeData(page, address + i, byteArr[i], typeSize);
}
}
```
使用单值 `writeData(page, address, value, typeSize)` 逐字节写入,避免 Rhino 方法重载歧义(`write(byte[])` vs `write(int)`)。
### 8.3 错误处理
`writeMemory` 函数先尝试写入 `PROGRAM` 页面,失败后自动回退到 `DATA` 页面。
## 9. 安全策略
### 9.1 Content Security Policy (CSP)
```html
<meta http-equiv="Content-Security-Policy"
content="default-src 'none';
style-src 'unsafe-inline' 'nonce-${nonce}';
script-src 'nonce-${nonce}';">
```
- 所有 `<style>``<script>` 标签必须携带匹配的 nonce
- 禁止外部资源加载
- 禁止内联事件处理器(`onclick` 等),使用 `addEventListener` 替代
## 10. 文件结构
```
packages/dsp-mod-debug/
├── src/
│ ├── memoryViewer.ts # WebviewViewProvider 实现 + 前端 HTML/JS
│ ├── extension.ts # 注册视图、监听调试会话事件
│ └── dssAdapter/
│ ├── adapter.ts # DAP 适配器,处理自定义请求路由
│ └── dssBridge.ts # DSS 进程通信桥接
└── dss_script/
└── dss-wrapper.js # DSS Rhino 脚本,封装底层调试 API
```
## 11. 已知限制
1. **DSS API 限制**: `Memory.IOFormat` 枚举不包含 Intel HEX 格式,导出的 "Hex" 实际为 TI HEX 格式
2. **saveRaw nLength 单位**: DSS 的 `saveRaw` 长度参数单位是 target memory words32 位),非字节数
3. **编码 workaround**: 中文路径依赖临时文件中转,增加了一次文件复制开销
4. **Rhino 兼容性**: Java 方法重载(如 `write(byte[])` vs `write(int)`)需要使用 `['method签名']` 显式指定

View File

@ -3997,12 +3997,7 @@ function writeMemory(address, data) {
return { success: false, error: 'Target is not connected' };
}
/**
* 向目标内存写入数据
*
* DSS API: session.memory.writeData(page, address, longValues, typeSize)
*/
print("[DSS] Writing memory at address " + address);
print("[DSS] writeMemory called: address=" + address + ", dataType=" + typeof data + ", data=" + data);
// 将十六进制地址字符串转换为数字
var numAddress = parseInt(address, 16);
@ -4298,9 +4293,11 @@ function toJavaLongArray(arr) {
* @param {Array} byteArr - 字节数组
*/
function writeDataArray(page, address, byteArr, typeSize) {
print("[DSS] writeDataArray: page=" + page + ", addr=0x" + address.toString(16) + ", len=" + byteArr.length + ", typeSize=" + typeSize);
for (var i = 0; i < byteArr.length; i++) {
session.memory.writeData(page, address + i, byteArr[i], typeSize);
}
print("[DSS] writeDataArray: completed writing " + byteArr.length + " bytes");
}
/**

View File

@ -235,6 +235,8 @@ export class DSSDebugSession extends LoggingDebugSession {
supportsEvaluateForHovers: true,
// 支持内存读取请求
supportsReadMemoryRequest: true,
// 支持内存写入请求
supportsWriteMemoryRequest: true,
// 不支持指定步入目标
supportsStepInTargetsRequest: false,
// 不支持代码补全
@ -1135,6 +1137,42 @@ export class DSSDebugSession extends LoggingDebugSession {
}
}
/**
* DAP writeMemory
*
* session.customRequest("writeMemory", ...)
* VS Code DAP
*
* @param response
* @param args base64
*/
protected async writeMemoryRequest(
response: DebugProtocol.WriteMemoryResponse,
args: DebugProtocol.WriteMemoryArguments
) {
try {
if (!this.bridge) {
this.sendErrorResponse(response, -1, "Bridge not initialized");
return;
}
const address = args.memoryReference;
const data = args.data; // base64 encoded
const bytes = Buffer.from(data, "base64");
const hexData = bytes.toString("hex");
this.debug(`[DSS-DAP] writeMemoryRequest: address=${address}, bytes=${bytes.length}`);
await this.bridge.writeMemory(address, hexData);
response.body = { bytesWritten: bytes.length };
this.sendResponse(response);
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
this.sendErrorResponse(response, -1, `Failed to write memory: ${error}`);
}
}
/**
*
*
@ -1215,6 +1253,8 @@ export class DSSDebugSession extends LoggingDebugSession {
return;
}
// writeMemory 已由标准 DAP writeMemoryRequest 处理,不再需要在此处理
// 未知命令
this.sendErrorResponse(response, -1, `Unknown custom request: ${command}`);
} catch (err) {

View File

@ -82,10 +82,15 @@ export class MemoryViewerProvider implements vscode.WebviewViewProvider {
view.webview.html = this._getHtml(view.webview);
view.webview.onDidReceiveMessage(async (msg) => {
console.log("[MemoryViewer] Received message from webview:", msg.command);
switch (msg.command) {
case "read":
await this._read(msg.address, msg.count, msg.prepend, msg.append);
break;
case "write":
console.log("[MemoryViewer] Write command received:", msg);
await this._write(msg.address, msg.hexData, msg.callbackId);
break;
case "autoRefresh":
this._setAutoRefresh(msg.enabled, msg.interval);
break;
@ -273,6 +278,38 @@ export class MemoryViewerProvider implements vscode.WebviewViewProvider {
}
}
/**
*
*
* DAP `writeMemory`
*
*
* @param address -
* @param hexData - "0A1B2C3D"
* @param callbackId - webview
*/
private async _write(address: string, hexData: string, callbackId: number): Promise<void> {
console.log(`[MemoryViewer] _write called: address=${address}, hexData=${hexData}, callbackId=${callbackId}`);
const session = vscode.debug.activeDebugSession;
if (!session) {
console.log("[MemoryViewer] _write failed: no active debug session");
this._view?.webview.postMessage({ command: "writeResult", callbackId, success: false, error: "没有活动的调试会话" });
return;
}
try {
// 将 hex 字符串转换为 base64DAP writeMemory 标准格式)
const bytes = Buffer.from(hexData, "hex");
const base64Data = bytes.toString("base64");
await session.customRequest("writeMemory", { memoryReference: address, data: base64Data });
this._view?.webview.postMessage({ command: "writeResult", callbackId, success: true });
this._view?.webview.postMessage({ command: "autoRead" });
} catch (e) {
const errMsg = e instanceof Error ? e.message : String(e);
console.log("[MemoryViewer] _write failed:", errMsg);
this._view?.webview.postMessage({ command: "writeResult", callbackId, success: false, error: errMsg });
}
}
/**
*
*
@ -334,7 +371,9 @@ body{font-family:var(--vscode-font-family);font-size:12px;color:var(--vscode-for
table{border-collapse:collapse;white-space:nowrap}
td{padding:0 3px;vertical-align:top}
.c-addr{color:var(--vscode-debugTokenExpression-name);text-align:right;padding-right:6px;border-right:1px solid var(--vscode-editorWidget-border);user-select:none}
.c-val{color:var(--vscode-debugTokenExpression-number);letter-spacing:.5px}
.c-val-row{display:flex;gap:4px;flex-wrap:wrap}
.c-val{color:var(--vscode-debugTokenExpression-number);letter-spacing:.5px;padding:0 2px;border-radius:2px;cursor:pointer}
.c-val:hover{background:var(--vscode-list-hoverBackground)}
.c-sep{color:var(--vscode-descriptionForeground);padding:0 2px;user-select:none}
.bar2{font-size:10px;color:var(--vscode-descriptionForeground);display:flex;justify-content:space-between;flex-shrink:0;padding-top:4px;border-top:1px solid var(--vscode-editorWidget-border)}
.err{color:var(--vscode-errorForeground)}
@ -439,12 +478,14 @@ tr:hover{background:var(--vscode-list-hoverBackground)}
<script nonce="${n}">
(function(){
var V=acquireVsCodeApi();
console.log('[MemoryViewer] VS Code API acquired:', typeof V, typeof V.postMessage);
var $=id=>document.getElementById(id);
var tb=$('tb'),tbl=$('tbl'),addr=$('addr'),st=$('st'),rg=$('rg'),sz=$('sz'),col=$('col'),fmt=$('fmt');
var curAddr=0,busy=false,scrollBusy=false,opBusy=false;
var CHUNK=4096,ROW_H=18;
var btns=['btnExport','btnImport','btnFill','go','pu','pd'];
var writeCallbackId=0,writeCallbacks={};
function setSessionActive(on){
btns.forEach(function(id){var b=$(id);if(b)b.disabled=!on;});
@ -528,10 +569,13 @@ function render(base,bytes){
var bpr=col.value*sz.value;var ds=parseInt(sz.value);var fv=fmt.value;var h='';
for(var i=0;i<bytes.length;i+=bpr){
var a=base+i,row=bytes.slice(i,i+bpr);
h+='<tr><td class="c-addr">'+fmtAddr(a)+'</td><td class="c-sep">:</td>';
var vp='';
for(var g=0;g<row.length;g+=ds){var grp=row.slice(g,g+ds);vp+=formatValue(grp,fv);if(g+ds<row.length)vp+=' ';}
h+='<td class="c-val">'+vp+'</td></tr>';
h+='<tr><td class="c-addr">'+fmtAddr(a)+'</td><td class="c-sep">:</td><td class="c-val-row">';
for(var g=0;g<row.length;g+=ds){
var grp=row.slice(g,g+ds);
var grpAddr=a+g;
h+='<span class="c-val" data-addr="0x'+grpAddr.toString(16)+'" data-bytes="'+grp.map(fmtHex).join('')+'">'+formatValue(grp,fv)+'</span>';
}
h+='</td></tr>';
}
return h;
}
@ -560,6 +604,97 @@ addr.onkeydown=function(e){if(e.key==='Enter')doRead()};
$('ar').onchange=function(){V.postMessage({command:'autoRefresh',enabled:$('ar').checked,interval:parseInt($('ari').value)||1000})};
sz.onchange=doRead;col.onchange=doRead;fmt.onchange=doRead;
// === 双击编辑值 ===
function valueToHexBytes(val,fv,szBytes){
var bs=[];
switch(fv){
case 'hex':
var s=val.replace(/^0x/i,'');if(s.length%2)s='0'+s;
for(var i=0;i<s.length;i+=2)bs.push(parseInt(s.substr(i,2),16));
break;
case 'uint':
var n=parseInt(val,10);if(isNaN(n))return null;
for(var i=0;i<szBytes;i++){bs.push(n&0xFF);n=n>>>8;}
break;
case 'int':
var n=parseInt(val,10);if(isNaN(n))return null;
if(n<0)n=n+Math.pow(2,szBytes*8);
for(var i=0;i<szBytes;i++){bs.push(n&0xFF);n=n>>>8;}
break;
case 'float':
var f=parseFloat(val);if(isNaN(f))return null;
var ab=new ArrayBuffer(4);new Float32Array(ab)[0]=f;
var u8=new Uint8Array(ab);for(var i=0;i<4;i++)bs.push(u8[i]);
break;
case 'double':
var f=parseFloat(val);if(isNaN(f))return null;
var ab=new ArrayBuffer(8);new Float64Array(ab)[0]=f;
var u8=new Uint8Array(ab);for(var i=0;i<8;i++)bs.push(u8[i]);
break;
default:return null;
}
return bs;
}
tbl.addEventListener('dblclick',function(e){
var td=e.target;if(!td||td.className!=='c-val')return;
var addrStr=td.getAttribute('data-addr');if(!addrStr)return;
var origBytes=td.getAttribute('data-bytes');if(!origBytes)return;
var szBytes=parseInt(sz.value);var fv=fmt.value;
var firstGroup=origBytes.split(' ')[0];
var inputVal='';
switch(fv){
case 'hex':inputVal=firstGroup.match(/.{2}/g).reverse().join('');break;
case 'uint':inputVal=''+bytesToUint(firstGroup.match(/.{2}/g).map(function(h){return parseInt(h,16)}),false);break;
case 'int':inputVal=''+bytesToInt(firstGroup.match(/.{2}/g).map(function(h){return parseInt(h,16)}),false);break;
case 'float':inputVal=''+bytesToFloat(firstGroup.match(/.{2}/g).map(function(h){return parseInt(h,16)}));break;
case 'double':inputVal=''+bytesToDouble(firstGroup.match(/.{2}/g).map(function(h){return parseInt(h,16)}));break;
}
var origText=td.textContent;
var input=document.createElement('input');
input.type='text';input.value=inputVal;
input.style.cssText='width:100%;background:var(--vscode-input-background);color:var(--vscode-input-foreground);border:1px solid var(--vscode-focusBorder);border-radius:2px;outline:none;padding:0 2px;font:inherit;';
td.textContent='';td.appendChild(input);input.focus();input.select();
var committed=false;
function commit(){
if(committed)return;committed=true;
st.textContent='正在提交修改...';st.className='loading';
var newVal=input.value.trim();
var hexBytes=valueToHexBytes(newVal,fv,szBytes);
console.log('[MemoryViewer] commit() called, newVal:', newVal, 'hexBytes:', hexBytes);
if(!hexBytes||hexBytes.length===0){
td.textContent=origText;
st.textContent='无效的输入值';st.className='err';
console.log('[MemoryViewer] commit() failed: empty hexBytes');
return;
}
// 检查是否包含 NaN无效的十六进制输入
for(var i=0;i<hexBytes.length;i++){
if(isNaN(hexBytes[i])){
td.textContent=origText;
st.textContent='无效的十六进制值';st.className='err';
console.log('[MemoryViewer] commit() failed: NaN in hexBytes');
return;
}
}
td.textContent=origText;
if(fv==='hex')hexBytes.reverse();
var hexStr='';for(var i=0;i<hexBytes.length;i++)hexStr+=fmtHex(hexBytes[i]);
var cbId=++writeCallbackId;
writeCallbacks[cbId]={addr:addrStr,td:td,origBytes:firstGroup,newHex:hexStr};
st.textContent='发送写入命令: '+addrStr+' = '+hexStr;st.className='';
console.log('[MemoryViewer] commit() sending postMessage:', {command:'write',address:addrStr,hexData:hexStr,callbackId:cbId});
V.postMessage({command:'write',address:addrStr,hexData:hexStr,callbackId:cbId});
console.log('[MemoryViewer] commit() postMessage sent');
}
input.onblur=function(){commit();};
input.onkeydown=function(ev){
if(ev.key==='Enter'){ev.preventDefault();commit();}
if(ev.key==='Escape'){committed=true;td.textContent=origText;}
};
});
new ResizeObserver(function(){updateSize();doRead()}).observe(document.body);
// === 对话框:导出 ===
@ -613,6 +748,14 @@ window.addEventListener('message',function(e){
}else if(m.command==='error'){st.textContent=m.text;st.className='err';busy=false;scrollBusy=false;setOpBusy(false);}
else if(m.command==='loading'){st.textContent=m.text+'...';st.className='loading';setOpBusy(true);}
else if(m.command==='info'){st.textContent=m.text;st.className='info';setOpBusy(false);}
else if(m.command==='writeResult'){
var cb=writeCallbacks[m.callbackId];
if(cb){
if(m.success){st.textContent='写入成功: '+cb.addr;st.className='info';}
else{st.textContent='写入失败: '+m.error;st.className='err';cb.td.textContent=cb.origBytes;}
delete writeCallbacks[m.callbackId];
}
}
else if(m.command==='sessionEnded'){$('ar').checked=false;st.textContent='会话结束';st.className='err';setSessionActive(false);setOpBusy(false);}
else if(m.command==='sessionActive'){setSessionActive(true);st.textContent='就绪';st.className='';}
else if(m.command==='autoRead'||m.command==='autoRefreshTick'){doRead();}