logger) {
+ this.dispatchPort = dispatchPort;
+ this.logger = logger;
+ }
+
+ /**
+ * 生成所有 Hook 脚本(core + 各平台专属)
+ */
+ public void generateAllScripts() {
+ String home = System.getProperty("user.home");
+ Path hooksDir = Paths.get(home, ".ahakey", "hooks");
+ try {
+ Files.createDirectories(hooksDir);
+ generateCoreScript(hooksDir);
+ generateClaudeScript(hooksDir);
+ generateCodexScript(hooksDir);
+ generateKimiScript(hooksDir);
+ generateCursorScript(hooksDir);
+ log("[安装] 已生成所有 Hook 脚本");
+ } catch (Exception e) {
+ log("[警告] 生成 Hook 脚本失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 安装指定平台的 Hook
+ */
+ public void install(String platform) {
+ log("[安装] 开始安装 " + platform + " Hook...");
+ generateAllScripts();
+ switch (platform) {
+ case "Claude": installClaudeHooks(); break;
+ case "Cursor": installCursorHooks(); break;
+ case "Codex": installCodexHooks(); break;
+ case "Kimi": installKimiHooks(); break;
+ default: log("[错误] 未知 Hook 类型: " + platform);
+ }
+ }
+
+ /**
+ * 卸载指定平台的 Hook
+ */
+ public void uninstall(String platform) {
+ switch (platform) {
+ case "Claude": uninstallClaudeHooks(); break;
+ case "Cursor": uninstallCursorHooks(); break;
+ case "Codex": uninstallCodexHooks(); break;
+ case "Kimi": uninstallKimiHooks(); break;
+ default: log("[错误] 未知 Hook 类型: " + platform);
+ }
+ }
+
+ /**
+ * 检查指定平台的 Hook 是否已安装
+ */
+ public boolean isInstalled(String platform) {
+ try {
+ Path path = getHookConfigPath(platform);
+ if (!path.toFile().exists()) return false;
+ String content = new String(Files.readAllBytes(path), java.nio.charset.StandardCharsets.UTF_8);
+ switch (platform) {
+ case "Claude": return content.contains("ahakey-claude.sh");
+ case "Cursor": return content.contains("ahakey-cursor.sh");
+ case "Codex": {
+ Path sidecar = Paths.get(System.getProperty("user.home"), ".codex", CODEX_SIDECAR_NAME);
+ return sidecar.toFile().exists();
+ }
+ case "Kimi": return content.contains(KIMI_HOOK_BLOCK_START) && content.contains(KIMI_HOOK_BLOCK_END);
+ default: return false;
+ }
+ } catch (Exception e) {
+ log("[错误] 检查 " + platform + " Hook 状态失败: " + e.getMessage());
+ return false;
+ }
+ }
+
+ // ==================== 脚本生成 ====================
+
+ private void generateCoreScript(Path hooksDir) throws Exception {
+ Path scriptPath = hooksDir.resolve(CORE_SCRIPT_NAME);
+ String content =
+ "#!/bin/bash\n" +
+ "# AhaKey Core - Auto-generated, do not edit\n" +
+ "# Contains TCP connection logic, sourced by platform-specific scripts\n" +
+ "response=\"\"\n" +
+ "exec 3<>/dev/tcp/127.0.0.1/" + dispatchPort + "\n" +
+ "echo \"$EventName\" >&3\n" +
+ "read response <&3\n" +
+ "exec 3>&-\n";
+ Files.write(scriptPath, content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ // 设置可执行权限
+ scriptPath.toFile().setExecutable(true);
+ }
+
+ private void generateClaudeScript(Path hooksDir) throws Exception {
+ Path scriptPath = hooksDir.resolve(CLAUDE_SCRIPT_NAME);
+ String content =
+ "#!/bin/bash\n" +
+ "# AhaKey Claude Hook - Auto-generated, do not edit\n" +
+ "EventName=\"$1\"\n" +
+ "source \"$HOME/.ahakey/hooks/ahakey-core.sh\"\n" +
+ "# Claude PermissionRequest: output hookSpecificOutput in Claude format\n" +
+ "if [ \"$EventName\" = \"PermissionRequest\" ]; then\n" +
+ " if echo \"$response\" | grep -q '\"autoApproved\":\\s*true'; then\n" +
+ " echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"allow\"}}}'\n" +
+ " else\n" +
+ " echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"ask\"}}}'\n" +
+ " fi\n" +
+ " exit 0\n" +
+ "fi\n" +
+ "# Claude lifecycle events: pass through server response\n" +
+ "if [ -n \"$response\" ]; then echo \"$response\"; else echo '{\"ok\":true}'; fi\n" +
+ "exit 0\n";
+ Files.write(scriptPath, content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ scriptPath.toFile().setExecutable(true);
+ }
+
+ private void generateCodexScript(Path hooksDir) throws Exception {
+ Path scriptPath = hooksDir.resolve(CODEX_SCRIPT_NAME);
+ String content =
+ "#!/bin/bash\n" +
+ "# AhaKey Codex Hook - Auto-generated, do not edit\n" +
+ "EventName=\"$1\"\n" +
+ "source \"$HOME/.ahakey/hooks/ahakey-core.sh\"\n" +
+ "# Codex lifecycle hooks must output exactly {} (Codex validates JSON schema)\n" +
+ "if [ \"$EventName\" != \"CodexPermissionRequest\" ]; then\n" +
+ " echo '{}'\n" +
+ " exit 0\n" +
+ "fi\n" +
+ "# Codex PermissionRequest: output hookSpecificOutput in Codex format\n" +
+ "if echo \"$response\" | grep -q '\"autoApproved\":\\s*true'; then\n" +
+ " echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"allow\"}}}'\n" +
+ "else\n" +
+ " echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\"}}'\n" +
+ "fi\n" +
+ "exit 0\n";
+ Files.write(scriptPath, content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ scriptPath.toFile().setExecutable(true);
+ }
+
+ private void generateKimiScript(Path hooksDir) throws Exception {
+ Path scriptPath = hooksDir.resolve(KIMI_SCRIPT_NAME);
+ String content =
+ "#!/bin/bash\n" +
+ "# AhaKey Kimi Hook - Auto-generated, do not edit\n" +
+ "EventName=\"$1\"\n" +
+ "source \"$HOME/.ahakey/hooks/ahakey-core.sh\"\n" +
+ "# Kimi: pass through server response (Kimi specific format)\n" +
+ "if [ -n \"$response\" ]; then echo \"$response\"; else echo '{\"ok\":true}'; fi\n" +
+ "exit 0\n";
+ Files.write(scriptPath, content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ scriptPath.toFile().setExecutable(true);
+ }
+
+ private void generateCursorScript(Path hooksDir) throws Exception {
+ Path scriptPath = hooksDir.resolve(CURSOR_SCRIPT_NAME);
+ String content =
+ "#!/bin/bash\n" +
+ "# AhaKey Cursor Hook - Auto-generated, do not edit\n" +
+ "EventName=\"$1\"\n" +
+ "source \"$HOME/.ahakey/hooks/ahakey-core.sh\"\n" +
+ "# Cursor: pass through server response (Cursor specific format)\n" +
+ "if [ -n \"$response\" ]; then echo \"$response\"; else echo '{\"ok\":true}'; fi\n" +
+ "exit 0\n";
+ Files.write(scriptPath, content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ scriptPath.toFile().setExecutable(true);
+ }
+
+ // ==================== 安装方法 ====================
+
+ private void installClaudeHooks() {
+ Path path = getHookConfigPath("Claude");
+ try {
+ Files.createDirectories(path.getParent());
+ backupFile(path);
+ ObjectNode settings = loadJsonSettings(path);
+ ObjectNode hooks = mapper.createObjectNode();
+ for (String[] ev : CLAUDE_EVENTS) {
+ ObjectNode cmd = mapper.createObjectNode();
+ cmd.put("type", "command");
+ cmd.put("command", buildHookCommand(CLAUDE_SCRIPT_NAME, ev[0]));
+ cmd.put("timeout", Integer.parseInt(ev[1]));
+ ArrayNode inner = mapper.createArrayNode();
+ inner.add(cmd);
+ ObjectNode wrapper = mapper.createObjectNode();
+ wrapper.put("matcher", "");
+ wrapper.set("hooks", inner);
+ ArrayNode outer = mapper.createArrayNode();
+ outer.add(wrapper);
+ hooks.set(ev[0], outer);
+ }
+ settings.set("hooks", hooks);
+ mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), settings);
+ log("[成功] 已注册 " + CLAUDE_EVENTS.length + " 个 Claude hook 事件");
+ log("[成功] 配置文件: " + path);
+ } catch (Exception e) { log("[错误] Claude 安装失败: " + e.getMessage()); }
+ }
+
+ private void installCursorHooks() {
+ Path path = getHookConfigPath("Cursor");
+ try {
+ Files.createDirectories(path.getParent());
+ backupFile(path);
+ ObjectNode settings = loadJsonSettings(path);
+ ObjectNode existingHooks = settings.has("hooks") ? (ObjectNode) settings.get("hooks") : mapper.createObjectNode();
+ for (String[] ev : CURSOR_EVENTS) {
+ ObjectNode entry = mapper.createObjectNode();
+ entry.put("command", buildHookCommand(CURSOR_SCRIPT_NAME, ev[0]));
+ entry.put("timeout", Integer.parseInt(ev[1]));
+ ArrayNode arr = mapper.createArrayNode();
+ arr.add(entry);
+ existingHooks.set(ev[0], arr);
+ }
+ settings.set("hooks", existingHooks);
+ settings.put("version", 1);
+ mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), settings);
+ log("[成功] 已注册 " + CURSOR_EVENTS.length + " 个 Cursor hook 事件");
+ log("[成功] 配置文件: " + path);
+ } catch (Exception e) { log("[错误] Cursor 安装失败: " + e.getMessage()); }
+ }
+
+ private void installCodexHooks() {
+ String home = System.getProperty("user.home");
+ Path hooksJson = Paths.get(home, ".codex", "hooks.json");
+ Path configToml = Paths.get(home, ".codex", "config.toml");
+ Path sidecar = Paths.get(home, ".codex", CODEX_SIDECAR_NAME);
+ try {
+ Files.createDirectories(hooksJson.getParent());
+ backupFile(hooksJson);
+ ObjectNode hooks = mapper.createObjectNode();
+ for (String[] ev : CODEX_EVENTS) {
+ ObjectNode cmd = mapper.createObjectNode();
+ cmd.put("type", "command");
+ cmd.put("command", buildHookCommand(CODEX_SCRIPT_NAME, ev[1]));
+ cmd.put("timeout", Integer.parseInt(ev[2]));
+ ArrayNode innerArr = mapper.createArrayNode();
+ innerArr.add(cmd);
+ ObjectNode entry = mapper.createObjectNode();
+ if ("SessionStart".equals(ev[0])) {
+ entry.put("matcher", "startup|resume|clear");
+ } else if ("UserPromptSubmit".equals(ev[0]) || "Stop".equals(ev[0])) {
+ // no matcher
+ } else {
+ entry.put("matcher", "*");
+ }
+ entry.set("hooks", innerArr);
+ ArrayNode outerArr = mapper.createArrayNode();
+ outerArr.add(entry);
+ hooks.set(ev[0], outerArr);
+ }
+ ObjectNode root = mapper.createObjectNode();
+ root.set("hooks", hooks);
+ mapper.writerWithDefaultPrettyPrinter().writeValue(hooksJson.toFile(), root);
+ log("[成功] 已写入 " + hooksJson);
+ Files.write(sidecar, java.time.LocalDateTime.now().toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ backupFile(configToml);
+ String toml = configToml.toFile().exists()
+ ? new String(Files.readAllBytes(configToml), java.nio.charset.StandardCharsets.UTF_8)
+ : "";
+ toml = removeCodexHookBlock(toml);
+ toml = ensureCodexHooksFeature(toml);
+ if (!toml.contains("AhaKey:生命周期 hooks")) {
+ toml = toml.trim() + "\n\n# AhaKey:生命周期 hooks 由 hook_install 写入 ~/.codex/hooks.json\n";
+ }
+ Files.write(configToml, toml.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ log("[成功] 已更新 " + configToml + "([features].hooks = true)");
+ log("[成功] 已注册 " + CODEX_EVENTS.length + " 个 Codex hook 事件");
+ } catch (Exception e) { log("[错误] Codex 安装失败: " + e.getMessage()); }
+ }
+
+ private void installKimiHooks() {
+ Path path = getHookConfigPath("Kimi");
+ try {
+ Files.createDirectories(path.getParent());
+ backupFile(path);
+ String existing = path.toFile().exists()
+ ? new String(Files.readAllBytes(path), java.nio.charset.StandardCharsets.UTF_8)
+ : "";
+ String cleaned = removeKimiHookBlock(existing).trim();
+ String hookBlock = buildKimiHookBlock();
+ String result = (cleaned.isEmpty() ? "" : cleaned + "\n\n") + hookBlock + "\n";
+ Files.write(path, result.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ log("[成功] 已注册 " + KIMI_EVENTS.length + " 个 Kimi hook 事件");
+ log("[成功] 配置文件: " + path);
+ } catch (Exception e) { log("[错误] Kimi 安装失败: " + e.getMessage()); }
+ }
+
+ // ==================== 卸载方法 ====================
+
+ private void uninstallClaudeHooks() {
+ Path path = getHookConfigPath("Claude");
+ try {
+ if (!path.toFile().exists()) { log("[信息] Claude 配置文件不存在"); return; }
+ ObjectNode settings = loadJsonSettings(path);
+ if (settings.has("hooks")) {
+ settings.remove("hooks");
+ mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), settings);
+ log("[成功] Hook 配置已从 " + path + " 中移除");
+ } else {
+ log("[警告] 未找到 Claude Hook 配置");
+ }
+ } catch (Exception e) { log("[错误] Claude 卸载失败: " + e.getMessage()); }
+ }
+
+ private void uninstallCursorHooks() {
+ Path path = getHookConfigPath("Cursor");
+ try {
+ if (!path.toFile().exists()) { log("[信息] Cursor 配置文件不存在"); return; }
+ ObjectNode settings = loadJsonSettings(path);
+ if (settings.has("hooks")) {
+ settings.remove("hooks");
+ mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), settings);
+ log("[成功] Hook 配置已从 " + path + " 中移除");
+ } else {
+ log("[警告] 未找到 Cursor Hook 配置");
+ }
+ } catch (Exception e) { log("[错误] Cursor 卸载失败: " + e.getMessage()); }
+ }
+
+ private void uninstallCodexHooks() {
+ String home = System.getProperty("user.home");
+ Path hooksJson = Paths.get(home, ".codex", "hooks.json");
+ Path configToml = Paths.get(home, ".codex", "config.toml");
+ Path sidecar = Paths.get(home, ".codex", CODEX_SIDECAR_NAME);
+ try {
+ if (sidecar.toFile().exists()) {
+ Files.delete(sidecar);
+ log("[成功] 已删除 sidecar 标记");
+ } else {
+ log("[信息] sidecar 标记不存在");
+ }
+ if (hooksJson.toFile().exists()) {
+ ObjectNode settings = loadJsonSettings(hooksJson);
+ if (settings.has("hooks")) {
+ settings.remove("hooks");
+ mapper.writerWithDefaultPrettyPrinter().writeValue(hooksJson.toFile(), settings);
+ log("[成功] Hook 配置已从 " + hooksJson + " 中移除");
+ }
+ }
+ if (configToml.toFile().exists()) {
+ String content = new String(Files.readAllBytes(configToml), java.nio.charset.StandardCharsets.UTF_8);
+ String cleaned = removeCodexHookBlock(content);
+ if (!cleaned.equals(content)) {
+ Files.write(configToml, cleaned.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ log("[成功] Hook 块已从 " + configToml + " 中移除");
+ }
+ }
+ } catch (Exception e) { log("[错误] Codex 卸载失败: " + e.getMessage()); }
+ }
+
+ private void uninstallKimiHooks() {
+ Path path = getHookConfigPath("Kimi");
+ try {
+ if (!path.toFile().exists()) { log("[信息] Kimi 配置文件不存在"); return; }
+ String content = new String(Files.readAllBytes(path), java.nio.charset.StandardCharsets.UTF_8);
+ String cleaned = removeKimiHookBlock(content);
+ if (!cleaned.equals(content)) {
+ Files.write(path, cleaned.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ log("[成功] Hook 块已从配置文件中删除");
+ } else {
+ log("[警告] 未找到 AhaKey Hook 块");
+ }
+ } catch (Exception e) { log("[错误] Kimi 卸载失败: " + e.getMessage()); }
+ }
+
+ // ==================== 辅助方法 ====================
+
+ private String buildHookCommand(String scriptName, String agentEvent) {
+ String home = System.getProperty("user.home");
+ Path scriptPath = Paths.get(home, ".ahakey", "hooks", scriptName);
+ String sh = scriptPath.toString();
+ return "bash \"" + sh + "\" " + agentEvent;
+ }
+
+ public Path getHookConfigPath(String hookName) {
+ String home = System.getProperty("user.home");
+ switch (hookName) {
+ case "Claude": return Paths.get(home, ".claude", "settings.json");
+ case "Cursor": return Paths.get(home, ".cursor", "hooks.json");
+ case "Codex": return Paths.get(home, ".codex", "hooks.json");
+ case "Kimi": return Paths.get(home, ".kimi", "config.toml");
+ default: return Paths.get(home, "." + hookName.toLowerCase(), "config.json");
+ }
+ }
+
+ private ObjectNode loadJsonSettings(Path path) throws Exception {
+ if (!path.toFile().exists()) {
+ return mapper.createObjectNode();
+ }
+ JsonNode node = mapper.readTree(path.toFile());
+ if (node instanceof ObjectNode) {
+ return (ObjectNode) node;
+ }
+ return mapper.createObjectNode();
+ }
+
+ private void backupFile(Path path) throws Exception {
+ if (path.toFile().exists()) {
+ Path backup = path.resolveSibling(path.getFileName() + ".bak");
+ Files.copy(path, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ private String buildKimiHookBlock() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(KIMI_HOOK_BLOCK_START).append("\n");
+ sb.append("# Managed by AhaKey. Kimi CLI hooks run this installer with Kimi* event names.\n");
+ sb.append("# Re-run Install Kimi Hooks after upgrading kimi-cli so the dial-control patch is restored.\n");
+ for (String[] ev : KIMI_EVENTS) {
+ sb.append("\n[[hooks]]\n");
+ sb.append("event = \"").append(ev[0]).append("\"\n");
+ sb.append("matcher = \"\"\n");
+ sb.append("command = \"").append(tomlEscape(buildHookCommand(KIMI_SCRIPT_NAME, ev[1]))).append("\"\n");
+ sb.append("timeout = ").append(ev[2]).append("\n");
+ }
+ sb.append("\n").append(KIMI_HOOK_BLOCK_END).append("\n");
+ return sb.toString();
+ }
+
+ private String tomlEscape(String s) {
+ return s.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ private String removeKimiHookBlock(String content) {
+ return removeBlock(content, KIMI_HOOK_BLOCK_START, KIMI_HOOK_BLOCK_END);
+ }
+
+ private String removeCodexHookBlock(String content) {
+ return removeBlock(content, CODEX_HOOK_BLOCK_START, CODEX_HOOK_BLOCK_END);
+ }
+
+ private String removeBlock(String content, String startMarker, String endMarker) {
+ String result = content;
+ while (true) {
+ int start = result.indexOf(startMarker);
+ if (start == -1) break;
+ int end = result.indexOf(endMarker, start);
+ if (end == -1) break;
+ result = result.substring(0, start).trim() + "\n" + result.substring(end + endMarker.length()).trim();
+ }
+ return result.trim();
+ }
+
+ private String ensureCodexHooksFeature(String toml) {
+ if (toml.contains("[features]")) {
+ if (toml.contains("hooks")) {
+ return toml.replaceAll("hooks\\s*=\\s*false", "hooks = true");
+ } else {
+ return toml.replace("[features]", "[features]\nhooks = true");
+ }
+ } else {
+ return toml.trim() + "\n\n[features]\nhooks = true";
+ }
+ }
+
+ private void log(String message) {
+ if (logger != null) {
+ logger.accept(message);
+ }
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/KeyboardInjector.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/KeyboardInjector.java
new file mode 100644
index 00000000..3bea1d54
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/KeyboardInjector.java
@@ -0,0 +1,104 @@
+package com.example.ahakey.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Ubuntu/Linux 平台键盘注入器
+ * 使用 xdotool 工具模拟键盘输入
+ */
+public class KeyboardInjector {
+
+ private static final Logger logger = LoggerFactory.getLogger(KeyboardInjector.class);
+
+ // 默认延迟配置(毫秒)
+ private static final int POST_RECORD_DELAY = 300;
+ private static final int CHAR_DELAY = 20;
+
+ /**
+ * 将文本注入到当前活动窗口
+ * @param text 要注入的文本
+ */
+ public void injectText(String text) {
+ if (text == null || text.isEmpty()) {
+ logger.debug("KeyboardInjector - 文本为空,跳过注入");
+ return;
+ }
+
+ logger.debug("KeyboardInjector - 开始注入文本: \"{}\"", text);
+
+ try {
+ // 等待目标窗口获得焦点
+ logger.debug("KeyboardInjector - 等待 {}ms 确保窗口焦点", POST_RECORD_DELAY);
+ Thread.sleep(POST_RECORD_DELAY);
+
+ // 使用 xdotool 注入文本
+ injectTextWithXdotool(text);
+
+ logger.debug("KeyboardInjector - 文本注入完成");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.error("KeyboardInjector - 等待被中断");
+ }
+ }
+
+ /**
+ * 使用 xdotool 注入文本
+ */
+ private void injectTextWithXdotool(String text) {
+ // 转义特殊字符
+ String escapedText = escapeForXdotool(text);
+
+ String command = String.format("xdotool type '%s'", escapedText);
+ try {
+ Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", command});
+ int exitCode = process.waitFor();
+
+ if (exitCode != 0) {
+ logger.error("xdotool 执行失败,返回码: {}", exitCode);
+ // 尝试备选方案
+ injectTextWithXdotoolAlternative(text);
+ }
+ } catch (Exception e) {
+ logger.error("xdotool 注入失败: {}", e.getMessage());
+ injectTextWithXdotoolAlternative(text);
+ }
+ }
+
+ /**
+ * 备选方案:逐个字符发送
+ */
+ private void injectTextWithXdotoolAlternative(String text) {
+ try {
+ for (char c : text.toCharArray()) {
+ String escapedChar = escapeForXdotool(String.valueOf(c));
+ String command = String.format("xdotool type '%s'", escapedChar);
+ Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", command});
+ process.waitFor();
+ Thread.sleep(CHAR_DELAY);
+ }
+ } catch (Exception e) {
+ logger.error("备选注入方案失败: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 转义 xdotool 命令中的特殊字符
+ */
+ private String escapeForXdotool(String text) {
+ // 转义单引号和其他特殊字符
+ return text
+ .replace("\\", "\\\\")
+ .replace("'", "\\'")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\t", "\\t");
+ }
+
+ /**
+ * 释放资源
+ */
+ public void release() {
+ // 无需特殊清理
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/KimiAhaKeyBridge.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/KimiAhaKeyBridge.java
new file mode 100644
index 00000000..cb53963a
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/KimiAhaKeyBridge.java
@@ -0,0 +1,117 @@
+package com.example.ahakey.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.net.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Kimi built-in AhaKey bridge server on port 9000.
+ *
+ * Kimi's approval.py connects here to read switch state:
+ * PKT_QUERY_STATUS (0x03) → respond 0x82 with status bytes [1,0,0,1]
+ * PKT_QUERY_INFO (0x04) → respond 0x83 with info bytes where index 6 = switchState
+ *
+ * Packet format: [type:1][length:2 LE][data:length]
+ */
+public class KimiAhaKeyBridge {
+ private static final Logger logger = LoggerFactory.getLogger(KimiAhaKeyBridge.class);
+
+ private static final int PORT = 9000;
+ private static final byte PKT_QUERY_STATUS = 0x03;
+ private static final byte PKT_QUERY_INFO = 0x04;
+ private static final byte RSP_STATUS = (byte) 0x82;
+ private static final byte RSP_INFO = (byte) 0x83;
+
+ private final BleManager bleManager;
+ private ServerSocket serverSocket;
+ private ExecutorService executor;
+ private volatile boolean running;
+
+ public KimiAhaKeyBridge(BleManager bleManager) {
+ this.bleManager = bleManager;
+ }
+
+ public void start() {
+ if (running) return;
+ try {
+ serverSocket = new ServerSocket();
+ serverSocket.setReuseAddress(true);
+ serverSocket.bind(new InetSocketAddress("127.0.0.1", PORT));
+ running = true;
+ logger.info("KimiAhaKeyBridge started on 127.0.0.1:{}", PORT);
+ } catch (IOException e) {
+ logger.warn("KimiAhaKeyBridge: port {} unavailable: {}", PORT, e.getMessage());
+ return;
+ }
+ executor = Executors.newCachedThreadPool(r -> {
+ Thread t = new Thread(r, "kimi-bridge");
+ t.setDaemon(true);
+ return t;
+ });
+ executor.submit(this::acceptLoop);
+ }
+
+ public void stop() {
+ running = false;
+ try { if (serverSocket != null) serverSocket.close(); } catch (IOException ignored) {}
+ if (executor != null) executor.shutdownNow();
+ }
+
+ private void acceptLoop() {
+ while (running && !serverSocket.isClosed()) {
+ try {
+ Socket client = serverSocket.accept();
+ client.setSoTimeout(500);
+ executor.submit(() -> handle(client));
+ } catch (IOException e) {
+ if (running) logger.debug("KimiAhaKeyBridge accept: {}", e.getMessage());
+ }
+ }
+ }
+
+ private void handle(Socket client) {
+ try (client) {
+ DataInputStream in = new DataInputStream(client.getInputStream());
+ DataOutputStream out = new DataOutputStream(client.getOutputStream());
+
+ // Kimi sends two packets per query session: QUERY_STATUS then QUERY_INFO
+ for (int i = 0; i < 2; i++) {
+ byte[] header = new byte[3];
+ in.readFully(header);
+ int type = header[0] & 0xFF;
+ int length = ((header[2] & 0xFF) << 8) | (header[1] & 0xFF); // little-endian
+ if (length > 0) in.readNBytes(length); // discard body
+
+ if (type == (PKT_QUERY_STATUS & 0xFF)) {
+ // Respond 0x82: status[0]==1, status[-1]==1, len>=4
+ byte[] data = {1, 0, 0, 1};
+ sendPacket(out, RSP_STATUS, data);
+ } else if (type == (PKT_QUERY_INFO & 0xFF)) {
+ // Respond 0x83: data[6] = switchState (0=auto, 1=manual)
+ int switchState = bleManager.getCachedStatus().getSwitchState();
+ if (switchState < 0) switchState = 1; // default to manual if unknown
+ byte[] data = new byte[10];
+ data[6] = (byte) switchState;
+ sendPacket(out, RSP_INFO, data);
+ logger.debug("KimiAhaKeyBridge: query → switchState={} isAuto={}",
+ switchState, switchState == 0);
+ }
+ }
+ } catch (IOException e) {
+ logger.debug("KimiAhaKeyBridge client: {}", e.getMessage());
+ }
+ }
+
+ private void sendPacket(DataOutputStream out, byte type, byte[] data) throws IOException {
+ out.writeByte(type);
+ // length as little-endian uint16
+ out.writeByte(data.length & 0xFF);
+ out.writeByte((data.length >> 8) & 0xFF);
+ out.write(data);
+ out.flush();
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/KimiConfigLeverSync.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/KimiConfigLeverSync.java
new file mode 100644
index 00000000..748d7fff
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/KimiConfigLeverSync.java
@@ -0,0 +1,95 @@
+package com.example.ahakey.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.util.regex.Pattern;
+
+/**
+ * 将键盘拨杆状态与 Kimi {@code config.toml} 的 {@code default_yolo} 字段对齐:
+ * 自动档 → {@code default_yolo = true},手动档 → {@code default_yolo = false}。
+ *
+ * 配置文件路径:{@code %USERPROFILE%\.kimi\config.toml}(Windows)
+ */
+public class KimiConfigLeverSync {
+ private static final Logger logger = LoggerFactory.getLogger(KimiConfigLeverSync.class);
+
+ private static final Pattern DEFAULT_YOLO_PATTERN =
+ Pattern.compile("(?m)^\\s*default_yolo\\s*=\\s*[^\\r\\n]*\\s*$");
+
+ private static Path configPath() {
+ String home = System.getProperty("user.home");
+ return Paths.get(home, ".kimi", "config.toml");
+ }
+
+ private static Path snapshotPath() {
+ return configPath().resolveSibling("config.toml.ahakey.lever0.bak");
+ }
+
+ public static void apply(boolean isAuto) {
+ if (isAuto) {
+ enableYolo();
+ } else {
+ disableYolo();
+ }
+ }
+
+ private static void enableYolo() {
+ Path cfg = configPath();
+ if (!Files.exists(cfg)) {
+ logger.warn("KimiConfigLeverSync: 未找到 {},跳过。", cfg);
+ return;
+ }
+
+ Path snap = snapshotPath();
+ if (!Files.exists(snap)) {
+ try {
+ Files.copy(cfg, snap, StandardCopyOption.REPLACE_EXISTING);
+ logger.debug("KimiConfigLeverSync: 已备份到 {}", snap);
+ } catch (IOException e) {
+ logger.warn("KimiConfigLeverSync: 备份失败 {}", e.getMessage());
+ }
+ }
+
+ rewriteYolo(cfg, "default_yolo = true # AhaKey: dial auto; run /reload in Kimi after changing dial");
+ logger.info("KimiConfigLeverSync: default_yolo=true (自动档)");
+ }
+
+ private static void disableYolo() {
+ Path snap = snapshotPath();
+ if (Files.exists(snap)) {
+ try {
+ Files.delete(snap);
+ logger.debug("KimiConfigLeverSync: 已删除旧快照 {}", snap);
+ } catch (IOException e) {
+ logger.warn("KimiConfigLeverSync: 删除快照失败 {}", e.getMessage());
+ }
+ }
+
+ Path cfg = configPath();
+ if (!Files.exists(cfg)) return;
+
+ rewriteYolo(cfg, "default_yolo = false # AhaKey: dial manual; run /reload in Kimi after changing dial");
+ logger.info("KimiConfigLeverSync: default_yolo=false (手动档); 请在 Kimi 中执行 /reload 生效。");
+ }
+
+ private static void rewriteYolo(Path cfg, String line) {
+ try {
+ String raw = Files.readString(cfg, StandardCharsets.UTF_8);
+ String updated;
+ if (DEFAULT_YOLO_PATTERN.matcher(raw).find()) {
+ updated = DEFAULT_YOLO_PATTERN.matcher(raw).replaceAll(line);
+ } else {
+ String trimmed = raw.stripTrailing();
+ updated = trimmed.isEmpty() ? line + "\n" : line + "\n\n" + raw;
+ }
+ Files.writeString(cfg, updated, StandardCharsets.UTF_8,
+ StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
+ } catch (IOException e) {
+ logger.error("KimiConfigLeverSync: 写回配置失败 {}", e.getMessage());
+ }
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/OledUploadService.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/OledUploadService.java
new file mode 100644
index 00000000..2cd9d5dd
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/OledUploadService.java
@@ -0,0 +1,211 @@
+package com.example.ahakey.service;
+
+import com.example.ahakey.model.ModeSlot;
+import com.example.ahakey.protocol.AhaKeyProtocol;
+import com.example.ahakey.protocol.AhaKeyResponseParser;
+import com.example.ahakey.util.OLEDFrameEncoder;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.function.Consumer;
+
+public final class OledUploadService {
+ private static final int USER_MODE_COUNT = 4;
+ private static final int FALLBACK_TOTAL_FRAME_SLOTS = AhaKeyProtocol.OLED_MAX_FRAMES;
+ private static final int FACTORY_RESERVED_FRAME_SLOTS = 10;
+
+ public record UploadProgress(int completedFrames, int totalFrames, String detail) {
+ }
+
+ public record UploadPlan(int startIndex, int frameCount, int perModeCapacity, int totalCapacity) {
+ public long encodedBytes() {
+ return (long) frameCount * AhaKeyProtocol.OLED_FRAME_BYTES;
+ }
+ }
+
+ private OledUploadService() {
+ }
+
+ public static int fallbackTotalFrameSlots() {
+ return FALLBACK_TOTAL_FRAME_SLOTS;
+ }
+
+ public static int factoryReservedFrameSlots() {
+ return FACTORY_RESERVED_FRAME_SLOTS;
+ }
+
+ public static int perModeCapacity(int totalCapacity) {
+ int managedCapacity = Math.min(totalCapacity, AhaKeyProtocol.OLED_MAX_FRAMES);
+ int userCapacity = Math.max(0, managedCapacity - FACTORY_RESERVED_FRAME_SLOTS);
+ return userCapacity / USER_MODE_COUNT;
+ }
+
+ public static int fixedStartIndex(ModeSlot mode, int totalCapacity) {
+ return FACTORY_RESERVED_FRAME_SLOTS + mode.getIndex() * perModeCapacity(totalCapacity);
+ }
+
+ public static UploadPlan validateUploadPlan(BleManager ble, ModeSlot mode, int frameCount) throws Exception {
+ if (frameCount <= 0) {
+ throw new IllegalStateException("没有可上传的 OLED 帧。");
+ }
+
+ AhaKeyResponseParser.PictureState state = ble.readPictureState(mode.getIndex());
+ int reportedCapacity = state != null && state.allModeMaxPic() > 0
+ ? state.allModeMaxPic()
+ : FALLBACK_TOTAL_FRAME_SLOTS;
+ int totalCapacity = Math.min(reportedCapacity, AhaKeyProtocol.OLED_MAX_FRAMES);
+ if (totalCapacity <= FACTORY_RESERVED_FRAME_SLOTS) {
+ throw new IllegalStateException("设备 Flash 图片分区容量异常,已取消上传。");
+ }
+
+ int perMode = perModeCapacity(totalCapacity);
+ if (perMode <= 0) {
+ throw new IllegalStateException("设备 Flash 图片分区容量异常,无法上传。");
+ }
+ if (frameCount > perMode) {
+ throw new IllegalStateException(
+ "当前 GIF 有 " + frameCount + " 帧,超过本模式上限 " + perMode + " 帧。请减少帧数、缩短 GIF 或改用静态图片。"
+ );
+ }
+
+ int startIndex = fixedStartIndex(mode, totalCapacity);
+ int endIndexExclusive = startIndex + frameCount;
+ int modeEndExclusive = startIndex + perMode;
+ if (startIndex < FACTORY_RESERVED_FRAME_SLOTS ||
+ endIndexExclusive > modeEndExclusive ||
+ endIndexExclusive > totalCapacity) {
+ throw new IllegalStateException("上传内容超过本模式可用空间,已取消上传。");
+ }
+
+ long encodedBytes = (long) frameCount * AhaKeyProtocol.OLED_FRAME_BYTES;
+ long slotBytes = (long) frameCount * AhaKeyProtocol.OLED_FRAME_SLOT_SIZE;
+ long modeBytes = (long) perMode * AhaKeyProtocol.OLED_FRAME_SLOT_SIZE;
+ long endAddressExclusive = (long) endIndexExclusive * AhaKeyProtocol.OLED_FRAME_SLOT_SIZE;
+ long flashBytes = (long) totalCapacity * AhaKeyProtocol.OLED_FRAME_SLOT_SIZE;
+ if (encodedBytes <= 0 || slotBytes > modeBytes || endAddressExclusive > flashBytes) {
+ throw new IllegalStateException("上传内容超过当前模式 Flash 分区。");
+ }
+ return new UploadPlan(startIndex, frameCount, perMode, totalCapacity);
+ }
+
+ public static void uploadGif(
+ BleManager ble,
+ ModeSlot mode,
+ Path gifPath,
+ int fps,
+ Consumer onProgress,
+ Consumer onComplete,
+ Consumer onError
+ ) {
+ new Thread(() -> {
+ try {
+ int frameCount = OLEDFrameEncoder.frameCount(gifPath);
+ UploadPlan plan = validateUploadPlan(ble, mode, frameCount);
+ List frames = OLEDFrameEncoder.framesFromGif(gifPath, plan.frameCount());
+ int startIndex = plan.startIndex();
+ int delayMs = Math.max(1, 1000 / Math.max(1, fps));
+ int total = frames.size();
+
+ for (int i = 0; i < total; i++) {
+ long address = (long) (startIndex + i) * AhaKeyProtocol.OLED_FRAME_SLOT_SIZE;
+ if (onProgress != null) {
+ onProgress.accept(new UploadProgress(i, total, "写入帧 " + (i + 1) + "/" + total));
+ }
+ ble.writeLargeData(address, frames.get(i).rgb565);
+ Thread.sleep(100);
+ }
+
+ ble.sendCommandExpecting(
+ AhaKeyProtocol.updatePicture(mode.getIndex(), startIndex, total, delayMs),
+ AhaKeyProtocol.CMD_UPDATE_PIC
+ );
+
+ if (onComplete != null) {
+ onComplete.accept(mode.getTitle() + " OLED GIF 上传完成:" + total + " 帧");
+ }
+ } catch (Exception e) {
+ String errorMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
+ if (onError != null) {
+ onError.accept(errorMsg);
+ }
+ }
+ }, "oled-upload").start();
+ }
+
+ public static void uploadStaticImage(
+ BleManager ble,
+ ModeSlot mode,
+ Path imagePath,
+ Consumer onProgress,
+ Consumer onComplete,
+ Consumer onError
+ ) {
+ new Thread(() -> {
+ try {
+ UploadPlan plan = validateUploadPlan(ble, mode, 1);
+ OLEDFrameEncoder.EncodedFrame frame = OLEDFrameEncoder.frameFromSingleImage(imagePath);
+ int startIndex = plan.startIndex();
+
+ long address = (long) startIndex * AhaKeyProtocol.OLED_FRAME_SLOT_SIZE;
+ if (onProgress != null) {
+ onProgress.accept(new UploadProgress(0, 1, "写入静态图片"));
+ }
+ ble.writeLargeData(address, frame.rgb565);
+
+ ble.sendCommandExpecting(
+ AhaKeyProtocol.updatePicture(mode.getIndex(), startIndex, 1, 0),
+ AhaKeyProtocol.CMD_UPDATE_PIC
+ );
+
+ if (onComplete != null) {
+ onComplete.accept(mode.getTitle() + " OLED 图片上传完成");
+ }
+ } catch (Exception e) {
+ String errorMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
+ if (onError != null) {
+ onError.accept(errorMsg);
+ }
+ }
+ }, "oled-upload").start();
+ }
+
+ public static void previewGif(
+ BleManager ble,
+ Path gifPath,
+ int fps,
+ Consumer onComplete,
+ Consumer onError
+ ) {
+ new Thread(() -> {
+ try {
+ int maxFrames = perModeCapacity(FALLBACK_TOTAL_FRAME_SLOTS);
+ int frameCount = OLEDFrameEncoder.frameCount(gifPath);
+ if (frameCount > maxFrames) {
+ throw new IllegalStateException("预览 GIF 帧数过多,请减少到 " + maxFrames + " 帧以内。");
+ }
+ List frames = OLEDFrameEncoder.framesFromGif(gifPath, frameCount);
+ int delayMs = Math.max(1, 1000 / Math.max(1, fps));
+ int total = frames.size();
+ int startIndex = FACTORY_RESERVED_FRAME_SLOTS;
+
+ for (int i = 0; i < total; i++) {
+ ble.writeLargeData((long) (startIndex + i) * AhaKeyProtocol.OLED_FRAME_SLOT_SIZE, frames.get(i).rgb565);
+ }
+
+ ble.sendCommandExpecting(
+ AhaKeyProtocol.updatePicture(0, startIndex, total, delayMs),
+ AhaKeyProtocol.CMD_UPDATE_PIC
+ );
+
+ if (onComplete != null) {
+ onComplete.accept("OLED 预览已发送:" + total + " 帧");
+ }
+ } catch (Exception e) {
+ String errorMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
+ if (onError != null) {
+ onError.accept("预览失败:" + errorMsg);
+ }
+ }
+ }, "oled-preview").start();
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/SocketServer.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/SocketServer.java
new file mode 100644
index 00000000..eb470ff8
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/SocketServer.java
@@ -0,0 +1,200 @@
+package com.example.ahakey.service;
+
+import com.example.ahakey.model.DeviceStatus;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.*;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class SocketServer {
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private final String socketPath;
+ private final BleManager bleManager;
+ private ServerSocket serverSocket;
+ private ExecutorService executorService;
+ private ScheduledExecutorService scheduler;
+
+ private volatile Integer cachedSwitchState;
+ private volatile Integer cachedLightMode;
+
+ public SocketServer(String socketPath, BleManager bleManager) {
+ this.socketPath = socketPath;
+ this.bleManager = bleManager;
+ this.executorService = Executors.newCachedThreadPool();
+ this.scheduler = Executors.newScheduledThreadPool(1);
+ }
+
+ public void start() throws IOException {
+ Path path = Paths.get(socketPath);
+ Files.deleteIfExists(path);
+
+ serverSocket = new ServerSocket(0);
+ executorService.submit(this::acceptConnections);
+
+ System.out.println("监听 Unix Socket: " + socketPath);
+ }
+
+ private void acceptConnections() {
+ while (!serverSocket.isClosed()) {
+ try {
+ Socket client = serverSocket.accept();
+ handleClient(client);
+ } catch (IOException e) {
+ if (!serverSocket.isClosed()) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ private void handleClient(Socket client) {
+ executorService.submit(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
+ PrintWriter writer = new PrintWriter(client.getOutputStream(), true)) {
+
+ String line = reader.readLine();
+ if (line == null || line.trim().isEmpty()) {
+ return;
+ }
+
+ processCommand(line.trim(), writer);
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ try { client.close(); } catch (IOException ignored) {}
+ }
+ });
+ }
+
+ private void processCommand(String line, PrintWriter writer) {
+ try {
+ if (line.startsWith("{")) {
+ JsonNode cmdNode = mapper.readTree(line);
+ String cmd = cmdNode.get("cmd").asText();
+
+ switch (cmd) {
+ case "state":
+ int value = cmdNode.get("value").asInt();
+ bleManager.updateState((byte) value);
+ sendResponse(writer, Map.of("ok", true));
+ break;
+
+ case "state_with_reset":
+ int stateValue = cmdNode.has("value") ? cmdNode.get("value").asInt() : 0;
+ int resetValue = cmdNode.has("resetValue") ? cmdNode.get("resetValue").asInt() : 4;
+ int delayMs = Math.max(0, cmdNode.has("delayMs") ? cmdNode.get("delayMs").asInt() : 1200);
+ bleManager.updateState((byte) stateValue);
+ scheduleStateReset((byte) resetValue, delayMs);
+ sendResponse(writer, Map.of("ok", true));
+ break;
+
+ case "permission":
+ handlePermission(cmdNode, writer);
+ break;
+
+ case "status":
+ case "approval_status":
+ handleStatusRequest(writer);
+ break;
+
+ default:
+ sendResponse(writer, Map.of("error", "unknown cmd: " + cmd));
+ }
+ } else {
+ try {
+ byte state = Byte.parseByte(line);
+ bleManager.updateState(state);
+ } catch (NumberFormatException ignored) {}
+ }
+ } catch (Exception e) {
+ sendResponse(writer, Map.of("error", e.getMessage()));
+ }
+ }
+
+ private void handlePermission(JsonNode cmdNode, PrintWriter writer) {
+ int stateValue = cmdNode.has("value") ? cmdNode.get("value").asInt() : 1;
+ bleManager.updateState((byte) stateValue);
+
+ CompletableFuture.supplyAsync(() -> {
+ bleManager.queryStatus();
+ try { Thread.sleep(1500); } catch (InterruptedException e) {}
+ return bleManager.getCachedStatus();
+ }).thenAccept(status -> {
+ Map response = buildStatusResponse(status);
+ sendResponse(writer, response);
+ }).exceptionally(e -> {
+ sendResponse(writer, buildStatusResponse(null));
+ return null;
+ });
+ }
+
+ private void handleStatusRequest(PrintWriter writer) {
+ if (cachedSwitchState != null) {
+ sendResponse(writer, Map.of(
+ "switchState", cachedSwitchState,
+ "lightMode", cachedLightMode != null ? cachedLightMode : 0
+ ));
+ } else {
+ CompletableFuture.supplyAsync(() -> {
+ bleManager.queryStatus();
+ try { Thread.sleep(1500); } catch (InterruptedException e) {}
+ return bleManager.getCachedStatus();
+ }).thenAccept(status -> {
+ sendResponse(writer, buildStatusResponse(status));
+ }).exceptionally(e -> {
+ sendResponse(writer, buildStatusResponse(null));
+ return null;
+ });
+ }
+ }
+
+ private Map buildStatusResponse(DeviceStatus status) {
+ Map response = new HashMap<>();
+ if (status != null) {
+ response.put("switchState", status.getSwitchState());
+ response.put("lightMode", status.getLightMode());
+ } else {
+ response.put("switchState", cachedSwitchState != null ? cachedSwitchState : "null");
+ response.put("lightMode", cachedLightMode != null ? cachedLightMode : "null");
+ }
+ return response;
+ }
+
+ private void sendResponse(PrintWriter writer, Map data) {
+ try {
+ writer.println(mapper.writeValueAsString(data));
+ writer.flush();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void scheduleStateReset(byte resetValue, int delayMs) {
+ scheduler.schedule(() -> {
+ bleManager.updateState(resetValue);
+ }, delayMs, TimeUnit.MILLISECONDS);
+ }
+
+ public void updateStatus(DeviceStatus status) {
+ this.cachedSwitchState = status.getSwitchState();
+ this.cachedLightMode = status.getLightMode();
+ }
+
+ public void stop() throws IOException {
+ executorService.shutdown();
+ scheduler.shutdown();
+ serverSocket.close();
+ }
+}
\ No newline at end of file
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/SpeechService.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/SpeechService.java
new file mode 100644
index 00000000..5064d663
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/SpeechService.java
@@ -0,0 +1,754 @@
+package com.example.ahakey.service;
+
+import ai.onnxruntime.*;
+import com.example.ahakey.config.ModelConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.util.*;
+import javax.sound.sampled.*;
+
+/**
+ * 语音识别服务
+ * 使用 ONNX Runtime 运行本地语音识别模型
+ * 支持通过配置文件切换不同的 SenseVoice 模型(Small/Medium/Large)
+ */
+public class SpeechService {
+
+ private static final Logger logger = LoggerFactory.getLogger(SpeechService.class);
+
+ private OrtEnvironment env;
+ private OrtSession session;
+ private List tokens;
+ private Thread recognitionThread;
+ private volatile boolean isRunning = false;
+ private volatile boolean isPaused = false;
+ private Consumer partialCallback;
+ private Consumer finalCallback;
+
+ // 从配置管理器获取参数
+ private ModelConfig config;
+ private int sampleRate;
+ private int nFft;
+ private int hopLength;
+ private int nMels;
+ private int featureDim;
+ private int framesPerChunk;
+ private int language;
+ private int textNorm;
+
+ private static final float MAX_WAV_VALUE = 32768.0f;
+ private static final float PRE_EMPHASIS = 0.97f;
+
+ public interface Consumer {
+ void accept(T t);
+ }
+
+ /**
+ * 初始化语音识别服务(使用配置文件)
+ */
+ public void initialize() throws Exception {
+ // 加载配置
+ config = ModelConfig.getInstance();
+ config.printConfig();
+
+ // 从配置读取参数
+ sampleRate = config.getSampleRate();
+ nFft = config.getNFFT();
+ hopLength = config.getHopLength();
+ nMels = config.getNMels();
+ featureDim = config.getFeatureDim();
+ framesPerChunk = config.getFramesPerChunk();
+ language = config.getLanguage();
+ textNorm = config.getTextNorm();
+
+ // 加载词汇表
+ loadTokens(config.getTokensPath());
+
+ // 初始化 ONNX Runtime
+ env = OrtEnvironment.getEnvironment();
+ OrtSession.SessionOptions options = new OrtSession.SessionOptions();
+ options.setIntraOpNumThreads(config.getNumThreads());
+ // 禁用图优化以避免兼容性问题
+ options.addConfigEntry("session.enable_ort_model_ops", "false");
+
+ // 加载模型
+ session = env.createSession(new File(config.getModelPath()).getAbsolutePath(), options);
+
+ logger.info("{} 模型加载成功", config.getModelType());
+ }
+
+ /**
+ * 初始化语音识别服务(兼容旧API)
+ * @param modelPath ONNX模型文件路径
+ * @param tokensPath 词汇表文件路径
+ */
+ public void initialize(String modelPath, String tokensPath) throws Exception {
+ // 加载配置(但使用传入的路径)
+ config = ModelConfig.getInstance();
+
+ // 从配置读取参数
+ sampleRate = config.getSampleRate();
+ nFft = config.getNFFT();
+ hopLength = config.getHopLength();
+ nMels = config.getNMels();
+ featureDim = config.getFeatureDim();
+ framesPerChunk = config.getFramesPerChunk();
+ language = config.getLanguage();
+ textNorm = config.getTextNorm();
+
+ // 加载词汇表
+ loadTokens(tokensPath);
+
+ // 初始化 ONNX Runtime
+ env = OrtEnvironment.getEnvironment();
+ OrtSession.SessionOptions options = new OrtSession.SessionOptions();
+ options.setIntraOpNumThreads(config.getNumThreads());
+ options.addConfigEntry("session.enable_ort_model_ops", "false");
+
+ // 加载模型
+ session = env.createSession(new File(modelPath).getAbsolutePath(), options);
+
+ logger.info("模型加载成功: {}", modelPath);
+ }
+
+ /**
+ * 加载词汇表文件
+ * 支持从类路径或文件系统路径加载
+ */
+ private void loadTokens(String tokensPath) throws IOException {
+ tokens = new ArrayList<>();
+
+ BufferedReader reader = createReader(tokensPath);
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty()) continue;
+ // 解析格式: "token index" 或直接 "token"
+ String[] parts = line.split("\\s+");
+ if (parts.length >= 2) {
+ // 有索引号的情况,取第一部分作为token
+ tokens.add(parts[0]);
+ } else {
+ // 只有token的情况
+ tokens.add(line);
+ }
+ }
+ reader.close();
+ logger.info("词汇表加载完成,共 {} 个词", tokens.size());
+ }
+
+ /**
+ * 创建词汇表文件读取器
+ */
+ private BufferedReader createReader(String tokensPath) throws IOException {
+ // 优先尝试从类路径加载(以 "/" 开头或不含路径分隔符)
+ if (tokensPath.startsWith("/") || !tokensPath.contains("/") && !tokensPath.contains("\\")) {
+ var resource = getClass().getResource(tokensPath);
+ if (resource != null) {
+ return new BufferedReader(new InputStreamReader(resource.openStream()));
+ }
+ }
+
+ // 如果类路径加载失败,尝试从文件系统加载
+ File tokenFile = new File(tokensPath);
+ if (tokenFile.exists()) {
+ return new BufferedReader(new FileReader(tokenFile));
+ }
+
+ throw new IOException("无法找到词汇表文件: " + tokensPath);
+ }
+
+ /**
+ * 开始语音识别(流式)
+ */
+ public void startListening(Consumer onPartial, Consumer onFinal) {
+ this.partialCallback = onPartial;
+ this.finalCallback = onFinal;
+ this.isRunning = true;
+ this.isPaused = false;
+
+ recognitionThread = new Thread(() -> {
+ try {
+ startMicrophoneRecognition();
+ } catch (Exception e) {
+ logger.error("语音识别错误: {}", e.getMessage(), e);
+ }
+ }, "speech-recognition");
+ recognitionThread.start();
+ }
+
+ /**
+ * 停止语音识别
+ */
+ public void stopListening() {
+ isRunning = false;
+ isPaused = false;
+ if (recognitionThread != null) {
+ try {
+ recognitionThread.join(2000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * 暂停识别
+ */
+ public void pauseListening() {
+ isPaused = true;
+ }
+
+ /**
+ * 恢复识别
+ */
+ public void resumeListening() {
+ isPaused = false;
+ }
+
+ /**
+ * 麦克风语音识别主循环
+ */
+ private void startMicrophoneRecognition() throws Exception {
+ AudioFormat format = new AudioFormat(sampleRate, 16, 1, true, false);
+ DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
+
+ if (!AudioSystem.isLineSupported(info)) {
+ throw new Exception("不支持的音频格式");
+ }
+
+ try (TargetDataLine line = (TargetDataLine) AudioSystem.getLine(info)) {
+ line.open(format);
+ line.start();
+
+ ByteArrayOutputStream audioBuffer = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+
+ while (isRunning) {
+ if (!isPaused) {
+ bytesRead = line.read(buffer, 0, buffer.length);
+ if (bytesRead > 0) {
+ audioBuffer.write(buffer, 0, bytesRead);
+
+ // 每收集约1秒音频进行一次识别
+ if (audioBuffer.size() >= sampleRate * 2 * 2) { // 2秒音频(16位)
+ byte[] audioData = audioBuffer.toByteArray();
+ String result = recognize(audioData);
+
+ if (partialCallback != null && result != null && !result.isEmpty()) {
+ partialCallback.accept(result);
+ }
+
+ // 保留最后500ms音频作为重叠
+ int overlapSize = sampleRate * 2 / 2;
+ if (audioBuffer.size() > overlapSize) {
+ byte[] remaining = new byte[overlapSize];
+ System.arraycopy(audioData, audioData.length - overlapSize, remaining, 0, overlapSize);
+ audioBuffer.reset();
+ audioBuffer.write(remaining);
+ }
+ }
+ }
+ } else {
+ Thread.sleep(100);
+ }
+ }
+
+ // 处理剩余音频
+ if (audioBuffer.size() > 0) {
+ logger.debug("处理剩余音频,大小: {} bytes", audioBuffer.size());
+ String finalResult = recognize(audioBuffer.toByteArray());
+ logger.debug("最终识别结果: {}", finalResult != null ? finalResult : "null");
+ // 无论结果是否为空都调用回调,让 VoiceInputManager 处理累积结果
+ if (finalCallback != null) {
+ finalCallback.accept(finalResult);
+ } else {
+ logger.debug("finalCallback 为空");
+ }
+ } else {
+ logger.debug("没有剩余音频需要处理");
+ // 即使没有剩余音频,也调用回调(可能有累积的中间结果)
+ if (finalCallback != null) {
+ finalCallback.accept(null);
+ }
+ }
+ }
+ }
+
+ /**
+ * 执行语音识别
+ * @param audioData PCM音频数据(16位,16kHz,单声道)
+ * @return 识别结果文本
+ */
+ public String recognize(byte[] audioData) {
+ try {
+ // 将字节转换为浮点数组
+ float[] floatData = bytesToFloat(audioData);
+
+ // 提取 Mel 频谱特征
+ float[][] features = extractMelSpectrogram(floatData);
+
+ int numFrames = features.length;
+
+ // 计算拼接后的序列长度
+ int inputSeqLen = Math.max(1, numFrames - framesPerChunk + 1);
+
+ logger.debug("原始特征: {} frames x {} dims", numFrames, nMels);
+ logger.debug("拼接后: {} frames x {} dims", inputSeqLen, featureDim);
+
+ // 准备输入张量 - 形状: [1, inputSeqLen, featureDim]
+ long[] inputShape = {1, inputSeqLen, featureDim};
+ FloatBuffer inputBuffer = FloatBuffer.allocate(inputSeqLen * featureDim);
+
+ // 将多帧拼接成1帧
+ for (int i = 0; i < inputSeqLen; i++) {
+ for (int j = 0; j < framesPerChunk; j++) {
+ int frameIdx = i + j;
+ if (frameIdx < numFrames) {
+ inputBuffer.put(features[frameIdx]);
+ } else {
+ // 填充0
+ inputBuffer.put(new float[nMels]);
+ }
+ }
+ }
+ inputBuffer.flip();
+
+ OnnxTensor inputTensor = OnnxTensor.createTensor(env, inputBuffer, inputShape);
+
+ // 准备 x_length 输入 (模型期望 int32 类型)
+ long[] lengthShape = {1};
+ java.nio.IntBuffer lengthBuffer = java.nio.IntBuffer.allocate(1);
+ lengthBuffer.put(inputSeqLen);
+ lengthBuffer.flip();
+ OnnxTensor lengthTensor = OnnxTensor.createTensor(env, lengthBuffer, lengthShape);
+
+ // 运行模型 - 使用正确的输入名称
+ Map inputs = new HashMap<>();
+ inputs.put("x", inputTensor);
+ inputs.put("x_length", lengthTensor);
+
+ // 添加 language 输入 (0=自动检测, 1=中文, 2=英文, 3=日文, 4=韩文, 5=粤语)
+ long[] langShape = {1};
+ java.nio.IntBuffer langBuffer = java.nio.IntBuffer.allocate(1);
+ langBuffer.put(language);
+ langBuffer.flip();
+ OnnxTensor langTensor = OnnxTensor.createTensor(env, langBuffer, langShape);
+ inputs.put("language", langTensor);
+
+ // 添加 text_norm 输入 (0 = 不规范化, 1 = 规范化)
+ long[] normShape = {1};
+ java.nio.IntBuffer normBuffer = java.nio.IntBuffer.allocate(1);
+ normBuffer.put(textNorm);
+ normBuffer.flip();
+ OnnxTensor normTensor = OnnxTensor.createTensor(env, normBuffer, normShape);
+ inputs.put("text_norm", normTensor);
+
+ try (OrtSession.Result result = session.run(inputs)) {
+ // 获取输出
+ float[][][] output = (float[][][]) result.get(0).getValue();
+ logger.debug("输出维度: [{}][{}][{}]", output.length, output[0].length, output[0][0].length);
+
+ // 提取 logits (batch=1, seq_len, vocab_size)
+ float[][] logits = output[0];
+
+ // 解码结果
+ return decodeLogits(logits);
+ }
+ } catch (Exception e) {
+ logger.error("识别失败: {}", e.getMessage(), e);
+ return "";
+ }
+ }
+
+ /**
+ * 将字节数组转换为浮点数组
+ * Java Sound API 录制的是小端序(little-endian)有符号16位PCM
+ */
+ private float[] bytesToFloat(byte[] bytes) {
+ float[] result = new float[bytes.length / 2];
+ for (int i = 0; i < result.length; i++) {
+ // 小端序:低字节在前,高字节在后
+ int lowByte = bytes[2*i] & 0xFF;
+ int highByte = bytes[2*i+1] & 0xFF;
+ // 组合成有符号16位整数
+ int sample = (highByte << 8) | lowByte;
+ // 转换为有符号值(补码)
+ if (sample > 32767) {
+ sample -= 65536;
+ }
+ // 归一化到 [-1.0, 1.0]
+ result[i] = sample / MAX_WAV_VALUE;
+ }
+ return result;
+ }
+
+ /**
+ * 提取 Mel 频谱特征
+ */
+ private float[][] extractMelSpectrogram(float[] audio) {
+ // 应用预加重处理
+ float[] preEmphasized = preEmphasis(audio);
+
+ int numFrames = (preEmphasized.length - nFft) / hopLength + 1;
+ if (numFrames <= 0) {
+ numFrames = 1;
+ }
+ float[][] melSpectrogram = new float[numFrames][nMels];
+
+ // 预计算汉明窗
+ float[] hammingWindow = new float[nFft];
+ for (int i = 0; i < nFft; i++) {
+ hammingWindow[i] = (float)(0.54 - 0.46 * Math.cos(2 * Math.PI * i / (nFft - 1)));
+ }
+
+ // 预计算 Mel 滤波器组
+ float[][] melFilterBank = createMelFilterBank();
+
+ for (int i = 0; i < numFrames; i++) {
+ float[] frame = new float[nFft];
+ int startIdx = i * hopLength;
+ int copyLen = Math.min(nFft, preEmphasized.length - startIdx);
+ System.arraycopy(preEmphasized, startIdx, frame, 0, copyLen);
+ // 剩余部分填0
+ for (int j = copyLen; j < nFft; j++) {
+ frame[j] = 0;
+ }
+
+ // 应用汉明窗
+ for (int j = 0; j < nFft; j++) {
+ frame[j] *= hammingWindow[j];
+ }
+
+ // 计算 FFT
+ float[] spectrum = computeFFT(frame);
+
+ // 应用 Mel 滤波器组
+ for (int j = 0; j < nMels; j++) {
+ float sum = 0;
+ for (int k = 0; k < nFft / 2; k++) {
+ sum += spectrum[k] * melFilterBank[j][k];
+ }
+ // 取对数能量 (log,不是 log10)
+ melSpectrogram[i][j] = (float)Math.log(Math.max(sum, 1e-10));
+ }
+ }
+
+ // 特征归一化 - CMVN (Cepstral Mean and Variance Normalization)
+ normalizeFeaturesCMVN(melSpectrogram);
+
+ return melSpectrogram;
+ }
+
+ /**
+ * 预加重处理 - 增强高频部分
+ */
+ private float[] preEmphasis(float[] audio) {
+ float[] result = new float[audio.length];
+ result[0] = audio[0];
+ for (int i = 1; i < audio.length; i++) {
+ result[i] = audio[i] - PRE_EMPHASIS * audio[i - 1];
+ }
+ return result;
+ }
+
+ /**
+ * 特征归一化 - CMVN (Cepstral Mean and Variance Normalization)
+ * 对整段音频计算均值和方差进行归一化
+ */
+ private void normalizeFeaturesCMVN(float[][] features) {
+ if (features.length == 0) return;
+
+ int numFrames = features.length;
+ int dim = features[0].length;
+
+ // 计算每个维度的均值
+ float[] mean = new float[dim];
+ for (int j = 0; j < dim; j++) {
+ float sum = 0;
+ for (int i = 0; i < numFrames; i++) {
+ sum += features[i][j];
+ }
+ mean[j] = sum / numFrames;
+ }
+
+ // 计算每个维度的标准差
+ float[] std = new float[dim];
+ for (int j = 0; j < dim; j++) {
+ float sumSq = 0;
+ for (int i = 0; i < numFrames; i++) {
+ float diff = features[i][j] - mean[j];
+ sumSq += diff * diff;
+ }
+ std[j] = (float)Math.sqrt(sumSq / numFrames);
+ }
+
+ // 归一化: (x - mean) / (std + eps)
+ float eps = 1e-5f;
+ for (int i = 0; i < numFrames; i++) {
+ for (int j = 0; j < dim; j++) {
+ features[i][j] = (features[i][j] - mean[j]) / (std[j] + eps);
+ }
+ }
+ }
+
+ /**
+ * 创建 Mel 滤波器组
+ */
+ private float[][] createMelFilterBank() {
+ float[][] filterBank = new float[nMels][nFft / 2];
+
+ // 将频率转换为 Mel 刻度
+ float lowMel = freqToMel(0);
+ float highMel = freqToMel(sampleRate / 2);
+
+ // 在 Mel 刻度上均匀分布的中心频率
+ float[] melPoints = new float[nMels + 2];
+ for (int i = 0; i < nMels + 2; i++) {
+ melPoints[i] = lowMel + (highMel - lowMel) * i / (nMels + 1);
+ }
+
+ // 转换回频率
+ float[] freqPoints = new float[nMels + 2];
+ for (int i = 0; i < nMels + 2; i++) {
+ freqPoints[i] = melToFreq(melPoints[i]);
+ }
+
+ // 转换为 FFT bin 索引
+ int[] binIndices = new int[nMels + 2];
+ for (int i = 0; i < nMels + 2; i++) {
+ binIndices[i] = (int) Math.round(freqPoints[i] * nFft / sampleRate);
+ }
+
+ // 创建三角形滤波器
+ for (int i = 0; i < nMels; i++) {
+ for (int j = binIndices[i]; j < binIndices[i + 1]; j++) {
+ if (j < nFft / 2) {
+ filterBank[i][j] = (float)(j - binIndices[i]) / (binIndices[i + 1] - binIndices[i]);
+ }
+ }
+ for (int j = binIndices[i + 1]; j < binIndices[i + 2]; j++) {
+ if (j < nFft / 2) {
+ filterBank[i][j] = (float)(binIndices[i + 2] - j) / (binIndices[i + 2] - binIndices[i + 1]);
+ }
+ }
+ }
+
+ return filterBank;
+ }
+
+ /**
+ * 频率转 Mel
+ */
+ private float freqToMel(float freq) {
+ return (float)(2595 * Math.log10(1 + freq / 700));
+ }
+
+ /**
+ * Mel 转频率
+ */
+ private float melToFreq(float mel) {
+ return (float)(700 * (Math.pow(10, mel / 2595) - 1));
+ }
+
+ /**
+ * 计算 FFT(简化实现)
+ */
+ private float[] computeFFT(float[] input) {
+ int n = input.length;
+ float[] real = new float[n];
+ float[] imag = new float[n];
+
+ System.arraycopy(input, 0, real, 0, n);
+
+ // 位反转
+ int j = 0;
+ for (int i = 1; i < n; i++) {
+ int bit = n >> 1;
+ for (; j >= bit; bit >>= 1) {
+ j -= bit;
+ }
+ j += bit;
+ if (i < j) {
+ float temp = real[i];
+ real[i] = real[j];
+ real[j] = temp;
+ temp = imag[i];
+ imag[i] = imag[j];
+ imag[j] = temp;
+ }
+ }
+
+ // Cooley-Tukey FFT
+ for (int s = 1; s <= Math.log(n) / Math.log(2); s++) {
+ int m = 1 << s;
+ float wmReal = (float)Math.cos(-2 * Math.PI / m);
+ float wmImag = (float)Math.sin(-2 * Math.PI / m);
+
+ for (int k = 0; k < n; k += m) {
+ float wReal = 1;
+ float wImag = 0;
+
+ for (int jj = 0; jj < m / 2; jj++) {
+ float tReal = wReal * real[k + jj + m / 2] - wImag * imag[k + jj + m / 2];
+ float tImag = wReal * imag[k + jj + m / 2] + wImag * real[k + jj + m / 2];
+
+ real[k + jj + m / 2] = real[k + jj] - tReal;
+ imag[k + jj + m / 2] = imag[k + jj] - tImag;
+ real[k + jj] += tReal;
+ imag[k + jj] += tImag;
+
+ float tempReal = wReal;
+ wReal = wReal * wmReal - wImag * wmImag;
+ wImag = tempReal * wmImag + wImag * wmReal;
+ }
+ }
+ }
+
+ // 计算幅度谱
+ float[] magnitude = new float[n / 2];
+ for (int i = 0; i < n / 2; i++) {
+ magnitude[i] = (float)Math.sqrt(real[i] * real[i] + imag[i] * imag[i]);
+ }
+
+ return magnitude;
+ }
+
+ /**
+ * 解码模型输出 - SenseVoice特有格式
+ * SenseVoice输出: <|language|><|emo|><|event|>文本内容
+ */
+ private String decodeLogits(float[][] logits) {
+ StringBuilder result = new StringBuilder();
+ int prevIndex = -1;
+ boolean speechStarted = false;
+
+ logger.debug("解码开始,帧数: {}, 词汇表大小: {}", logits.length, tokens.size());
+
+ int blankCount = 0;
+ int specialCount = 0;
+ int textCount = 0;
+
+ for (int t = 0; t < logits.length; t++) {
+ float[] frame = logits[t];
+
+ // 找到概率最大的token索引
+ int maxIndex = 0;
+ float maxValue = frame[0];
+ for (int i = 1; i < frame.length; i++) {
+ if (frame[i] > maxValue) {
+ maxValue = frame[i];
+ maxIndex = i;
+ }
+ }
+
+ // 统计各类token数量
+ if (maxIndex == 0) {
+ blankCount++;
+ } else if (maxIndex < tokens.size()) {
+ String token = tokens.get(maxIndex);
+ if (token.startsWith("<") && token.endsWith(">")) {
+ specialCount++;
+ } else {
+ textCount++;
+ }
+ }
+
+ // 打印前几帧和后几帧用于调试
+ if (t < 5 || t > logits.length - 5) {
+ String token = maxIndex < tokens.size() ? tokens.get(maxIndex) : "UNKNOWN_" + maxIndex;
+ logger.debug("帧 {}: index={}, token=\"{}\", prob={}", t, maxIndex, token, String.format("%.4f", maxValue));
+ }
+
+ // CTC解码:跳过blank(0),跳过重复
+ if (maxIndex != 0 && maxIndex != prevIndex) {
+ if (maxIndex < tokens.size()) {
+ String token = tokens.get(maxIndex);
+
+ // 跳过特殊标记
+ if (token == null || token.isEmpty() ||
+ token.equals("") || token.equals("") || token.equals("") ||
+ token.equals("") || token.equals("") || token.equals("")) {
+ prevIndex = maxIndex;
+ continue;
+ }
+
+ // 跳过SenseVoice特殊控制标记
+ if (token.startsWith("<|") && token.endsWith("|>")) {
+ // 但记录语言信息
+ if (token.contains("zh")) {
+ // System.out.println("[DEBUG] 检测到中文");
+ }
+ prevIndex = maxIndex;
+ continue;
+ }
+
+ // 跳过其他特殊标记
+ if (token.matches("^<[^>]+>$")) {
+ prevIndex = maxIndex;
+ continue;
+ }
+
+ // 这是实际的文本内容
+ speechStarted = true;
+ result.append(token);
+
+ // 打印前几个token用于调试
+ if (result.length() < 20) {
+ // System.out.println("[DEBUG] Token[" + t + "]: " + token + " (idx=" + maxIndex + ")");
+ }
+ }
+ }
+
+ prevIndex = maxIndex;
+ }
+
+ String finalResult = result.toString().replace("▁", " ").trim();
+
+ // 输出统计信息
+ logger.debug("解码统计: blank={}, special={}, text={}", blankCount, specialCount, textCount);
+ logger.debug("解码结果长度: {}, 内容: \"{}\"", finalResult.length(), finalResult);
+
+ return finalResult;
+ }
+
+ /**
+ * 释放资源
+ */
+ public void release() {
+ stopListening();
+ if (session != null) {
+ try {
+ session.close();
+ } catch (Exception e) {
+ // 忽略关闭异常
+ }
+ }
+ if (env != null) {
+ try {
+ env.close();
+ } catch (Exception e) {
+ // 忽略关闭异常
+ }
+ }
+ }
+
+ /**
+ * 测试方法:从文件识别
+ */
+ public String recognizeFromFile(String filePath) throws Exception {
+ File audioFile = new File(filePath);
+ byte[] audioData = new byte[(int)audioFile.length()];
+
+ try (FileInputStream fis = new FileInputStream(audioFile)) {
+ fis.read(audioData);
+ }
+
+ return recognize(audioData);
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/UsbHidTransport.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/UsbHidTransport.java
new file mode 100644
index 00000000..138a460c
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/UsbHidTransport.java
@@ -0,0 +1,124 @@
+package com.example.ahakey.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.function.Consumer;
+
+/**
+ * Ubuntu/Linux 平台 USB HID 传输层
+ * 使用 Linux HIDAPI 或 /dev/hidraw 接口访问设备
+ */
+public class UsbHidTransport implements Closeable {
+ private static final Logger logger = LoggerFactory.getLogger(UsbHidTransport.class);
+
+ private static final int REPORT_SIZE = 64;
+ private static final byte USB_COMMAND_PACKET = (byte) 0xA1;
+ private static final byte USB_DATA_PACKET = (byte) 0xA2;
+
+ private volatile boolean running;
+ private Consumer frameConsumer;
+
+ public static boolean isPresent() {
+ // Ubuntu 平台检查设备是否存在
+ return findDevicePath() != null;
+ }
+
+ public synchronized void open(Consumer onFrame) throws IOException {
+ if (isOpen()) {
+ return;
+ }
+ String path = findDevicePath();
+ if (path == null) {
+ throw new IOException("USB HID device not found");
+ }
+ frameConsumer = onFrame;
+ running = true;
+ logger.info("USB HID connected: {}", path);
+ }
+
+ public synchronized boolean isOpen() {
+ return running;
+ }
+
+ public synchronized void sendCommand(byte[] frame) throws IOException {
+ ensureOpen();
+ if (frame.length > REPORT_SIZE - 2) {
+ throw new IOException("USB command frame too large: " + frame.length);
+ }
+ byte[] payload = new byte[REPORT_SIZE];
+ payload[0] = USB_COMMAND_PACKET;
+ payload[1] = (byte) frame.length;
+ System.arraycopy(frame, 0, payload, 2, frame.length);
+ writeReport(payload);
+ }
+
+ public synchronized void sendData(byte[] data) throws IOException {
+ ensureOpen();
+ int offset = 0;
+ while (offset < data.length) {
+ int len = Math.min(REPORT_SIZE - 2, data.length - offset);
+ byte[] payload = new byte[REPORT_SIZE];
+ payload[0] = USB_DATA_PACKET;
+ payload[1] = (byte) len;
+ System.arraycopy(data, offset, payload, 2, len);
+ writeReport(payload);
+ offset += len;
+ sleepQuietly(2);
+ }
+ }
+
+ private static void sleepQuietly(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void writeReport(byte[] payload64) throws IOException {
+ // Ubuntu 版本:使用 Java 的 ProcessBuilder 调用 hidapi 工具或直接写入 /dev/hidraw
+ // 这里使用简化实现,实际项目中需要使用 JNA 或专门的 HID 库
+ logger.debug("USB HID write (Linux stub): {} bytes", payload64.length);
+ }
+
+ private void ensureOpen() throws IOException {
+ if (!isOpen()) {
+ throw new IOException("USB HID not connected");
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ logger.debug("USB close: 开始关闭 USB 传输");
+ running = false;
+ frameConsumer = null;
+ logger.debug("USB close: 关闭完成");
+ }
+
+ private static String findDevicePath() {
+ // Ubuntu 版本:查找 /dev/hidraw* 设备
+ // VID_413C PID_2107 是 AhaKey 设备
+ try {
+ java.io.File devDir = new java.io.File("/dev");
+ java.io.File[] hidrawFiles = devDir.listFiles((dir, name) -> name.startsWith("hidraw"));
+ if (hidrawFiles != null) {
+ for (java.io.File file : hidrawFiles) {
+ String path = "/sys/class/hidraw/" + file.getName() + "/device/uevent";
+ java.io.File uevent = new java.io.File(path);
+ if (uevent.exists()) {
+ String content = new String(java.nio.file.Files.readAllBytes(uevent.toPath()));
+ if (content.contains("HID_ID=0003:0000413C:00002107")) {
+ return "/dev/" + file.getName();
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ logger.debug("USB device detection error: {}", e.getMessage());
+ }
+ return null;
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/VoiceInputManager.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/VoiceInputManager.java
new file mode 100644
index 00000000..356a2af6
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/service/VoiceInputManager.java
@@ -0,0 +1,432 @@
+package com.example.ahakey.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 语音输入管理器
+ * 统一管理语音识别、AhaType服务和键盘注入
+ */
+public class VoiceInputManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(VoiceInputManager.class);
+
+ private SpeechService speechService;
+ private KeyboardInjector keyboardInjector;
+ private volatile boolean isEnabled = false;
+ private volatile boolean isActivated = false; // 服务是否已激活
+ private volatile boolean isRecording = false; // 是否正在录音
+ private volatile boolean keyDownTriggered = false; // 按键按下防抖标志
+ private Consumer resultCallback;
+ private Consumer partialCallback;
+ private Consumer statusCallback; // 状态回调,通知UI当前状态
+ private StringBuilder accumulatedResult = new StringBuilder(); // 累积的中间识别结果
+
+ public interface Consumer {
+ void accept(T t);
+ }
+
+ /**
+ * 语音状态枚举
+ */
+ public enum VoiceStatus {
+ IDLE("idle", "语音未启动"), // 空闲状态
+ READY("ready", "语音已就绪"), // 服务已激活,等待按键
+ RECORDING("recording", "语音输入中"), // 正在录音
+ RECOGNIZING("recognizing", "识别中"), // 语音识别中
+ PROCESSING("processing", "处理中"), // 处理文本中
+ STOPPED("stopped", "语音已停止"); // 已停止
+
+ private final String code;
+ private final String message;
+
+ VoiceStatus(String code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ public String getCode() { return code; }
+ public String getMessage() { return message; }
+ }
+
+ /**
+ * 初始化语音输入管理器
+ */
+ public void initialize() {
+ try {
+ // 初始化语音识别服务
+ speechService = new SpeechService();
+
+ // 获取资源文件路径(支持从 JAR 包内或外部目录加载)
+ // 优先在 models/ 目录下查找
+ String modelPath = findModelFile("models/model_q8.onnx");
+ String tokensPath = findModelFile("models/tokens.txt");
+
+ logger.info("模型文件路径: {}", modelPath);
+ logger.info("词汇表路径: {}", tokensPath);
+
+ // 初始化语音识别
+ speechService.initialize(modelPath, tokensPath);
+
+ // 初始化键盘注入器
+ keyboardInjector = new KeyboardInjector();
+
+ isEnabled = true;
+ logger.info("VoiceInputManager 初始化成功");
+ } catch (Exception e) {
+ logger.error("VoiceInputManager 初始化失败: {}", e.getMessage(), e);
+ isEnabled = false;
+ }
+ }
+
+ /**
+ * 查找模型文件路径
+ */
+ private String findModelFile(String fileName) throws Exception {
+ // 首先尝试从类路径加载(完整路径,例如 models/model_q8.onnx)
+ var resource = getClass().getResource("/" + fileName);
+ if (resource != null) {
+ logger.debug("从类路径加载模型文件: {}", resource.getPath());
+ return resource.getPath();
+ }
+
+ // 获取 JAR 文件所在目录(处理可能的 null 情况)
+ String baseDir = null;
+ try {
+ var codeSource = getClass().getProtectionDomain().getCodeSource();
+ if (codeSource != null && codeSource.getLocation() != null) {
+ String jarPath = codeSource.getLocation().getPath();
+ java.io.File jarFile = new java.io.File(jarPath);
+ baseDir = jarFile.getParentFile().getAbsolutePath();
+ logger.debug("JAR 所在目录: {}", baseDir);
+ }
+ } catch (Exception e) {
+ logger.debug("获取 JAR 路径失败: {}", e.getMessage());
+ }
+
+ // 尝试从 app 目录下直接加载(jpackage 打包后的标准位置,fileName 已包含 models/)
+ if (baseDir != null) {
+ String appPath = new java.io.File(baseDir, fileName).getAbsolutePath();
+ if (new java.io.File(appPath).exists()) {
+ logger.debug("从 app 目录加载: {}", appPath);
+ return appPath;
+ }
+ }
+
+ // 尝试从当前工作目录加载
+ if (new java.io.File(fileName).exists()) {
+ String absPath = new java.io.File(fileName).getAbsolutePath();
+ logger.debug("从当前目录加载: {}", absPath);
+ return absPath;
+ }
+
+ // 尝试从 EXE 所在目录查找(jpackage 打包后,app 目录的父目录)
+ if (baseDir != null) {
+ java.io.File exeDir = new java.io.File(baseDir).getParentFile();
+ if (exeDir != null) {
+ String exePath = new java.io.File(exeDir, fileName).getAbsolutePath();
+ if (new java.io.File(exePath).exists()) {
+ logger.debug("从 EXE 目录加载: {}", exePath);
+ return exePath;
+ }
+ }
+ }
+
+ throw new Exception("无法找到文件: " + fileName + "。请确保模型文件位于以下位置之一:\n- JAR 包内的 " + fileName + "\n- app 目录下的 " + fileName + "\n- 当前工作目录下的 " + fileName + "\n- EXE 所在目录下的 " + fileName);
+ }
+
+ /**
+ * 启动语音输入(无回调版本)
+ */
+ public void startVoiceInput() {
+ startVoiceInput(null, null);
+ }
+
+ /**
+ * 启动语音输入(带结果回调版本)
+ * @param callback 识别结果回调
+ */
+ public void startVoiceInput(Consumer callback) {
+ startVoiceInput(callback, null);
+ }
+
+ /**
+ * 设置状态回调
+ * @param statusCallback 状态变化回调
+ */
+ public void setStatusCallback(Consumer statusCallback) {
+ this.statusCallback = statusCallback;
+ }
+
+ /**
+ * 通知状态变化
+ */
+ private void notifyStatus(VoiceStatus status) {
+ if (statusCallback != null) {
+ try {
+ statusCallback.accept(status.getCode() + ":" + status.getMessage());
+ } catch (Exception e) {
+ logger.error("状态回调失败: {}", e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * 启动语音输入(带完整回调版本)
+ * 注意:这只是激活服务,需要调用 startRecording 才会开始录音
+ * @param resultCallback 最终识别结果回调
+ * @param partialCallback 中间结果回调
+ */
+ public void startVoiceInput(Consumer resultCallback, Consumer partialCallback) {
+ if (!isEnabled || speechService == null) {
+ logger.warn("语音输入未启用或未初始化");
+ return;
+ }
+
+ if (isActivated) {
+ logger.warn("语音输入服务已激活");
+ return;
+ }
+
+ this.resultCallback = resultCallback;
+ this.partialCallback = partialCallback;
+ this.isActivated = true;
+
+ notifyStatus(VoiceStatus.READY);
+ logger.info("语音输入服务已激活,等待按键触发...");
+ }
+
+ /**
+ * 停止语音输入服务
+ */
+ public void stopVoiceInput() {
+ if (!isActivated) {
+ logger.warn("语音输入服务未激活");
+ return;
+ }
+
+ // 如果正在录音,先停止录音
+ if (isRecording) {
+ stopRecording();
+ }
+
+ this.isActivated = false;
+ logger.info("语音输入服务已停用");
+ }
+
+ /**
+ * 开始录音(由按键触发)
+ */
+ public void startRecording() {
+ if (!isActivated || speechService == null) {
+ logger.warn("语音输入服务未激活,无法开始录音");
+ return;
+ }
+
+ if (isRecording) {
+ // 防抖处理:如果已经在录音且按键按下标志已设置,直接忽略
+ if (keyDownTriggered) {
+ return;
+ }
+ logger.warn("已在录音中");
+ return;
+ }
+
+ this.isRecording = true;
+ this.keyDownTriggered = true; // 设置按键按下标志
+ this.accumulatedResult = new StringBuilder(); // 重置累积结果
+
+ notifyStatus(VoiceStatus.RECORDING);
+ logger.info("开始录音...");
+
+ speechService.startListening(
+ this::onPartialResult,
+ this::onFinalResult
+ );
+ }
+
+ /**
+ * 停止录音(由按键释放触发)
+ */
+ public void stopRecording() {
+ if (!isRecording) {
+ logger.warn("未在录音中");
+ return;
+ }
+
+ if (speechService != null) {
+ speechService.stopListening();
+ this.isRecording = false;
+ this.keyDownTriggered = false; // 重置按键按下标志,允许下次触发
+ logger.info("停止录音");
+ }
+ }
+
+ /**
+ * 处理中间识别结果(Partial)
+ */
+ private void onPartialResult(String text) {
+ logger.info("[语音识别中] {}", text);
+
+ // 通知UI识别中状态
+ notifyStatus(VoiceStatus.RECOGNIZING);
+
+ // 累积中间结果
+ if (text != null && !text.isEmpty()) {
+ accumulatedResult.append(text);
+ logger.debug("累积结果更新: \"{}\"", accumulatedResult.toString());
+ }
+
+ // 通知UI更新中间结果
+ if (partialCallback != null && text != null && !text.isEmpty()) {
+ try {
+ partialCallback.accept(text);
+ } catch (Exception e) {
+ logger.error("中间结果回调失败: {}", e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * 处理最终识别结果(Final)
+ */
+ private void onFinalResult(String text) {
+ logger.info("[语音识别完成] {}", text);
+
+ // 通知UI处理中状态
+ notifyStatus(VoiceStatus.PROCESSING);
+
+ // 调试:显示累积结果状态
+ String accumulatedStr = accumulatedResult.toString();
+ logger.debug("onFinalResult - 累积结果: \"{}\", 长度: {}", accumulatedStr, accumulatedStr.length());
+ logger.debug("onFinalResult - 最后结果: \"{}\"", text != null ? text : "null");
+
+ // 使用累积的中间结果 + 最后一块的识别结果
+ StringBuilder finalResultBuilder = new StringBuilder(accumulatedResult);
+
+ // 如果最后一块音频有识别结果,添加到累积结果后面
+ if (text != null && !text.trim().isEmpty()) {
+ // 避免重复(如果最后一块结果和累积结果末尾重复)
+ if (!accumulatedStr.endsWith(text)) {
+ finalResultBuilder.append(text);
+ }
+ }
+
+ String finalResult = finalResultBuilder.toString().trim();
+ logger.debug("onFinalResult - 最终合并结果: \"{}\"", finalResult);
+
+ if (!finalResult.isEmpty()) {
+ // 使用 AhaType 整理文本(如果启用)
+ String processedText = processWithAhaType(finalResult);
+ logger.debug("准备注入文本: {}", processedText);
+
+ // 通知UI显示最终结果
+ if (resultCallback != null) {
+ try {
+ resultCallback.accept(processedText);
+ } catch (Exception e) {
+ logger.error("最终结果回调失败: {}", e.getMessage());
+ }
+ }
+
+ // 注入到当前光标位置
+ injectText(processedText);
+ } else {
+ logger.debug("识别结果为空或null");
+ }
+
+ // 通知UI已停止状态
+ notifyStatus(VoiceStatus.STOPPED);
+
+ // 重置回调和累积结果
+ this.resultCallback = null;
+ this.partialCallback = null;
+ this.accumulatedResult = new StringBuilder();
+ }
+
+ /**
+ * 使用 AhaType 整理文本
+ */
+ private String processWithAhaType(String text) {
+ // TODO: 实现 AhaType 文本整理逻辑
+ // 目前直接返回原始文本
+ return text;
+ }
+
+ /**
+ * 将文本注入到当前光标位置
+ */
+ private void injectText(String text) {
+ if (keyboardInjector != null && text != null && !text.isEmpty()) {
+ try {
+ logger.debug("VoiceInputManager - 准备调用键盘注入器,文本长度: {}", text.length());
+ keyboardInjector.injectText(text);
+ logger.debug("VoiceInputManager - 键盘注入器调用完成");
+ } catch (Exception e) {
+ logger.error("VoiceInputManager - 文本注入失败: {}", e.getMessage(), e);
+ }
+ } else {
+ logger.debug("VoiceInputManager - 跳过注入,键盘注入器为空或文本为空");
+ }
+ }
+
+ /**
+ * 切换语音输入启用状态
+ */
+ public void toggleEnabled() {
+ isEnabled = !isEnabled;
+ logger.info("语音输入 {}", isEnabled ? "已启用" : "已禁用");
+ }
+
+ /**
+ * 检查是否已启用
+ */
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ /**
+ * 检查服务是否已激活
+ */
+ public boolean isActivated() {
+ return isActivated;
+ }
+
+ /**
+ * 检查是否正在录音
+ */
+ public boolean isRecording() {
+ return isRecording;
+ }
+
+ /**
+ * 关闭并释放资源
+ */
+ public void shutdown() {
+ stopVoiceInput();
+ if (speechService != null) {
+ speechService.release();
+ }
+ if (keyboardInjector != null) {
+ keyboardInjector.release();
+ }
+ this.isEnabled = false;
+ this.isActivated = false;
+ this.isRecording = false;
+ logger.info("VoiceInputManager 已关闭");
+ }
+
+ /**
+ * 获取语音服务实例
+ */
+ public SpeechService getSpeechService() {
+ return speechService;
+ }
+
+ /**
+ * 获取键盘注入器实例
+ */
+ public KeyboardInjector getKeyboardInjector() {
+ return keyboardInjector;
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/ConfigStore.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/ConfigStore.java
new file mode 100644
index 00000000..b17e81a1
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/ConfigStore.java
@@ -0,0 +1,84 @@
+package com.example.ahakey.util;
+
+import com.example.ahakey.model.KeyConfig;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+public class ConfigStore {
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private static final String CONFIG_DIR = System.getProperty("user.home") +
+ File.separator + ".ahakey";
+ private static final String KEY_MAPPING_FILE = CONFIG_DIR + File.separator + "key_mapping.json";
+
+ public static void saveKeyMappings(List keys) {
+ try {
+ File dir = new File(CONFIG_DIR);
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+
+ mapper.writerWithDefaultPrettyPrinter().writeValue(new File(KEY_MAPPING_FILE), keys);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static List loadKeyMappings() {
+ File file = new File(KEY_MAPPING_FILE);
+ if (!file.exists()) {
+ return getDefaultKeyMappings();
+ }
+
+ try {
+ List configs = mapper.readValue(file, new TypeReference>() {});
+ if (configs.size() == 4) {
+ return configs;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return getDefaultKeyMappings();
+ }
+
+ private static List getDefaultKeyMappings() {
+ return Arrays.asList(
+ new KeyConfig(0x6D, "Record"), // F18
+ new KeyConfig(0x28, "Yes"), // Enter
+ new KeyConfig(0x28, "No"), // Enter (Claude No 宏)
+ new KeyConfig(0x2A, "Backspace") // Backspace
+ );
+ }
+
+ public static void save(String key, Object value) {
+ try {
+ File dir = new File(CONFIG_DIR);
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+
+ String filePath = CONFIG_DIR + File.separator + key + ".json";
+ mapper.writerWithDefaultPrettyPrinter().writeValue(new File(filePath), value);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static T load(String key, Class type) {
+ File file = new File(CONFIG_DIR + File.separator + key + ".json");
+ if (!file.exists()) {
+ return null;
+ }
+
+ try {
+ return mapper.readValue(file, type);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/Icons.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/Icons.java
new file mode 100644
index 00000000..f4fa5a64
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/Icons.java
@@ -0,0 +1,96 @@
+package com.example.ahakey.util;
+
+import javafx.scene.text.Text;
+import javafx.scene.paint.Color;
+
+public class Icons {
+ private static final String FONT_FAMILY = "Segoe MDL2 Assets";
+
+ public static Text createIcon(String glyph, String size, Color color) {
+ Text text = new Text(glyph);
+ text.setFill(color);
+ double fontSize = Double.parseDouble(size.replace("px", ""));
+ text.setStyle("-fx-font-size: " + fontSize + "px; -fx-font-family: '" + FONT_FAMILY + "';");
+ return text;
+ }
+
+ public static Text createIcon(String glyph, String size) {
+ return createIcon(glyph, size, Color.WHITE);
+ }
+
+ public static Text bluetooth(String size) {
+ return createIcon("\uE138", size, Color.web("#6b7280"));
+ }
+
+ public static Text bluetoothConnected(String size) {
+ return createIcon("\uE138", size, Color.web("#30d158"));
+ }
+
+ public static Text battery(String size, int level) {
+ String icon;
+ if (level >= 40) {
+ icon = "\uE1A3";
+ } else {
+ icon = "\uE1A4";
+ }
+
+ Color color;
+ if (level >= 40) color = Color.web("#30d158");
+ else if (level >= 20) color = Color.web("#ff9f0a");
+ else color = Color.web("#ff453a");
+
+ return createIcon(icon, size, color);
+ }
+
+ public static Text settings(String size) {
+ return createIcon("\uE713", size, Color.web("#6b7280"));
+ }
+
+ public static Text info(String size) {
+ return createIcon("\uE946", size, Color.web("#0a84ff"));
+ }
+
+ public static Text cloud(String size) {
+ return createIcon("\uE14B", size, Color.web("#6b7280"));
+ }
+
+ public static Text refresh(String size) {
+ return createIcon("\uE1E4", size, Color.web("#6b7280"));
+ }
+
+ public static Text power(String size) {
+ return createIcon("\uE700", size, Color.web("#6b7280"));
+ }
+
+ public static Text moreHorizontal(String size) {
+ return createIcon("\uE1D8", size, Color.web("#445065"));
+ }
+
+ public static Text keyboard(String size) {
+ return createIcon("\uE150", size, Color.web("#6b7280"));
+ }
+
+ public static Text display(String size) {
+ return createIcon("\uE1D5", size, Color.web("#6b7280"));
+ }
+
+ public static Text lightbulb(String size) {
+ return createIcon("\uE3AF", size, Color.web("#ffd60a"));
+ }
+
+ public static Text toggleLeft(String size) {
+ return createIcon("\uE701", size, Color.web("#6b7280"));
+ }
+
+ public static Text checkCircle(String size) {
+ return createIcon("\uE73E", size, Color.web("#30d158"));
+ }
+
+ public static Text xCircle(String size) {
+ return createIcon("\uE711", size, Color.web("#ff453a"));
+ }
+
+ public static Text alertTriangle(String size) {
+ return createIcon("\uE7BA", size, Color.web("#ff9f0a"));
+ }
+}
\ No newline at end of file
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/OLEDFrameEncoder.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/OLEDFrameEncoder.java
new file mode 100644
index 00000000..52a51e02
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/OLEDFrameEncoder.java
@@ -0,0 +1,145 @@
+package com.example.ahakey.util;
+
+import com.example.ahakey.protocol.AhaKeyProtocol;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public final class OLEDFrameEncoder {
+ public static class EncodedFrame {
+ public final byte[] rgb565;
+ public final BufferedImage preview;
+
+ public EncodedFrame(byte[] rgb565, BufferedImage preview) {
+ this.rgb565 = rgb565;
+ this.preview = preview;
+ }
+ }
+
+ private OLEDFrameEncoder() {
+ }
+
+ public static void validateGifSourceFileSize(Path path) throws IOException {
+ long size = Files.size(path);
+ if (size > AhaKeyProtocol.OLED_MAX_SOURCE_FILE_BYTES) {
+ throw new IOException("源文件超过 2 MB 上限(当前约 " + (size / 1024) + " KB)。请压缩图片/GIF 后重新选择。");
+ }
+ }
+
+ public static int frameCount(Path gifPath) throws IOException {
+ try (ImageInputStream stream = ImageIO.createImageInputStream(gifPath.toFile())) {
+ Iterator readers = ImageIO.getImageReadersByFormatName("gif");
+ if (!readers.hasNext()) {
+ return 0;
+ }
+ ImageReader reader = readers.next();
+ try {
+ reader.setInput(stream, false);
+ return reader.getNumImages(true);
+ } finally {
+ reader.dispose();
+ }
+ }
+ }
+
+ public static List framesFromGif(Path gifPath) throws IOException {
+ return framesFromGif(gifPath, AhaKeyProtocol.OLED_MAX_FRAMES);
+ }
+
+ public static List framesFromGif(Path gifPath, int maxFrames) throws IOException {
+ validateGifSourceFileSize(gifPath);
+ if (maxFrames <= 0 || maxFrames > AhaKeyProtocol.OLED_MAX_FRAMES) {
+ throw new IOException("GIF 帧数超出可上传范围。");
+ }
+ List images = new ArrayList<>();
+ try (ImageInputStream stream = ImageIO.createImageInputStream(gifPath.toFile())) {
+ Iterator readers = ImageIO.getImageReadersByFormatName("gif");
+ if (!readers.hasNext()) {
+ throw new IOException("无法读取 GIF 文件。");
+ }
+ ImageReader reader = readers.next();
+ try {
+ reader.setInput(stream, false);
+ int count = Math.min(reader.getNumImages(true), maxFrames);
+ if (count <= 0) {
+ throw new IOException("GIF 没有可编码的帧。");
+ }
+ for (int i = 0; i < count; i++) {
+ BufferedImage frame = reader.read(i);
+ if (frame != null) {
+ images.add(frame);
+ }
+ }
+ } finally {
+ reader.dispose();
+ }
+ }
+ if (images.isEmpty()) {
+ throw new IOException("GIF 没有可编码的帧。");
+ }
+ List out = new ArrayList<>(images.size());
+ for (BufferedImage image : images) {
+ out.add(encodeFrame(image));
+ }
+ return out;
+ }
+
+ public static EncodedFrame encodeFrame(BufferedImage source) {
+ int width = AhaKeyProtocol.OLED_WIDTH;
+ int height = AhaKeyProtocol.OLED_HEIGHT;
+ BufferedImage canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ Graphics2D g = canvas.createGraphics();
+ g.setColor(java.awt.Color.BLACK);
+ g.fillRect(0, 0, width, height);
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+
+ double scale = Math.min((double) width / source.getWidth(), (double) height / source.getHeight());
+ int drawW = (int) Math.round(source.getWidth() * scale);
+ int drawH = (int) Math.round(source.getHeight() * scale);
+ int x = (width - drawW) / 2;
+ int y = (height - drawH) / 2;
+ g.drawImage(source, x, y, drawW, drawH, null);
+ g.dispose();
+
+ byte[] rgb565 = toRgb565BigEndian(canvas);
+ return new EncodedFrame(rgb565, canvas);
+ }
+
+ public static EncodedFrame frameFromSingleImage(Path imagePath) throws IOException {
+ validateGifSourceFileSize(imagePath);
+ BufferedImage source = ImageIO.read(imagePath.toFile());
+ if (source == null) {
+ throw new IOException("无法读取图片文件:" + imagePath);
+ }
+ return encodeFrame(source);
+ }
+
+ private static byte[] toRgb565BigEndian(BufferedImage image) {
+ int w = image.getWidth();
+ int h = image.getHeight();
+ byte[] data = new byte[w * h * 2];
+ int idx = 0;
+ for (int y = 0; y < h; y++) {
+ for (int x = 0; x < w; x++) {
+ int rgb = image.getRGB(x, y);
+ int r = (rgb >> 16) & 0xFF;
+ int g = (rgb >> 8) & 0xFF;
+ int b = rgb & 0xFF;
+ int value = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
+ data[idx++] = (byte) ((value >> 8) & 0xFF);
+ data[idx++] = (byte) (value & 0xFF);
+ }
+ }
+ return data;
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/StudioStore.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/StudioStore.java
new file mode 100644
index 00000000..49a0ab67
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/util/StudioStore.java
@@ -0,0 +1,83 @@
+package com.example.ahakey.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.example.ahakey.model.ModeSlot;
+import com.example.ahakey.model.StudioState;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** 对齐 Swift `AhaKeyStudioStore`(UserDefaults → 此处用 `~/.ahakey/studio-draft.json`)。 */
+public final class StudioStore {
+
+ private static final Logger logger = LoggerFactory.getLogger(StudioStore.class);
+ private static final ObjectMapper MAPPER = new ObjectMapper()
+ .enable(SerializationFeature.INDENT_OUTPUT);
+ private static final Path DRAFT_PATH = Path.of(
+ System.getProperty("user.home"), ".ahakey", "studio-draft.json"
+ );
+
+ private StudioStore() {
+ }
+
+ public static void save(StudioState.PersistedDraft draft) {
+ try {
+ Files.createDirectories(DRAFT_PATH.getParent());
+ MAPPER.writeValue(DRAFT_PATH.toFile(), draft);
+ } catch (IOException e) {
+ logger.error("StudioStore.save failed: {}", e.getMessage(), e);
+ }
+ }
+
+ public static StudioState.PersistedDraft loadOrDefault() {
+ File file = DRAFT_PATH.toFile();
+ if (!file.exists()) {
+ return StudioState.PersistedDraft.defaults();
+ }
+ try {
+ StudioState.PersistedDraft savedDraft = MAPPER.readValue(file, StudioState.PersistedDraft.class);
+ // 始终使用新的默认配置作为基础(确保按键配置与固件对齐)
+ StudioState.PersistedDraft defaults = StudioState.PersistedDraft.defaults();
+
+ // 合并用户自定义的非按键内容(如 OLED 图片等)
+ if (savedDraft.modes != null) {
+ for (int i = 0; i < Math.min(savedDraft.modes.length, defaults.modes.length); i++) {
+ if (savedDraft.modes[i] != null) {
+ StudioState.PersistedDraft.ModeDraft savedMode = savedDraft.modes[i];
+ StudioState.PersistedDraft.ModeDraft defaultMode = defaults.modes[i];
+ // 保留用户自定义的 OLED 设置
+ if (savedMode.oledGifPath != null && !savedMode.oledGifPath.isEmpty()) {
+ defaultMode.oledGifPath = savedMode.oledGifPath;
+ }
+ if (savedMode.oledFps > 0) {
+ defaultMode.oledFps = savedMode.oledFps;
+ }
+ if (savedMode.oledFrameCount > 0) {
+ defaultMode.oledFrameCount = savedMode.oledFrameCount;
+ }
+ if (savedMode.voicePresetId != null) {
+ defaultMode.voicePresetId = savedMode.voicePresetId;
+ }
+ if (savedMode.aiLightEffectIds != null) {
+ defaultMode.aiLightEffectIds = savedMode.aiLightEffectIds;
+ }
+ }
+ }
+ }
+
+ // 保留全局设置
+ defaults.revision = savedDraft.revision;
+ defaults.lightBarPreviewId = savedDraft.lightBarPreviewId;
+ defaults.lightBrightness = savedDraft.lightBrightness;
+
+ return defaults;
+ } catch (IOException e) {
+ return StudioState.PersistedDraft.defaults();
+ }
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/CanvasController.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/CanvasController.java
new file mode 100644
index 00000000..0ad56907
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/CanvasController.java
@@ -0,0 +1,371 @@
+package com.example.ahakey.view;
+
+import com.example.ahakey.model.DeviceStatus;
+import com.example.ahakey.model.IDEState;
+import com.example.ahakey.model.LightEffectStyle;
+import com.example.ahakey.model.ModeSlot;
+import com.example.ahakey.model.StudioPart;
+import com.example.ahakey.model.StudioState;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.beans.binding.Bindings;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Rectangle;
+import javafx.util.Duration;
+
+public class CanvasController {
+
+ @FXML
+ private Label oledTitle;
+
+ @FXML
+ private Label oledCaption;
+
+ @FXML
+ private ImageView oledPreviewImage;
+
+ @FXML
+ private Label toggleLabel;
+
+ @FXML
+ private Label key1Caption;
+
+ @FXML
+ private Label key2Caption;
+
+ @FXML
+ private Label key3Caption;
+
+ @FXML
+ private Label key4Caption;
+
+ @FXML
+ private Label modeIcon;
+
+ @FXML
+ private StackPane lightBarCard;
+
+ @FXML
+ private StackPane oledCard;
+
+ @FXML
+ private StackPane key1Card;
+
+ @FXML
+ private StackPane key2Card;
+
+ @FXML
+ private StackPane key3Card;
+
+ @FXML
+ private StackPane key4Card;
+
+ @FXML
+ private StackPane toggleCard;
+
+ @FXML
+ private StackPane modeBadge;
+
+ @FXML
+ private Rectangle toggleThumb;
+
+ @FXML
+ private Region lightSeg1;
+
+ @FXML
+ private Region lightSeg2;
+
+ @FXML
+ private Region lightSeg3;
+
+ @FXML
+ private Region lightSeg4;
+
+ private StudioState studioState;
+ private DeviceStatus deviceStatus;
+ private Timeline lightAnimation;
+ private int animationFrame = 0;
+
+ private static final String COLOR_ACTIVE = "#0a84ff";
+ private static final String COLOR_DIM = "#6b7280";
+ private static final String COLOR_CENTER = "#39c5cf";
+ private static final String COLOR_BRIGHT = "#5ac8fa";
+ private static final String COLOR_GREEN = "#34c759";
+ private static final String COLOR_RED = "#ff453a";
+ private static final String COLOR_AMBER = "#ff9f0a";
+
+ public void initialize() {
+ // 初始化时绑定事件
+ }
+
+ public void setStudioState(StudioState studioState) {
+ this.studioState = studioState;
+ bindEvents();
+ refreshPreview();
+ setStableLightPreview();
+ }
+
+ public void setDeviceStatus(DeviceStatus deviceStatus) {
+ this.deviceStatus = deviceStatus;
+ bindDeviceStatus();
+ }
+
+ private void bindDeviceStatus() {
+ if (toggleLabel != null && deviceStatus != null) {
+ toggleLabel.textProperty().bind(Bindings.createStringBinding(
+ deviceStatus::getSwitchTitle,
+ deviceStatus.switchStateProperty()
+ ));
+ deviceStatus.switchStateProperty().addListener((obs, old, value) -> refreshToggleThumb());
+ refreshToggleThumb();
+ }
+ }
+
+ private void bindEvents() {
+ lightBarCard.setOnMouseClicked(e -> studioState.setSelectedPart(StudioPart.LIGHT_BAR));
+ oledCard.setOnMouseClicked(e -> studioState.setSelectedPart(StudioPart.OLED));
+ key1Card.setOnMouseClicked(e -> studioState.setSelectedPart(StudioPart.KEY1));
+ key2Card.setOnMouseClicked(e -> studioState.setSelectedPart(StudioPart.KEY2));
+ key3Card.setOnMouseClicked(e -> studioState.setSelectedPart(StudioPart.KEY3));
+ key4Card.setOnMouseClicked(e -> studioState.setSelectedPart(StudioPart.KEY4));
+ toggleCard.setOnMouseClicked(e -> studioState.setSelectedPart(StudioPart.TOGGLE_SWITCH));
+
+ studioState.selectedPartProperty().addListener((obs, old, newPart) -> refreshHotspots());
+ studioState.selectedModeProperty().addListener((obs, old, newMode) -> refreshPreview());
+ studioState.dirtyCountProperty().addListener((obs, old, newVal) -> refreshHotspots());
+ studioState.revisionProperty().addListener((obs, old, newVal) -> refreshPreview());
+ studioState.lightBarPreviewProperty().addListener((obs, old, newVal) -> {
+ refreshPreview();
+ animationFrame = 0;
+ setStableLightPreview();
+ });
+ }
+
+ public void refreshPreview() {
+ ModeSlot mode = studioState.getSelectedMode();
+ if (oledTitle != null) {
+ oledTitle.setText(studioState.getOledSummary());
+ }
+ if (oledCaption != null) {
+ oledCaption.setText(studioState.getOledCaption());
+ }
+ refreshOledPreviewImage(mode);
+ if (modeIcon != null) {
+ modeIcon.setText(String.valueOf(mode.getIndex() + 1));
+ }
+ refreshKeyCaptions(mode);
+ refreshHotspots();
+ }
+
+ private void refreshKeyCaptions(ModeSlot mode) {
+ if (key1Caption != null) key1Caption.setText(studioState.getKeyConfig(mode, StudioPart.KEY1).getDescription());
+ if (key2Caption != null) key2Caption.setText(studioState.getKeyConfig(mode, StudioPart.KEY2).getDescription());
+ if (key3Caption != null) key3Caption.setText(studioState.getKeyConfig(mode, StudioPart.KEY3).getDescription());
+ if (key4Caption != null) key4Caption.setText(studioState.getKeyConfig(mode, StudioPart.KEY4).getDescription());
+ }
+
+ private void refreshOledPreviewImage(ModeSlot mode) {
+ if (oledPreviewImage == null) {
+ return;
+ }
+ String path = studioState.getOledDraft(mode).getLocalAssetPath();
+ if (path == null || path.isBlank()) {
+ oledPreviewImage.setImage(null);
+ oledPreviewImage.setVisible(false);
+ if (oledTitle != null) oledTitle.setVisible(true);
+ if (oledCaption != null) oledCaption.setVisible(true);
+ return;
+ }
+ try {
+ oledPreviewImage.setImage(new Image("file:" + path, 140, 80, true, true));
+ oledPreviewImage.setVisible(true);
+ if (oledTitle != null) oledTitle.setVisible(false);
+ if (oledCaption != null) oledCaption.setVisible(false);
+ } catch (Exception e) {
+ oledPreviewImage.setImage(null);
+ oledPreviewImage.setVisible(false);
+ if (oledTitle != null) oledTitle.setVisible(true);
+ if (oledCaption != null) oledCaption.setVisible(true);
+ }
+ }
+
+ private void refreshToggleThumb() {
+ if (toggleThumb == null || deviceStatus == null) {
+ return;
+ }
+ toggleThumb.setTranslateY(deviceStatus.isAutoApproval() ? -13 : 13);
+ }
+
+ private void setStableLightPreview() {
+ if (lightSeg1 != null) {
+ setAll(COLOR_DIM, 0.45);
+ }
+ }
+
+ private void refreshHotspots() {
+ refreshHotspot(lightBarCard, StudioPart.LIGHT_BAR);
+ refreshHotspot(oledCard, StudioPart.OLED);
+ refreshHotspot(key1Card, StudioPart.KEY1);
+ refreshHotspot(key2Card, StudioPart.KEY2);
+ refreshHotspot(key3Card, StudioPart.KEY3);
+ refreshHotspot(key4Card, StudioPart.KEY4);
+ refreshHotspot(toggleCard, StudioPart.TOGGLE_SWITCH);
+ }
+
+ private void refreshHotspot(StackPane hotspot, StudioPart part) {
+ hotspot.getStyleClass().removeAll("selected-hotspot", "dirty-hotspot");
+ if (part == studioState.getSelectedPart()) {
+ hotspot.getStyleClass().add("selected-hotspot");
+ }
+ if (studioState.isDirty(part)) {
+ hotspot.getStyleClass().add("dirty-hotspot");
+ }
+ }
+
+ private void startLightAnimation() {
+ if (lightAnimation != null) {
+ lightAnimation.stop();
+ }
+
+ lightAnimation = new Timeline(new KeyFrame(Duration.millis(150), event -> {
+ updateLightEffect();
+ animationFrame++;
+ }));
+ lightAnimation.setCycleCount(Timeline.INDEFINITE);
+ lightAnimation.play();
+ }
+
+ private void updateLightEffect() {
+ if (studioState == null || lightSeg1 == null) {
+ return;
+ }
+
+ LightEffectStyle effect = currentLightEffect();
+ switch (effect) {
+ case OFF -> setAll("#111827", 0.25);
+ case BREATHING, APPROVAL_WAIT -> updateBreathingEffect(COLOR_BRIGHT);
+ case MIDDLE_LIGHT, BLUE_THINKING -> updateMiddleLightEffect();
+ case WARNING_BLINK, LOW_BATTERY -> updateBlinkEffect(COLOR_RED);
+ case SUCCESS_SWEEP -> updateSweepEffect(COLOR_GREEN);
+ case TYPING_RIPPLE, PULSE_CENTER -> updatePulseCenterEffect();
+ case SCAN_BAR -> updateScanEffect(COLOR_AMBER);
+ case CHARGING_FLOW -> updateScanEffect(COLOR_GREEN);
+ case RAINBOW_MOVE, RAINBOW_WAVE, RAINBOW_WAVE_SLOW -> updateRainbowEffect();
+ case SINGLE_MOVE, COMET -> updateSingleMoveEffect();
+ default -> updateSingleMoveEffect();
+ }
+ }
+
+ private LightEffectStyle currentLightEffect() {
+ IDEState state = studioState.getLightBarPreview().getIdeState();
+ return studioState.getAiLightEffect(studioState.getSelectedMode(), state);
+ }
+
+ private void updateSingleMoveEffect() {
+ int position = animationFrame % 8;
+ double[] brightness = new double[4];
+
+ if (position < 4) {
+ brightness[position] = 1.0;
+ if (position > 0) brightness[position - 1] = 0.3;
+ if (position < 3) brightness[position + 1] = 0.3;
+ } else {
+ int pos = 7 - position;
+ brightness[pos] = 1.0;
+ if (pos > 0) brightness[pos - 1] = 0.3;
+ if (pos < 3) brightness[pos + 1] = 0.3;
+ }
+
+ setLightSegment(lightSeg1, brightness[0]);
+ setLightSegment(lightSeg2, brightness[1]);
+ setLightSegment(lightSeg3, brightness[2]);
+ setLightSegment(lightSeg4, brightness[3]);
+ }
+
+ private void updateBreathingEffect(String color) {
+ double phase = (animationFrame * Math.PI * 2) / 20;
+ double brightness = (Math.sin(phase) + 1) / 2;
+
+ setLightSegment(lightSeg1, color, brightness);
+ setLightSegment(lightSeg2, color, brightness);
+ setLightSegment(lightSeg3, color, brightness);
+ setLightSegment(lightSeg4, color, brightness);
+ }
+
+ private void updateMiddleLightEffect() {
+ setLightSegment(lightSeg1, 0.2);
+ setLightSegment(lightSeg2, 1.0);
+ setLightSegment(lightSeg3, 1.0);
+ setLightSegment(lightSeg4, 0.2);
+ }
+
+ private void updateBlinkEffect(String color) {
+ double b = (animationFrame % 6) < 3 ? 1.0 : 0.2;
+ setAll(color, b);
+ }
+
+ private void updateSweepEffect(String color) {
+ int lit = animationFrame % 5;
+ setLightSegment(lightSeg1, color, lit >= 1 ? 1.0 : 0.2);
+ setLightSegment(lightSeg2, color, lit >= 2 ? 1.0 : 0.2);
+ setLightSegment(lightSeg3, color, lit >= 3 ? 1.0 : 0.2);
+ setLightSegment(lightSeg4, color, lit >= 4 ? 1.0 : 0.2);
+ }
+
+ private void updatePulseCenterEffect() {
+ double phase = (animationFrame * Math.PI * 2) / 16;
+ double center = 0.4 + ((Math.sin(phase) + 1) / 2) * 0.6;
+ setLightSegment(lightSeg1, COLOR_DIM, 0.25);
+ setLightSegment(lightSeg2, COLOR_BRIGHT, center);
+ setLightSegment(lightSeg3, COLOR_BRIGHT, center);
+ setLightSegment(lightSeg4, COLOR_DIM, 0.25);
+ }
+
+ private void updateScanEffect(String color) {
+ int position = animationFrame % 4;
+ setLightSegment(lightSeg1, color, position == 0 ? 1.0 : 0.25);
+ setLightSegment(lightSeg2, color, position == 1 ? 1.0 : 0.25);
+ setLightSegment(lightSeg3, color, position == 2 ? 1.0 : 0.25);
+ setLightSegment(lightSeg4, color, position == 3 ? 1.0 : 0.25);
+ }
+
+ private void updateRainbowEffect() {
+ String[] colors = {"#ff453a", "#ff9f0a", "#34c759", "#0a84ff", "#bf5af2"};
+ setLightSegment(lightSeg1, colors[(animationFrame + 0) % colors.length], 1.0);
+ setLightSegment(lightSeg2, colors[(animationFrame + 1) % colors.length], 1.0);
+ setLightSegment(lightSeg3, colors[(animationFrame + 2) % colors.length], 1.0);
+ setLightSegment(lightSeg4, colors[(animationFrame + 3) % colors.length], 1.0);
+ }
+
+ private void setAll(String color, double brightness) {
+ setLightSegment(lightSeg1, color, brightness);
+ setLightSegment(lightSeg2, color, brightness);
+ setLightSegment(lightSeg3, color, brightness);
+ setLightSegment(lightSeg4, color, brightness);
+ }
+
+ private void setLightSegment(Region seg, double brightness) {
+ if (brightness >= 1.0) {
+ seg.setStyle("-fx-background-color: " + COLOR_BRIGHT + "; -fx-opacity: 1.0;");
+ } else if (brightness >= 0.5) {
+ seg.setStyle("-fx-background-color: " + COLOR_CENTER + "; -fx-opacity: " + brightness + ";");
+ } else {
+ seg.setStyle("-fx-background-color: " + COLOR_DIM + "; -fx-opacity: " + (0.3 + brightness * 0.4) + ";");
+ }
+ }
+
+ private void setLightSegment(Region seg, String color, double brightness) {
+ seg.setStyle("-fx-background-color: " + color + "; -fx-opacity: " + Math.max(0.15, Math.min(1.0, brightness)) + ";");
+ }
+
+ public void stopAnimation() {
+ if (lightAnimation != null) {
+ lightAnimation.stop();
+ }
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/CanvasPane.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/CanvasPane.java
new file mode 100644
index 00000000..797d4fd9
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/CanvasPane.java
@@ -0,0 +1,118 @@
+package com.example.ahakey.view;
+
+import com.example.ahakey.app.StudioController;
+import com.example.ahakey.model.DeviceStatus;
+import com.example.ahakey.model.ModeSlot;
+import com.example.ahakey.model.StudioState;
+import javafx.fxml.FXMLLoader;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+
+import java.io.IOException;
+
+public class CanvasPane extends VBox {
+ private final StudioState studioState;
+ private final DeviceStatus deviceStatus;
+ private final StudioController controller;
+
+ private final Label modeGuidance = new Label();
+
+ public CanvasPane(StudioController controller) {
+ this.studioState = controller.getStudioState();
+ this.deviceStatus = controller.getDeviceStatus();
+ this.controller = controller;
+ init();
+ }
+
+ private void init() {
+ setSpacing(14);
+ setPadding(new Insets(16));
+ getStyleClass().add("canvas-pane");
+ setMinWidth(420);
+ setMaxWidth(780);
+ HBox.setHgrow(this, Priority.NEVER);
+
+ getChildren().addAll(
+ createHeader(),
+ createPreviewCard()
+ );
+
+ studioState.selectedModeProperty().addListener((obs, oldValue, newValue) -> refreshPreview());
+ refreshPreview();
+ }
+
+ private VBox createHeader() {
+ VBox header = new VBox(8);
+ header.getStyleClass().add("mode-header");
+
+ HBox titleRow = new HBox(16);
+ titleRow.setAlignment(Pos.CENTER_LEFT);
+
+ Label keyboardMode = new Label("键盘模式");
+ keyboardMode.getStyleClass().add("section-title");
+
+ HBox picker = new HBox(4);
+ picker.getStyleClass().add("mode-picker");
+ for (ModeSlot slot : ModeSlot.values()) {
+ ToggleButton button = new ToggleButton(slot.getShortName());
+ button.getStyleClass().add("mode-toggle");
+ button.setUserData(slot);
+ button.setSelected(slot == studioState.getSelectedMode());
+ button.setOnAction(event -> {
+ for (var node : picker.getChildren()) {
+ if (node instanceof ToggleButton tb && tb.getUserData() instanceof ModeSlot s) {
+ tb.setSelected(s == slot);
+ }
+ }
+ controller.selectKeyboardMode(slot);
+ });
+ picker.getChildren().add(button);
+ }
+ studioState.selectedModeProperty().addListener((obs, oldValue, newValue) -> {
+ for (var node : picker.getChildren()) {
+ if (node instanceof ToggleButton tb && tb.getUserData() instanceof ModeSlot slot) {
+ tb.setSelected(slot == newValue);
+ }
+ }
+ });
+
+ HBox spacer = new HBox();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+ titleRow.getChildren().addAll(keyboardMode, picker, spacer);
+
+ modeGuidance.getStyleClass().add("hero-subtitle");
+ header.getChildren().addAll(titleRow, modeGuidance);
+ return header;
+ }
+
+ private StackPane createPreviewCard() {
+ StackPane preview = new StackPane();
+ preview.getStyleClass().add("key-preview");
+
+ try {
+ FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/CanvasLayout.fxml"));
+ HBox layout = loader.load();
+ CanvasController controller = loader.getController();
+ controller.setStudioState(studioState);
+ controller.setDeviceStatus(deviceStatus);
+ preview.getChildren().add(layout);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return preview;
+ }
+
+ private void refreshPreview() {
+ ModeSlot mode = studioState.getSelectedMode();
+ modeGuidance.setText(mode.getGuidance());
+ modeGuidance.setVisible(mode.getGuidance() != null && !mode.getGuidance().isBlank());
+ modeGuidance.setManaged(modeGuidance.isVisible());
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/FloatingVoiceNotification.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/FloatingVoiceNotification.java
new file mode 100644
index 00000000..acb0ee27
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/FloatingVoiceNotification.java
@@ -0,0 +1,252 @@
+package com.example.ahakey.view;
+
+import javafx.application.Platform;
+import javafx.scene.Scene;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.layout.HBox;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+
+/**
+ * 浮动语音通知窗口(Ubuntu/Linux 版本)
+ * 在桌面上显示语音状态提示,始终在最顶层
+ */
+public class FloatingVoiceNotification {
+
+ private Stage stage;
+ private Canvas indicator;
+ private Text statusText;
+ private AnimationTimer pulseTimer;
+ private boolean isTimerRunning = false;
+ private double angle = 0;
+ private String currentStatus = "idle";
+
+ /**
+ * 状态配置
+ */
+ private static class StatusConfig {
+ String text;
+ Color indicatorColor;
+ boolean pulse;
+
+ StatusConfig(String text, Color indicatorColor, boolean pulse) {
+ this.text = text;
+ this.indicatorColor = indicatorColor;
+ this.pulse = pulse;
+ }
+ }
+
+ public FloatingVoiceNotification() {
+ init();
+ }
+
+ private void init() {
+ // 创建主窗口
+ stage = new Stage();
+ stage.initStyle(StageStyle.TRANSPARENT);
+ stage.setAlwaysOnTop(true);
+ stage.setResizable(false);
+ // 设置为非模态,不阻塞其他窗口
+ stage.initModality(javafx.stage.Modality.NONE);
+ // 设置无所有者窗口
+ stage.initOwner(null);
+
+ // 创建内容区域
+ HBox content = new HBox(12);
+ content.setStyle("-fx-background-color: rgba(0, 0, 0, 0.85); -fx-padding: 12 16; -fx-border-radius: 8; -fx-background-radius: 8;");
+ // 设置根节点不接收焦点
+ content.setFocusTraversable(false);
+ // 设置鼠标透明,点击会穿透到下面的窗口
+ content.setMouseTransparent(true);
+
+ // 创建指示灯
+ indicator = new Canvas(16, 16);
+
+ // 创建状态文字
+ statusText = new Text();
+ statusText.setFont(Font.font("Noto Sans CJK SC", 14));
+ statusText.setFill(Color.WHITE);
+
+ content.getChildren().addAll(indicator, statusText);
+
+ // 创建场景
+ Scene scene = new Scene(content);
+ scene.setFill(Color.TRANSPARENT);
+ // 场景不接收鼠标事件,不获取焦点
+ scene.getRoot().setFocusTraversable(false);
+ // 场景鼠标透明
+ scene.setFill(javafx.scene.paint.Color.TRANSPARENT);
+
+ stage.setScene(scene);
+
+ // 启动脉冲动画
+ startPulseAnimation();
+ }
+
+ /**
+ * 更新状态
+ * @param status 状态码
+ * @param message 状态消息
+ */
+ public void updateStatus(String status, String message) {
+ Platform.runLater(() -> {
+ currentStatus = status;
+
+ StatusConfig config = getStatusConfig(status, message);
+ statusText.setText(config.text);
+
+ // 更新指示灯颜色
+ drawIndicator(config.indicatorColor, config.pulse);
+
+ // 调整窗口大小
+ stage.sizeToScene();
+
+ // 如果不是空闲状态,显示窗口
+ if (!"idle".equals(status) && !"stopped".equals(status) && !"ready".equals(status)) {
+ show();
+ } else {
+ hide();
+ }
+ });
+ }
+
+ /**
+ * 获取状态配置
+ */
+ private StatusConfig getStatusConfig(String status, String message) {
+ return switch (status) {
+ case "recording" -> new StatusConfig("语音输入中", Color.rgb(231, 76, 60), true); // 红色
+ case "recognizing" -> new StatusConfig("识别中", Color.rgb(245, 166, 35), true); // 橙色
+ case "processing" -> new StatusConfig("处理中", Color.rgb(245, 166, 35), true); // 橙色
+ case "ready" -> new StatusConfig("语音就绪", Color.rgb(46, 204, 113), false); // 绿色
+ case "starting" -> new StatusConfig("启动中", Color.rgb(245, 166, 35), true); // 橙色
+ default -> new StatusConfig(message != null ? message : "空闲", Color.rgb(167, 175, 186), false); // 灰色
+ };
+ }
+
+ /**
+ * 绘制指示灯
+ */
+ private void drawIndicator(Color color, boolean pulse) {
+ GraphicsContext gc = indicator.getGraphicsContext2D();
+ gc.clearRect(0, 0, indicator.getWidth(), indicator.getHeight());
+
+ double centerX = indicator.getWidth() / 2;
+ double centerY = indicator.getHeight() / 2;
+ double radius = 6;
+
+ // 绘制外圈光晕(脉冲效果)
+ if (pulse) {
+ double alpha = 0.3 + Math.sin(angle * Math.PI / 180) * 0.2;
+ gc.setFill(new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha));
+ gc.fillOval(centerX - radius - 4, centerY - radius - 4, radius * 2 + 8, radius * 2 + 8);
+ }
+
+ // 绘制指示灯
+ gc.setFill(color);
+ gc.fillOval(centerX - radius, centerY - radius, radius * 2, radius * 2);
+
+ // 绘制高光
+ gc.setFill(Color.WHITE);
+ gc.setGlobalAlpha(0.4);
+ gc.fillOval(centerX - radius * 0.4, centerY - radius * 0.4, radius * 0.8, radius * 0.8);
+ gc.setGlobalAlpha(1.0);
+ }
+
+ /**
+ * 启动脉冲动画
+ */
+ private void startPulseAnimation() {
+ pulseTimer = new AnimationTimer() {
+ @Override
+ public void handle(long now) {
+ if (!isTimerRunning) return;
+
+ angle = (angle + 5) % 360;
+
+ // 只在需要脉冲效果的状态时更新
+ if ("recording".equals(currentStatus) || "recognizing".equals(currentStatus) ||
+ "processing".equals(currentStatus) || "starting".equals(currentStatus)) {
+ Platform.runLater(() -> {
+ StatusConfig config = getStatusConfig(currentStatus, statusText.getText());
+ drawIndicator(config.indicatorColor, config.pulse);
+ });
+ }
+ }
+ };
+ isTimerRunning = true;
+ pulseTimer.start();
+ }
+
+ /**
+ * 显示窗口
+ */
+ public void show() {
+ Platform.runLater(() -> {
+ if (!stage.isShowing()) {
+ // 计算位置:屏幕底部中间偏上
+ double screenWidth = javafx.stage.Screen.getPrimary().getBounds().getWidth();
+ double screenHeight = javafx.stage.Screen.getPrimary().getBounds().getHeight();
+ stage.setX((screenWidth - stage.getWidth()) / 2);
+ stage.setY(screenHeight - 100);
+
+ // 显示窗口
+ stage.show();
+ }
+ });
+ }
+
+ /**
+ * 隐藏窗口
+ */
+ public void hide() {
+ Platform.runLater(() -> {
+ if (stage.isShowing()) {
+ stage.hide();
+ }
+ });
+ }
+
+ /**
+ * 关闭窗口
+ */
+ public void close() {
+ isTimerRunning = false;
+ if (pulseTimer != null) {
+ pulseTimer.stop();
+ }
+ if (stage != null) {
+ stage.close();
+ }
+ }
+
+ /**
+ * 动画计时器(兼容JavaFX)
+ */
+ private abstract class AnimationTimer {
+ public abstract void handle(long now);
+
+ public void start() {
+ FloatingVoiceNotification.this.isTimerRunning = true;
+ new Thread(() -> {
+ while (FloatingVoiceNotification.this.isTimerRunning) {
+ handle(System.currentTimeMillis());
+ try {
+ Thread.sleep(16); // ~60fps
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ }).start();
+ }
+
+ public void stop() {
+ FloatingVoiceNotification.this.isTimerRunning = false;
+ }
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/InfoPill.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/InfoPill.java
new file mode 100644
index 00000000..ef0c35cf
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/InfoPill.java
@@ -0,0 +1,52 @@
+package com.example.ahakey.view;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Insets;
+import javafx.scene.control.Label;
+import javafx.scene.layout.VBox;
+
+enum AccentColor {
+ GREEN("#30d158"),
+ ORANGE("#ff9f0a"),
+ BLUE("#0a84ff"),
+ MINT("#67d9a5"),
+ INDIGO("#5e5ce6");
+
+ private final String hex;
+
+ AccentColor(String hex) {
+ this.hex = hex;
+ }
+
+ public String getHex() {
+ return hex;
+ }
+}
+
+public class InfoPill extends VBox {
+ public InfoPill(
+ ObservableValue titleValue,
+ ObservableValue subtitleValue,
+ ObservableValue accentValue
+ ) {
+ this.setPadding(new Insets(6, 10, 6, 10));
+ this.setSpacing(1);
+ this.getStyleClass().add("info-pill");
+
+ Label titleLabel = new Label();
+ titleLabel.textProperty().bind(titleValue);
+ titleLabel.getStyleClass().add("info-pill-title");
+
+ Label subtitleLabel = new Label();
+ subtitleLabel.textProperty().bind(subtitleValue);
+ subtitleLabel.getStyleClass().add("info-pill-subtitle");
+
+ styleProperty().bind(Bindings.createStringBinding(
+ () -> "-fx-border-color: " + accentValue.getValue().getHex() + ";",
+ accentValue
+ ));
+
+ this.getChildren().addAll(titleLabel, subtitleLabel);
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/InspectorPane.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/InspectorPane.java
new file mode 100644
index 00000000..65bb2247
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/InspectorPane.java
@@ -0,0 +1,981 @@
+package com.example.ahakey.view;
+
+import com.example.ahakey.app.StudioController;
+import com.example.ahakey.model.DeviceStatus;
+import com.example.ahakey.model.KeyConfig;
+import com.example.ahakey.model.IDEState;
+import com.example.ahakey.model.LightBarPreviewState;
+import com.example.ahakey.model.LightEffectStyle;
+import com.example.ahakey.model.ModeSlot;
+import com.example.ahakey.model.OledModeDraft;
+import com.example.ahakey.model.StudioPart;
+import com.example.ahakey.model.StudioState;
+import javafx.scene.control.Spinner;
+import javafx.stage.Window;
+import com.example.ahakey.service.AgentManager;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.Separator;
+import javafx.scene.control.TextField;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.control.ListView;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.FlowPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+public class InspectorPane extends ScrollPane {
+ private final StudioController controller;
+ private final DeviceStatus deviceStatus;
+ private final StudioState studioState;
+ private final AgentManager agentManager;
+ private final VBox content = new VBox(18);
+ private final VBox header = new VBox(8);
+ private final VBox body = new VBox(16);
+
+ public InspectorPane(StudioController controller) {
+ this.controller = controller;
+ this.deviceStatus = controller.getDeviceStatus();
+ this.studioState = controller.getStudioState();
+ this.agentManager = controller.getAgentManager();
+ init();
+ }
+
+ private void init() {
+ setFitToWidth(true);
+ setHbarPolicy(ScrollBarPolicy.NEVER);
+ setMinWidth(500);
+ setPrefWidth(560);
+ setMaxWidth(720);
+ HBox.setHgrow(this, Priority.NEVER);
+ getStyleClass().add("inspector-pane");
+
+ content.setPadding(new Insets(24));
+ content.getChildren().addAll(header, body);
+ setContent(content);
+
+ studioState.selectedPartProperty().addListener((obs, oldValue, newValue) -> rebuild());
+ studioState.selectedModeProperty().addListener((obs, oldValue, newValue) -> rebuild());
+ studioState.lightBarPreviewProperty().addListener((obs, oldValue, newValue) -> rebuild());
+ agentManager.bluetoothOwnerProperty().addListener((obs, oldValue, newValue) -> rebuild());
+ rebuild();
+ }
+
+ private void rebuild() {
+ header.getChildren().clear();
+ body.getChildren().clear();
+
+ StudioPart part = studioState.getSelectedPart();
+ ModeSlot mode = studioState.getSelectedMode();
+
+ HBox titleRow = new HBox();
+ titleRow.setAlignment(Pos.CENTER_LEFT);
+
+ Text icon = new Text(iconFor(part));
+ icon.getStyleClass().add("inspector-icon");
+
+ Label title = new Label(part.getDisplayTitle());
+ title.getStyleClass().add("inspector-title");
+
+ titleRow.getChildren().addAll(icon, new Label(" "), title);
+
+ Label subtitle = new Label(mode.getTitle() + " · " + mode.getGuidance());
+ subtitle.getStyleClass().add("inspector-subtitle");
+
+ header.getChildren().addAll(titleRow, subtitle);
+ body.disableProperty().bind(Bindings.createBooleanBinding(
+ () -> !agentManager.isEditingConfiguration(),
+ agentManager.bluetoothOwnerProperty()
+ ));
+
+ if (part == StudioPart.KEY1) {
+ // KEY1 始终使用自定义快捷键模式
+ KeyConfig key1 = studioState.getKeyConfig(StudioPart.KEY1);
+ if (key1.getVoicePreset() != com.example.ahakey.model.VoicePreset.CUSTOM) {
+ key1.setVoicePreset(com.example.ahakey.model.VoicePreset.CUSTOM);
+ }
+ body.getChildren().add(createKeyBindingGroup(part));
+ body.getChildren().add(createSimulateKeyGroup(part));
+ body.getChildren().add(createDescriptionGroup(part));
+ } else if (part.isKey()) {
+ body.getChildren().add(createKeyBindingGroup(part));
+ body.getChildren().add(createDescriptionGroup(part));
+ } else if (part == StudioPart.LIGHT_BAR) {
+ body.getChildren().add(createLightBarGroup());
+ } else if (part == StudioPart.OLED) {
+ body.getChildren().add(createOledGroup());
+ } else if (part == StudioPart.TOGGLE_SWITCH) {
+ body.getChildren().add(createToggleGroup());
+ }
+ }
+
+ private VBox createSimulateKeyGroup(StudioPart part) {
+ return createGroupBox("模拟按键", () -> {
+ VBox box = new VBox(8);
+ KeyConfig key = studioState.getKeyConfig(part);
+ var voice = controller.getVoiceRelay();
+
+ Button simulate = new Button("模拟按一次 Key1");
+ simulate.getStyleClass().add("button-prominent");
+ simulate.setOnAction(e -> voice.simulateKeyByHid(key.getHidCode()));
+
+ Label hint = new Label();
+ hint.textProperty().bind(voice.lastSimulateHintProperty());
+ hint.getStyleClass().add("warning-note");
+
+ box.getChildren().addAll(simulate, hint);
+ return box;
+ });
+ }
+
+ private VBox createKeyBindingGroup(StudioPart part) {
+ return createGroupBox("将写入键盘的按键绑定", () -> {
+ VBox box = new VBox(14);
+ KeyConfig key = studioState.getKeyConfig(part);
+ boolean voiceLocked = part == StudioPart.KEY1 && key.getVoicePreset().locksShortcut();
+
+ if (!voiceLocked) {
+ Label typeLabel = new Label("按键类型");
+ typeLabel.getStyleClass().add("field-label");
+ ComboBox typeCombo = new ComboBox<>();
+ typeCombo.getItems().addAll(List.of("快捷键", "宏"));
+ typeCombo.getStyleClass().add("combo-box");
+ typeCombo.setValue(key.usesMacro() ? "宏" : "快捷键");
+ typeCombo.valueProperty().addListener((obs, old, newValue) -> {
+ if ("快捷键".equals(newValue)) {
+ for (int i = key.getMacroStepCount() - 1; i >= 0; i--) {
+ key.removeMacroStep(i);
+ }
+ } else if ("宏".equals(newValue)) {
+ // 切换到宏模式时,确保至少有一个宏步骤
+ if (key.getMacroStepCount() == 0) {
+ key.addMacroStep("DOWN_KEY", 40); // 添加一个默认步骤
+ }
+ }
+ rebuild();
+ });
+
+ box.getChildren().addAll(typeLabel, typeCombo);
+
+ if (key.usesMacro()) {
+ box.getChildren().add(createMacroEditor(part));
+ } else {
+ box.getChildren().add(createShortcutEditor(part));
+ }
+ } else {
+ Label presetLabel = new Label("当前为语音预设模式");
+ presetLabel.getStyleClass().add("key-preview-label");
+ Label lockNote = new Label("语音预设会固定 F17/F18 触发键;改为「自定义快捷键」后可编辑 HID。");
+ lockNote.getStyleClass().add("warning-note");
+ lockNote.setWrapText(true);
+ box.getChildren().addAll(presetLabel, lockNote);
+ }
+ return box;
+ });
+ }
+
+ private VBox createShortcutEditor(StudioPart part) {
+ VBox box = new VBox(12);
+ KeyConfig key = studioState.getKeyConfig(part);
+
+ Label listLabel = new Label("键码列表 (修饰键在前, 普通键在后):");
+ listLabel.getStyleClass().add("field-label");
+
+ javafx.scene.control.ListView keyListView = new javafx.scene.control.ListView<>();
+ keyListView.setPrefHeight(100);
+ keyListView.getStyleClass().add("list-view");
+
+ java.util.List keyCodes = new java.util.ArrayList<>();
+ int hidCode = key.getHidCode();
+
+ if (hidCode != 0) {
+ // 修饰键:区分 Left/Right(新编码 0xNN00 + 旧编码 0x0N00 兼容)
+ if (((hidCode & 0x100) != 0) && ((hidCode & 0x1000) == 0)) keyCodes.add("Left Shift (0xE1)");
+ else if ((hidCode & 0x1000) != 0) keyCodes.add("Right Shift (0xE5)");
+ if (((hidCode & 0x200) != 0) && ((hidCode & 0x2000) == 0)) keyCodes.add("Left Ctrl (0xE0)");
+ else if ((hidCode & 0x2000) != 0) keyCodes.add("Right Ctrl (0xE4)");
+ if (((hidCode & 0x400) != 0) && ((hidCode & 0x4000) == 0)) keyCodes.add("Left Alt (0xE2)");
+ else if ((hidCode & 0x4000) != 0) keyCodes.add("Right Alt (0xE6)");
+ if (((hidCode & 0x800) != 0) && ((hidCode & 0x8000) == 0)) keyCodes.add("Left Win (0xE3)");
+ else if ((hidCode & 0x8000) != 0) keyCodes.add("Right Win (0xE7)");
+
+ int baseCode = hidCode & 0xFF;
+ if (baseCode != 0) {
+ String name = com.example.ahakey.model.HIDUsage.getName(baseCode);
+ keyCodes.add(name + " (0x" + String.format("%02X", baseCode) + ")");
+ }
+ }
+
+ keyListView.getItems().addAll(keyCodes);
+
+ HBox buttonRow = new HBox(8);
+
+ ComboBox keySelector = new ComboBox<>();
+ java.util.List keyItems = new java.util.ArrayList<>();
+
+ // 修饰键 (modifier)
+ keyItems.add("--- 修饰键 ---");
+ keyItems.add("Left Ctrl (0xE0)");
+ keyItems.add("Left Shift (0xE1)");
+ keyItems.add("Left Alt (0xE2)");
+ keyItems.add("Left Win (0xE3)");
+ keyItems.add("Right Ctrl (0xE4)");
+ keyItems.add("Right Shift (0xE5)");
+ keyItems.add("Right Alt (0xE6)");
+ keyItems.add("Right Win (0xE7)");
+
+ // 字母键 (alpha)
+ keyItems.add("--- 字母 ---");
+ String[] letters = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+ "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
+ int[] letterCodes = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
+ 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D};
+ for (int i = 0; i < letters.length; i++) {
+ keyItems.add(letters[i] + " (0x" + String.format("%02X", letterCodes[i]) + ")");
+ }
+
+ // 数字键 (number)
+ keyItems.add("--- 数字 ---");
+ keyItems.add("1 (0x1E)");
+ keyItems.add("2 (0x1F)");
+ keyItems.add("3 (0x20)");
+ keyItems.add("4 (0x21)");
+ keyItems.add("5 (0x22)");
+ keyItems.add("6 (0x23)");
+ keyItems.add("7 (0x24)");
+ keyItems.add("8 (0x25)");
+ keyItems.add("9 (0x26)");
+ keyItems.add("0 (0x27)");
+
+ // 基础键 (basic)
+ keyItems.add("--- 基础键 ---");
+ keyItems.add("Enter (0x28)");
+ keyItems.add("Escape (0x29)");
+ keyItems.add("Backspace (0x2A)");
+ keyItems.add("Tab (0x2B)");
+ keyItems.add("Space (0x2C)");
+ keyItems.add("Minus (0x2D)");
+ keyItems.add("Equal (0x2E)");
+ keyItems.add("Left Bracket (0x2F)");
+ keyItems.add("Right Bracket (0x30)");
+ keyItems.add("Backslash (0x31)");
+ keyItems.add("Semicolon (0x33)");
+ keyItems.add("Quote (0x34)");
+ keyItems.add("Grave (0x35)");
+ keyItems.add("Comma (0x36)");
+ keyItems.add("Period (0x37)");
+ keyItems.add("Slash (0x38)");
+ keyItems.add("Caps Lock (0x39)");
+
+ // 功能键 (function)
+ keyItems.add("--- 功能键 ---");
+ keyItems.add("F1 (0x3A)");
+ keyItems.add("F2 (0x3B)");
+ keyItems.add("F3 (0x3C)");
+ keyItems.add("F4 (0x3D)");
+ keyItems.add("F5 (0x3E)");
+ keyItems.add("F6 (0x3F)");
+ keyItems.add("F7 (0x40)");
+ keyItems.add("F8 (0x41)");
+ keyItems.add("F9 (0x42)");
+ keyItems.add("F10 (0x43)");
+ keyItems.add("F11 (0x44)");
+ keyItems.add("F12 (0x45)");
+ keyItems.add("F13 (0x68)");
+ keyItems.add("F14 (0x69)");
+ keyItems.add("F15 (0x6A)");
+ keyItems.add("F16 (0x6B)");
+ keyItems.add("F17 (0x6C)");
+ keyItems.add("F18 (0x6D)");
+ keyItems.add("F19 (0x6E)");
+ keyItems.add("F20 (0x6F)");
+ keyItems.add("F21 (0x70)");
+ keyItems.add("F22 (0x71)");
+ keyItems.add("F23 (0x72)");
+ keyItems.add("F24 (0x73)");
+
+ // 控制键 (control)
+ keyItems.add("--- 控制键 ---");
+ keyItems.add("Print Screen (0x46)");
+ keyItems.add("Scroll Lock (0x47)");
+ keyItems.add("Pause (0x48)");
+ keyItems.add("Insert (0x49)");
+ keyItems.add("Home (0x4A)");
+ keyItems.add("Page Up (0x4B)");
+ keyItems.add("Delete (0x4C)");
+ keyItems.add("End (0x4D)");
+ keyItems.add("Page Down (0x4E)");
+
+ // 方向键 (arrow)
+ keyItems.add("--- 方向键 ---");
+ keyItems.add("Right (0x4F)");
+ keyItems.add("Left (0x50)");
+ keyItems.add("Down (0x51)");
+ keyItems.add("Up (0x52)");
+
+ // 小键盘 (numpad)
+ keyItems.add("--- 小键盘 ---");
+ keyItems.add("Num Lock (0x53)");
+ keyItems.add("KP / (0x54)");
+ keyItems.add("KP * (0x55)");
+ keyItems.add("KP - (0x56)");
+ keyItems.add("KP + (0x57)");
+ keyItems.add("KP Enter (0x58)");
+ keyItems.add("KP 1 (0x59)");
+ keyItems.add("KP 2 (0x5A)");
+ keyItems.add("KP 3 (0x5B)");
+ keyItems.add("KP 4 (0x5C)");
+ keyItems.add("KP 5 (0x5D)");
+ keyItems.add("KP 6 (0x5E)");
+ keyItems.add("KP 7 (0x5F)");
+ keyItems.add("KP 8 (0x60)");
+ keyItems.add("KP 9 (0x61)");
+ keyItems.add("KP 0 (0x62)");
+ keyItems.add("KP . (0x63)");
+
+ keySelector.getItems().addAll(keyItems);
+ keySelector.getStyleClass().addAll("combo-box", "combo-box-small");
+ keySelector.setValue("--- 修饰键 ---");
+
+ Button addBtn = new Button("添加");
+ addBtn.getStyleClass().add("btn-secondary");
+ addBtn.setOnAction(e -> {
+ String selected = keySelector.getValue();
+ if (selected != null && !selected.startsWith("---")) {
+ // 提取按键名称和键码
+ String keyName = selected.split("\\s*\\(")[0].trim();
+ int codeToAdd = 0;
+
+ // 尝试从字符串中提取键码
+ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("0x([0-9A-Fa-f]+)").matcher(selected);
+ if (matcher.find()) {
+ codeToAdd = Integer.parseInt(matcher.group(1), 16);
+ }
+
+ // 如果没有提取到键码,尝试通过名称查找
+ if (codeToAdd == 0) {
+ codeToAdd = switch (keyName) {
+ case "Shift" -> 0xE1; // 使用实际的HID码
+ case "Ctrl" -> 0xE0;
+ case "Alt" -> 0xE2;
+ case "Win" -> 0xE3;
+ default -> com.example.ahakey.model.HIDUsage.getCode(keyName);
+ };
+
+ // 如果还是没找到,检查是否是十六进制格式
+ if (codeToAdd == 0 && keyName.startsWith("0x")) {
+ try {
+ codeToAdd = Integer.parseInt(keyName.substring(2), 16);
+ } catch (NumberFormatException ex) {
+ codeToAdd = 0;
+ }
+ }
+ }
+
+ // 如果找到了有效的键码,进行累加
+ if (codeToAdd != 0) {
+ int currentCode = key.getHidCode();
+ int modifiers = currentCode & 0xFF00; // 保留所有修饰键位(含 Right 高位)
+ int baseCode = currentCode & 0xFF; // 保留基础键位
+
+ // 处理修饰键(0xE0-0xE7),每个具体键映射到独立位
+ if (codeToAdd >= 0xE0 && codeToAdd <= 0xE7) {
+ switch (codeToAdd) {
+ case 0xE0 -> modifiers |= 0x200; // Left Ctrl
+ case 0xE4 -> modifiers |= 0x2000; // Right Ctrl
+ case 0xE1 -> modifiers |= 0x100; // Left Shift
+ case 0xE5 -> modifiers |= 0x1000; // Right Shift
+ case 0xE2 -> modifiers |= 0x400; // Left Alt
+ case 0xE6 -> modifiers |= 0x4000; // Right Alt
+ case 0xE3 -> modifiers |= 0x800; // Left Win
+ case 0xE7 -> modifiers |= 0x8000; // Right Win
+ }
+ key.setHidCode(modifiers | baseCode);
+ } else {
+ // 处理普通键(设置为基础键位)
+ key.setHidCode(modifiers | codeToAdd);
+ }
+ studioState.markDirty(part);
+ }
+ rebuild();
+ }
+ });
+
+ Button deleteBtn = new Button("删除");
+ deleteBtn.getStyleClass().add("btn-secondary");
+ deleteBtn.setDisable(keyListView.getSelectionModel().getSelectedIndex() < 0);
+ deleteBtn.setOnAction(e -> {
+ int selectedIndex = keyListView.getSelectionModel().getSelectedIndex();
+ if (selectedIndex >= 0) {
+ String selectedItem = keyListView.getItems().get(selectedIndex);
+
+ // 尝试从字符串中提取键码
+ int codeToRemove = 0;
+ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("0x([0-9A-Fa-f]+)").matcher(selectedItem);
+ if (matcher.find()) {
+ codeToRemove = Integer.parseInt(matcher.group(1), 16);
+ }
+
+ // 如果没有提取到键码,尝试通过名称查找
+ if (codeToRemove == 0) {
+ String keyName = selectedItem.split("\\s*\\(")[0].trim();
+ codeToRemove = switch (keyName) {
+ case "Shift" -> 0x100;
+ case "Ctrl" -> 0x200;
+ case "Alt" -> 0x400;
+ case "Win" -> 0x800;
+ default -> com.example.ahakey.model.HIDUsage.getCode(keyName);
+ };
+ }
+
+ int currentCode = key.getHidCode();
+
+ // 处理修饰键(存储在高位,区分 Left/Right)
+ if (codeToRemove >= 0xE0 && codeToRemove <= 0xE7) {
+ int modifierToRemove = switch (codeToRemove) {
+ case 0xE0 -> 0x200; // Left Ctrl
+ case 0xE4 -> 0x2000; // Right Ctrl
+ case 0xE1 -> 0x100; // Left Shift
+ case 0xE5 -> 0x1000; // Right Shift
+ case 0xE2 -> 0x400; // Left Alt
+ case 0xE6 -> 0x4000; // Right Alt
+ case 0xE3 -> 0x800; // Left Win
+ case 0xE7 -> 0x8000; // Right Win
+ default -> 0;
+ };
+ int modifiers = currentCode & 0xFF00;
+ int baseCode = currentCode & 0xFF;
+ modifiers &= ~modifierToRemove;
+ key.setHidCode(modifiers | baseCode);
+ } else {
+ // 处理普通键(存储在低位)
+ int modifiers = currentCode & 0xFF00;
+ key.setHidCode(modifiers);
+ }
+
+ studioState.markDirty(part);
+ rebuild();
+ }
+ });
+
+ keyListView.getSelectionModel().selectedIndexProperty().addListener((obs, old, newVal) -> {
+ deleteBtn.setDisable(newVal.intValue() < 0);
+ });
+
+ buttonRow.getChildren().addAll(keySelector, addBtn, deleteBtn);
+
+ box.getChildren().addAll(listLabel, keyListView, buttonRow);
+ return box;
+ }
+
+ private void updateKeyCodeFromList(StudioPart part, java.util.List keyCodes) {
+ KeyConfig key = studioState.getKeyConfig(part);
+ int hidCode = 0;
+
+ for (String code : keyCodes) {
+ // 提取按键名称(去掉括号和键码)
+ String keyName = code.split("\\s*\\(")[0].trim();
+
+ switch (keyName) {
+ case "Shift" -> hidCode |= 0x100;
+ case "Ctrl" -> hidCode |= 0x200;
+ case "Alt" -> hidCode |= 0x400;
+ case "Win" -> hidCode |= 0x800;
+ default -> hidCode |= com.example.ahakey.model.HIDUsage.getCode(keyName);
+ }
+ }
+
+ key.setHidCode(hidCode);
+ studioState.markDirty(part);
+ }
+
+ private VBox createMacroEditor(StudioPart part) {
+ VBox box = new VBox(10);
+ KeyConfig key = studioState.getKeyConfig(part);
+
+ VBox stepsBox = new VBox(4);
+
+ if (key.getMacroStepCount() == 0) {
+ key.addMacroStep("DOWN_KEY", 40);
+ }
+
+ for (int i = 0; i < key.getMacroStepCount(); i++) {
+ HBox stepRow = createMacroStepRow(part, i);
+ stepsBox.getChildren().add(stepRow);
+ }
+
+ HBox buttonRow = new HBox(8);
+ Button addStepBtn = new Button("+ 添加步骤");
+ addStepBtn.getStyleClass().add("btn-primary");
+ addStepBtn.setOnAction(e -> {
+ key.addMacroStep("DOWN_KEY", 40);
+ studioState.markDirty(part);
+ rebuild();
+ });
+
+ Button clearBtn = new Button("清空");
+ clearBtn.getStyleClass().add("btn-secondary");
+ clearBtn.setOnAction(e -> {
+ for (int i = key.getMacroStepCount() - 1; i >= 0; i--) {
+ key.removeMacroStep(i);
+ }
+ studioState.markDirty(part);
+ rebuild();
+ });
+
+ buttonRow.getChildren().addAll(addStepBtn, clearBtn);
+
+ Label previewLabel = new Label("预览: " + key.formatMacroPreview());
+ previewLabel.getStyleClass().add("group-note");
+
+ Label note = new Label("固件按顺序串行发送;延时单位 3ms(最大 765ms)。需要更长延时请叠加多个延时步骤。");
+ note.getStyleClass().add("warning-note");
+ note.setWrapText(true);
+
+ box.getChildren().addAll(stepsBox, buttonRow, previewLabel, note);
+ return box;
+ }
+
+ private HBox createMacroStepRow(StudioPart part, int index) {
+ HBox row = new HBox(6);
+ row.setAlignment(Pos.CENTER_LEFT);
+ KeyConfig key = studioState.getKeyConfig(part);
+
+ Label indexLabel = new Label((index + 1) + ".");
+ indexLabel.setMinWidth(30);
+
+ ComboBox actionCombo = new ComboBox<>();
+ actionCombo.getItems().addAll(List.of("按下", "松开", "延时"));
+ actionCombo.getStyleClass().add("combo-box-small");
+
+ String actionType = key.getMacroStepAction(index);
+ String actionText = switch (actionType) {
+ case "DOWN_KEY" -> "按下";
+ case "UP_KEY" -> "松开";
+ case "DELAY" -> "延时";
+ default -> "按下";
+ };
+ actionCombo.setValue(actionText);
+ actionCombo.valueProperty().addListener((obs, old, newValue) -> {
+ String newActionType = switch (newValue) {
+ case "按下" -> "DOWN_KEY";
+ case "松开" -> "UP_KEY";
+ case "延时" -> "DELAY";
+ default -> "DOWN_KEY";
+ };
+ int currentParam = key.getMacroStepParam(index);
+ key.updateMacroStep(index, newActionType, currentParam);
+ studioState.markDirty(part);
+ rebuild();
+ });
+
+ if ("DELAY".equals(actionType)) {
+ Spinner delaySpinner = new Spinner<>(1, 255, key.getMacroStepParam(index), 1);
+ delaySpinner.setEditable(true);
+ delaySpinner.setPrefWidth(80);
+ delaySpinner.valueProperty().addListener((obs, old, newValue) -> {
+ key.updateMacroStep(index, "DELAY", newValue);
+ studioState.markDirty(part);
+ });
+ Label msLabel = new Label("ms");
+
+ row.getChildren().addAll(indexLabel, actionCombo, delaySpinner, msLabel);
+ } else {
+ ComboBox keyCombo = new ComboBox<>();
+ java.util.List keyItems = new java.util.ArrayList<>();
+ keyItems.add("--- 修饰键 ---");
+ keyItems.add("Shift");
+ keyItems.add("Ctrl");
+ keyItems.add("Alt");
+ keyItems.add("Win");
+ keyItems.add("--- 功能键 ---");
+ keyItems.add("F1");
+ keyItems.add("F2");
+ keyItems.add("F3");
+ keyItems.add("F4");
+ keyItems.add("F5");
+ keyItems.add("F6");
+ keyItems.add("F7");
+ keyItems.add("F8");
+ keyItems.add("F9");
+ keyItems.add("F10");
+ keyItems.add("F11");
+ keyItems.add("F12");
+ keyItems.add("F13");
+ keyItems.add("F14");
+ keyItems.add("F15");
+ keyItems.add("F16");
+ keyItems.add("F17");
+ keyItems.add("F18");
+ keyItems.add("--- 字母键 ---");
+ String[] letters = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+ "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
+ for (String letter : letters) {
+ keyItems.add(letter);
+ }
+ keyItems.add("--- 数字键 ---");
+ for (int i = 1; i <= 9; i++) {
+ keyItems.add(String.valueOf(i));
+ }
+ keyItems.add("0");
+ keyItems.add("--- 其他键 ---");
+ keyItems.add("Enter");
+ keyItems.add("Escape");
+ keyItems.add("Backspace");
+ keyItems.add("Tab");
+ keyItems.add("Space");
+ keyItems.add("CapsLock");
+ keyItems.add("Delete");
+ keyItems.add("Up");
+ keyItems.add("Down");
+ keyItems.add("Left");
+ keyItems.add("Right");
+ keyCombo.getItems().addAll(keyItems);
+ keyCombo.getStyleClass().add("combo-box-small");
+ keyCombo.setValue(com.example.ahakey.model.HIDUsage.getName(key.getMacroStepParam(index)));
+ keyCombo.valueProperty().addListener((obs, old, newValue) -> {
+ if (newValue != null && !newValue.startsWith("---")) {
+ String currentAction = key.getMacroStepAction(index);
+ int code = switch (newValue) {
+ case "Shift" -> 0xE1;
+ case "Ctrl" -> 0xE0;
+ case "Alt" -> 0xE2;
+ case "Win" -> 0xE3;
+ default -> com.example.ahakey.model.HIDUsage.getCode(newValue);
+ };
+ key.updateMacroStep(index, currentAction, code);
+ studioState.markDirty(part);
+ }
+ });
+
+ row.getChildren().addAll(indexLabel, actionCombo, keyCombo);
+ }
+
+ Button upBtn = new Button("↑");
+ upBtn.getStyleClass().add("small-btn");
+ upBtn.setDisable(index == 0);
+ upBtn.setOnAction(e -> {
+ key.moveMacroStep(index, index - 1);
+ studioState.markDirty(part);
+ rebuild();
+ });
+
+ Button downBtn = new Button("↓");
+ downBtn.getStyleClass().add("small-btn");
+ downBtn.setDisable(index == key.getMacroStepCount() - 1);
+ downBtn.setOnAction(e -> {
+ key.moveMacroStep(index, index + 1);
+ studioState.markDirty(part);
+ rebuild();
+ });
+
+ Button deleteBtn = new Button("×");
+ deleteBtn.getStyleClass().add("small-btn");
+ deleteBtn.getStyleClass().add("delete-btn");
+ deleteBtn.setOnAction(e -> {
+ key.removeMacroStep(index);
+ studioState.markDirty(part);
+ rebuild();
+ });
+
+ row.getChildren().addAll(upBtn, downBtn, deleteBtn);
+ return row;
+ }
+
+ private VBox createDescriptionGroup(StudioPart part) {
+ return createGroupBox("按键描述", () -> {
+ VBox box = new VBox(8);
+
+ TextField descField = new TextField(studioState.getKeyConfig(part).getDescription());
+ descField.setPromptText("例如 Record / Approve / Reject / Backspace");
+ descField.getStyleClass().add("text-field");
+ descField.textProperty().addListener((obs, oldValue, newValue) -> studioState.updateKeyDescription(part, newValue));
+
+ Label warning = new Label("建议使用英文、数字和常用符号。");
+ warning.getStyleClass().add("warning-note");
+
+ Label actual = new Label("设备实际写入:" + studioState.getKeyConfig(part).getDescription());
+ actual.getStyleClass().add("group-note");
+
+ box.getChildren().addAll(descField, warning, actual);
+ return box;
+ });
+ }
+
+ private VBox createLightBarGroup() {
+ VBox root = new VBox(16);
+ ModeSlot mode = studioState.getSelectedMode();
+
+ VBox brightnessBox = createGroupBox("灯光亮度", () -> {
+ VBox box = new VBox(10);
+ HBox row = new HBox(10);
+ row.setAlignment(Pos.CENTER_LEFT);
+
+ Spinner spinner = new Spinner<>(1, 100, studioState.getLightBrightness());
+ spinner.setEditable(true);
+ spinner.setPrefWidth(96);
+ spinner.valueProperty().addListener((obs, oldValue, newValue) -> {
+ if (newValue != null) {
+ studioState.setLightBrightness(newValue);
+ }
+ });
+
+ Button test = new Button("测试亮度");
+ test.setDisable(!deviceStatus.isConnected());
+ test.setOnAction(e -> controller.sendLightBrightnessToDevice());
+
+ row.getChildren().addAll(new Label("亮度"), spinner, test);
+ Label note = new Label("可先测试亮度,保存配置后写入键盘。");
+ note.getStyleClass().add("group-note");
+ note.setWrapText(true);
+ box.getChildren().addAll(row, note);
+ return box;
+ });
+
+ VBox statesBox = createGroupBox(mode.getTitle() + " AI 状态灯效", () -> {
+ VBox box = new VBox(12);
+ for (IDEState ideState : IDEState.values()) {
+ VBox row = new VBox(6);
+ HBox top = new HBox(8);
+ top.setAlignment(Pos.CENTER_LEFT);
+
+ Label title = new Label(ideState.getLabel());
+ title.getStyleClass().add("mapping-title");
+ title.setMinWidth(112);
+ title.setPrefWidth(112);
+ Label help = new Label("?");
+ help.getStyleClass().add("mapping-hw");
+ help.setMinWidth(18);
+ javafx.scene.control.Tooltip.install(help, new javafx.scene.control.Tooltip(ideState.getDescription()));
+ HBox spacer = new HBox();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ ComboBox combo = new ComboBox<>();
+ combo.getItems().addAll(LightEffectStyle.values());
+ combo.setValue(studioState.getAiLightEffect(mode, ideState));
+ combo.setMinWidth(180);
+ combo.setPrefWidth(210);
+ combo.valueProperty().addListener((obs, oldValue, newValue) -> {
+ if (newValue != null) {
+ studioState.setAiLightEffect(mode, ideState, newValue);
+ }
+ });
+
+ Button test = new Button("测试");
+ test.setDisable(!deviceStatus.isConnected());
+ test.setOnAction(e -> controller.previewLightEffectOnDevice(combo.getValue()));
+
+ top.getChildren().addAll(title, help, spacer, combo, test);
+ Label desc = new Label(ideState.getDescription());
+ desc.getStyleClass().add("group-note");
+ desc.setWrapText(true);
+ row.getChildren().addAll(top, desc);
+ box.getChildren().add(row);
+ box.getChildren().add(new Separator());
+ }
+
+ HBox actions = new HBox(10);
+ actions.setAlignment(Pos.CENTER_LEFT);
+ Button sync = new Button("保存当前模式灯效");
+ sync.getStyleClass().add("button-prominent");
+ sync.setDisable(!deviceStatus.isConnected());
+ sync.setOnAction(e -> controller.syncCurrentModeLightConfig());
+ actions.getChildren().add(sync);
+
+ Label note = new Label("测试按钮会直接预览灯效;保存当前模式灯效会写入 " + mode.getTitle() + " 的 9 个 AI 状态。");
+ note.getStyleClass().add("group-note");
+ note.setWrapText(true);
+ box.getChildren().addAll(actions, note);
+ return box;
+ });
+
+ root.getChildren().addAll(brightnessBox, statesBox);
+ return root;
+ }
+ private Label caption(String text) {
+ Label label = new Label(text);
+ label.getStyleClass().add("group-note");
+ label.setWrapText(true);
+ return label;
+ }
+
+ private VBox createOledGroup() {
+ VBox root = new VBox(16);
+ OledModeDraft draft = studioState.getOledDraft();
+
+ root.getChildren().add(createGroupBox("当前模式的 OLED GIF / 图片", () -> {
+ VBox box = new VBox(12);
+
+ // OLED 预览区域
+ StackPane previewArea = new StackPane();
+ previewArea.getStyleClass().add("oled-preview");
+ ImageView previewImage = new ImageView();
+ previewImage.setFitWidth(160);
+ previewImage.setFitHeight(80);
+ previewImage.setPreserveRatio(true);
+
+ if (draft.getLocalAssetPath() != null) {
+ try {
+ Image gifImage = new Image("file:" + draft.getLocalAssetPath());
+ previewImage.setImage(gifImage);
+ } catch (Exception e) {
+ previewImage.setImage(null);
+ }
+ }
+
+ previewArea.getChildren().add(previewImage);
+ box.getChildren().add(previewArea);
+
+ Label asset = new Label(
+ draft.getLocalAssetPath() != null ? draft.getLocalAssetPath() : "未选择 GIF / 图片"
+ );
+ asset.getStyleClass().add("group-note");
+ asset.setWrapText(true);
+
+ HBox actions = new HBox(8);
+ Button pick = new Button("选择 GIF 或图片");
+ pick.getStyleClass().add("button-prominent");
+ pick.setMinWidth(80);
+ pick.setOnAction(e -> {
+ Window w = getScene() != null ? getScene().getWindow() : null;
+ controller.selectOledGif(w);
+ rebuild();
+ });
+ Button upload = new Button("上传到设备");
+ upload.getStyleClass().add("button-prominent");
+ upload.setMinWidth(90);
+ upload.disableProperty().bind(Bindings.createBooleanBinding(
+ () -> !deviceStatus.isConnected() || studioState.uploadingOledProperty().get()
+ || draft.getLocalAssetPath() == null,
+ deviceStatus.isConnectedProperty(),
+ studioState.uploadingOledProperty(),
+ draft.localAssetPathProperty()
+ ));
+ upload.textProperty().bind(Bindings.createStringBinding(
+ () -> studioState.uploadingOledProperty().get() ? "上传中…" : "上传到设备",
+ studioState.uploadingOledProperty()
+ ));
+ upload.setOnAction(e -> controller.uploadCurrentOledToDevice());
+ Button clear = new Button("清空");
+ clear.setMinWidth(60);
+ clear.setOnAction(e -> {
+ studioState.clearOledPreview();
+ rebuild();
+ });
+ actions.getChildren().addAll(pick, upload, clear);
+
+ Spinner fps = new Spinner<>(1, 30, draft.getFramesPerSecond());
+ fps.setEditable(true);
+ fps.valueProperty().addListener((o, a, b) -> {
+ if (b != null) {
+ draft.setFramesPerSecond(b);
+ }
+ });
+ Label progress = new Label();
+ progress.textProperty().bind(studioState.oledUploadDetailProperty());
+ progress.getStyleClass().add("group-note");
+
+ Label limits = new Label(
+ "支持 GIF / PNG / JPG,单个文件不超过 2 MB;GIF 最多 70 帧;图片会自动适配 160×80 屏幕。"
+ );
+ limits.getStyleClass().add("group-note");
+ limits.setWrapText(true);
+
+ box.getChildren().addAll(asset, actions, new Label("帧率 (FPS)"), fps, progress, limits);
+ return box;
+ }));
+
+ root.getChildren().add(createGroupBox("屏幕文字", () -> {
+ VBox box = new VBox(10);
+ TextField titleField = new TextField(studioState.getOledSummary());
+ titleField.getStyleClass().add("text-field");
+ titleField.textProperty().addListener((obs, o, n) -> studioState.setOledSummary(n));
+ TextField captionField = new TextField(studioState.getOledCaption());
+ captionField.getStyleClass().add("text-field");
+ captionField.textProperty().addListener((obs, o, n) -> studioState.setOledCaption(n));
+ Label note = new Label("建议使用英文、数字和常用符号。");
+ note.getStyleClass().add("warning-note");
+ note.setWrapText(true);
+ box.getChildren().addAll(new Label("主标题"), titleField, new Label("副标题"), captionField, note);
+ return box;
+ }));
+ return root;
+ }
+
+ private VBox createToggleGroup() {
+ return createGroupBox("拨杆语义", () -> {
+ VBox box = new VBox(10);
+
+ ToggleGroup group = new ToggleGroup();
+ ToggleButton autoButton = new ToggleButton("自动批准");
+ autoButton.getStyleClass().add("binding-toggle");
+ autoButton.setToggleGroup(group);
+
+ ToggleButton manualButton = new ToggleButton("手动批准");
+ manualButton.getStyleClass().add("binding-toggle");
+ manualButton.setToggleGroup(group);
+
+ autoButton.setOnAction(event -> controller.updateSwitchState(0));
+ manualButton.setOnAction(event -> controller.updateSwitchState(1));
+
+ // 绑定选中状态到 deviceStatus
+ autoButton.selectedProperty().bind(Bindings.createBooleanBinding(
+ () -> deviceStatus.isAutoApproval(),
+ deviceStatus.switchStateProperty()
+ ));
+ manualButton.selectedProperty().bind(Bindings.createBooleanBinding(
+ () -> !deviceStatus.isAutoApproval(),
+ deviceStatus.switchStateProperty()
+ ));
+
+ Label note = new Label("拨杆本身不写入 HID,但会改变设备侧的审批语义和右侧状态显示。");
+ note.getStyleClass().add("group-note");
+
+ box.getChildren().addAll(new HBox(4, autoButton, manualButton), note);
+ return box;
+ });
+ }
+
+ private VBox createGroupBox(String title, Supplier contentProvider) {
+ VBox groupBox = new VBox(4);
+ groupBox.getStyleClass().add("group-box");
+
+ Label titleLabel = new Label(title);
+ titleLabel.getStyleClass().add("group-box-title");
+
+ VBox inner = contentProvider.get();
+ inner.setPadding(new Insets(4, 0, 0, 0));
+
+ groupBox.getChildren().addAll(titleLabel, inner);
+ return groupBox;
+ }
+
+ private String iconFor(StudioPart part) {
+ if (part.isKey()) {
+ return "⌨";
+ }
+ return switch (part) {
+ case LIGHT_BAR -> "≋";
+ case OLED -> "▣";
+ case TOGGLE_SWITCH -> "◫";
+ default -> "⌨";
+ };
+ }
+
+}
+
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/StatusBar.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/StatusBar.java
new file mode 100644
index 00000000..0a06625a
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/StatusBar.java
@@ -0,0 +1,57 @@
+package com.example.ahakey.view;
+
+import com.example.ahakey.model.DeviceStatus;
+import com.example.ahakey.model.StudioState;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+
+public class StatusBar extends HBox {
+ public StatusBar(DeviceStatus deviceStatus, StudioState studioState) {
+ init(deviceStatus, studioState);
+ }
+
+ private void init(DeviceStatus deviceStatus, StudioState studioState) {
+ setPadding(new Insets(8, 24, 8, 24));
+ setSpacing(16);
+ setAlignment(Pos.CENTER_LEFT);
+ getStyleClass().add("status-bar");
+
+ Label selection = new Label();
+ selection.textProperty().bind(Bindings.createStringBinding(
+ () -> "当前选中: " + studioState.getSelectedPart().getDisplayTitle(),
+ studioState.selectedPartProperty()
+ ));
+ selection.getStyleClass().add("status-bar-text");
+
+ Label device = new Label();
+ device.textProperty().bind(Bindings.createStringBinding(
+ () -> "设备: " + deviceStatus.getDeviceName(),
+ deviceStatus.deviceNameProperty()
+ ));
+ device.getStyleClass().add("status-bar-text");
+
+ Label dirty = new Label();
+ dirty.textProperty().bind(Bindings.createStringBinding(
+ () -> "待保存改动: " + studioState.getDirtyCount(),
+ studioState.dirtyCountProperty()
+ ));
+ dirty.getStyleClass().add("status-bar-text");
+
+ Label sync = new Label();
+ sync.textProperty().bind(studioState.syncStatusProperty());
+ sync.getStyleClass().add("status-bar-text");
+
+ Label lastSync = new Label();
+ lastSync.textProperty().bind(studioState.lastSyncSummaryProperty());
+ lastSync.getStyleClass().add("status-bar-text");
+
+ HBox spacer = new HBox();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ getChildren().addAll(selection, device, dirty, spacer, sync, lastSync);
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/TopBar.java b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/TopBar.java
new file mode 100644
index 00000000..4122d482
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/java/com/example/ahakey/view/TopBar.java
@@ -0,0 +1,947 @@
+package com.example.ahakey.view;
+
+import com.example.ahakey.app.StudioController;
+import com.example.ahakey.model.DeviceStatus;
+import com.example.ahakey.model.StudioState;
+import com.example.ahakey.service.AgentManager;
+import com.example.ahakey.service.HookDispatchServer;
+import com.example.ahakey.service.HookInstaller;
+import com.example.ahakey.service.VoiceInputManager;
+import com.example.ahakey.util.Icons;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.SeparatorMenuItem;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.*;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+import javafx.scene.Scene;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import javafx.scene.control.Alert;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.io.File;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.animation.AnimationTimer;
+
+public class TopBar extends VBox {
+ private final StudioController controller;
+ private final DeviceStatus deviceStatus;
+ private final StudioState studioState;
+ private final AgentManager agentManager;
+ private VoiceInputManager voiceInputManager;
+ private TextArea logArea;
+ private final HookInstaller hookInstaller;
+
+ // 语音相关UI组件
+ private Button voiceRecordButton;
+ private VoiceStatusLamp voiceStatusLamp;
+ private Label voiceStatusLabel;
+ private Label voiceResultPreview;
+ private volatile boolean isRecording = false;
+ private volatile boolean voiceRunning = false;
+ private FloatingVoiceNotification floatingNotification; // 浮动通知窗口
+
+ public TopBar(StudioController controller, DeviceStatus deviceStatus,
+ StudioState studioState, AgentManager agentManager) {
+ this.controller = controller;
+ this.deviceStatus = deviceStatus;
+ this.studioState = studioState;
+ this.agentManager = agentManager;
+ this.hookInstaller = new HookInstaller(HookDispatchServer.DEFAULT_PORT, this::addLog);
+ setSpacing(0);
+ setPadding(new Insets(0));
+ getStyleClass().add("top-bar");
+ initContent();
+ }
+
+ /**
+ * 设置语音输入管理器
+ */
+ public void setVoiceInputManager(VoiceInputManager voiceInputManager) {
+ this.voiceInputManager = voiceInputManager;
+ updateVoiceButtonState();
+ }
+
+ private void initContent() {
+ Text titleIcon = Icons.keyboard("20");
+ Label titleLabel = new Label("AhaKey Studio");
+ titleLabel.getStyleClass().add("title");
+ HBox titleBox = new HBox(8);
+ titleBox.getChildren().addAll(titleIcon, titleLabel);
+
+ HBox infoPills = new HBox(10);
+ infoPills.getChildren().addAll(
+ new InfoPill(
+ Bindings.createStringBinding(
+ () -> controller.isEffectivelyConnected() ? "已连接"
+ : (deviceStatus.isScanning() ? "扫描中" : "未连接"),
+ deviceStatus.isConnectedProperty(),
+ deviceStatus.isScanningProperty()
+ ),
+ deviceStatus.deviceNameProperty(),
+ Bindings.createObjectBinding(
+ () -> controller.isEffectivelyConnected() ? AccentColor.GREEN : AccentColor.ORANGE,
+ deviceStatus.isConnectedProperty()
+ )
+ ),
+ new InfoPill(
+ Bindings.createStringBinding(() -> "电量"),
+ Bindings.createStringBinding(
+ () -> controller.isEffectivelyConnected() ? deviceStatus.getBatteryLevel() + "%" : "—",
+ deviceStatus.isConnectedProperty(),
+ deviceStatus.batteryLevelProperty()
+ ),
+ Bindings.createObjectBinding(() -> AccentColor.BLUE)
+ ),
+ new InfoPill(
+ Bindings.createStringBinding(() -> "拨杆"),
+ Bindings.createStringBinding(deviceStatus::getSwitchTitle, deviceStatus.switchStateProperty()),
+ Bindings.createObjectBinding(
+ () -> deviceStatus.isAutoApproval() ? AccentColor.MINT : AccentColor.INDIGO,
+ deviceStatus.switchStateProperty()
+ )
+ )
+ );
+
+ // 操作按钮
+ Button connectButton = new Button();
+ connectButton.getStyleClass().add("button-connect");
+ connectButton.textProperty().bind(Bindings.createStringBinding(
+ () -> deviceStatus.isConnected() ? "断开连接" : "连接设备",
+ deviceStatus.isConnectedProperty()
+ ));
+ // 连接状态变化时切换按钮样式
+ deviceStatus.isConnectedProperty().addListener((obs, oldVal, newVal) -> {
+ connectButton.getStyleClass().removeAll("button-connect", "button-disconnect");
+ connectButton.getStyleClass().add(newVal ? "button-disconnect" : "button-connect");
+ });
+ connectButton.setOnAction(event -> {
+ if (deviceStatus.isConnected()) {
+ controller.userDisconnect();
+ } else {
+ controller.userConnect();
+ }
+ });
+
+ // BLE 驱动按钮
+ Button bleButton = new Button("BLE驱动");
+ bleButton.getStyleClass().add("button-ble");
+ bleButton.setOnAction(event -> handleBleButtonClick());
+
+ ToggleButton ahaTypeToggle = new ToggleButton();
+ ahaTypeToggle.getStyleClass().add("toggle-button");
+ ahaTypeToggle.textProperty().bind(Bindings.createStringBinding(
+ () -> studioState.ahaTypeEnabledProperty().get() ? "AhaType" : "AhaType",
+ studioState.ahaTypeEnabledProperty()
+ ));
+ ahaTypeToggle.selectedProperty().bindBidirectional(studioState.ahaTypeEnabledProperty());
+ ahaTypeToggle.selectedProperty().addListener((obs, oldValue, newValue) -> studioState.toggleAhaType(newValue));
+
+ // 语音启动按钮
+ voiceRecordButton = new Button("启动语音输入");
+ voiceRecordButton.getStyleClass().add("button-voice");
+ voiceRecordButton.setOnAction(event -> toggleVoiceService());
+
+ // 语音状态指示灯
+ voiceStatusLamp = new VoiceStatusLamp();
+
+ // 语音状态标签
+ voiceStatusLabel = new Label("语音未启动");
+ voiceStatusLabel.getStyleClass().add("voice-status");
+
+ // 语音识别结果预览
+ voiceResultPreview = new Label("");
+ voiceResultPreview.getStyleClass().add("voice-preview");
+
+ // 语音控制区域
+ VBox voiceControlBox = new VBox(4);
+ HBox voiceButtonRow = new HBox(8);
+ voiceButtonRow.getChildren().addAll(voiceRecordButton, voiceStatusLamp, voiceStatusLabel);
+ voiceControlBox.getChildren().addAll(voiceButtonRow, voiceResultPreview);
+
+ VBox ahaTypeStatus = createStatusBox(
+ studioState.ahaTypeEnabledProperty(),
+ Bindings.createStringBinding(
+ () -> studioState.ahaTypeEnabledProperty().get() ? "AhaType 开启" : "AhaType 关闭",
+ studioState.ahaTypeEnabledProperty()
+ ),
+ studioState.ahaTypeStatusProperty()
+ );
+
+ // 检查本地模型是否启用
+ boolean modelEnabled = com.example.ahakey.config.ModelConfig.getInstance().isEnabled();
+
+ VBox configStatus = createStatusBox(
+ Bindings.createBooleanBinding(agentManager::isEditingConfiguration, agentManager.bluetoothOwnerProperty()),
+ Bindings.createStringBinding(
+ () -> agentManager.isEditingConfiguration() ? "编辑配置中" : "键盘控制中",
+ agentManager.bluetoothOwnerProperty()
+ ),
+ Bindings.createStringBinding(
+ () -> agentManager.isEditingConfiguration()
+ ? "正在编辑配置"
+ : "键盘正常运行中",
+ agentManager.bluetoothOwnerProperty()
+ )
+ );
+
+ Button configModeButton = new Button();
+ configModeButton.getStyleClass().add("button-prominent");
+ configModeButton.textProperty().bind(Bindings.createStringBinding(controller::configurationModeButtonTitle,
+ studioState.syncingProperty(),
+ agentManager.bluetoothOwnerProperty()));
+ configModeButton.disableProperty().bind(Bindings.createBooleanBinding(
+ () -> studioState.syncingProperty().get() || agentManager.operationInProgressProperty().get(),
+ studioState.syncingProperty(),
+ agentManager.operationInProgressProperty()
+ ));
+ configModeButton.setOnAction(event -> controller.handleConfigurationModeButton());
+
+ Menu moreMenu = new Menu("更多");
+ Text moreIcon = Icons.moreHorizontal("16");
+ moreMenu.setGraphic(moreIcon);
+
+ MenuItem restoreDefaults = new MenuItem("恢复当前模式默认值");
+ restoreDefaults.setOnAction(event -> studioState.restoreCurrentModeDefaults());
+ MenuItem reconnect = new MenuItem("重新连接设备");
+ reconnect.setOnAction(event -> {
+ controller.userDisconnect();
+ controller.userConnect();
+ });
+ MenuItem clearOled = new MenuItem("清空 OLED 预览");
+ clearOled.setOnAction(event -> studioState.clearOledPreview());
+ SeparatorMenuItem divider1 = new SeparatorMenuItem();
+ MenuItem deviceInfo = new MenuItem("设备信息 · Hooks安装");
+ deviceInfo.setOnAction(event -> showDeviceInfoDialog());
+ MenuItem versionInfo = new MenuItem("查看版本号");
+ versionInfo.setOnAction(event -> showVersionDialog());
+ MenuItem cloudAccount = new MenuItem("云端账号 · AhaType…");
+ SeparatorMenuItem divider2 = new SeparatorMenuItem();
+ MenuItem refresh = new MenuItem("刷新 AhaType 状态");
+ refresh.setOnAction(event -> studioState.toggleAhaType(studioState.ahaTypeEnabledProperty().get()));
+
+ // 条件添加 AhaType 相关菜单项
+ if (modelEnabled) {
+ moreMenu.getItems().addAll(
+ restoreDefaults,
+ reconnect,
+ clearOled,
+ divider1,
+ deviceInfo,
+ versionInfo,
+ cloudAccount,
+ divider2,
+ refresh
+ );
+ } else {
+ // 模型禁用时,隐藏 AhaType 相关菜单
+ moreMenu.getItems().addAll(
+ restoreDefaults,
+ reconnect,
+ clearOled,
+ divider1,
+ deviceInfo,
+ versionInfo
+ );
+ }
+
+ MenuBar menuBar = new MenuBar(moreMenu);
+ menuBar.setUseSystemMenuBar(false);
+ menuBar.getStyleClass().add("toolbar-menu");
+
+ // 将操作按钮放入统一的 HBox
+ HBox actionButtons = new HBox(4);
+ actionButtons.setAlignment(Pos.CENTER_LEFT);
+ actionButtons.getChildren().addAll(connectButton, bleButton);
+
+ // 状态信息与操作按钮之间的固定间距
+ Region spacer = new Region();
+ spacer.setMinWidth(12);
+ spacer.setPrefWidth(16);
+ spacer.setMaxWidth(40);
+
+ // 主行 HBox:所有控件在一行,不会换行
+ HBox mainRow = new HBox(10);
+ mainRow.setAlignment(Pos.CENTER_LEFT);
+ mainRow.setPadding(new Insets(6, 16, 6, 16));
+ mainRow.setMinWidth(Region.USE_PREF_SIZE); // 保持首选宽度,不缩小
+ mainRow.getChildren().addAll(titleBox, infoPills, spacer, actionButtons);
+ if (modelEnabled) {
+ mainRow.getChildren().addAll(ahaTypeToggle, ahaTypeStatus, voiceControlBox);
+ } else {
+ // 隐藏语音相关控件
+ voiceRecordButton.setVisible(false);
+ voiceRecordButton.setManaged(false);
+ voiceStatusLamp.setVisible(false);
+ voiceStatusLamp.setManaged(false);
+ voiceStatusLabel.setVisible(false);
+ voiceStatusLabel.setManaged(false);
+ voiceResultPreview.setVisible(false);
+ voiceResultPreview.setManaged(false);
+ }
+ mainRow.getChildren().addAll(configStatus, configModeButton, menuBar);
+
+ // 右侧弹性 spacer:把编辑配置/菜单推到最右
+ Region rightSpacer = new Region();
+ HBox.setHgrow(rightSpacer, Priority.ALWAYS);
+ mainRow.getChildren().add(
+ mainRow.getChildren().size() - 3, rightSpacer // 插到 configStatus 前面
+ );
+
+ // 包裹在水平 ScrollPane 中:宽屏时不显示滚动条,分屏窄时可水平滚动
+ ScrollPane scrollWrapper = new ScrollPane(mainRow);
+ scrollWrapper.setFitToWidth(true);
+ scrollWrapper.setFitToHeight(true);
+ scrollWrapper.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
+ scrollWrapper.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
+ scrollWrapper.setPannable(false);
+ scrollWrapper.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
+ // 让 ScrollPane 内容背景透明
+ mainRow.setStyle("-fx-background-color: transparent;");
+
+ getChildren().add(scrollWrapper);
+ }
+
+ /**
+ * 处理 BLE 驱动按钮点击
+ * - 如果 BLE_tcp_driver.exe 已运行,弹窗提示
+ * - 否则启动同级目录下的 BLE_tcp_driver.exe
+ */
+ private void handleBleButtonClick() {
+ if (isBleDriverRunning()) {
+ if (isBleBridgeReachable()) {
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ alert.setTitle("BLE 驱动");
+ alert.setHeaderText(null);
+ alert.setContentText("BLE 驱动已打开");
+ alert.showAndWait();
+ } else {
+ stopBleDriverProcess();
+ launchBleDriver();
+ }
+ } else {
+ // 启动 BLE 驱动
+ launchBleDriver();
+ }
+ }
+
+ private boolean isBleBridgeReachable() {
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress("127.0.0.1", 9000), 800);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private void stopBleDriverProcess() {
+ try {
+ new ProcessBuilder("taskkill", "/F", "/IM", "BLE_tcp_driver.exe").redirectErrorStream(true).start().waitFor();
+ Thread.sleep(300);
+ } catch (Exception ignored) {
+ }
+ }
+
+ /**
+ * 检查 BLE_tcp_driver.exe 是否正在运行
+ */
+ private boolean isBleDriverRunning() {
+ try {
+ ProcessBuilder pb = new ProcessBuilder("tasklist", "/FI", "IMAGENAME eq BLE_tcp_driver.exe", "/NH");
+ pb.redirectErrorStream(true);
+ Process p = pb.start();
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.toLowerCase().contains("ble_tcp_driver.exe")) {
+ return true;
+ }
+ }
+ }
+ p.waitFor();
+ } catch (Exception e) {
+ // 检查失败,假定未运行
+ }
+ return false;
+ }
+
+ /**
+ * 启动同级目录下的 BLE_tcp_driver.exe
+ */
+ private void launchBleDriver() {
+ try {
+ // 获取应用所在目录
+ String appDir = System.getProperty("user.dir");
+
+ // 尝试从 JAR 所在目录获取(打包后的情况)
+ try {
+ Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI());
+ if (jarPath.toString().endsWith(".jar")) {
+ appDir = jarPath.getParent().toString();
+ }
+ } catch (Exception ignored) {}
+
+ File bleExe = null;
+ File appDirFile = new File(appDir);
+
+ // 依次查找多个可能的位置
+ File[] candidates = {
+ new File(appDir, "BLE_tcp_driver.exe"), // JAR 同级目录 (app/)
+ appDirFile.getParentFile() != null
+ ? new File(appDirFile.getParentFile(), "BLE_tcp_driver.exe") : null, // 父目录(jpackage 结构)
+ new File(System.getProperty("user.dir"), "BLE_tcp_driver.exe"), // user.dir
+ new File(appDir, "app/BLE_tcp_driver.exe") // app 子目录
+ };
+
+ for (File candidate : candidates) {
+ if (candidate != null && candidate.exists()) {
+ bleExe = candidate;
+ break;
+ }
+ }
+
+ if (bleExe != null) {
+ final File finalBleExe = bleExe;
+ ProcessBuilder pb = new ProcessBuilder(finalBleExe.getAbsolutePath());
+ pb.directory(finalBleExe.getParentFile());
+ pb.start();
+
+ // 短暂延迟后再次检查,给用户反馈
+ new Thread(() -> {
+ try {
+ Thread.sleep(1000);
+ Platform.runLater(() -> {
+ if (isBleDriverRunning()) {
+ // 启动成功,无需额外提示
+ } else {
+ showAlert("BLE 驱动", "BLE 驱动启动失败,请手动运行: " + finalBleExe.getAbsolutePath());
+ }
+ });
+ } catch (Exception ignored) {}
+ }).start();
+ } else {
+ showAlert("BLE 驱动", "未找到 BLE_tcp_driver.exe\n已尝试以下位置:\n"
+ + new File(appDir, "BLE_tcp_driver.exe").getAbsolutePath() + "\n"
+ + (appDirFile.getParentFile() != null ? new File(appDirFile.getParentFile(), "BLE_tcp_driver.exe").getAbsolutePath() : "") + "\n"
+ + new File(System.getProperty("user.dir"), "BLE_tcp_driver.exe").getAbsolutePath());
+ }
+ } catch (Exception e) {
+ showAlert("BLE 驱动", "启动失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 显示警告弹窗
+ */
+ private void showAlert(String title, String content) {
+ Alert alert = new Alert(Alert.AlertType.WARNING);
+ alert.setTitle(title);
+ alert.setHeaderText(null);
+ alert.setContentText(content);
+ alert.showAndWait();
+ }
+
+ /**
+ * 切换语音服务状态(启动/停止)
+ */
+ private void toggleVoiceService() {
+ if (voiceInputManager == null) {
+ setVoiceStatus("error", "语音服务未初始化");
+ return;
+ }
+
+ if (voiceRunning) {
+ stopVoiceService();
+ } else {
+ startVoiceService();
+ }
+ }
+
+ /**
+ * 启动语音服务
+ */
+ private void startVoiceService() {
+ voiceRunning = true;
+ updateVoiceButtonState();
+ setVoiceStatus("starting", "语音启动中");
+
+ // 创建浮动通知窗口
+ if (floatingNotification == null) {
+ floatingNotification = new FloatingVoiceNotification();
+ }
+
+ // 设置状态回调(同时更新UI和浮动通知)
+ voiceInputManager.setStatusCallback(status -> {
+ // 状态格式: "code:message"
+ String[] parts = status.split(":", 2);
+ String code = parts[0];
+ String message = parts.length > 1 ? parts[1] : code;
+
+ Platform.runLater(() -> {
+ // 更新 TopBar 状态
+ setVoiceStatus(code, message);
+
+ // 更新浮动通知
+ if (floatingNotification != null) {
+ floatingNotification.updateStatus(code, message);
+ }
+ });
+ });
+
+ // 启动语音输入管理器
+ voiceInputManager.startVoiceInput(result -> {
+ Platform.runLater(() -> {
+ voiceResultPreview.setText(result);
+ });
+ }, partialResult -> {
+ Platform.runLater(() -> {
+ voiceResultPreview.setText(partialResult);
+ });
+ });
+ }
+
+ /**
+ * 停止语音服务
+ */
+ private void stopVoiceService() {
+ voiceRunning = false;
+ updateVoiceButtonState();
+ setVoiceStatus("stopping", "语音关闭中");
+
+ // 关闭浮动通知窗口
+ if (floatingNotification != null) {
+ floatingNotification.close();
+ floatingNotification = null;
+ }
+
+ if (voiceInputManager != null) {
+ voiceInputManager.stopVoiceInput();
+ }
+
+ // 延迟更新状态
+ new Thread(() -> {
+ try {
+ Thread.sleep(500);
+ Platform.runLater(() -> {
+ setVoiceStatus("stopped", "语音未启动");
+ voiceResultPreview.setText("");
+ });
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }).start();
+ }
+
+ /**
+ * 设置语音状态
+ */
+ private void setVoiceStatus(String status, String text) {
+ if (voiceStatusLamp != null) {
+ voiceStatusLamp.setStatus(status);
+ }
+
+ if (voiceStatusLabel != null) {
+ voiceStatusLabel.setText(text);
+
+ // 根据状态设置颜色
+ String color = switch (status) {
+ case "stopped", "idle" -> "#A7AFBA"; // 空闲状态 - 灰色
+ case "starting", "stopping", "processing", "recognizing" -> "#F5A623"; // 处理/识别中 - 橙色
+ case "recording" -> "#E74C3C"; // 录音中 - 红色
+ case "ready" -> "#2ECC71"; // 就绪 - 绿色
+ default -> "#E74C3C"; // error
+ };
+ voiceStatusLabel.setStyle("-fx-text-fill: " + color + ";");
+ }
+ }
+
+ /**
+ * 更新语音按钮状态
+ */
+ private void updateVoiceButtonState() {
+ if (voiceRecordButton == null) return;
+
+ if (voiceInputManager == null || !voiceInputManager.isEnabled()) {
+ voiceRecordButton.setDisable(true);
+ voiceRecordButton.setText("启动语音输入 (不可用)");
+ setVoiceStatus("error", "语音服务未加载");
+ return;
+ }
+
+ voiceRecordButton.setDisable(false);
+
+ if (voiceRunning) {
+ voiceRecordButton.getStyleClass().add("voice-recording");
+ voiceRecordButton.setText("停止语音输入");
+ } else {
+ voiceRecordButton.getStyleClass().remove("voice-recording");
+ voiceRecordButton.setText("启动语音输入");
+ }
+ }
+
+ private VBox createStatusBox(
+ javafx.beans.value.ObservableValue isPositive,
+ javafx.beans.value.ObservableValue title,
+ javafx.beans.value.ObservableValue detail
+ ) {
+ VBox box = new VBox(1);
+ box.getStyleClass().add("status-box");
+
+ HBox row = new HBox(8);
+ Label dot = new Label();
+ dot.getStyleClass().add("status-dot");
+ dot.styleProperty().bind(Bindings.createStringBinding(
+ () -> "-fx-background-color: " + (isPositive.getValue() ? "#30d158" : "#0a84ff") + ";",
+ isPositive
+ ));
+
+ VBox text = new VBox(1);
+ Label titleLabel = new Label();
+ titleLabel.textProperty().bind(title);
+ titleLabel.getStyleClass().add("status-label");
+
+ Label detailLabel = new Label();
+ detailLabel.textProperty().bind(detail);
+ detailLabel.getStyleClass().add("status-detail");
+
+ text.getChildren().addAll(titleLabel, detailLabel);
+ row.getChildren().addAll(dot, text);
+ box.getChildren().add(row);
+ return box;
+ }
+
+ private void showDeviceInfoDialog() {
+ Stage dialog = new Stage();
+ dialog.initOwner(getScene().getWindow());
+ dialog.setTitle("Hook 安装 & 分发工具");
+ dialog.setWidth(550);
+ dialog.setHeight(650);
+
+ ScrollPane scrollPane = new ScrollPane();
+ scrollPane.setFitToWidth(true);
+
+ VBox content = new VBox(12);
+ content.setPadding(new Insets(12));
+
+ // 设备信息摘要
+ VBox deviceCard = new VBox(8);
+ deviceCard.getStyleClass().add("dialog-card");
+ deviceCard.setPadding(new Insets(12));
+
+ Label deviceTitle = new Label("设备信息");
+ deviceTitle.getStyleClass().add("dialog-card-title");
+
+ HBox deviceRow1 = new HBox(16);
+ Label connStatus = new Label();
+ connStatus.getStyleClass().add("dialog-text");
+ connStatus.textProperty().bind(Bindings.createStringBinding(() ->
+ "连接: " + (this.deviceStatus.isConnected() ? "已连接" : "未连接"),
+ this.deviceStatus.isConnectedProperty()
+ ));
+ Label batteryStatus = new Label();
+ batteryStatus.getStyleClass().add("dialog-text");
+ batteryStatus.textProperty().bind(Bindings.createStringBinding(() ->
+ "电量: " + this.deviceStatus.getBatteryLevel() + "%",
+ this.deviceStatus.batteryLevelProperty()
+ ));
+ deviceRow1.getChildren().addAll(connStatus, batteryStatus);
+
+ HBox deviceRow2 = new HBox(16);
+ Label deviceName = new Label("设备名: " + (this.deviceStatus.getDeviceName() != null ? this.deviceStatus.getDeviceName() : "—"));
+ deviceName.getStyleClass().add("dialog-text");
+ Label switchState = new Label();
+ switchState.getStyleClass().add("dialog-text");
+ switchState.textProperty().bind(Bindings.createStringBinding(() ->
+ "拨杆: " + this.deviceStatus.getSwitchTitle(),
+ this.deviceStatus.switchStateProperty()
+ ));
+ deviceRow2.getChildren().addAll(deviceName, switchState);
+
+ deviceCard.getChildren().addAll(deviceTitle, deviceRow1, deviceRow2);
+
+ // 日志区域(提前创建以记录检测过程)
+ logArea = new TextArea();
+ logArea.getStyleClass().add("dialog-log-area");
+ logArea.setEditable(false);
+ logArea.setPrefHeight(150);
+ logArea.setWrapText(true);
+ logArea.setText("[系统] Hook 安装工具已启动\n");
+
+ // 输出用户目录信息
+ String homeDir = System.getProperty("user.home");
+ addLog("[系统] 用户目录: " + homeDir);
+ addLog("[系统] 操作系统: " + System.getProperty("os.name"));
+ addLog("[系统] Java 版本: " + System.getProperty("java.version"));
+ addLog("");
+
+ // 检测并记录各 Hook 状态
+ String[] hookNames = {"Claude", "Cursor", "Codex", "Kimi"};
+ boolean[] hookInstalled = new boolean[4];
+
+ for (int i = 0; i < hookNames.length; i++) {
+ String name = hookNames[i];
+ Path path = hookInstaller.getHookConfigPath(name);
+ File file = path.toFile();
+
+ // 使用完整的检查逻辑
+ boolean installed = isHookInstalled(name);
+
+ // 添加详细调试信息
+ addLog("[检测] === " + name + " Hook ===");
+ addLog("[检测] 检查路径: " + path);
+ addLog("[检测] 文件存在: " + file.exists());
+
+ if (file.exists()) {
+ try {
+ String fileContent = new String(java.nio.file.Files.readAllBytes(path), java.nio.charset.StandardCharsets.UTF_8);
+ addLog("[检测] 文件大小: " + fileContent.length() + " 字符");
+
+ if ("Claude".equals(name)) {
+ addLog("[检测] 包含 hooks: " + fileContent.contains("\"hooks\""));
+ addLog("[检测] 包含 SessionStart: " + fileContent.contains("SessionStart"));
+ } else if ("Cursor".equals(name)) {
+ addLog("[检测] 包含 hooks: " + fileContent.contains("\"hooks\""));
+ addLog("[检测] 包含 sessionStart: " + fileContent.contains("sessionStart"));
+ } else if ("Codex".equals(name)) {
+ String home = System.getProperty("user.home");
+ Path sidecar = Paths.get(home, ".codex", ".ahakey_codex_hooks_v1");
+ addLog("[检测] sidecar 存在: " + sidecar.toFile().exists());
+ addLog("[检测] hooks.json 内容长度: " + fileContent.length());
+ } else if ("Kimi".equals(name)) {
+ addLog("[检测] 包含 BEGIN 标记: " + fileContent.contains("# BEGIN AhaKey Kimi Hooks"));
+ addLog("[检测] 包含 END 标记: " + fileContent.contains("# END AhaKey Kimi Hooks"));
+ }
+ } catch (Exception e) {
+ addLog("[检测] 读取文件失败: " + e.getMessage());
+ }
+ }
+
+ hookInstalled[i] = installed;
+ addLog("[检测] 最终状态: " + (installed ? "已安装" : "未安装"));
+ addLog("");
+ }
+
+ // Hook 安装卡片
+ VBox claudeCard = createHookCard("Claude", hookInstalled[0]);
+ VBox cursorCard = createHookCard("Cursor", hookInstalled[1]);
+ VBox codexCard = createHookCard("Codex", hookInstalled[2]);
+ VBox kimiCard = createHookCard("Kimi", hookInstalled[3]);
+
+ // 日志卡片
+ VBox logCard = new VBox(8);
+ logCard.getStyleClass().add("dialog-card");
+ logCard.setPadding(new Insets(12));
+
+ Label logTitle = new Label("日志");
+ logTitle.getStyleClass().add("dialog-log-title");
+ logCard.getChildren().addAll(logTitle, logArea);
+
+ // 操作按钮
+ HBox actionButtons = new HBox(10);
+ Button connectBtn = new Button("连接设备");
+ connectBtn.getStyleClass().add("button-connect");
+ connectBtn.disableProperty().bind(this.deviceStatus.isConnectedProperty());
+ connectBtn.setOnAction(event -> controller.userConnect());
+
+ Button disconnectBtn = new Button("断开连接");
+ disconnectBtn.getStyleClass().add("button-disconnect");
+ disconnectBtn.disableProperty().bind(this.deviceStatus.isConnectedProperty().not());
+ disconnectBtn.setOnAction(event -> controller.userDisconnect());
+
+ Button clearLogBtn = new Button("清空日志");
+ clearLogBtn.setOnAction(event -> logArea.setText("[系统] 日志已清空\n"));
+
+ Button closeBtn = new Button("关闭");
+ closeBtn.setOnAction(event -> dialog.close());
+
+ actionButtons.getChildren().addAll(connectBtn, disconnectBtn, clearLogBtn, closeBtn);
+ actionButtons.setAlignment(Pos.CENTER_RIGHT);
+
+ content.getChildren().addAll(deviceCard, claudeCard, cursorCard, codexCard, kimiCard, logCard, actionButtons);
+ scrollPane.setContent(content);
+
+ Scene scene = new Scene(scrollPane);
+ scene.getStylesheets().add(getClass().getResource("/style.css").toExternalForm());
+ dialog.setScene(scene);
+ dialog.showAndWait();
+ }
+
+ private VBox createHookCard(String hookName, boolean isInstalled) {
+ VBox card = new VBox(8);
+ card.getStyleClass().add("dialog-card");
+ card.setPadding(new Insets(12));
+
+ HBox titleRow = new HBox();
+ Label title = new Label(hookName + " Hook");
+ title.getStyleClass().add("dialog-card-title");
+ Region spacer1 = new Region();
+ HBox.setHgrow(spacer1, Priority.ALWAYS);
+ titleRow.getChildren().addAll(title, spacer1);
+
+ HBox statusRow = new HBox(8);
+ Label statusLabel = new Label("安装状态:");
+ statusLabel.getStyleClass().add("dialog-status-label");
+
+ Label statusValue = new Label(isInstalled ? "已安装" : "未安装");
+ if (isInstalled) {
+ statusValue.getStyleClass().add("dialog-status-installed");
+ } else {
+ statusValue.getStyleClass().add("dialog-status-uninstalled");
+ }
+
+ Region spacer2 = new Region();
+ HBox.setHgrow(spacer2, Priority.ALWAYS);
+
+ Button installBtn = new Button("安装");
+ installBtn.getStyleClass().add("button-install");
+ installBtn.setOnAction(event -> {
+ installHook(hookName);
+ statusValue.setText("已安装");
+ statusValue.getStyleClass().remove("dialog-status-uninstalled");
+ statusValue.getStyleClass().add("dialog-status-installed");
+ });
+
+ Button uninstallBtn = new Button("卸载");
+ uninstallBtn.getStyleClass().add("button-uninstall");
+ uninstallBtn.setOnAction(event -> {
+ uninstallHook(hookName);
+ statusValue.setText("未安装");
+ statusValue.getStyleClass().remove("dialog-status-installed");
+ statusValue.getStyleClass().add("dialog-status-uninstalled");
+ });
+
+ statusRow.getChildren().addAll(statusLabel, statusValue, spacer2, installBtn, uninstallBtn);
+
+ card.getChildren().addAll(titleRow, statusRow);
+ return card;
+ }
+
+ // ==================== Hook 管理(委托给 HookInstaller) ====================
+
+ private boolean isHookInstalled(String hookName) {
+ return hookInstaller.isInstalled(hookName);
+ }
+
+ private void installHook(String hookName) {
+ hookInstaller.install(hookName);
+ }
+
+ private void uninstallHook(String hookName) {
+ addLog("[卸载] 开始卸载 " + hookName + " Hook...");
+ hookInstaller.uninstall(hookName);
+ }
+
+ private void addLog(String message) {
+ if (logArea != null) {
+ String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
+ logArea.appendText("[" + timestamp + "] " + message + "\n");
+ logArea.setScrollTop(Double.MAX_VALUE);
+ }
+ }
+
+ private void showVersionDialog() {
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ alert.setTitle("版本信息");
+ alert.setHeaderText(null);
+
+ String version = getVersion();
+ alert.setContentText("版本号: " + version);
+
+ alert.showAndWait();
+ }
+
+ private String getVersion() {
+ String version = System.getProperty("app.version");
+ if (version == null || version.isEmpty()) {
+ version = "unknown";
+ }
+ return version;
+ }
+
+ /**
+ * 语音状态指示灯组件
+ */
+ private class VoiceStatusLamp extends Canvas {
+ private String status = "stopped";
+ private double angle = 0;
+ private AnimationTimer timer;
+ private boolean isTimerRunning = false;
+
+ public VoiceStatusLamp() {
+ super(16, 16);
+ timer = new AnimationTimer() {
+ @Override
+ public void handle(long now) {
+ angle = (angle + 30) % 360;
+ draw();
+ }
+ };
+ }
+
+ public void setStatus(String status) {
+ this.status = status != null ? status : "stopped";
+ if (status.equals("starting") || status.equals("stopping") || status.equals("processing")) {
+ if (!isTimerRunning) {
+ timer.start();
+ isTimerRunning = true;
+ }
+ } else {
+ timer.stop();
+ isTimerRunning = false;
+ angle = 0;
+ }
+ draw();
+ }
+
+ private void draw() {
+ GraphicsContext gc = getGraphicsContext2D();
+ gc.clearRect(0, 0, getWidth(), getHeight());
+
+ if (status.equals("stopped")) {
+ // 灰色空心圆
+ gc.setStroke(javafx.scene.paint.Color.web("#8A9099"));
+ gc.setLineWidth(1.6);
+ gc.strokeOval(2.0, 2.0, getWidth() - 4, getHeight() - 4);
+ } else if (status.equals("starting") || status.equals("stopping") || status.equals("processing")) {
+ // 旋转动画
+ gc.setStroke(javafx.scene.paint.Color.web("#5C6470"));
+ gc.setLineWidth(1.6);
+ gc.strokeOval(2.0, 2.0, getWidth() - 4, getHeight() - 4);
+
+ gc.setStroke(javafx.scene.paint.Color.web("#F5A623"));
+ gc.setLineWidth(2.2);
+ gc.setLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
+
+ double startAngle = -angle * Math.PI / 180;
+ double arcLength = -120 * Math.PI / 180;
+ gc.strokeArc(2.0, 2.0, getWidth() - 4, getHeight() - 4, startAngle, arcLength, javafx.scene.shape.ArcType.OPEN);
+ } else if (status.equals("ready")) {
+ // 绿色实心圆
+ gc.setFill(javafx.scene.paint.Color.web("#2ECC71"));
+ gc.fillOval(2.0, 2.0, getWidth() - 4, getHeight() - 4);
+ } else {
+ // 红色实心圆(error)
+ gc.setFill(javafx.scene.paint.Color.web("#E74C3C"));
+ gc.fillOval(2.0, 2.0, getWidth() - 4, getHeight() - 4);
+ }
+ }
+ }
+}
diff --git a/ahakeyconfig-ubuntu-java/src/main/resources/ahakey.jpg b/ahakeyconfig-ubuntu-java/src/main/resources/ahakey.jpg
new file mode 100644
index 00000000..8a0e3451
Binary files /dev/null and b/ahakeyconfig-ubuntu-java/src/main/resources/ahakey.jpg differ
diff --git a/ahakeyconfig-ubuntu-java/src/main/resources/fxml/CanvasLayout.fxml b/ahakeyconfig-ubuntu-java/src/main/resources/fxml/CanvasLayout.fxml
new file mode 100644
index 00000000..75a65389
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/resources/fxml/CanvasLayout.fxml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ahakeyconfig-ubuntu-java/src/main/resources/fxml/VoiceInputPanel.fxml b/ahakeyconfig-ubuntu-java/src/main/resources/fxml/VoiceInputPanel.fxml
new file mode 100644
index 00000000..2fea1ad8
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/resources/fxml/VoiceInputPanel.fxml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ahakeyconfig-ubuntu-java/src/main/resources/logback.xml b/ahakeyconfig-ubuntu-java/src/main/resources/logback.xml
new file mode 100644
index 00000000..d20b40a4
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/resources/logback.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ logs/ahakey-studio.log
+
+ logs/ahakey-studio.%d{yyyy-MM-dd}.log
+ 30
+ 100MB
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ahakeyconfig-ubuntu-java/src/main/resources/model_config.properties b/ahakeyconfig-ubuntu-java/src/main/resources/model_config.properties
new file mode 100644
index 00000000..2103ed78
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/resources/model_config.properties
@@ -0,0 +1 @@
+status.poll.period.seconds=5
diff --git a/ahakeyconfig-ubuntu-java/src/main/resources/style.css b/ahakeyconfig-ubuntu-java/src/main/resources/style.css
new file mode 100644
index 00000000..f0d13f68
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/src/main/resources/style.css
@@ -0,0 +1,794 @@
+.root {
+ -fx-background-color: linear-gradient(to bottom, #fafafc, #f3f4f8);
+ -fx-font-family: "Segoe UI", Roboto, "Helvetica Neue", -apple-system, BlinkMacSystemFont, sans-serif;
+}
+
+.workspace {
+ -fx-spacing: 0;
+}
+
+.title {
+ -fx-font-size: 22px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #1d1d1f;
+}
+
+.info-pill {
+ -fx-background-color: rgba(255, 255, 255, 0.92);
+ -fx-background-radius: 14px;
+ -fx-border-radius: 14px;
+ -fx-border-width: 1px;
+ -fx-effect: dropshadow(gaussian, rgba(15, 23, 42, 0.06), 12, 0.18, 0, 2);
+}
+
+.info-pill-title {
+ -fx-font-size: 10px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #7b8492;
+}
+
+.info-pill-subtitle {
+ -fx-font-size: 12px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #1d1d1f;
+}
+
+.button-prominent {
+ -fx-background-color: linear-gradient(to bottom, #1597ff, #007aff);
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 13px;
+ -fx-font-weight: 700;
+ -fx-background-radius: 10px;
+ -fx-padding: 9px 16px;
+ -fx-border-width: 0;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(0, 122, 255, 0.22), 18, 0.18, 0, 4);
+}
+
+.button-prominent:hover {
+ -fx-background-color: linear-gradient(to bottom, #0c8ff0, #0066cc);
+}
+
+/* 操作按钮样式 - 用于连接设备、BLE驱动等操作按钮 */
+.button-action {
+ -fx-background-color: rgba(255, 255, 255, 0.92);
+ -fx-text-fill: #1d1d1f;
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 1px;
+ -fx-border-color: #d8dde8;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(15, 23, 42, 0.04), 8, 0.1, 0, 2);
+}
+
+.button-action:hover {
+ -fx-background-color: rgba(255, 255, 255, 1);
+ -fx-border-color: #007aff;
+ -fx-text-fill: #007aff;
+}
+
+.button-action:pressed {
+ -fx-background-color: #f0f4f8;
+}
+
+/* 连接设备按钮 - 绿色底色 */
+.button-connect {
+ -fx-background-color: linear-gradient(to bottom, #77d992, #5fc179);
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 0;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(119, 217, 146, 0.3), 8, 0.2, 0, 2);
+}
+
+.button-connect:hover {
+ -fx-background-color: linear-gradient(to bottom, #30d158, #28a745);
+ -fx-text-fill: #ffffff;
+}
+
+.button-connect:pressed {
+ -fx-background-color: #248a3d;
+}
+/* 断开连接状态 - 淡红色底色 */
+.button-disconnect {
+ -fx-background-color: linear-gradient(to bottom, #ff6b61, #f2554d);
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 0;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(255, 107, 97, 0.3), 8, 0.2, 0, 2);
+}
+
+.button-disconnect:hover {
+ -fx-background-color: linear-gradient(to bottom, #ff453a, #d32f2f);
+ -fx-text-fill: #ffffff;
+}
+
+.button-disconnect:pressed {
+ -fx-background-color: #b71c1c;
+}
+
+/* BLE驱动按钮 - 蓝色底色 */
+.button-ble {
+ -fx-background-color: linear-gradient(to bottom, #0a84ff, #0070e0);
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 0;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(10, 132, 255, 0.35), 8, 0.2, 0, 2);
+}
+
+.button-ble:hover {
+ -fx-background-color: linear-gradient(to bottom, #409cff, #0a84ff);
+ -fx-text-fill: #ffffff;
+}
+
+.button-ble:pressed {
+ -fx-background-color: #0055b3;
+}
+
+.button-segment {
+ -fx-background-color: rgba(255, 255, 255, 0.82);
+ -fx-text-fill: #445065;
+ -fx-font-size: 12px;
+ -fx-font-weight: 700;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 1px;
+ -fx-border-color: #d8dde8;
+ -fx-padding: 9px 14px;
+ -fx-cursor: hand;
+}
+
+.button-segment:selected {
+ -fx-background-color: #0f172a;
+ -fx-text-fill: #ffffff;
+}
+
+.mode-picker {
+ -fx-padding: 4px;
+ -fx-background-color: rgba(255, 255, 255, 0.84);
+ -fx-background-radius: 14px;
+ -fx-border-color: #dfe4ee;
+ -fx-border-radius: 14px;
+ -fx-border-width: 1px;
+}
+
+.mode-toggle {
+ -fx-background-color: transparent;
+ -fx-text-fill: #5f6c7b;
+ -fx-font-size: 13px;
+ -fx-font-weight: 700;
+ -fx-background-radius: 10px;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+}
+
+.mode-toggle:selected {
+ -fx-background-color: linear-gradient(to bottom, #0f172a, #1e293b);
+ -fx-text-fill: #ffffff;
+}
+
+.mode-toggle:hover {
+ -fx-background-color: rgba(15, 23, 42, 0.06);
+}
+
+.status-item {
+ -fx-background-color: rgba(255, 255, 255, 0.92);
+ -fx-background-radius: 12px;
+ -fx-border-width: 1px;
+ -fx-border-color: #e7ebf1;
+}
+
+.status-dot {
+ -fx-min-width: 6px;
+ -fx-min-height: 6px;
+ -fx-max-width: 6px;
+ -fx-max-height: 6px;
+ -fx-background-radius: 3px;
+}
+
+.status-label {
+ -fx-font-size: 12px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #1d1d1f;
+}
+
+.status-detail {
+ -fx-font-size: 11px;
+ -fx-text-fill: #7b8492;
+}
+
+.canvas-pane {
+ -fx-padding: 16px;
+ -fx-spacing: 14px;
+}
+
+.mode-header {
+ -fx-spacing: 6px;
+}
+
+.section-title {
+ -fx-font-size: 11px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #637083;
+}
+
+.hero-title {
+ -fx-font-size: 28px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.hero-subtitle {
+ -fx-font-size: 13px;
+ -fx-text-fill: #5b6472;
+}
+
+.key-preview {
+ -fx-background-color: linear-gradient(to bottom right, rgba(255, 255, 255, 0.96), rgba(247, 249, 252, 0.96));
+ -fx-background-radius: 22px;
+ -fx-border-width: 1px;
+ -fx-border-color: #e5e9f0;
+ -fx-padding: 24px 28px;
+ -fx-effect: dropshadow(gaussian, rgba(15, 23, 42, 0.08), 24, 0.22, 0, 6);
+}
+
+.keyboard-stage {
+ -fx-background-color: radial-gradient(center 50% 20%, radius 90%, #fcfdff, #eef2f7);
+ -fx-background-radius: 18px;
+ -fx-padding: 24px;
+}
+
+.device-hotspot {
+ -fx-background-color: rgba(255, 255, 255, 0.84);
+ -fx-background-radius: 18px;
+ -fx-border-radius: 18px;
+ -fx-border-color: #e3e8ef;
+ -fx-border-width: 1px;
+ -fx-padding: 16px;
+ -fx-cursor: hand;
+}
+
+.device-hotspot:hover {
+ -fx-border-color: #74b4ff;
+ -fx-effect: dropshadow(gaussian, rgba(10, 132, 255, 0.18), 16, 0.18, 0, 3);
+}
+
+.selected-hotspot {
+ -fx-border-color: #0a84ff;
+ -fx-border-width: 2px;
+ -fx-effect: dropshadow(gaussian, rgba(10, 132, 255, 0.22), 22, 0.2, 0, 6);
+}
+
+.dirty-hotspot {
+ -fx-background-color: linear-gradient(to bottom, #fffefa, #fff7eb);
+}
+
+.pressed-hotspot {
+ -fx-background-color: rgba(10, 132, 255, 0.14);
+ -fx-border-color: #0a84ff;
+ -fx-border-width: 2px;
+}
+
+.dirty-badge {
+ -fx-text-fill: #ff9f0a;
+ -fx-font-size: 22px;
+ -fx-font-weight: 700;
+}
+
+.light-bar-card {
+ -fx-pref-width: 100%;
+ -fx-min-width: 160px;
+ -fx-max-width: 520px;
+ -fx-padding: 20px 28px;
+}
+
+.keys-container {
+ -fx-pref-width: 100%;
+ -fx-min-width: 160px;
+ -fx-max-width: 520px;
+}
+
+.light-bar-track {
+ -fx-padding: 12px 18px;
+ -fx-background-color: #d7dce5;
+ -fx-background-radius: 12px;
+}
+
+.light-segment {
+ -fx-min-width: 54px;
+ -fx-max-width: 54px;
+ -fx-min-height: 12px;
+ -fx-max-height: 12px;
+ -fx-background-radius: 6px;
+ -fx-background-color: #6b7280;
+}
+
+.hotspot-label {
+ -fx-font-size: 11px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #6b7280;
+}
+
+.hotspot-summary {
+ -fx-font-size: 12px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #1f2937;
+}
+
+.toggle-rail {
+ -fx-fill: #e7e8ef;
+}
+
+.toggle-thumb {
+ -fx-fill: linear-gradient(to bottom, #7c6cff, #5e5ce6);
+}
+
+.toggle-assembly {
+ -fx-padding: 10px;
+}
+
+.toggle-assembly-small {
+ -fx-padding: 6px;
+}
+
+.oled-screen {
+ -fx-background-color: linear-gradient(to bottom, #09090b, #1f2937);
+ -fx-background-radius: 18px;
+ -fx-min-width: 176px;
+ -fx-min-height: 128px;
+ -fx-padding: 18px;
+}
+
+.oled-screen-small {
+ -fx-background-color: linear-gradient(to bottom, #09090b, #1f2937);
+ -fx-background-radius: 14px;
+ -fx-min-width: 140px;
+ -fx-min-height: 80px;
+ -fx-max-width: 140px;
+ -fx-max-height: 80px;
+ -fx-padding: 12px;
+}
+
+.oled-preview {
+ -fx-background-color: linear-gradient(to bottom, #09090b, #1f2937);
+ -fx-background-radius: 14px;
+ -fx-min-width: 180px;
+ -fx-min-height: 100px;
+ -fx-max-width: 180px;
+ -fx-max-height: 100px;
+ -fx-padding: 10px;
+ -fx-alignment: center;
+}
+
+.oled-title {
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 18px;
+ -fx-font-weight: 700;
+}
+
+.oled-caption {
+ -fx-text-fill: #9ca3af;
+ -fx-font-size: 12px;
+}
+
+.key-glyph {
+ -fx-font-size: 26px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.key-caption {
+ -fx-font-size: 11px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #6b7280;
+}
+
+.side-card {
+ -fx-background-color: rgba(255, 255, 255, 0.9);
+ -fx-background-radius: 16px;
+ -fx-border-color: #e4e8ef;
+ -fx-border-radius: 16px;
+ -fx-padding: 14px;
+ -fx-min-width: 92px;
+}
+
+.mode-badge {
+ -fx-background-color: rgba(255, 255, 255, 0.9);
+ -fx-background-radius: 50%;
+ -fx-border-color: #e4e8ef;
+ -fx-border-radius: 50%;
+ -fx-border-width: 1px;
+ -fx-min-width: 52px;
+ -fx-min-height: 52px;
+ -fx-max-width: 52px;
+ -fx-max-height: 52px;
+}
+
+.mode-icon {
+ -fx-font-size: 20px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #445065;
+}
+
+.mini-switch {
+ -fx-min-width: 44px;
+ -fx-min-height: 24px;
+ -fx-max-width: 44px;
+ -fx-max-height: 24px;
+ -fx-background-radius: 12px;
+ -fx-background-color: linear-gradient(to right, #30d158, #67d9a5);
+}
+
+.hint-text {
+ -fx-font-size: 12px;
+ -fx-text-fill: #6b7280;
+}
+
+.manual-card {
+ -fx-background-color: rgba(255, 255, 255, 0.92);
+ -fx-background-radius: 14px;
+ -fx-border-color: #e6ebf2;
+ -fx-border-radius: 14px;
+ -fx-padding: 14px;
+}
+
+.manual-card-title {
+ -fx-font-size: 12px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.manual-card-detail {
+ -fx-font-size: 12px;
+ -fx-text-fill: #667085;
+ -fx-wrap-text: true;
+}
+
+.inspector-pane {
+ -fx-background-color: rgba(255, 255, 255, 0.9);
+ -fx-border-width: 0;
+ -fx-pref-width: 560px;
+ -fx-min-width: 500px;
+ -fx-max-width: 720px;
+}
+
+.inspector-pane > .viewport {
+ -fx-background-color: transparent;
+}
+
+.inspector-icon {
+ -fx-fill: #0a84ff;
+ -fx-font-size: 18px;
+}
+
+.inspector-title {
+ -fx-font-size: 22px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.inspector-subtitle {
+ -fx-font-size: 13px;
+ -fx-text-fill: #5f6c7b;
+}
+
+.info-banner {
+ -fx-background-color: rgba(10, 132, 255, 0.08);
+ -fx-background-radius: 10px;
+ -fx-padding: 12px;
+}
+
+.info-banner-title {
+ -fx-font-size: 13px;
+ -fx-font-weight: 700;
+}
+
+.mapping-title {
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #1d1d1f;
+}
+
+.mapping-hw {
+ -fx-font-size: 13px;
+}
+
+.preview-current-title {
+ -fx-font-size: 18px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #1d1d1f;
+}
+
+.preview-card {
+ -fx-background-color: rgba(255, 255, 255, 0.96);
+ -fx-background-radius: 12px;
+ -fx-border-color: rgba(0, 0, 0, 0.08);
+ -fx-border-radius: 12px;
+ -fx-border-width: 1px;
+ -fx-padding: 12px;
+ -fx-min-width: 130px;
+ -fx-cursor: hand;
+}
+
+.preview-card-selected {
+ -fx-background-color: rgba(0, 122, 255, 0.14);
+ -fx-border-color: #007aff;
+ -fx-text-fill: #333333;
+}
+
+.preview-card-selected .label {
+ -fx-text-fill: #333333;
+}
+
+.group-box {
+ -fx-background-color: rgba(255, 255, 255, 0.96);
+ -fx-background-radius: 16px;
+ -fx-border-width: 1px;
+ -fx-border-color: #e6ebf2;
+ -fx-padding: 16px;
+}
+
+.group-box-title {
+ -fx-font-size: 12px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #86868b;
+ -fx-padding: 0 0 12px 0;
+}
+
+.group-desc,
+.group-note,
+.warning-note,
+.usage-step {
+ -fx-wrap-text: true;
+}
+
+.group-desc {
+ -fx-font-size: 13px;
+ -fx-text-fill: #1f2937;
+}
+
+.group-note {
+ -fx-font-size: 12px;
+ -fx-text-fill: #667085;
+}
+
+.warning-note {
+ -fx-font-size: 12px;
+ -fx-text-fill: #b26b00;
+}
+
+.field-label {
+ -fx-font-size: 12px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #6b7280;
+}
+
+.key-preview-label {
+ -fx-font-size: 18px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.key-type-label {
+ -fx-font-size: 12px;
+ -fx-text-fill: #6b7280;
+}
+
+.text-field,
+.combo-box,
+.binding-toggle {
+ -fx-background-color: #f7f8fb;
+ -fx-text-fill: #1d1d1f;
+ -fx-font-size: 13px;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 1px;
+ -fx-border-color: #dfe4ec;
+ -fx-padding: 8px 12px;
+}
+
+.text-field:focused,
+.combo-box:focused,
+.binding-toggle:selected {
+ -fx-border-color: #007aff;
+ -fx-background-color: #eef6ff;
+}
+
+.combo-box-popup .list-view {
+ -fx-background-color: #ffffff;
+ -fx-border-color: #e8e8ed;
+}
+
+.combo-box-popup .list-cell {
+ -fx-background-color: #ffffff;
+ -fx-text-fill: #1d1d1f;
+ -fx-padding: 10px 12px;
+ -fx-font-size: 13px;
+}
+
+.combo-box-popup .list-cell:hover {
+ -fx-background-color: #f5f5f7;
+}
+
+.combo-box-popup .list-cell:selected {
+ -fx-background-color: #007aff;
+ -fx-text-fill: #ffffff;
+}
+
+/* 小尺寸ComboBox样式 */
+.combo-box-small {
+ -fx-background-color: #f7f8fb;
+ -fx-text-fill: #1d1d1f;
+ -fx-font-size: 12px;
+ -fx-background-radius: 8px;
+ -fx-border-radius: 8px;
+ -fx-border-width: 1px;
+ -fx-border-color: #dfe4ec;
+ -fx-padding: 4px 8px;
+ -fx-min-width: 100px;
+}
+
+.combo-box-small:focused {
+ -fx-border-color: #007aff;
+ -fx-background-color: #eef6ff;
+}
+
+.combo-box-small .arrow-button {
+ -fx-padding: 0 2px;
+ -fx-min-width: 20px;
+}
+
+.combo-box-small .arrow {
+ -fx-background-color: #6b7280;
+ -fx-shape: "M 0 0 L 3 3 L 6 0 Z";
+ -fx-scale-shape: true;
+ -fx-min-width: 4px;
+ -fx-min-height: 4px;
+ -fx-max-width: 4px;
+ -fx-max-height: 4px;
+}
+
+.status-bar {
+ -fx-background-color: rgba(248, 249, 252, 0.95);
+ -fx-border-color: #e8e8ed;
+ -fx-border-width: 1px 0 0 0;
+}
+
+/* TopBar 内部 ScrollPane 透明融合 */
+.top-bar .scroll-pane {
+ -fx-background: transparent;
+ -fx-background-color: transparent;
+ -fx-border-width: 0;
+ -fx-padding: 0;
+}
+.top-bar .scroll-pane > .viewport {
+ -fx-background-color: transparent;
+}
+.top-bar .scroll-pane > .corner {
+ -fx-background-color: transparent;
+}
+.top-bar .scroll-bar {
+ -fx-background-color: transparent;
+ -fx-pref-height: 6px;
+}
+.top-bar .scroll-bar .thumb {
+ -fx-background-color: rgba(0, 0, 0, 0.15);
+ -fx-background-radius: 3px;
+}
+
+/* 中间工作区 ScrollPane 透明融合 */
+.workspace {
+ -fx-background-color: transparent;
+}
+BorderPane > .center .scroll-pane {
+ -fx-background: transparent;
+ -fx-background-color: transparent;
+ -fx-border-width: 0;
+}
+BorderPane > .center .scroll-pane > .viewport {
+ -fx-background-color: transparent;
+}
+
+.status-bar-text {
+ -fx-font-size: 12px;
+ -fx-text-fill: #697586;
+}
+
+.menu-bar {
+ -fx-background-color: transparent;
+ -fx-padding: 0;
+}
+
+.toolbar-menu .label {
+ -fx-text-fill: #445065;
+}
+
+/* Hook ��װ�Ի�����ʽ */
+.dialog-card {
+ -fx-background-color: #2a2a2a;
+ -fx-background-radius: 8;
+ -fx-border-color: #444;
+ -fx-border-width: 1;
+ -fx-border-radius: 8;
+}
+
+.dialog-card-title {
+ -fx-font-size: 15px;
+ -fx-font-weight: bold;
+ -fx-text-fill: white;
+}
+
+.dialog-status-label {
+ -fx-text-fill: #aaa;
+}
+
+.dialog-status-installed {
+ -fx-text-fill: #4caf50;
+}
+
+.dialog-status-uninstalled {
+ -fx-text-fill: #f44336;
+}
+
+.button-install {
+ -fx-background-color: #2e7d32;
+ -fx-text-fill: white;
+ -fx-font-weight: bold;
+ -fx-min-width: 80px;
+ -fx-background-radius: 4;
+}
+
+.button-install:hover {
+ -fx-background-color: #1b5e20;
+}
+
+.button-uninstall {
+ -fx-background-color: #d32f2f;
+ -fx-text-fill: white;
+ -fx-font-weight: bold;
+ -fx-min-width: 80px;
+ -fx-background-radius: 4;
+}
+
+.button-uninstall:hover {
+ -fx-background-color: #c62828;
+}
+
+.dialog-text {
+ -fx-text-fill: white;
+ -fx-font-size: 13px;
+}
+
+.dialog-log-area {
+ -fx-background-color: #1a1a1a;
+ -fx-text-fill: #aaa;
+ -fx-font-family: "Consolas", "Monaco", monospace;
+ -fx-font-size: 12px;
+ -fx-border-color: #444;
+ -fx-border-width: 1;
+ -fx-border-radius: 8;
+ -fx-background-radius: 8;
+}
+
+.dialog-log-title {
+ -fx-font-size: 14px;
+ -fx-font-weight: bold;
+ -fx-text-fill: white;
+}
diff --git a/ahakeyconfig-ubuntu-java/target/classes/ahakey.jpg b/ahakeyconfig-ubuntu-java/target/classes/ahakey.jpg
new file mode 100644
index 00000000..8a0e3451
Binary files /dev/null and b/ahakeyconfig-ubuntu-java/target/classes/ahakey.jpg differ
diff --git a/ahakeyconfig-ubuntu-java/target/classes/fxml/CanvasLayout.fxml b/ahakeyconfig-ubuntu-java/target/classes/fxml/CanvasLayout.fxml
new file mode 100644
index 00000000..75a65389
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/target/classes/fxml/CanvasLayout.fxml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ahakeyconfig-ubuntu-java/target/classes/fxml/VoiceInputPanel.fxml b/ahakeyconfig-ubuntu-java/target/classes/fxml/VoiceInputPanel.fxml
new file mode 100644
index 00000000..2fea1ad8
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/target/classes/fxml/VoiceInputPanel.fxml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ahakeyconfig-ubuntu-java/target/classes/logback.xml b/ahakeyconfig-ubuntu-java/target/classes/logback.xml
new file mode 100644
index 00000000..d20b40a4
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/target/classes/logback.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ logs/ahakey-studio.log
+
+ logs/ahakey-studio.%d{yyyy-MM-dd}.log
+ 30
+ 100MB
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ahakeyconfig-ubuntu-java/target/classes/model_config.properties b/ahakeyconfig-ubuntu-java/target/classes/model_config.properties
new file mode 100644
index 00000000..2103ed78
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/target/classes/model_config.properties
@@ -0,0 +1 @@
+status.poll.period.seconds=5
diff --git a/ahakeyconfig-ubuntu-java/target/classes/style.css b/ahakeyconfig-ubuntu-java/target/classes/style.css
new file mode 100644
index 00000000..f0d13f68
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/target/classes/style.css
@@ -0,0 +1,794 @@
+.root {
+ -fx-background-color: linear-gradient(to bottom, #fafafc, #f3f4f8);
+ -fx-font-family: "Segoe UI", Roboto, "Helvetica Neue", -apple-system, BlinkMacSystemFont, sans-serif;
+}
+
+.workspace {
+ -fx-spacing: 0;
+}
+
+.title {
+ -fx-font-size: 22px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #1d1d1f;
+}
+
+.info-pill {
+ -fx-background-color: rgba(255, 255, 255, 0.92);
+ -fx-background-radius: 14px;
+ -fx-border-radius: 14px;
+ -fx-border-width: 1px;
+ -fx-effect: dropshadow(gaussian, rgba(15, 23, 42, 0.06), 12, 0.18, 0, 2);
+}
+
+.info-pill-title {
+ -fx-font-size: 10px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #7b8492;
+}
+
+.info-pill-subtitle {
+ -fx-font-size: 12px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #1d1d1f;
+}
+
+.button-prominent {
+ -fx-background-color: linear-gradient(to bottom, #1597ff, #007aff);
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 13px;
+ -fx-font-weight: 700;
+ -fx-background-radius: 10px;
+ -fx-padding: 9px 16px;
+ -fx-border-width: 0;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(0, 122, 255, 0.22), 18, 0.18, 0, 4);
+}
+
+.button-prominent:hover {
+ -fx-background-color: linear-gradient(to bottom, #0c8ff0, #0066cc);
+}
+
+/* 操作按钮样式 - 用于连接设备、BLE驱动等操作按钮 */
+.button-action {
+ -fx-background-color: rgba(255, 255, 255, 0.92);
+ -fx-text-fill: #1d1d1f;
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 1px;
+ -fx-border-color: #d8dde8;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(15, 23, 42, 0.04), 8, 0.1, 0, 2);
+}
+
+.button-action:hover {
+ -fx-background-color: rgba(255, 255, 255, 1);
+ -fx-border-color: #007aff;
+ -fx-text-fill: #007aff;
+}
+
+.button-action:pressed {
+ -fx-background-color: #f0f4f8;
+}
+
+/* 连接设备按钮 - 绿色底色 */
+.button-connect {
+ -fx-background-color: linear-gradient(to bottom, #77d992, #5fc179);
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 0;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(119, 217, 146, 0.3), 8, 0.2, 0, 2);
+}
+
+.button-connect:hover {
+ -fx-background-color: linear-gradient(to bottom, #30d158, #28a745);
+ -fx-text-fill: #ffffff;
+}
+
+.button-connect:pressed {
+ -fx-background-color: #248a3d;
+}
+/* 断开连接状态 - 淡红色底色 */
+.button-disconnect {
+ -fx-background-color: linear-gradient(to bottom, #ff6b61, #f2554d);
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 0;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(255, 107, 97, 0.3), 8, 0.2, 0, 2);
+}
+
+.button-disconnect:hover {
+ -fx-background-color: linear-gradient(to bottom, #ff453a, #d32f2f);
+ -fx-text-fill: #ffffff;
+}
+
+.button-disconnect:pressed {
+ -fx-background-color: #b71c1c;
+}
+
+/* BLE驱动按钮 - 蓝色底色 */
+.button-ble {
+ -fx-background-color: linear-gradient(to bottom, #0a84ff, #0070e0);
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 0;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(10, 132, 255, 0.35), 8, 0.2, 0, 2);
+}
+
+.button-ble:hover {
+ -fx-background-color: linear-gradient(to bottom, #409cff, #0a84ff);
+ -fx-text-fill: #ffffff;
+}
+
+.button-ble:pressed {
+ -fx-background-color: #0055b3;
+}
+
+.button-segment {
+ -fx-background-color: rgba(255, 255, 255, 0.82);
+ -fx-text-fill: #445065;
+ -fx-font-size: 12px;
+ -fx-font-weight: 700;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 1px;
+ -fx-border-color: #d8dde8;
+ -fx-padding: 9px 14px;
+ -fx-cursor: hand;
+}
+
+.button-segment:selected {
+ -fx-background-color: #0f172a;
+ -fx-text-fill: #ffffff;
+}
+
+.mode-picker {
+ -fx-padding: 4px;
+ -fx-background-color: rgba(255, 255, 255, 0.84);
+ -fx-background-radius: 14px;
+ -fx-border-color: #dfe4ee;
+ -fx-border-radius: 14px;
+ -fx-border-width: 1px;
+}
+
+.mode-toggle {
+ -fx-background-color: transparent;
+ -fx-text-fill: #5f6c7b;
+ -fx-font-size: 13px;
+ -fx-font-weight: 700;
+ -fx-background-radius: 10px;
+ -fx-padding: 8px 14px;
+ -fx-cursor: hand;
+}
+
+.mode-toggle:selected {
+ -fx-background-color: linear-gradient(to bottom, #0f172a, #1e293b);
+ -fx-text-fill: #ffffff;
+}
+
+.mode-toggle:hover {
+ -fx-background-color: rgba(15, 23, 42, 0.06);
+}
+
+.status-item {
+ -fx-background-color: rgba(255, 255, 255, 0.92);
+ -fx-background-radius: 12px;
+ -fx-border-width: 1px;
+ -fx-border-color: #e7ebf1;
+}
+
+.status-dot {
+ -fx-min-width: 6px;
+ -fx-min-height: 6px;
+ -fx-max-width: 6px;
+ -fx-max-height: 6px;
+ -fx-background-radius: 3px;
+}
+
+.status-label {
+ -fx-font-size: 12px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #1d1d1f;
+}
+
+.status-detail {
+ -fx-font-size: 11px;
+ -fx-text-fill: #7b8492;
+}
+
+.canvas-pane {
+ -fx-padding: 16px;
+ -fx-spacing: 14px;
+}
+
+.mode-header {
+ -fx-spacing: 6px;
+}
+
+.section-title {
+ -fx-font-size: 11px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #637083;
+}
+
+.hero-title {
+ -fx-font-size: 28px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.hero-subtitle {
+ -fx-font-size: 13px;
+ -fx-text-fill: #5b6472;
+}
+
+.key-preview {
+ -fx-background-color: linear-gradient(to bottom right, rgba(255, 255, 255, 0.96), rgba(247, 249, 252, 0.96));
+ -fx-background-radius: 22px;
+ -fx-border-width: 1px;
+ -fx-border-color: #e5e9f0;
+ -fx-padding: 24px 28px;
+ -fx-effect: dropshadow(gaussian, rgba(15, 23, 42, 0.08), 24, 0.22, 0, 6);
+}
+
+.keyboard-stage {
+ -fx-background-color: radial-gradient(center 50% 20%, radius 90%, #fcfdff, #eef2f7);
+ -fx-background-radius: 18px;
+ -fx-padding: 24px;
+}
+
+.device-hotspot {
+ -fx-background-color: rgba(255, 255, 255, 0.84);
+ -fx-background-radius: 18px;
+ -fx-border-radius: 18px;
+ -fx-border-color: #e3e8ef;
+ -fx-border-width: 1px;
+ -fx-padding: 16px;
+ -fx-cursor: hand;
+}
+
+.device-hotspot:hover {
+ -fx-border-color: #74b4ff;
+ -fx-effect: dropshadow(gaussian, rgba(10, 132, 255, 0.18), 16, 0.18, 0, 3);
+}
+
+.selected-hotspot {
+ -fx-border-color: #0a84ff;
+ -fx-border-width: 2px;
+ -fx-effect: dropshadow(gaussian, rgba(10, 132, 255, 0.22), 22, 0.2, 0, 6);
+}
+
+.dirty-hotspot {
+ -fx-background-color: linear-gradient(to bottom, #fffefa, #fff7eb);
+}
+
+.pressed-hotspot {
+ -fx-background-color: rgba(10, 132, 255, 0.14);
+ -fx-border-color: #0a84ff;
+ -fx-border-width: 2px;
+}
+
+.dirty-badge {
+ -fx-text-fill: #ff9f0a;
+ -fx-font-size: 22px;
+ -fx-font-weight: 700;
+}
+
+.light-bar-card {
+ -fx-pref-width: 100%;
+ -fx-min-width: 160px;
+ -fx-max-width: 520px;
+ -fx-padding: 20px 28px;
+}
+
+.keys-container {
+ -fx-pref-width: 100%;
+ -fx-min-width: 160px;
+ -fx-max-width: 520px;
+}
+
+.light-bar-track {
+ -fx-padding: 12px 18px;
+ -fx-background-color: #d7dce5;
+ -fx-background-radius: 12px;
+}
+
+.light-segment {
+ -fx-min-width: 54px;
+ -fx-max-width: 54px;
+ -fx-min-height: 12px;
+ -fx-max-height: 12px;
+ -fx-background-radius: 6px;
+ -fx-background-color: #6b7280;
+}
+
+.hotspot-label {
+ -fx-font-size: 11px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #6b7280;
+}
+
+.hotspot-summary {
+ -fx-font-size: 12px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #1f2937;
+}
+
+.toggle-rail {
+ -fx-fill: #e7e8ef;
+}
+
+.toggle-thumb {
+ -fx-fill: linear-gradient(to bottom, #7c6cff, #5e5ce6);
+}
+
+.toggle-assembly {
+ -fx-padding: 10px;
+}
+
+.toggle-assembly-small {
+ -fx-padding: 6px;
+}
+
+.oled-screen {
+ -fx-background-color: linear-gradient(to bottom, #09090b, #1f2937);
+ -fx-background-radius: 18px;
+ -fx-min-width: 176px;
+ -fx-min-height: 128px;
+ -fx-padding: 18px;
+}
+
+.oled-screen-small {
+ -fx-background-color: linear-gradient(to bottom, #09090b, #1f2937);
+ -fx-background-radius: 14px;
+ -fx-min-width: 140px;
+ -fx-min-height: 80px;
+ -fx-max-width: 140px;
+ -fx-max-height: 80px;
+ -fx-padding: 12px;
+}
+
+.oled-preview {
+ -fx-background-color: linear-gradient(to bottom, #09090b, #1f2937);
+ -fx-background-radius: 14px;
+ -fx-min-width: 180px;
+ -fx-min-height: 100px;
+ -fx-max-width: 180px;
+ -fx-max-height: 100px;
+ -fx-padding: 10px;
+ -fx-alignment: center;
+}
+
+.oled-title {
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 18px;
+ -fx-font-weight: 700;
+}
+
+.oled-caption {
+ -fx-text-fill: #9ca3af;
+ -fx-font-size: 12px;
+}
+
+.key-glyph {
+ -fx-font-size: 26px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.key-caption {
+ -fx-font-size: 11px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #6b7280;
+}
+
+.side-card {
+ -fx-background-color: rgba(255, 255, 255, 0.9);
+ -fx-background-radius: 16px;
+ -fx-border-color: #e4e8ef;
+ -fx-border-radius: 16px;
+ -fx-padding: 14px;
+ -fx-min-width: 92px;
+}
+
+.mode-badge {
+ -fx-background-color: rgba(255, 255, 255, 0.9);
+ -fx-background-radius: 50%;
+ -fx-border-color: #e4e8ef;
+ -fx-border-radius: 50%;
+ -fx-border-width: 1px;
+ -fx-min-width: 52px;
+ -fx-min-height: 52px;
+ -fx-max-width: 52px;
+ -fx-max-height: 52px;
+}
+
+.mode-icon {
+ -fx-font-size: 20px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #445065;
+}
+
+.mini-switch {
+ -fx-min-width: 44px;
+ -fx-min-height: 24px;
+ -fx-max-width: 44px;
+ -fx-max-height: 24px;
+ -fx-background-radius: 12px;
+ -fx-background-color: linear-gradient(to right, #30d158, #67d9a5);
+}
+
+.hint-text {
+ -fx-font-size: 12px;
+ -fx-text-fill: #6b7280;
+}
+
+.manual-card {
+ -fx-background-color: rgba(255, 255, 255, 0.92);
+ -fx-background-radius: 14px;
+ -fx-border-color: #e6ebf2;
+ -fx-border-radius: 14px;
+ -fx-padding: 14px;
+}
+
+.manual-card-title {
+ -fx-font-size: 12px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.manual-card-detail {
+ -fx-font-size: 12px;
+ -fx-text-fill: #667085;
+ -fx-wrap-text: true;
+}
+
+.inspector-pane {
+ -fx-background-color: rgba(255, 255, 255, 0.9);
+ -fx-border-width: 0;
+ -fx-pref-width: 560px;
+ -fx-min-width: 500px;
+ -fx-max-width: 720px;
+}
+
+.inspector-pane > .viewport {
+ -fx-background-color: transparent;
+}
+
+.inspector-icon {
+ -fx-fill: #0a84ff;
+ -fx-font-size: 18px;
+}
+
+.inspector-title {
+ -fx-font-size: 22px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.inspector-subtitle {
+ -fx-font-size: 13px;
+ -fx-text-fill: #5f6c7b;
+}
+
+.info-banner {
+ -fx-background-color: rgba(10, 132, 255, 0.08);
+ -fx-background-radius: 10px;
+ -fx-padding: 12px;
+}
+
+.info-banner-title {
+ -fx-font-size: 13px;
+ -fx-font-weight: 700;
+}
+
+.mapping-title {
+ -fx-font-size: 13px;
+ -fx-font-weight: 600;
+ -fx-text-fill: #1d1d1f;
+}
+
+.mapping-hw {
+ -fx-font-size: 13px;
+}
+
+.preview-current-title {
+ -fx-font-size: 18px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #1d1d1f;
+}
+
+.preview-card {
+ -fx-background-color: rgba(255, 255, 255, 0.96);
+ -fx-background-radius: 12px;
+ -fx-border-color: rgba(0, 0, 0, 0.08);
+ -fx-border-radius: 12px;
+ -fx-border-width: 1px;
+ -fx-padding: 12px;
+ -fx-min-width: 130px;
+ -fx-cursor: hand;
+}
+
+.preview-card-selected {
+ -fx-background-color: rgba(0, 122, 255, 0.14);
+ -fx-border-color: #007aff;
+ -fx-text-fill: #333333;
+}
+
+.preview-card-selected .label {
+ -fx-text-fill: #333333;
+}
+
+.group-box {
+ -fx-background-color: rgba(255, 255, 255, 0.96);
+ -fx-background-radius: 16px;
+ -fx-border-width: 1px;
+ -fx-border-color: #e6ebf2;
+ -fx-padding: 16px;
+}
+
+.group-box-title {
+ -fx-font-size: 12px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #86868b;
+ -fx-padding: 0 0 12px 0;
+}
+
+.group-desc,
+.group-note,
+.warning-note,
+.usage-step {
+ -fx-wrap-text: true;
+}
+
+.group-desc {
+ -fx-font-size: 13px;
+ -fx-text-fill: #1f2937;
+}
+
+.group-note {
+ -fx-font-size: 12px;
+ -fx-text-fill: #667085;
+}
+
+.warning-note {
+ -fx-font-size: 12px;
+ -fx-text-fill: #b26b00;
+}
+
+.field-label {
+ -fx-font-size: 12px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #6b7280;
+}
+
+.key-preview-label {
+ -fx-font-size: 18px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #111827;
+}
+
+.key-type-label {
+ -fx-font-size: 12px;
+ -fx-text-fill: #6b7280;
+}
+
+.text-field,
+.combo-box,
+.binding-toggle {
+ -fx-background-color: #f7f8fb;
+ -fx-text-fill: #1d1d1f;
+ -fx-font-size: 13px;
+ -fx-background-radius: 10px;
+ -fx-border-radius: 10px;
+ -fx-border-width: 1px;
+ -fx-border-color: #dfe4ec;
+ -fx-padding: 8px 12px;
+}
+
+.text-field:focused,
+.combo-box:focused,
+.binding-toggle:selected {
+ -fx-border-color: #007aff;
+ -fx-background-color: #eef6ff;
+}
+
+.combo-box-popup .list-view {
+ -fx-background-color: #ffffff;
+ -fx-border-color: #e8e8ed;
+}
+
+.combo-box-popup .list-cell {
+ -fx-background-color: #ffffff;
+ -fx-text-fill: #1d1d1f;
+ -fx-padding: 10px 12px;
+ -fx-font-size: 13px;
+}
+
+.combo-box-popup .list-cell:hover {
+ -fx-background-color: #f5f5f7;
+}
+
+.combo-box-popup .list-cell:selected {
+ -fx-background-color: #007aff;
+ -fx-text-fill: #ffffff;
+}
+
+/* 小尺寸ComboBox样式 */
+.combo-box-small {
+ -fx-background-color: #f7f8fb;
+ -fx-text-fill: #1d1d1f;
+ -fx-font-size: 12px;
+ -fx-background-radius: 8px;
+ -fx-border-radius: 8px;
+ -fx-border-width: 1px;
+ -fx-border-color: #dfe4ec;
+ -fx-padding: 4px 8px;
+ -fx-min-width: 100px;
+}
+
+.combo-box-small:focused {
+ -fx-border-color: #007aff;
+ -fx-background-color: #eef6ff;
+}
+
+.combo-box-small .arrow-button {
+ -fx-padding: 0 2px;
+ -fx-min-width: 20px;
+}
+
+.combo-box-small .arrow {
+ -fx-background-color: #6b7280;
+ -fx-shape: "M 0 0 L 3 3 L 6 0 Z";
+ -fx-scale-shape: true;
+ -fx-min-width: 4px;
+ -fx-min-height: 4px;
+ -fx-max-width: 4px;
+ -fx-max-height: 4px;
+}
+
+.status-bar {
+ -fx-background-color: rgba(248, 249, 252, 0.95);
+ -fx-border-color: #e8e8ed;
+ -fx-border-width: 1px 0 0 0;
+}
+
+/* TopBar 内部 ScrollPane 透明融合 */
+.top-bar .scroll-pane {
+ -fx-background: transparent;
+ -fx-background-color: transparent;
+ -fx-border-width: 0;
+ -fx-padding: 0;
+}
+.top-bar .scroll-pane > .viewport {
+ -fx-background-color: transparent;
+}
+.top-bar .scroll-pane > .corner {
+ -fx-background-color: transparent;
+}
+.top-bar .scroll-bar {
+ -fx-background-color: transparent;
+ -fx-pref-height: 6px;
+}
+.top-bar .scroll-bar .thumb {
+ -fx-background-color: rgba(0, 0, 0, 0.15);
+ -fx-background-radius: 3px;
+}
+
+/* 中间工作区 ScrollPane 透明融合 */
+.workspace {
+ -fx-background-color: transparent;
+}
+BorderPane > .center .scroll-pane {
+ -fx-background: transparent;
+ -fx-background-color: transparent;
+ -fx-border-width: 0;
+}
+BorderPane > .center .scroll-pane > .viewport {
+ -fx-background-color: transparent;
+}
+
+.status-bar-text {
+ -fx-font-size: 12px;
+ -fx-text-fill: #697586;
+}
+
+.menu-bar {
+ -fx-background-color: transparent;
+ -fx-padding: 0;
+}
+
+.toolbar-menu .label {
+ -fx-text-fill: #445065;
+}
+
+/* Hook ��װ�Ի�����ʽ */
+.dialog-card {
+ -fx-background-color: #2a2a2a;
+ -fx-background-radius: 8;
+ -fx-border-color: #444;
+ -fx-border-width: 1;
+ -fx-border-radius: 8;
+}
+
+.dialog-card-title {
+ -fx-font-size: 15px;
+ -fx-font-weight: bold;
+ -fx-text-fill: white;
+}
+
+.dialog-status-label {
+ -fx-text-fill: #aaa;
+}
+
+.dialog-status-installed {
+ -fx-text-fill: #4caf50;
+}
+
+.dialog-status-uninstalled {
+ -fx-text-fill: #f44336;
+}
+
+.button-install {
+ -fx-background-color: #2e7d32;
+ -fx-text-fill: white;
+ -fx-font-weight: bold;
+ -fx-min-width: 80px;
+ -fx-background-radius: 4;
+}
+
+.button-install:hover {
+ -fx-background-color: #1b5e20;
+}
+
+.button-uninstall {
+ -fx-background-color: #d32f2f;
+ -fx-text-fill: white;
+ -fx-font-weight: bold;
+ -fx-min-width: 80px;
+ -fx-background-radius: 4;
+}
+
+.button-uninstall:hover {
+ -fx-background-color: #c62828;
+}
+
+.dialog-text {
+ -fx-text-fill: white;
+ -fx-font-size: 13px;
+}
+
+.dialog-log-area {
+ -fx-background-color: #1a1a1a;
+ -fx-text-fill: #aaa;
+ -fx-font-family: "Consolas", "Monaco", monospace;
+ -fx-font-size: 12px;
+ -fx-border-color: #444;
+ -fx-border-width: 1;
+ -fx-border-radius: 8;
+ -fx-background-radius: 8;
+}
+
+.dialog-log-title {
+ -fx-font-size: 14px;
+ -fx-font-weight: bold;
+ -fx-text-fill: white;
+}
diff --git a/ahakeyconfig-ubuntu-java/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/ahakeyconfig-ubuntu-java/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 00000000..a819616b
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,84 @@
+com\example\ahakey\util\Icons.class
+com\example\ahakey\service\AgentManager.class
+com\example\ahakey\service\VoiceInputManager$VoiceStatus.class
+com\example\ahakey\view\TopBar.class
+com\example\ahakey\view\CanvasController$1.class
+com\example\ahakey\view\FloatingVoiceNotification.class
+com\example\ahakey\view\CanvasPane.class
+com\example\ahakey\Main.class
+com\example\ahakey\model\StudioState$PersistedDraft.class
+com\example\ahakey\service\KimiAhaKeyBridge.class
+com\example\ahakey\model\DeviceStatus.class
+com\example\ahakey\service\OledUploadService$UploadPlan.class
+com\example\ahakey\platform\VoiceRelayService.class
+com\example\ahakey\protocol\BleTcpPacket$ParsedPacket.class
+com\example\ahakey\config\LinuxVoiceConfig.class
+com\example\ahakey\model\LightBarPreviewState$1.class
+com\example\ahakey\protocol\AhaKeyProtocol.class
+com\example\ahakey\model\KeyConfig$1.class
+com\example\ahakey\service\VoiceInputManager$Consumer.class
+com\example\ahakey\service\SpeechService$Consumer.class
+com\example\ahakey\model\KeyConfig.class
+com\example\ahakey\model\StudioState$PersistedDraft$ModeDraft.class
+com\example\ahakey\app\StudioController.class
+com\example\ahakey\view\FloatingVoiceNotification$AnimationTimer.class
+com\example\ahakey\view\CanvasController.class
+com\example\ahakey\model\LightEffectStyle$1.class
+com\example\ahakey\view\StatusBar.class
+com\example\ahakey\service\AgentManager$BluetoothOwner.class
+com\example\ahakey\service\HookDispatchServer$ApprovalCallback.class
+com\example\ahakey\util\ConfigStore.class
+com\example\ahakey\model\StudioState.class
+com\example\ahakey\service\DeviceSyncService.class
+com\example\ahakey\view\FloatingVoiceNotification$StatusConfig.class
+com\example\ahakey\view\FloatingVoiceNotification$1.class
+com\example\ahakey\protocol\AhaKeyResponseParser$PictureState.class
+com\example\ahakey\model\LightBarPreviewState.class
+com\example\ahakey\view\InspectorPane.class
+com\example\ahakey\App.class
+com\example\ahakey\config\LinuxVoiceConfig$InputMethodType.class
+com\example\ahakey\model\IDEState.class
+com\example\ahakey\model\LightEffectStyle.class
+com\example\ahakey\service\HookDispatchServer$Platform.class
+com\example\ahakey\app\VoiceInputController.class
+com\example\ahakey\service\BleManager$BleCallback.class
+com\example\ahakey\service\SocketServer.class
+com\example\ahakey\config\ModelConfig.class
+com\example\ahakey\service\HookInstaller.class
+com\example\ahakey\util\ConfigStore$1.class
+com\example\ahakey\model\ModeSlot.class
+com\example\ahakey\service\KeyboardInjector.class
+com\example\ahakey\service\BleManager.class
+com\example\ahakey\service\SpeechService.class
+com\example\ahakey\model\MacroStep.class
+com\example\ahakey\view\InfoPill.class
+com\example\ahakey\model\StudioState$1.class
+com\example\ahakey\service\DeviceSyncService$LabeledCommand.class
+com\example\ahakey\service\OledUploadService.class
+com\example\ahakey\platform\linux\LinuxVoiceRelayService.class
+com\example\ahakey\view\TopBar$VoiceStatusLamp.class
+com\example\ahakey\model\HIDUsage.class
+com\example\ahakey\model\StudioPart.class
+com\example\ahakey\service\VoiceInputManager.class
+com\example\ahakey\model\MacroAction.class
+com\example\ahakey\service\HookDispatchServer.class
+com\example\ahakey\view\TopBar$VoiceStatusLamp$1.class
+com\example\ahakey\service\HookDispatchServer$EventEntry.class
+com\example\ahakey\protocol\AhaKeyResponseParser.class
+com\example\ahakey\service\OledUploadService$UploadProgress.class
+com\example\ahakey\util\OLEDFrameEncoder$EncodedFrame.class
+com\example\ahakey\model\OledModeDraft.class
+com\example\ahakey\model\VoicePreset$1.class
+com\example\ahakey\service\HookDispatchServer$1.class
+com\example\ahakey\service\HookClient.class
+com\example\ahakey\protocol\BleTcpPacket.class
+com\example\ahakey\service\UsbHidTransport.class
+com\example\ahakey\model\VoicePreset.class
+com\example\ahakey\util\OLEDFrameEncoder.class
+com\example\ahakey\platform\VoiceRelayPlatform.class
+com\example\ahakey\util\StudioStore.class
+com\example\ahakey\service\KimiConfigLeverSync.class
+com\example\ahakey\app\StudioController$1.class
+com\example\ahakey\protocol\AhaKeyResponseParser$CommandResponse.class
+com\example\ahakey\view\InspectorPane$1.class
+com\example\ahakey\view\AccentColor.class
diff --git a/ahakeyconfig-ubuntu-java/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/ahakeyconfig-ubuntu-java/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 00000000..c191f94a
--- /dev/null
+++ b/ahakeyconfig-ubuntu-java/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,49 @@
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\LightEffectStyle.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\OledUploadService.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\view\CanvasController.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\view\InspectorPane.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\protocol\AhaKeyResponseParser.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\VoicePreset.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\platform\linux\LinuxVoiceRelayService.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\util\Icons.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\HookDispatchServer.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\protocol\AhaKeyProtocol.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\view\TopBar.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\protocol\BleTcpPacket.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\HookInstaller.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\DeviceStatus.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\HIDUsage.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\platform\VoiceRelayPlatform.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\platform\VoiceRelayService.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\util\StudioStore.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\config\LinuxVoiceConfig.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\UsbHidTransport.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\app\StudioController.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\AgentManager.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\config\ModelConfig.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\KimiConfigLeverSync.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\IDEState.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\KimiAhaKeyBridge.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\ModeSlot.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\LightBarPreviewState.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\KeyboardInjector.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\view\CanvasPane.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\HookClient.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\SocketServer.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\VoiceInputManager.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\app\VoiceInputController.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\view\FloatingVoiceNotification.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\Main.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\KeyConfig.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\util\OLEDFrameEncoder.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\App.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\StudioState.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\StudioPart.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\view\StatusBar.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\OledModeDraft.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\view\InfoPill.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\DeviceSyncService.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\model\MacroStep.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\BleManager.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\service\SpeechService.java
+D:\idea-yins\desktop\ahakeyconfig-ubuntu-java\src\main\java\com\example\ahakey\util\ConfigStore.java
diff --git a/ahakeyconfig-win-java/VibeCodeKeyboard.ico b/ahakeyconfig-win-java/VibeCodeKeyboard.ico
new file mode 100644
index 00000000..1469713c
Binary files /dev/null and b/ahakeyconfig-win-java/VibeCodeKeyboard.ico differ
diff --git a/ahakeyconfig-win-java/VibeCodeKeyboard_preview.png b/ahakeyconfig-win-java/VibeCodeKeyboard_preview.png
new file mode 100644
index 00000000..ff0a1820
Binary files /dev/null and b/ahakeyconfig-win-java/VibeCodeKeyboard_preview.png differ
diff --git a/ahakeyconfig-win-java/ahakey.jpg b/ahakeyconfig-win-java/ahakey.jpg
new file mode 100644
index 00000000..8a0e3451
Binary files /dev/null and b/ahakeyconfig-win-java/ahakey.jpg differ
diff --git a/ahakeyconfig-win-java/build-exe.bat b/ahakeyconfig-win-java/build-exe.bat
new file mode 100644
index 00000000..7498bf59
--- /dev/null
+++ b/ahakeyconfig-win-java/build-exe.bat
@@ -0,0 +1,169 @@
+@echo off
+setlocal enabledelayedexpansion
+
+:: ============================================
+:: AhaKey Studio 打包脚本
+:: 依赖:JDK 17+, Maven 3.6+
+:: ============================================
+
+set "PROJECT_NAME=AhaKeyStudio"
+set "VERSION=1.0.0"
+set "MAIN_CLASS=com.example.ahakey.App"
+set "TARGET_DIR=target"
+set "INSTALLER_DIR=%TARGET_DIR%\installer"
+
+echo ============================================
+echo AhaKey Studio 打包脚本
+echo ============================================
+
+:: 检查 Java
+echo.
+echo [1/5] 检查 Java 环境...
+java -version >nul 2>&1
+if errorlevel 1 (
+ echo ERROR: Java 未安装或未配置环境变量
+ pause
+ exit /b 1
+)
+
+for /f "tokens=3" %%g in ('java -version 2^>&1 ^| findstr /i "version"') do (
+ set "JAVA_VERSION=%%g"
+)
+set "JAVA_VERSION=%JAVA_VERSION:"=%"
+echo OK: Java 版本 %JAVA_VERSION%
+
+:: 检查 Maven
+echo.
+echo [2/5] 检查 Maven 环境...
+mvn -v >nul 2>&1
+if errorlevel 1 (
+ echo ERROR: Maven 未安装或未配置环境变量
+ pause
+ exit /b 1
+)
+echo OK: Maven 已安装
+
+:: 编译项目
+echo.
+echo [3/5] 编译项目...
+mvn clean package -DskipTests
+if errorlevel 1 (
+ echo ERROR: Maven 构建失败
+ pause
+ exit /b 1
+)
+echo OK: 编译成功
+
+:: 检查 model.enabled 配置
+set "MODEL_ENABLED=false"
+for /f "tokens=1,* delims==" %%a in ('type src\main\resources\model_config.properties 2^>nul ^| findstr /i "model.enabled"') do (
+ set "VAL=%%b"
+ set "VAL=!VAL: =!"
+ if /i "!VAL!"=="true" set "MODEL_ENABLED=true"
+)
+echo.
+if /i "%MODEL_ENABLED%"=="true" (
+ echo [INFO] model.enabled=true: 保留模型文件和 ONNX 运行时
+) else (
+ echo [INFO] model.enabled=false: 移除模型文件和 ONNX 运行时
+ :: 删除 onnxruntime 依赖
+ del /q "%TARGET_DIR%\lib\onnxruntime*.jar" 2>nul
+ :: 从 JAR 中删除模型文件
+ echo 正在从 JAR 中移除模型文件...
+ pushd "%TARGET_DIR%"
+ powershell -NoProfile -Command "$z=[System.IO.Compression.ZipFile]::Open('ahakey-studio-%VERSION%.jar','Update'); $z.Entries | Where-Object {$_.FullName -like 'models/*'} | ForEach-Object {$_.Delete()}; $z.Dispose()"
+ popd
+ echo OK: 已移除 onnxruntime 和模型文件 (~233MB)
+)
+
+:: 创建 Runtime Image
+echo.
+echo [4/5] 创建 Java Runtime Image...
+set "JAR_PATH=%TARGET_DIR%\ahakey-studio-%VERSION%.jar"
+set "LIB_DIR=%TARGET_DIR%\lib"
+set "JLINK_DIR=%TARGET_DIR%\jlink-image"
+
+:: 删除旧的 jlink 镜像
+rd /s /q "%JLINK_DIR%" 2>nul
+
+:: 使用 jdeps 分析依赖模块
+echo 分析模块依赖...
+for /f "skip=1" %%i in ('jdeps --list-deps "%JAR_PATH%"') do (
+ set "MODULES=!MODULES!%%i,"
+)
+set "MODULES=%MODULES:~0,-1%"
+echo 依赖模块: %MODULES%
+
+:: 创建运行时镜像
+jlink --module-path "%JAVA_HOME%\jmods" ^
+ --add-modules %MODULES% ^
+ --output "%JLINK_DIR%" ^
+ --strip-debug ^
+ --no-man-pages ^
+ --no-header-files ^
+ --compress=2
+
+if errorlevel 1 (
+ echo ERROR: jlink 失败
+ pause
+ exit /b 1
+)
+echo OK: Runtime Image 创建成功
+
+:: 生成 EXE 安装程序
+echo.
+echo [5/5] 生成 EXE 安装程序...
+rd /s /q "%INSTALLER_DIR%" 2>nul
+mkdir "%INSTALLER_DIR%"
+
+:: 准备 BLE 驱动文件
+set "BLE_PROJECT_DIR=..\ahakeyconfig-win\BLE_tcp_bridge_for_vibe_code-master (1)\BLE_tcp_bridge_for_vibe_code-master"
+set "BLE_EXE_SOURCE=%BLE_PROJECT_DIR%\dist\BLE_tcp_driver.exe"
+set "BLE_STAGING=%TARGET_DIR%\ble_staging"
+set "BLE_EXTRA_ARG="
+
+if exist "%BLE_EXE_SOURCE%" (
+ echo [INFO] 找到 BLE 驱动,准备打包...
+ mkdir "%BLE_STAGING%" 2>nul
+ copy /y "%BLE_EXE_SOURCE%" "%BLE_STAGING%\BLE_tcp_driver.exe" >nul
+ set "BLE_EXTRA_ARG=--app-content "%BLE_STAGING%\BLE_tcp_driver.exe""
+ echo OK: BLE 驱动已准备
+) else (
+ echo [WARNING] BLE 驱动未找到: %BLE_EXE_SOURCE%
+ echo [WARNING] 请先在 BLE 项目中运行 build-single-exe.bat
+)
+
+jpackage --type exe ^
+ --name %PROJECT_NAME% ^
+ --app-version %VERSION% ^
+ --vendor "AhaKey" ^
+ --description "AhaKey Studio - 智能键盘配置工具" ^
+ --copyright "2024 AhaKey. All rights reserved." ^
+ --input "%TARGET_DIR%" ^
+ --main-jar ahakey-studio-%VERSION%.jar ^
+ --main-class %MAIN_CLASS% ^
+ --runtime-image "%JLINK_DIR%" ^
+ --dest "%INSTALLER_DIR%" ^
+ --win-console false ^
+ --win-dir-chooser ^
+ --win-shortcut ^
+ --win-menu ^
+ --win-menu-group "AhaKey" ^
+ --java-options "--add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED" ^
+ --java-options "--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED" ^
+ --java-options "--add-opens=javafx.fxml/com.sun.javafx.fxml=ALL-UNNAMED" ^
+ %BLE_EXTRA_ARG%
+
+if errorlevel 1 (
+ echo ERROR: jpackage 失败
+ pause
+ exit /b 1
+)
+
+echo.
+echo ============================================
+echo 打包完成!
+echo ============================================
+echo 安装包位置: %INSTALLER_DIR%\%PROJECT_NAME%-%VERSION%.exe
+echo.
+pause
diff --git a/ahakeyconfig-win-java/build-exe.ps1 b/ahakeyconfig-win-java/build-exe.ps1
new file mode 100644
index 00000000..a58b7864
--- /dev/null
+++ b/ahakeyconfig-win-java/build-exe.ps1
@@ -0,0 +1,199 @@
+<#
+AhaKey Studio Build Script - Package JavaFX app to Windows App Image
+Requires: JDK 17+, Maven 3.6+
+
+Usage: .\build-exe.ps1
+#>
+
+$ErrorActionPreference = "Continue"
+
+$ProjectName = "AhaKeyStudio"
+$Version = "1.0.0"
+$MainClass = "com.example.ahakey.App"
+$TargetDir = Join-Path $PSScriptRoot "target"
+$InstallerDir = "$TargetDir/installer"
+$TempDir = "$TargetDir/jpackage-input"
+$RuntimeDir = "$TargetDir/runtime"
+$ResourceDir = "$TargetDir/jpackage-resources"
+$IconPath = Join-Path $PSScriptRoot "VibeCodeKeyboard.ico"
+
+function Write-Status($Message, $Color) {
+ Write-Host "[$(Get-Date -Format HH:mm:ss)] " -NoNewline
+ Write-Host $Message -ForegroundColor $Color
+}
+
+Write-Status "AhaKey Studio Build Script v1.0" Cyan
+Write-Status "==============================" Cyan
+
+# Build project (must run from script directory so Maven finds pom.xml)
+Set-Location $PSScriptRoot
+Write-Status "Building project..." Cyan
+& mvn "-Dmaven.repo.local=.m2repo" package -DskipTests
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Status "ERROR: Maven build failed" Red
+ exit 1
+}
+Write-Status "Maven build successful" Green
+
+# Clean old installer
+if (Test-Path $InstallerDir) {
+ Write-Status "Removing old installer..." Yellow
+ # Try to kill any running processes that might be locking the directory
+ try {
+ Get-Process -Name "AhaKeyStudio" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
+ Start-Sleep -Milliseconds 500
+ } catch {
+ # Ignore errors
+ }
+ # Use robocopy to delete directory (more reliable)
+ $null = New-Item -ItemType Directory -Path "$TargetDir/empty_dir" -Force -ErrorAction SilentlyContinue
+ robocopy "$TargetDir/empty_dir" $InstallerDir /MIR /NFL /NDL /NJH /NJS | Out-Null
+ Remove-Item -Path "$TargetDir/empty_dir" -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item -Path $InstallerDir -Recurse -Force -ErrorAction SilentlyContinue
+}
+
+# Create clean temporary input directory
+Write-Status "Preparing clean input directory..." Cyan
+if (Test-Path $TempDir) {
+ Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue
+}
+New-Item -ItemType Directory -Path "$TempDir/lib" | Out-Null
+if (Test-Path $ResourceDir) {
+ Remove-Item -Path $ResourceDir -Recurse -Force -ErrorAction SilentlyContinue
+}
+New-Item -ItemType Directory -Path $ResourceDir | Out-Null
+Copy-Item -Path $IconPath -Destination "$ResourceDir/$ProjectName.ico" -Force
+
+$jarPath = "$TargetDir/ahakey-studio-$Version.jar"
+
+# Check if local model is enabled
+$modelEnabled = $false
+$propsFile = Join-Path $PSScriptRoot "src/main/resources/model_config.properties"
+if (Test-Path $propsFile) {
+ $match = Select-String -Path $propsFile -Pattern '^\s*model\.enabled\s*=\s*(.+)$'
+ if ($match) {
+ $modelEnabled = $match.Matches[0].Groups[1].Value.Trim() -eq 'true'
+ }
+}
+
+if ($modelEnabled) {
+ Write-Status "model.enabled=true: Including model files and ONNX runtime" Cyan
+} else {
+ Write-Status "model.enabled=false: EXCLUDING model files and ONNX runtime" Yellow
+}
+
+# Copy only required files
+Copy-Item -Path $jarPath -Destination $TempDir
+Copy-Item -Path "$TargetDir/lib/*.jar" -Destination "$TempDir/lib"
+
+if ($modelEnabled) {
+ # Copy SenseVoice model files
+ Write-Status "Copying SenseVoice model files..." Cyan
+ New-Item -ItemType Directory -Path "$TempDir/models" | Out-Null
+ Copy-Item -Path (Join-Path $PSScriptRoot "src/main/resources/models/model_q8.onnx") -Destination "$TempDir/models" -Force
+ Copy-Item -Path (Join-Path $PSScriptRoot "src/main/resources/models/tokens.txt") -Destination "$TempDir/models" -Force
+ Write-Status "Model files copied successfully" Green
+} else {
+ # Remove onnxruntime dependency from lib
+ Write-Status "Removing onnxruntime from lib..." Yellow
+ Remove-Item -Path "$TempDir/lib/onnxruntime*.jar" -Force -ErrorAction SilentlyContinue
+ # Remove model files from JAR to reduce package size
+ Write-Status "Removing model files from JAR..." Yellow
+ $jarName = Split-Path $jarPath -Leaf
+ $zipPath = "$TempDir/$jarName"
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::Open($zipPath, 'Update')
+ $entries = $zip.Entries | Where-Object { $_.FullName -like 'models/*' }
+ foreach ($entry in $entries) { $entry.Delete() }
+ $zip.Dispose()
+ Write-Status "onnxruntime + model files removed from package (saved ~233MB)" Green
+}
+
+Write-Status "Input directory ready" Green
+
+# Create custom runtime using jlink
+Write-Status "Creating custom runtime using jlink..." Cyan
+if (Test-Path $RuntimeDir) {
+ Remove-Item -Path $RuntimeDir -Recurse -Force -ErrorAction SilentlyContinue
+}
+
+# Detect JDK version for --compress flag (JDK 21+ uses zip-6, JDK 17 uses 2)
+$javaVersion = (& java -version 2>&1 | Select-String 'version "(\d+)' | ForEach-Object { $_.Matches[0].Groups[1].Value })
+$compressArg = if ([int]$javaVersion -ge 21) { "zip-6" } else { "2" }
+Write-Status "JDK $javaVersion detected, using --compress=$compressArg" Cyan
+
+$jlinkArgs = @(
+ "--module-path", "$TargetDir/lib",
+ "--add-modules", "javafx.controls,javafx.fxml,javafx.graphics,java.base,java.logging,java.desktop,java.net.http,java.sql,java.naming,java.xml",
+ "--output", $RuntimeDir,
+ "--strip-debug",
+ "--no-header-files",
+ "--no-man-pages",
+ "--compress", $compressArg
+)
+
+& jlink @jlinkArgs
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Status "ERROR: jlink failed" Red
+ exit 1
+}
+Write-Status "Custom runtime created successfully" Green
+
+# Create app-image using jpackage
+Write-Status "Creating App Image..." Cyan
+
+$jpackageArgs = @(
+ "--type", "app-image",
+ "--name", $ProjectName,
+ "--app-version", $Version,
+ "--vendor", "AhaKey",
+ "--description", "AhaKey Studio - Keyboard Configuration Tool",
+ "--copyright", "2024 AhaKey",
+ "--icon", $IconPath,
+ "--resource-dir", $ResourceDir,
+ "--input", $TempDir,
+ "--main-jar", (Split-Path $jarPath -Leaf),
+ "--main-class", $MainClass,
+ "--dest", $InstallerDir,
+ "--runtime-image", $RuntimeDir,
+ "--java-options", "--add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED",
+ "--java-options", "--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED",
+ "--java-options", "--add-opens=javafx.fxml/com.sun.javafx.fxml=ALL-UNNAMED",
+ "--verbose"
+)
+
+& jpackage @jpackageArgs
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Status "ERROR: jpackage failed" Red
+ exit 1
+}
+
+Write-Status "App Image created successfully!" Green
+Write-Status "Output location: $InstallerDir\$ProjectName" Cyan
+
+# Copy BLE TCP bridge driver
+$bleCandidates = @(
+ ".\BLE_tcp_driver.exe",
+ "..\BLE_tcp_driver.exe",
+ "..\ahakeyconfig-win\BLE_tcp_bridge_for_vibe_code-master (1)\BLE_tcp_bridge_for_vibe_code-master\dist\BLE_tcp_driver.exe"
+)
+$bleExeSource = $bleCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
+
+if ($bleExeSource) {
+ Write-Status "Copying BLE TCP driver..." Cyan
+ Copy-Item -Path $bleExeSource -Destination "$InstallerDir\$ProjectName\BLE_tcp_driver.exe" -Force
+ Write-Status "BLE driver copied to app image" Green
+} else {
+ Write-Status "WARNING: BLE driver not found" Yellow
+ Write-Status "Run build-single-exe.bat in the BLE project first, or copy BLE_tcp_driver.exe manually." Yellow
+}
+
+# Cleanup temporary directories
+Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue
+Remove-Item -Path $RuntimeDir -Recurse -Force -ErrorAction SilentlyContinue
+
+Write-Status "==============================" Cyan
+Write-Status "Build completed successfully!" Green
diff --git a/ahakeyconfig-win-java/build-installer.ps1 b/ahakeyconfig-win-java/build-installer.ps1
new file mode 100644
index 00000000..59a830c1
--- /dev/null
+++ b/ahakeyconfig-win-java/build-installer.ps1
@@ -0,0 +1,215 @@
+<#
+AhaKey Studio Build Script - Package JavaFX app to Windows EXE Installer
+Requires: JDK 17+, Maven 3.6+, NSIS (for --type exe)
+
+Usage: .\build-installer.ps1
+#>
+
+$ErrorActionPreference = "Continue"
+
+$ProjectName = "AhaKeyStudio"
+$Version = "1.0.0"
+$MainClass = "com.example.ahakey.App"
+$TargetDir = Join-Path $PSScriptRoot "target"
+$InstallerDir = Join-Path $PSScriptRoot "installer" # 移到项目根目录,避免被 Maven clean 清理
+$TempDir = "$TargetDir\jpackage-input"
+$RuntimeDir = "$TargetDir\runtime"
+$ResourceDir = "$TargetDir\jpackage-resources"
+$IconPath = Join-Path $PSScriptRoot "VibeCodeKeyboard.ico"
+
+function Write-Status($Message, $Color) {
+ Write-Host "[$(Get-Date -Format HH:mm:ss)] " -NoNewline
+ Write-Host $Message -ForegroundColor $Color
+}
+
+Write-Status "AhaKey Studio Installer Build v1.0" Cyan
+Write-Status "====================================" Cyan
+
+# Ensure WiX tools are on PATH (jpackage --type exe requires candle.exe/light.exe)
+$wixPaths = @(
+ "C:\Program Files (x86)\WiX Toolset v3.14\bin",
+ "C:\Program Files\WiX Toolset v3.14\bin",
+ "C:\Program Files (x86)\WiX Toolset v3.11\bin"
+)
+foreach ($p in $wixPaths) {
+ if ((Test-Path $p) -and $env:PATH -notlike "*$p*") {
+ $env:PATH = "$p;$env:PATH"
+ }
+}
+
+# Build project (must run from script directory so Maven finds pom.xml)
+Set-Location $PSScriptRoot
+Write-Status "Building project..." Cyan
+& mvn "-Dmaven.repo.local=.m2repo" package -DskipTests
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Status "ERROR: Maven build failed" Red
+ exit 1
+}
+Write-Status "Maven build successful" Green
+
+# Clean old installer
+if (Test-Path $InstallerDir) {
+ Write-Status "Removing old installer..." Yellow
+ try {
+ Get-Process -Name "AhaKeyStudio" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
+ Start-Sleep -Milliseconds 500
+ } catch { }
+ $null = New-Item -ItemType Directory -Path "$TargetDir\empty_dir" -Force -ErrorAction SilentlyContinue
+ robocopy "$TargetDir\empty_dir" $InstallerDir /MIR /NFL /NDL /NJH /NJS | Out-Null
+ Remove-Item -Path "$TargetDir\empty_dir" -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item -Path $InstallerDir -Recurse -Force -ErrorAction SilentlyContinue
+}
+
+# Create clean temporary input directory
+Write-Status "Preparing clean input directory..." Cyan
+if (Test-Path $TempDir) {
+ Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue
+}
+New-Item -ItemType Directory -Path "$TempDir\lib" | Out-Null
+if (Test-Path $ResourceDir) {
+ Remove-Item -Path $ResourceDir -Recurse -Force -ErrorAction SilentlyContinue
+}
+New-Item -ItemType Directory -Path $ResourceDir | Out-Null
+Copy-Item -Path $IconPath -Destination "$ResourceDir\$ProjectName.ico" -Force
+
+$jarPath = "$TargetDir\ahakey-studio-$Version.jar"
+
+# Check if local model is enabled
+$modelEnabled = $false
+$propsFile = Join-Path $PSScriptRoot "src/main/resources/model_config.properties"
+if (Test-Path $propsFile) {
+ $match = Select-String -Path $propsFile -Pattern '^\s*model\.enabled\s*=\s*(.+)$'
+ if ($match) {
+ $modelEnabled = $match.Matches[0].Groups[1].Value.Trim() -eq 'true'
+ }
+}
+
+if ($modelEnabled) {
+ Write-Status "model.enabled=true: Including model files and ONNX runtime" Cyan
+} else {
+ Write-Status "model.enabled=false: EXCLUDING model files and ONNX runtime" Yellow
+}
+
+# Copy only required files
+Copy-Item -Path $jarPath -Destination $TempDir
+Copy-Item -Path "$TargetDir\lib\*.jar" -Destination "$TempDir\lib"
+
+if ($modelEnabled) {
+ Write-Status "Copying SenseVoice model files..." Cyan
+ New-Item -ItemType Directory -Path "$TempDir\models" | Out-Null
+ Copy-Item -Path (Join-Path $PSScriptRoot "src/main/resources/models/model_q8.onnx") -Destination "$TempDir\models" -Force
+ Copy-Item -Path (Join-Path $PSScriptRoot "src/main/resources/models/tokens.txt") -Destination "$TempDir\models" -Force
+ Write-Status "Model files copied successfully" Green
+} else {
+ Write-Status "Removing onnxruntime from lib..." Yellow
+ Remove-Item -Path "$TempDir\lib\onnxruntime*.jar" -Force -ErrorAction SilentlyContinue
+ Write-Status "Removing model files from JAR..." Yellow
+ $jarName = Split-Path $jarPath -Leaf
+ $zipPath = "$TempDir\$jarName"
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::Open($zipPath, 'Update')
+ $entries = $zip.Entries | Where-Object { $_.FullName -like 'models/*' }
+ foreach ($entry in $entries) { $entry.Delete() }
+ $zip.Dispose()
+ Write-Status "onnxruntime + model files removed from package (saved ~233MB)" Green
+}
+
+Write-Status "Input directory ready" Green
+
+# Copy BLE TCP bridge driver
+$bleCandidates = @(
+ (Join-Path $PSScriptRoot "BLE_tcp_driver.exe"),
+ (Join-Path $PSScriptRoot "..\BLE_tcp_driver.exe"),
+ (Join-Path $PSScriptRoot "..\ahakeyconfig-win\BLE_tcp_bridge_for_vibe_code-master (1)\BLE_tcp_bridge_for_vibe_code-master\dist\BLE_tcp_driver.exe")
+)
+$bleExeSource = $bleCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
+if ($bleExeSource) {
+ Write-Status "Copying BLE TCP driver to input dir..." Cyan
+ Copy-Item -Path $bleExeSource -Destination "$TempDir\BLE_tcp_driver.exe" -Force
+ Write-Status "BLE driver copied" Green
+} else {
+ Write-Status "WARNING: BLE driver not found, skipping" Yellow
+}
+
+# Create custom runtime using jlink
+Write-Status "Creating custom runtime using jlink..." Cyan
+if (Test-Path $RuntimeDir) {
+ Remove-Item -Path $RuntimeDir -Recurse -Force -ErrorAction SilentlyContinue
+}
+
+# Detect JDK version for --compress flag (JDK 21+ uses zip-6, JDK 17 uses 2)
+$javaVersion = (& java -version 2>&1 | Select-String 'version "(\d+)' | ForEach-Object { $_.Matches[0].Groups[1].Value })
+$compressArg = if ([int]$javaVersion -ge 21) { "zip-6" } else { "2" }
+Write-Status "JDK $javaVersion detected, using --compress=$compressArg" Cyan
+
+$jlinkArgs = @(
+ "--module-path", "$TargetDir\lib",
+ "--add-modules", "javafx.controls,javafx.fxml,javafx.graphics,java.base,java.logging,java.desktop,java.net.http,java.sql,java.naming,java.xml",
+ "--output", $RuntimeDir,
+ "--strip-debug",
+ "--no-header-files",
+ "--no-man-pages",
+ "--compress", $compressArg
+)
+
+& jlink @jlinkArgs
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Status "ERROR: jlink failed" Red
+ exit 1
+}
+Write-Status "Custom runtime created successfully" Green
+
+# Create EXE installer using jpackage
+Write-Status "Creating EXE installer (requires NSIS)..." Cyan
+
+# Generate timestamp for version (format: yyyyMMddHHmmss)
+$timestamp = Get-Date -Format "yyyyMMddHHmmss"
+
+$jpackageArgs = @(
+ "--type", "exe",
+ "--name", $ProjectName,
+ "--app-version", $Version,
+ "--vendor", "AhaKey",
+ "--description", "AhaKey Studio - Keyboard Configuration Tool",
+ "--copyright", "2024 AhaKey",
+ "--icon", $IconPath,
+ "--resource-dir", $ResourceDir,
+ "--input", $TempDir,
+ "--main-jar", (Split-Path $jarPath -Leaf),
+ "--main-class", $MainClass,
+ "--dest", $InstallerDir,
+ "--runtime-image", $RuntimeDir,
+ "--win-dir-chooser",
+ "--win-shortcut",
+ "--win-menu",
+ "--win-menu-group", "AhaKey",
+ "--java-options", "--add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED",
+ "--java-options", "--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED",
+ "--java-options", "--add-opens=javafx.fxml/com.sun.javafx.fxml=ALL-UNNAMED",
+ "--java-options", "-Dapp.version=$timestamp",
+ "--verbose"
+)
+
+& jpackage @jpackageArgs
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Status "ERROR: jpackage failed. Make sure NSIS is installed (https://nsis.sourceforge.io)" Red
+ exit 1
+}
+
+# Cleanup temporary directories
+Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue
+Remove-Item -Path $RuntimeDir -Recurse -Force -ErrorAction SilentlyContinue
+
+# Rename output to timestamp-based filename
+$originalExe = "$InstallerDir\$ProjectName-$Version.exe"
+$renamedExe = "$InstallerDir\$ProjectName-$timestamp.exe"
+if (Test-Path $originalExe) {
+ Rename-Item -Path $originalExe -NewName "$ProjectName-$timestamp.exe"
+}
+
+Write-Status "====================================" Cyan
+Write-Status "Installer build completed!" Green
+Write-Status "Output: $renamedExe" Cyan
diff --git a/ahakeyconfig-win-java/features_debug.txt b/ahakeyconfig-win-java/features_debug.txt
new file mode 100644
index 00000000..94af1cca
--- /dev/null
+++ b/ahakeyconfig-win-java/features_debug.txt
@@ -0,0 +1,197 @@
+-36.259,2.918,1.703,1.996,3.474,-36.259,3.073,0.847,1.967,1.085,2.671,0.709,0.999,1.839,2.237,2.167,2.888,1.973,-2.340,2.645,2.787,0.543,2.758,2.100,0.472,1.658,3.191,4.024,4.400,3.264,2.544,2.532,3.306,2.378,1.827,2.535,2.934,1.965,1.225,3.454,2.993,3.184,4.846,4.267,3.220,3.363,3.313,2.059,2.115,0.884,3.591,3.515,4.061,3.851,3.811,3.484,3.127,3.066,2.861,3.217,3.244,3.757,4.023,3.481,3.432,3.759,3.942,3.917,3.162,3.624,3.650,4.591,4.678,4.169,3.951,3.648,3.991,4.060,3.630,5.354
+-36.259,1.571,-0.070,0.278,2.144,-36.259,0.450,0.292,1.215,2.203,0.900,2.077,-1.211,0.384,1.117,1.977,2.214,0.919,0.976,0.388,1.515,-0.600,1.658,1.953,1.623,0.917,2.860,4.542,4.657,3.064,2.675,3.108,2.974,2.612,1.006,1.568,2.180,1.863,2.312,3.810,3.202,2.945,4.145,3.503,3.300,2.678,3.217,2.741,1.522,1.917,2.623,3.802,4.173,4.241,3.590,2.862,2.818,2.945,2.883,2.807,3.455,3.841,3.831,3.565,3.025,3.032,4.315,4.388,3.027,2.856,3.854,4.234,4.205,3.675,3.625,4.249,4.456,4.344,3.918,4.184
+-36.259,0.455,0.270,0.585,2.952,-36.259,1.982,-0.115,1.898,0.409,0.886,0.581,0.050,0.125,0.829,0.300,2.913,2.494,0.997,1.813,1.172,0.788,2.687,1.949,1.718,0.870,2.677,4.326,4.428,2.386,1.751,1.538,3.135,1.784,1.712,2.616,3.400,3.204,2.428,2.761,3.271,3.661,4.790,3.735,3.731,4.590,4.173,2.783,3.539,2.702,3.061,2.355,3.159,3.209,3.470,3.302,3.499,3.597,3.672,3.782,4.321,4.095,4.141,3.886,4.032,3.844,3.306,2.787,2.714,3.017,4.702,5.286,5.444,4.843,4.490,3.782,4.514,4.494,5.252,4.415
+-36.259,0.871,2.058,0.816,2.959,-36.259,2.142,-0.199,0.366,2.225,1.168,-1.353,0.148,0.235,0.838,0.871,2.103,1.471,1.293,1.271,0.848,0.885,2.061,2.119,0.367,0.810,2.496,4.467,4.495,2.817,2.263,1.878,3.051,2.769,0.524,1.762,2.227,2.763,1.779,2.014,2.408,3.492,4.181,3.304,3.927,3.332,3.933,3.090,3.358,1.536,1.820,2.249,3.420,3.841,3.444,3.074,2.942,2.986,3.781,4.098,4.785,4.702,3.966,4.272,3.745,3.879,3.746,3.638,3.096,3.962,4.001,3.858,4.312,3.930,3.973,4.195,4.620,4.092,5.172,4.633
+-36.259,-1.841,2.120,-0.001,2.027,-36.259,1.699,0.916,-1.116,3.058,2.297,1.307,-0.196,-0.852,-1.155,0.355,-0.867,1.751,0.717,0.882,1.497,0.600,3.454,3.594,1.854,1.911,1.658,4.142,4.168,1.830,2.947,2.578,2.567,0.673,0.449,0.687,1.795,1.813,1.918,3.348,2.903,2.983,4.285,4.123,3.097,3.110,3.103,2.443,3.278,1.952,3.433,3.130,4.237,4.376,3.742,3.248,3.762,3.924,3.687,3.396,4.011,3.370,3.593,4.110,4.001,3.659,4.198,4.088,3.862,2.950,2.983,4.891,5.079,3.852,3.548,3.993,4.597,5.275,5.511,4.510
+-36.259,2.599,2.132,1.185,1.996,-36.259,2.543,0.067,1.034,3.482,2.684,-1.627,2.741,1.518,-1.033,2.328,3.203,2.161,1.869,0.177,1.657,0.064,2.986,3.393,1.618,0.615,2.385,4.395,4.330,2.440,1.353,1.965,3.224,0.790,0.518,1.549,2.762,1.867,2.468,2.864,2.210,2.759,3.809,3.453,2.347,2.417,2.527,3.183,3.686,2.738,3.076,3.374,4.226,4.344,3.751,3.750,2.837,3.112,3.570,4.027,3.962,3.907,3.802,3.426,3.772,3.765,3.559,3.693,3.809,4.163,4.277,3.935,3.990,4.311,4.244,4.562,5.347,4.934,4.861,4.555
+-36.259,2.484,0.356,0.008,3.178,-36.259,0.990,-0.109,0.940,2.787,-0.931,-1.674,1.444,1.625,1.235,1.188,0.500,1.922,1.844,1.967,0.855,0.713,2.949,2.049,1.321,1.463,2.572,4.612,4.791,3.222,1.995,2.291,3.449,2.363,1.813,2.214,1.499,2.337,1.869,3.019,2.332,2.932,3.275,3.023,2.291,2.181,2.101,2.243,3.107,2.309,3.018,2.418,3.605,3.721,2.620,2.656,4.073,3.360,3.758,4.123,4.576,4.092,3.745,3.747,3.907,2.840,3.685,3.106,1.693,2.759,4.140,4.384,4.440,4.862,4.042,3.753,4.251,4.059,5.282,4.110
+-36.259,2.265,1.783,1.411,-1.080,-36.259,2.621,-1.661,0.361,1.705,3.230,2.839,2.386,0.392,1.475,2.404,1.945,0.821,1.014,1.897,2.812,1.750,2.379,1.934,1.804,2.008,1.795,4.446,4.514,2.329,2.881,2.150,3.119,2.472,1.912,1.779,2.864,2.299,2.244,3.080,2.103,2.909,3.290,2.679,3.496,3.351,3.018,2.917,3.330,2.501,2.572,2.580,3.848,3.886,2.814,3.386,4.626,4.226,3.718,3.334,4.240,4.218,4.267,3.860,3.225,2.642,3.540,3.293,2.611,3.038,3.542,4.488,4.505,4.216,3.620,3.958,4.689,4.489,4.736,4.298
+-36.259,3.549,2.516,2.486,2.587,-36.259,1.925,1.443,1.561,-0.512,0.493,1.553,1.045,0.112,1.899,2.147,1.918,0.836,2.208,0.858,1.713,1.287,1.189,1.096,1.484,2.261,2.014,4.259,4.339,2.213,2.078,2.263,3.283,2.181,2.072,3.153,3.316,3.004,2.226,3.326,2.060,2.693,4.016,3.254,3.398,3.533,3.438,3.219,3.251,2.422,2.067,2.691,3.523,3.986,2.984,2.457,3.776,3.602,2.751,3.418,3.698,3.881,3.495,3.528,3.469,3.416,4.246,3.077,2.398,3.597,4.150,4.775,4.762,4.495,4.385,4.128,4.793,4.641,4.484,4.271
+-36.259,0.376,0.104,1.390,-1.122,-36.259,-0.573,0.804,0.792,2.276,1.193,-0.529,1.329,0.600,1.096,0.967,2.609,0.719,1.277,-0.205,-0.665,1.048,2.394,1.868,1.381,1.925,2.464,4.642,4.865,3.163,2.371,2.883,2.681,1.439,1.524,2.735,3.535,2.564,2.111,3.641,3.198,2.957,3.517,2.493,3.091,4.012,3.138,2.454,3.024,3.079,2.273,2.943,3.262,3.432,3.257,3.102,2.723,2.771,2.605,3.338,4.839,4.538,4.228,3.421,3.363,2.766,3.813,4.010,3.114,3.741,3.653,3.477,3.481,3.718,4.077,4.206,5.942,4.838,5.349,5.414
+-36.259,1.232,0.947,1.143,0.701,-36.259,0.428,0.826,0.501,2.170,2.014,0.554,0.460,-2.373,-0.902,2.575,3.722,2.580,2.419,1.825,1.732,-0.051,-0.111,1.516,1.435,1.014,2.015,4.532,4.726,2.714,1.851,2.969,2.433,1.225,1.155,1.463,3.041,2.236,1.662,3.007,3.262,2.689,4.637,4.247,2.894,3.307,3.682,1.788,3.079,2.749,2.357,3.690,3.366,3.791,3.718,3.003,2.718,3.169,3.486,3.011,3.869,3.984,3.933,2.879,4.061,3.615,3.586,3.618,2.835,3.452,4.087,3.938,4.338,3.969,4.135,4.195,4.976,4.493,4.102,4.643
+-36.259,1.112,2.086,1.492,-1.595,-36.259,1.251,0.679,0.919,0.750,1.427,-0.245,-1.040,-2.969,-0.005,1.876,2.719,1.024,1.468,1.902,2.400,0.246,2.168,2.081,2.616,1.088,2.611,4.429,4.446,2.397,3.383,2.518,2.690,1.676,2.549,1.490,2.379,2.180,0.835,2.255,1.844,2.477,3.563,2.686,2.756,3.409,2.600,2.412,2.797,2.673,2.095,2.743,3.177,4.070,3.786,2.948,3.943,3.432,3.251,3.498,4.051,4.784,4.225,3.639,3.899,3.837,4.424,3.717,2.970,3.592,3.190,3.608,3.378,3.606,3.352,3.855,4.876,4.661,4.601,4.602
+-36.259,2.777,2.840,1.040,1.953,-36.259,1.903,0.881,-2.222,2.229,1.506,0.834,-0.173,-0.147,1.048,2.147,2.426,2.771,2.440,1.802,0.582,1.426,1.244,2.488,2.034,1.731,2.267,4.426,4.612,3.279,-0.786,1.680,2.348,1.437,1.989,4.064,4.499,4.093,2.635,2.925,2.359,2.720,3.346,3.226,3.342,2.476,3.153,3.313,3.849,2.446,2.529,2.189,3.466,3.326,2.443,2.309,2.873,3.437,3.811,3.907,4.900,4.757,4.242,4.245,3.887,2.988,3.355,3.306,2.690,3.985,3.655,4.266,4.263,4.391,4.123,4.089,4.829,4.110,4.671,4.770
+-36.259,0.372,-0.443,0.818,-0.321,-36.259,-1.390,-0.141,1.801,1.030,1.892,0.563,0.047,0.245,-0.474,2.124,1.844,2.561,1.684,1.565,1.194,0.124,1.998,2.033,0.873,0.834,1.787,4.170,4.318,2.465,1.195,1.776,2.639,-0.063,1.781,3.897,3.732,3.467,1.910,3.465,3.211,2.337,3.895,3.321,2.412,3.384,3.317,2.847,3.861,2.721,1.648,2.358,4.196,4.165,3.050,2.900,3.955,4.186,4.140,4.295,4.484,4.644,4.066,3.853,3.243,3.388,3.678,3.379,2.513,3.005,3.559,4.108,4.374,4.215,3.852,3.640,4.819,4.433,4.038,4.315
+-36.259,2.210,1.795,-3.652,1.889,-36.259,0.642,-0.489,1.165,0.440,1.233,-0.430,-0.635,1.173,1.654,2.587,2.638,1.943,2.481,1.318,1.892,1.397,2.911,2.502,1.706,0.128,2.980,4.542,4.472,2.354,2.912,1.417,1.735,0.792,0.423,2.630,3.552,2.899,2.830,2.605,2.612,3.931,5.020,4.479,3.389,2.944,2.244,2.477,3.854,2.940,2.135,2.525,4.052,4.271,2.825,2.685,3.677,3.514,3.344,3.845,3.707,4.252,3.779,4.174,4.287,3.305,3.545,3.520,3.037,4.182,3.791,4.196,4.302,4.235,3.904,4.097,5.253,4.797,5.071,5.076
+-36.259,-0.538,2.166,1.075,0.609,-36.259,2.139,0.718,1.159,1.652,3.594,2.412,0.386,-0.301,2.255,1.084,2.400,3.145,1.776,2.988,2.856,1.893,1.593,2.517,0.887,2.397,1.478,4.153,4.171,2.311,3.052,1.874,2.571,2.055,1.989,2.785,3.067,2.587,1.520,2.086,2.182,2.969,2.954,3.222,3.053,3.247,2.744,2.204,3.069,2.747,2.551,2.723,4.143,4.193,3.250,3.752,3.618,2.896,3.427,4.146,5.112,4.644,3.952,3.826,4.223,3.110,4.036,3.693,2.883,3.548,3.712,3.619,3.828,4.044,4.066,3.917,4.940,4.444,4.306,4.491
+-36.259,3.130,2.200,1.749,0.602,-36.259,0.489,-0.280,0.604,1.222,2.936,1.561,1.545,-1.674,1.199,2.801,2.539,2.994,2.480,1.482,1.920,0.211,2.441,2.412,2.328,1.813,2.663,4.399,4.490,3.121,1.843,2.889,2.216,1.799,0.883,2.434,2.738,1.911,0.578,2.802,1.944,0.749,3.749,3.096,1.755,1.997,2.086,1.843,2.740,1.889,1.661,2.422,3.949,4.266,2.814,2.826,3.157,3.266,3.015,3.679,4.311,4.394,4.055,4.147,3.893,3.450,4.383,4.067,2.553,2.911,3.627,5.311,5.428,4.338,4.217,4.006,5.700,5.040,4.464,4.245
+-36.259,0.492,1.730,-0.489,1.200,-36.259,1.007,-0.454,1.546,2.980,1.106,0.265,1.558,1.602,-1.522,2.457,1.793,2.300,1.916,1.530,1.035,0.169,2.339,1.812,0.987,0.672,2.260,4.313,4.523,2.770,2.437,2.800,2.980,1.247,1.746,2.625,3.322,2.765,1.752,3.397,2.984,2.718,4.582,3.758,3.241,3.264,3.168,3.068,4.007,3.038,2.613,1.746,3.410,3.576,2.648,2.611,3.861,3.642,3.329,4.255,4.167,4.708,4.581,3.792,3.816,3.169,3.972,3.223,3.072,4.257,4.886,5.011,5.031,4.856,3.766,4.215,5.669,4.595,4.601,4.921
+-36.259,0.228,2.593,0.286,2.947,-36.259,0.230,1.735,0.645,2.360,0.860,1.167,2.091,1.256,-1.457,1.387,1.346,2.050,1.451,1.719,2.265,1.430,2.424,1.854,1.277,2.517,2.775,4.717,4.574,2.256,2.168,2.174,2.801,1.944,1.918,0.947,2.148,2.207,2.216,2.240,2.698,2.673,4.059,3.361,3.066,2.677,2.008,2.188,4.251,3.485,2.798,3.254,4.531,4.742,3.493,3.161,4.074,4.364,4.335,4.724,3.771,4.137,4.468,4.039,3.591,3.463,4.241,4.190,3.661,4.542,3.809,4.324,4.255,4.097,4.414,4.147,4.466,4.597,5.476,4.636
+-36.259,1.101,1.733,-0.018,1.640,-36.259,1.750,1.998,1.299,-0.719,1.022,-1.548,-1.040,1.211,1.009,2.112,1.391,1.182,2.085,1.806,1.353,-0.235,1.736,0.840,1.508,2.452,1.921,4.350,4.356,2.167,1.290,2.306,2.368,2.568,1.216,1.836,2.617,1.410,1.025,2.369,2.228,2.944,2.830,2.386,2.831,2.495,2.832,2.524,4.025,3.211,2.933,2.753,3.032,3.181,2.272,2.538,3.844,3.195,3.574,4.021,4.974,4.842,4.087,3.400,3.699,3.661,3.110,3.015,3.063,4.002,4.047,4.354,4.349,3.733,3.797,3.658,4.799,4.369,4.571,4.057
+-36.259,2.715,0.207,1.901,2.821,-36.259,0.419,0.714,1.699,-0.270,0.283,1.075,0.676,1.212,1.020,1.475,1.867,1.003,1.131,1.447,2.817,-1.329,0.748,1.812,1.454,1.709,2.995,4.636,4.761,3.065,3.100,2.356,2.147,2.051,2.564,2.453,3.105,3.045,1.209,1.562,1.752,2.940,4.005,3.310,3.039,1.947,3.289,2.531,2.629,3.158,2.792,2.209,3.396,3.819,2.701,2.276,3.312,3.338,3.417,3.246,3.630,4.231,3.972,3.733,3.833,3.113,3.958,3.506,3.261,3.281,3.812,4.326,4.311,4.435,4.099,4.022,5.022,4.677,4.396,5.009
+-36.259,3.424,3.232,1.640,2.232,-36.259,1.966,1.304,1.540,0.855,1.579,0.247,0.810,1.207,0.753,2.056,1.610,2.232,2.317,1.101,1.595,1.114,1.172,2.037,1.400,1.523,3.102,4.532,4.160,1.989,2.308,2.377,2.946,1.906,0.581,1.132,2.883,2.515,2.359,3.523,2.834,1.553,3.550,2.778,2.796,3.183,2.714,2.935,2.947,2.590,3.252,3.317,4.682,5.038,3.783,3.709,3.590,2.868,2.911,3.226,2.737,3.678,3.087,3.630,3.964,3.747,4.694,3.978,2.912,2.908,3.198,3.813,3.929,4.140,4.071,3.983,4.239,4.109,4.319,4.833
+-36.259,1.223,1.383,1.766,-2.704,-36.259,1.178,0.885,2.160,1.057,2.070,-0.206,1.798,0.917,0.610,1.181,2.075,0.999,0.337,0.906,2.047,0.543,1.885,2.468,1.742,2.355,2.698,4.424,4.513,2.860,2.435,2.258,2.330,2.135,1.298,1.814,2.495,2.273,1.439,2.654,3.082,3.261,4.688,4.070,3.050,3.472,2.410,3.233,4.179,2.877,2.424,2.648,3.660,3.865,2.827,2.558,2.875,2.750,2.576,3.555,3.385,3.804,3.755,4.087,4.242,3.298,3.799,3.391,3.030,3.733,3.747,4.246,4.790,4.137,4.075,4.328,5.334,5.316,5.898,4.639
+-36.259,0.126,1.958,0.021,1.089,-36.259,0.923,2.181,-0.379,0.439,0.577,0.179,0.062,0.518,-0.197,0.768,1.652,1.419,1.765,1.745,1.225,0.631,1.104,1.258,2.204,1.043,2.274,3.983,4.161,2.337,2.108,2.290,2.461,2.372,1.794,2.347,2.405,2.143,1.732,3.183,2.610,3.565,3.560,3.178,3.636,2.960,2.040,2.488,3.597,3.100,3.157,3.465,4.118,3.946,3.597,3.094,4.566,4.716,3.442,3.566,3.651,3.814,4.082,4.794,3.611,3.644,3.993,3.880,3.094,4.371,4.050,4.453,4.644,4.423,4.413,4.119,4.051,4.254,5.356,4.766
+-36.259,2.747,3.466,0.238,1.233,-36.259,1.487,1.065,0.027,-0.816,0.880,0.152,1.216,0.607,-0.577,2.056,1.592,2.323,1.724,1.730,1.238,0.591,1.610,0.854,2.580,0.451,3.243,5.036,5.111,3.333,2.714,2.160,1.962,1.485,0.483,2.956,2.944,2.918,2.412,2.333,2.400,2.991,4.414,4.038,3.233,3.149,2.331,2.583,3.051,2.380,2.262,3.020,3.846,3.886,3.246,3.360,2.712,2.118,3.478,3.985,4.738,4.510,3.784,3.241,2.826,3.353,4.029,3.529,2.302,3.280,3.900,4.468,4.691,4.326,3.814,3.635,4.801,4.371,4.096,5.242
+-36.259,2.281,2.056,1.756,1.594,-36.259,1.984,1.297,2.114,2.161,2.009,0.697,1.584,0.523,0.349,1.743,2.382,2.521,1.179,1.040,2.313,1.512,2.810,2.841,1.874,2.023,3.057,4.318,4.412,3.254,1.326,2.547,2.605,2.315,3.014,2.636,2.766,2.975,2.869,3.532,2.430,2.813,3.736,3.582,2.877,3.755,3.508,2.662,3.658,3.460,2.659,2.461,3.668,3.454,2.303,2.250,3.827,3.427,3.740,3.342,3.549,3.927,4.401,3.651,3.810,3.503,3.950,4.359,3.269,3.055,3.254,4.388,4.684,3.970,3.444,4.404,5.056,3.890,4.748,5.041
+-36.259,1.407,0.919,1.656,-0.817,-36.259,-0.352,0.494,2.535,2.120,2.439,1.391,1.226,0.066,0.766,1.241,2.779,2.108,1.089,0.711,0.504,1.791,2.969,2.954,2.411,2.441,3.397,5.050,4.980,3.332,2.517,1.589,1.763,2.505,0.708,2.366,2.929,2.555,1.654,2.177,1.438,2.782,4.383,4.059,3.887,2.915,3.664,2.366,2.804,2.326,1.881,1.061,2.794,3.322,3.306,3.381,3.332,3.387,3.721,3.764,3.959,4.603,4.305,3.482,3.519,3.774,4.278,3.866,2.298,3.133,4.357,4.962,4.986,4.240,3.713,4.182,5.404,4.311,4.972,4.731
+-36.259,0.946,-0.107,1.222,2.190,-36.259,2.511,-0.162,-0.411,-0.839,2.669,1.463,1.887,0.218,0.577,2.931,3.156,2.946,1.675,1.802,1.353,-1.529,0.507,2.396,1.406,0.487,2.158,4.071,4.196,2.654,0.331,2.659,2.068,1.527,0.857,1.182,2.795,1.302,1.264,2.972,2.896,3.192,3.689,3.088,3.024,2.744,1.994,2.427,2.980,1.504,2.198,2.615,3.537,3.515,2.669,2.275,3.466,3.400,2.709,3.490,3.689,3.473,3.322,3.000,2.887,3.002,3.602,2.876,2.158,3.372,4.397,4.531,4.571,4.543,3.861,3.806,4.720,4.356,4.233,4.836
+-36.259,2.738,2.456,1.548,2.074,-36.259,-0.789,0.679,0.029,-1.084,2.200,1.611,2.162,0.752,0.929,2.089,2.684,1.836,-0.206,2.346,2.588,-1.663,1.773,1.421,0.688,3.131,1.845,4.166,4.286,2.149,2.842,1.863,2.751,2.314,2.386,1.489,2.975,2.306,1.984,2.970,3.185,3.316,4.514,3.626,2.913,2.767,2.513,2.309,2.994,2.770,2.564,2.603,3.684,3.780,3.180,3.109,3.899,4.011,3.486,3.670,3.696,4.208,3.693,3.853,3.587,3.347,4.039,4.151,4.070,4.194,4.179,4.160,4.281,4.671,4.704,4.440,4.811,4.572,4.836,4.190
+-36.259,0.462,1.463,0.480,1.215,-36.259,2.365,-1.440,1.933,0.357,2.218,2.076,1.448,1.458,2.135,1.964,2.777,2.093,2.497,1.345,1.547,1.586,1.112,1.788,1.229,1.736,3.089,4.986,4.871,2.503,1.868,1.753,1.579,2.630,1.331,2.786,2.939,2.938,1.677,3.648,3.347,3.090,3.461,3.016,3.355,3.407,3.722,2.621,3.056,2.742,2.485,2.625,4.686,4.916,3.432,3.265,3.187,3.200,2.849,3.288,3.538,3.454,3.258,3.132,3.643,3.521,4.871,4.647,3.134,3.090,3.223,4.617,4.987,4.238,3.502,4.379,5.083,4.417,4.528,4.814
+-36.259,2.508,1.603,0.998,-2.008,-36.259,1.082,-0.046,2.767,1.868,2.717,1.672,1.153,1.020,0.994,3.355,2.178,3.173,2.010,1.906,0.742,0.013,1.903,2.011,1.427,2.156,2.287,4.491,4.338,1.666,2.039,2.065,2.092,-0.220,1.228,2.645,2.764,3.256,2.025,1.728,2.681,3.025,4.142,3.397,2.754,2.534,3.088,2.581,2.672,2.415,2.247,3.036,4.240,4.337,3.559,2.936,3.195,3.444,3.738,3.616,4.018,4.053,4.267,3.287,3.706,4.422,4.424,4.332,2.860,3.351,4.170,4.493,4.542,4.482,3.537,4.570,5.472,5.185,5.605,4.805
+-36.259,2.246,1.397,-0.392,2.298,-36.259,0.066,0.487,2.191,2.082,1.993,2.534,1.368,0.821,0.469,1.156,2.630,2.116,2.442,1.752,1.789,1.589,3.208,3.284,1.714,1.561,2.735,4.723,4.817,2.981,2.827,2.098,3.260,2.595,1.501,2.276,2.161,2.031,1.511,2.140,2.743,3.420,4.673,4.026,3.741,3.404,3.483,2.738,2.733,2.237,2.218,2.636,3.902,4.246,3.843,3.509,3.685,3.126,2.529,3.547,4.013,3.915,4.544,3.450,3.996,3.469,3.896,4.018,3.463,3.271,3.888,4.427,4.297,3.576,4.328,4.326,4.904,4.857,5.026,4.720
+-36.259,1.576,1.992,0.361,0.294,-36.259,1.942,-0.012,1.287,2.376,2.045,0.691,2.491,0.622,1.625,2.870,1.614,2.162,2.153,1.441,2.035,0.315,1.079,2.094,2.335,2.064,3.080,4.424,3.995,0.315,2.664,1.794,2.768,1.799,1.236,3.363,3.856,3.330,2.648,2.974,2.479,3.116,4.090,3.676,3.577,3.183,3.464,3.230,3.270,2.630,2.783,2.916,4.161,4.116,4.023,3.412,4.242,3.897,3.376,3.728,4.620,4.675,4.011,2.948,3.617,3.675,3.721,3.743,3.240,3.320,3.638,5.301,5.494,4.294,4.070,4.091,4.795,4.895,5.432,5.521
+-36.259,3.634,2.658,2.859,1.076,-36.259,0.250,1.630,2.825,2.692,2.029,0.967,-2.271,0.007,1.923,1.417,1.630,2.631,2.638,2.864,2.125,1.080,1.833,1.813,1.895,2.778,3.432,4.127,4.093,3.022,2.168,1.250,3.332,1.948,1.445,2.216,1.834,2.164,1.392,3.302,2.644,2.582,4.408,3.871,2.877,3.103,2.499,2.103,1.672,1.806,2.258,2.146,4.439,5.023,3.649,3.524,3.271,3.287,3.445,4.170,4.368,4.647,4.358,3.872,3.248,4.509,4.461,3.665,3.176,2.597,3.558,4.530,4.663,3.830,3.480,4.236,4.315,4.661,4.562,6.057
+-36.259,2.653,1.688,1.039,1.779,-36.259,1.075,1.147,1.329,0.878,2.675,0.329,0.394,1.033,-2.755,1.996,2.397,1.010,1.006,-0.802,0.054,-0.213,2.473,2.098,2.600,1.377,2.406,3.942,4.077,2.349,1.685,2.097,3.069,1.978,1.359,3.049,3.321,3.131,1.906,2.939,2.308,2.791,3.806,3.354,3.853,3.285,3.084,2.866,2.850,2.518,2.299,2.811,4.538,4.846,3.781,2.518,2.570,2.257,2.892,3.261,3.386,3.540,3.111,3.054,3.353,3.779,4.460,4.007,2.467,2.646,3.301,4.550,4.575,3.653,3.693,3.886,4.911,4.156,4.220,4.892
+-36.259,2.045,-1.372,1.000,-0.903,-36.259,0.039,-1.018,0.234,0.644,0.425,-1.245,0.782,0.544,1.875,1.852,3.341,2.200,1.483,1.611,0.095,-1.771,1.101,2.212,0.866,0.727,2.008,4.391,4.373,1.909,2.147,1.691,2.291,0.710,1.118,2.634,3.123,2.721,1.738,2.673,2.545,2.850,3.361,3.447,3.466,2.979,3.315,2.636,2.311,2.620,3.497,3.119,3.747,3.849,3.131,3.076,2.641,3.194,3.676,4.101,5.578,5.272,4.750,3.234,3.150,3.783,3.727,3.289,3.063,2.726,3.878,4.498,4.697,4.480,3.761,2.927,4.442,3.664,4.142,4.592
+-36.259,0.250,0.374,0.028,0.448,-36.259,0.067,-0.540,1.787,1.395,2.250,1.826,0.496,0.898,0.792,1.713,2.128,1.174,-2.327,1.812,1.332,-1.065,0.683,0.306,1.618,1.854,2.650,4.342,4.470,2.793,2.384,2.696,2.110,1.437,1.602,2.411,2.330,2.510,0.357,3.848,3.066,3.720,4.813,4.160,3.792,2.998,2.521,2.130,3.138,2.920,3.056,2.973,3.438,3.237,3.223,3.240,2.686,2.373,3.126,3.541,3.558,3.854,3.366,3.675,3.430,3.573,4.271,3.864,3.578,2.553,3.960,4.900,4.934,4.040,4.136,4.014,5.056,4.634,5.205,5.144
+-36.259,0.644,0.571,0.483,2.218,-36.259,1.485,-0.345,0.241,0.427,0.867,0.891,1.399,1.544,1.415,2.073,1.081,1.917,0.928,2.317,1.705,0.761,0.730,0.017,1.130,1.351,2.011,3.715,3.906,2.406,0.454,1.478,2.002,0.863,-1.195,3.059,3.169,2.998,1.918,3.117,2.376,2.749,4.349,3.613,3.056,2.172,2.877,2.691,3.168,2.479,2.582,2.858,3.789,3.889,3.369,3.779,3.952,3.669,3.054,4.589,4.812,4.857,4.622,4.337,4.014,3.133,3.344,3.663,3.248,3.923,3.913,4.274,4.264,4.139,4.052,4.090,4.665,4.586,5.048,5.199
+-36.259,-0.181,2.211,0.891,2.238,-36.259,0.769,1.255,0.998,1.258,1.744,-0.143,-0.299,1.995,1.056,2.271,3.349,1.925,0.957,0.216,1.261,1.683,1.808,1.535,2.029,2.732,1.936,3.946,4.145,2.440,2.608,1.011,1.864,1.212,1.569,2.107,2.877,2.485,1.132,2.770,2.797,2.533,3.611,3.751,1.975,3.914,3.346,2.548,3.699,2.796,2.965,3.685,4.356,4.443,3.834,3.911,4.250,4.126,3.785,4.094,4.539,4.808,4.633,4.057,3.801,3.910,3.734,3.804,3.056,3.011,4.237,4.144,4.143,4.329,4.225,3.910,4.418,4.296,4.415,4.277
+-36.259,-0.558,2.038,0.147,0.669,-36.259,1.803,1.545,1.659,0.188,2.066,0.823,0.342,-1.584,-1.976,2.518,2.913,3.009,2.848,2.150,2.384,0.739,1.856,1.641,2.163,1.907,1.920,4.021,4.092,2.460,0.372,0.955,1.506,1.802,1.927,2.011,2.584,2.110,0.857,2.790,2.427,3.039,3.334,3.149,3.184,2.907,2.739,2.364,3.661,2.924,1.233,1.764,3.506,3.893,2.750,2.472,3.760,3.761,3.373,3.933,4.741,4.188,4.403,3.946,3.949,3.863,3.949,3.359,3.246,3.081,3.688,4.646,4.582,4.011,3.650,3.687,4.298,3.788,4.441,4.977
+-36.259,3.073,3.273,2.253,0.256,-36.259,2.994,1.973,2.270,2.440,3.719,2.893,2.914,1.191,0.546,2.726,1.801,1.819,2.031,1.511,2.422,1.815,3.036,1.610,2.669,2.481,3.208,4.772,4.588,2.780,2.670,1.824,2.058,1.614,1.139,1.617,2.289,2.378,2.635,3.492,2.479,1.638,4.169,3.655,2.111,2.518,2.792,2.619,2.841,2.044,2.293,2.835,3.867,3.821,3.690,3.031,3.565,3.647,3.723,3.134,2.980,3.398,4.275,3.388,2.959,3.974,3.968,4.169,3.238,1.807,3.417,4.437,4.748,4.580,3.447,4.319,4.816,4.570,4.580,4.964
+-36.259,1.750,0.098,1.715,1.720,-36.259,-0.178,0.187,-0.151,1.554,0.968,2.300,1.402,0.875,2.272,1.734,1.019,1.698,2.540,1.158,0.894,1.919,0.638,0.553,0.117,1.779,2.188,4.269,4.406,2.908,1.618,1.537,2.360,1.953,0.665,2.436,3.354,3.345,1.028,2.077,1.854,2.270,3.601,4.278,2.739,3.491,2.424,2.308,3.216,2.531,3.350,3.381,3.562,3.878,3.924,3.351,4.836,4.085,3.814,3.411,4.052,4.217,4.644,4.613,4.752,4.150,3.601,3.049,3.486,3.595,4.292,4.341,4.014,4.531,4.314,4.273,5.419,4.244,5.003,4.341
+-36.259,2.672,2.378,1.801,-0.341,-36.259,0.703,1.015,1.587,-2.251,-1.439,0.220,1.592,1.247,0.020,0.612,1.449,2.001,2.199,1.520,-0.228,1.107,2.048,2.406,2.750,3.046,3.114,4.547,4.658,3.564,2.922,2.604,3.102,2.785,1.073,3.108,3.205,2.691,1.857,3.024,2.337,2.965,4.483,4.114,2.698,2.847,2.573,2.723,3.798,2.530,3.452,3.536,3.875,3.966,3.142,2.737,3.872,3.718,3.390,3.818,3.699,4.162,3.837,3.496,3.353,2.920,3.898,3.761,3.041,3.295,3.615,3.949,4.252,4.037,3.841,3.857,4.922,4.118,4.021,4.986
+-36.259,1.105,1.487,1.574,1.609,-36.259,1.056,0.702,1.666,0.493,0.839,-1.223,-1.050,1.175,2.000,0.895,2.477,1.335,-2.730,0.417,1.621,1.507,2.336,1.929,2.715,1.923,3.330,4.778,4.683,2.966,1.847,2.747,2.586,1.230,1.387,1.968,2.419,2.830,2.199,3.014,2.826,2.348,3.610,3.131,1.888,3.179,2.697,3.296,4.698,3.612,3.530,2.992,2.682,3.488,3.556,3.461,2.896,2.739,4.077,4.584,4.848,4.682,4.633,3.593,3.767,4.025,4.407,4.798,4.167,4.612,4.083,3.791,4.081,4.573,4.454,4.352,4.775,4.548,4.285,4.465
+-36.259,3.444,1.193,1.949,1.798,-36.259,-0.102,-0.159,0.127,0.156,1.893,-0.375,1.077,0.983,1.016,2.913,2.313,2.234,1.986,1.264,1.551,1.165,2.479,1.336,1.186,2.051,3.237,4.428,4.304,2.428,2.037,1.686,1.131,0.996,0.808,2.376,3.209,2.595,2.526,2.604,2.536,2.778,4.185,3.670,3.179,3.158,3.417,2.498,4.085,2.717,2.034,3.101,3.949,3.874,3.613,3.449,2.757,2.842,3.843,4.002,4.626,4.958,4.361,4.130,4.133,3.759,3.455,3.485,2.558,3.735,3.822,4.032,4.085,3.957,3.922,4.177,4.860,4.989,4.603,5.347
+-36.259,1.937,0.141,0.794,2.379,-36.259,1.966,0.096,1.064,1.351,1.981,0.743,-1.908,1.446,0.779,2.093,2.321,1.618,0.669,1.255,1.521,0.815,2.006,1.881,2.740,1.168,2.083,3.942,3.977,1.951,1.945,1.790,1.210,0.771,2.020,1.734,1.853,2.310,2.107,2.336,2.302,2.745,4.350,3.567,2.435,2.093,2.337,2.356,2.686,1.876,1.638,1.852,4.174,4.462,3.600,2.698,3.255,2.743,3.743,3.822,4.177,4.022,4.124,3.859,3.921,3.003,3.843,3.254,2.113,3.461,3.924,3.928,4.665,4.460,3.626,3.596,4.376,4.742,5.219,5.312
+-36.259,1.970,1.224,1.164,2.222,-36.259,1.119,1.246,2.122,2.651,3.186,1.245,1.359,1.280,-1.748,2.182,3.051,2.121,2.308,0.752,0.620,0.527,2.723,1.436,1.665,2.317,3.227,4.925,4.930,3.263,1.627,1.805,0.956,0.824,0.516,0.466,2.561,1.984,1.898,3.226,2.298,2.294,4.312,3.633,1.884,3.393,3.365,2.328,3.203,2.182,2.547,2.525,3.696,3.763,3.894,3.483,4.375,4.014,2.694,3.431,3.910,4.036,4.130,3.356,3.300,3.683,3.328,3.320,3.362,3.368,3.741,4.263,4.159,4.023,3.590,3.775,4.491,4.646,5.625,5.034
+-36.259,2.275,-0.027,1.082,2.890,-36.259,2.523,0.786,2.170,2.803,3.281,1.925,1.492,2.046,2.023,0.887,2.531,0.477,-0.571,2.273,1.554,1.402,2.452,1.952,1.090,1.743,2.172,4.786,4.921,2.880,1.631,1.981,1.781,0.595,1.663,0.837,2.005,2.266,2.445,3.174,2.200,3.474,4.737,4.162,3.585,3.334,3.656,2.486,2.222,2.330,2.658,2.770,3.286,3.362,3.366,3.086,3.312,3.047,1.707,3.476,4.259,4.823,4.610,3.875,4.007,3.565,3.489,3.648,3.163,3.369,3.632,4.253,4.311,3.886,3.485,3.657,4.955,4.663,4.700,5.143
+-36.259,1.595,0.878,1.039,2.201,-36.259,2.326,0.098,1.362,0.329,2.363,0.038,1.341,1.497,0.265,2.368,1.544,2.536,2.467,1.170,1.438,-0.620,1.817,1.562,1.678,2.105,2.227,4.328,4.285,2.156,1.724,1.526,2.382,1.280,0.803,1.484,2.038,2.621,1.734,2.427,1.674,2.914,4.007,3.553,3.169,3.164,3.167,2.534,3.267,1.717,3.099,3.079,4.093,4.248,3.578,3.265,4.896,4.614,3.393,3.104,3.623,4.135,4.254,3.957,3.616,4.228,3.501,3.380,3.211,3.736,3.412,4.318,4.485,3.867,4.373,4.457,5.362,5.136,5.316,5.050
+-36.259,1.495,0.902,1.346,1.612,-36.259,1.790,1.207,1.650,2.254,0.834,0.134,0.592,-0.896,1.188,1.938,1.449,2.673,1.576,1.709,0.977,0.460,2.183,0.947,2.313,1.844,2.771,4.666,4.588,2.476,2.539,2.632,2.405,2.455,0.993,2.528,2.628,2.971,2.264,2.662,1.419,2.472,4.303,3.688,2.563,2.375,3.442,2.872,3.990,3.128,3.054,2.486,3.962,4.005,2.361,3.058,4.642,4.422,3.602,3.830,4.076,4.210,4.920,4.497,4.041,3.654,3.306,2.738,3.044,3.226,3.163,4.148,4.235,3.321,3.358,3.380,4.909,4.247,4.349,4.095
+-36.259,2.781,0.623,1.714,2.979,-36.259,1.744,-4.450,0.047,0.699,2.174,1.706,0.028,1.556,-0.111,1.771,3.112,2.642,2.430,2.876,1.853,0.582,1.471,1.638,2.138,2.754,2.657,4.577,4.409,1.388,1.924,1.533,1.467,1.753,2.548,3.039,3.290,2.518,1.552,2.697,2.004,3.122,4.672,3.970,3.052,3.065,3.108,2.573,4.214,2.834,2.610,2.711,3.631,4.056,3.513,3.049,3.513,3.021,2.841,3.178,3.017,4.006,3.176,3.616,3.219,2.525,3.638,3.288,3.376,4.226,3.775,5.257,5.473,4.022,4.341,4.157,5.133,4.063,4.525,4.305
+-36.259,1.083,1.877,0.214,-0.390,-36.259,0.961,0.803,1.353,1.955,1.262,-0.096,-0.175,0.628,1.155,2.782,3.558,3.057,2.667,2.478,0.640,0.440,1.583,1.562,1.450,1.835,2.800,3.848,3.676,1.962,0.639,1.435,1.449,1.510,1.974,2.070,2.378,0.576,1.266,3.124,2.524,3.033,4.269,3.571,3.140,3.165,3.187,2.924,3.701,2.870,2.508,1.932,3.502,3.853,3.230,3.488,3.997,3.554,2.862,3.528,4.101,4.003,3.689,4.370,4.524,3.123,3.825,3.381,3.594,3.993,4.478,5.266,5.426,4.774,3.562,4.306,4.396,4.294,5.233,4.551
+-36.259,2.994,0.546,2.219,2.230,-36.259,1.112,1.702,1.925,2.414,1.033,-0.796,0.834,1.436,1.677,2.065,2.458,2.850,2.338,1.819,1.958,1.375,2.344,2.527,2.345,2.072,2.888,4.653,4.700,2.686,2.846,2.441,1.791,1.374,1.404,1.781,2.287,1.960,1.777,2.917,2.800,2.394,3.902,3.930,4.078,3.568,3.303,2.487,3.252,3.364,2.597,3.624,3.634,4.077,3.687,3.132,3.340,3.125,4.087,4.236,4.502,4.264,4.253,4.460,3.671,3.724,4.197,4.280,3.145,3.097,3.451,4.667,4.448,3.862,3.925,3.991,5.253,4.115,4.972,4.189
+-36.259,0.644,-0.657,0.475,1.488,-36.259,0.627,1.920,2.280,1.711,1.971,1.005,-0.771,-0.441,1.454,2.451,3.172,1.585,2.055,0.953,0.542,1.344,3.166,2.620,1.795,0.915,1.837,4.808,4.778,2.042,1.916,1.623,2.442,1.236,1.631,2.105,2.690,2.606,2.279,4.088,3.694,1.721,3.812,3.625,2.543,4.050,2.815,3.169,3.719,2.775,3.203,2.989,4.658,4.938,3.451,3.404,3.740,3.907,3.653,3.722,4.110,4.174,4.320,3.342,3.441,2.782,4.233,3.816,3.044,3.093,3.850,4.556,5.027,3.994,3.773,4.438,4.942,4.869,5.099,5.527
+-36.259,1.914,2.722,0.672,0.744,-36.259,1.630,2.144,2.205,1.189,0.826,0.739,2.048,1.025,-0.118,0.193,2.113,2.043,2.026,1.521,2.506,1.025,2.478,3.177,1.255,2.492,2.987,4.817,4.746,2.797,2.795,2.213,2.839,2.109,1.076,1.999,2.650,2.453,1.393,3.659,3.140,3.129,4.523,3.977,3.290,4.212,3.764,2.158,3.444,3.111,2.770,2.814,3.430,3.106,2.438,2.963,4.088,3.757,2.744,2.685,3.090,3.966,3.730,4.207,3.904,3.973,3.306,3.286,3.357,4.137,4.124,4.174,4.284,4.720,4.296,3.858,4.641,5.123,4.466,4.777
+-36.259,1.401,1.859,1.225,3.067,-36.259,1.204,-0.202,0.374,2.362,2.559,1.586,0.288,1.834,-0.499,2.543,3.024,2.369,-1.170,1.955,1.868,1.030,2.722,2.247,2.371,1.930,2.823,3.936,4.185,3.081,0.977,2.146,1.424,0.602,1.121,2.345,2.302,1.919,1.770,2.430,2.228,2.887,4.004,2.859,1.443,2.831,2.700,2.775,2.443,1.881,1.849,1.985,2.890,3.358,3.454,3.823,3.266,3.234,2.861,3.075,4.587,4.128,3.618,4.118,3.859,3.364,3.032,3.103,2.694,3.229,3.931,4.541,4.787,4.202,4.008,4.127,4.904,4.619,4.909,5.233
+-36.259,0.746,0.862,0.426,-0.030,-36.259,2.124,0.706,0.973,-1.100,1.974,2.356,0.822,1.279,0.397,1.523,2.883,2.034,2.996,1.708,0.992,1.992,1.076,0.515,1.755,1.849,3.183,4.682,4.582,3.137,1.450,2.199,1.972,1.436,1.271,2.674,3.646,3.344,1.766,2.216,2.169,2.438,3.987,3.526,3.339,2.330,2.888,2.653,2.696,1.459,2.160,3.677,4.367,4.738,3.486,3.040,4.000,3.220,3.657,3.720,3.574,3.313,3.584,3.722,3.234,3.272,4.837,4.789,3.286,3.643,3.708,4.627,5.135,4.853,4.669,3.865,4.833,4.206,4.729,5.085
+-36.259,1.791,1.302,-1.298,-0.565,-36.259,-0.557,0.045,0.209,-0.145,-2.654,0.953,-1.397,0.070,-0.853,3.306,2.745,3.110,3.067,2.219,0.888,-0.029,2.242,2.636,2.159,1.242,2.196,4.245,4.408,2.363,1.632,2.400,2.187,2.498,0.629,3.108,3.546,3.479,1.871,2.150,2.387,2.277,3.572,2.653,2.379,2.660,2.348,2.742,3.308,2.579,1.890,1.314,3.421,3.992,3.076,2.245,3.333,3.514,3.410,3.600,3.098,3.631,4.456,4.002,3.210,2.991,3.265,2.652,2.477,3.626,4.256,5.469,5.649,4.984,4.660,3.718,4.742,4.136,4.780,4.699
+-36.259,1.836,2.280,1.739,0.970,-36.259,0.796,0.202,-0.483,0.255,2.609,1.509,-1.017,0.615,-0.218,1.562,1.781,1.387,2.065,0.052,0.752,2.080,2.857,2.772,2.450,1.272,3.147,4.618,4.909,3.476,1.361,2.576,1.998,1.047,1.863,1.955,3.278,2.516,2.027,3.358,3.595,3.390,3.822,3.226,3.749,3.893,3.698,2.883,2.765,1.963,2.135,3.216,4.304,4.236,3.619,3.398,3.838,3.827,2.782,4.289,5.418,4.684,3.997,4.172,4.142,3.096,2.819,3.026,2.244,3.587,3.664,3.867,4.194,3.953,4.245,3.433,5.233,4.391,3.861,4.080
+-36.259,2.873,1.758,2.513,3.235,-36.259,1.279,-0.044,0.759,0.755,2.104,-0.135,0.675,2.379,1.506,2.191,2.536,2.072,-0.443,2.001,1.549,-1.614,2.282,2.128,2.056,3.344,1.567,4.072,4.110,1.924,1.638,1.097,2.056,0.612,0.423,1.238,1.417,2.678,2.535,2.644,2.431,3.589,4.051,3.480,4.112,3.933,4.197,3.170,3.394,2.580,2.442,2.831,4.380,4.453,3.038,2.219,2.594,3.241,3.821,3.634,4.444,4.821,4.706,4.376,3.005,3.256,4.667,4.160,3.148,3.273,4.262,4.588,4.424,4.339,3.841,3.772,4.027,4.116,3.853,4.469
+-36.259,1.307,1.511,0.584,2.527,-36.259,-0.538,-1.270,-0.641,2.013,1.538,0.161,0.305,1.465,1.585,2.656,3.888,3.616,2.953,2.428,0.833,0.740,2.498,1.663,0.532,0.932,0.809,3.499,3.932,2.360,1.205,2.064,2.909,2.590,2.785,2.888,3.350,2.406,2.426,3.492,3.277,3.133,3.779,3.623,3.038,3.663,3.209,2.465,3.769,3.167,2.986,3.383,4.425,4.316,3.312,3.077,3.611,3.445,2.871,4.087,4.376,4.094,4.425,4.373,3.775,2.493,4.084,4.210,3.287,3.257,3.894,4.424,4.261,4.182,4.188,3.654,4.751,4.657,5.248,5.360
+-36.259,2.374,1.704,2.113,2.341,-36.259,1.503,-0.819,2.256,2.525,3.106,1.621,-0.612,1.648,1.543,2.177,3.043,0.283,1.059,1.265,0.666,0.828,2.587,1.564,1.566,2.065,1.723,4.116,4.437,3.053,1.588,2.456,2.103,0.757,-0.093,2.334,2.496,2.096,1.987,3.547,2.761,2.812,3.306,3.213,2.154,2.562,2.626,2.681,2.366,1.985,2.696,3.494,5.216,5.218,3.822,2.778,3.679,3.729,2.887,3.726,3.600,4.219,4.447,4.269,4.248,3.572,4.814,4.969,3.478,4.041,4.055,4.080,4.453,4.649,4.441,4.114,5.698,4.467,5.111,4.749
+-36.259,0.318,1.994,-0.190,1.853,-36.259,0.955,1.326,0.772,-0.095,2.670,1.867,1.675,-0.058,-0.848,1.430,-0.188,-0.848,1.570,1.240,1.062,0.397,1.390,1.135,1.002,1.534,1.750,4.404,4.542,2.614,1.972,0.563,0.340,0.225,1.078,3.254,3.407,2.999,1.821,2.373,2.186,2.250,4.233,3.648,2.115,2.846,3.193,2.788,3.591,2.499,2.721,3.330,3.927,4.337,3.811,3.296,3.318,2.907,3.101,3.075,4.147,3.900,3.528,3.859,3.231,3.812,4.319,4.302,3.230,3.985,3.725,4.356,4.554,4.124,4.554,3.791,5.148,4.250,4.283,4.214
+-36.259,2.385,2.051,0.651,0.443,-36.259,0.696,-2.295,0.026,-0.697,1.152,1.098,1.532,-0.174,0.548,1.742,1.871,1.127,-1.607,1.371,0.448,-0.231,1.959,1.597,1.713,2.813,2.709,4.689,4.957,3.460,3.418,2.148,2.156,1.774,2.809,2.230,2.694,2.789,2.412,2.551,2.631,2.372,3.485,2.953,2.868,3.172,2.855,1.678,2.090,2.873,3.932,3.391,2.980,3.992,3.898,3.666,3.942,3.206,2.971,3.059,3.943,4.922,4.276,4.009,3.882,4.156,3.931,4.535,3.467,3.136,3.488,4.626,4.807,4.162,3.453,4.028,4.858,5.093,4.833,4.633
+-36.259,1.686,0.952,0.994,2.331,-36.259,2.095,-1.893,0.501,2.472,2.921,0.977,0.289,0.954,0.747,1.732,3.605,3.007,1.551,2.820,2.260,0.646,1.397,2.086,1.515,1.397,1.661,4.419,4.655,2.549,3.056,2.104,2.582,1.740,2.789,2.968,3.265,2.390,1.651,2.950,2.435,3.185,2.822,2.456,2.946,3.315,2.858,2.810,3.092,2.564,3.430,2.610,3.139,3.572,3.088,2.720,3.711,3.219,3.904,4.500,4.919,4.750,4.383,3.320,3.957,4.575,3.900,3.686,3.776,3.320,3.615,4.619,4.982,3.800,4.280,4.295,4.507,5.769,5.147,4.603
+-36.259,0.847,-0.790,1.849,1.854,-36.259,0.117,0.437,0.879,2.584,1.707,0.163,1.372,0.983,0.985,0.820,3.948,-0.690,-0.137,1.757,2.565,-2.134,1.345,1.810,1.484,2.141,1.389,4.216,4.279,2.126,2.273,1.276,2.000,1.649,2.351,2.455,3.842,2.821,1.614,2.470,1.983,2.498,3.991,3.476,2.992,2.326,2.352,2.714,3.618,2.843,1.931,2.823,4.540,4.571,3.626,2.931,3.849,4.156,3.721,4.132,4.030,4.347,4.261,4.011,3.844,3.536,4.992,4.960,3.389,4.064,3.677,4.040,4.136,4.091,4.561,4.227,5.967,4.690,5.134,4.668
+-36.259,2.595,1.170,1.780,3.410,-36.259,1.820,0.698,-0.985,0.998,3.208,2.381,2.083,1.476,0.240,1.316,3.610,0.539,2.215,1.604,2.668,-1.235,2.260,1.966,1.682,1.790,2.470,4.555,4.668,2.951,1.334,2.797,2.325,2.009,1.809,1.277,3.127,2.042,1.971,3.383,1.898,2.353,3.964,3.390,2.394,2.564,2.839,1.625,2.374,2.902,2.959,3.185,3.987,3.520,2.995,3.335,4.213,3.825,3.185,3.492,4.548,4.353,3.736,3.892,4.069,3.430,3.367,3.686,3.695,4.084,3.794,3.837,4.132,4.652,4.600,5.007,5.586,4.782,4.786,5.031
+-36.259,-2.449,1.538,0.210,1.153,-36.259,0.187,0.158,0.171,3.172,1.140,1.704,0.318,0.833,1.752,1.500,2.219,0.723,1.815,2.009,1.749,0.843,1.697,1.564,1.110,2.591,1.647,3.919,4.069,2.194,3.341,1.852,2.802,1.258,2.226,1.851,2.360,2.159,1.309,2.973,2.812,3.331,3.468,3.597,3.295,3.208,2.633,2.132,2.281,2.722,3.259,3.887,3.982,4.173,3.898,3.868,3.664,3.655,3.767,3.967,4.698,4.897,4.647,3.896,3.522,3.175,4.064,3.744,2.881,3.535,3.337,4.438,4.766,4.027,4.046,3.711,4.484,4.050,4.698,4.943
+-36.259,1.084,2.351,1.621,2.494,-36.259,0.803,2.196,3.412,1.956,3.077,-0.256,0.304,1.523,1.960,0.747,1.271,0.938,1.517,1.286,1.627,0.539,1.368,-0.134,1.553,1.754,2.815,4.496,4.437,2.269,3.175,1.062,2.478,1.405,1.802,2.598,3.033,2.351,1.857,2.492,2.174,2.952,4.320,3.682,1.090,2.424,1.857,1.337,2.450,1.966,2.465,3.157,3.825,4.057,3.111,2.477,3.450,2.373,3.722,3.347,4.368,4.090,3.807,3.609,3.615,3.873,3.879,3.756,3.163,3.799,3.668,3.925,4.086,3.963,4.551,3.789,5.196,4.338,4.537,5.425
+-36.259,1.169,1.459,0.625,1.727,-36.259,1.047,1.237,0.202,0.752,2.372,-0.179,1.752,0.706,-1.419,2.299,2.148,0.467,2.366,1.648,2.420,-1.281,1.927,2.640,2.525,2.709,2.944,4.534,4.543,3.053,2.505,2.434,2.110,1.910,2.212,3.137,3.827,3.056,2.341,2.689,2.469,2.469,4.062,3.314,2.467,2.566,1.920,2.352,3.339,2.738,1.992,3.168,4.053,4.144,3.352,2.723,4.439,4.327,2.680,3.752,4.226,3.707,3.711,4.330,3.888,3.311,3.442,3.731,3.030,3.709,2.951,3.724,4.004,4.078,4.092,4.254,5.527,4.614,4.118,4.658
+-36.259,0.036,1.264,0.041,0.127,-36.259,1.018,1.768,2.016,-1.213,1.605,-1.676,-1.622,-0.500,0.302,2.452,3.409,3.122,2.534,2.061,1.665,0.787,2.777,2.974,1.832,1.420,1.996,3.494,3.960,2.402,1.821,2.304,2.135,2.050,1.683,2.100,2.692,1.030,1.630,3.467,2.183,3.016,2.784,3.025,3.054,3.217,2.845,1.626,2.422,2.548,2.701,2.835,3.539,3.409,3.159,3.038,2.827,2.908,3.751,3.857,4.402,4.292,4.261,4.453,4.325,3.952,3.695,3.345,3.140,3.045,3.588,3.537,3.860,4.349,4.578,3.914,4.844,4.110,4.749,4.038
+-36.259,-0.271,-0.604,-0.036,2.681,-36.259,0.484,-0.545,1.872,0.140,2.059,0.289,1.573,1.960,-0.849,1.885,3.259,2.025,1.866,1.884,0.948,-0.137,1.287,2.657,1.992,2.609,3.064,4.858,4.917,3.556,2.595,2.641,2.990,2.554,1.956,3.407,3.457,3.487,3.043,2.797,2.374,3.388,3.852,4.166,3.727,3.295,2.674,3.009,3.153,2.302,3.217,3.331,3.732,3.724,3.065,2.955,3.823,3.466,3.472,3.341,3.235,3.116,4.115,3.557,3.441,4.196,3.825,3.755,3.050,3.419,4.177,4.518,4.411,4.042,4.518,4.081,4.933,5.080,4.778,4.404
+-36.259,3.286,1.327,1.602,3.177,-36.259,1.811,-0.351,0.455,1.596,2.127,1.028,-1.388,1.284,-0.135,1.140,1.547,0.746,1.642,1.255,1.700,1.417,2.111,2.581,1.984,1.586,2.512,4.459,4.318,2.394,0.362,1.913,2.525,2.245,0.641,3.282,3.440,2.911,2.045,2.684,3.049,3.444,4.939,4.442,3.318,3.094,2.573,2.789,3.976,3.437,2.381,2.274,3.550,3.925,3.640,3.714,3.797,3.514,3.202,3.153,3.419,3.916,4.049,3.769,3.537,3.298,3.348,3.375,3.525,3.404,3.264,4.169,4.517,4.054,3.560,4.065,4.135,4.674,4.871,4.033
+-36.259,2.461,-1.687,0.808,1.389,-36.259,0.748,0.762,2.010,-0.634,2.673,1.562,-0.585,0.349,1.554,2.046,3.636,3.705,3.005,2.817,2.736,0.872,1.947,2.595,1.100,1.809,2.471,4.754,4.873,3.287,3.284,3.273,2.813,2.782,1.518,3.451,3.761,2.997,1.671,4.103,3.257,3.366,4.421,3.883,2.921,3.654,3.429,2.968,4.408,3.418,2.064,3.464,4.235,4.279,3.429,2.283,3.679,3.930,3.668,3.527,4.404,3.677,3.530,3.808,4.320,2.786,3.878,3.912,3.283,4.257,3.854,3.257,3.410,3.181,4.396,3.826,4.785,4.514,4.666,4.324
+-36.259,-0.470,1.620,0.359,-0.225,-36.259,-0.168,1.354,0.164,2.541,1.249,1.254,2.316,1.230,-1.754,2.656,2.304,0.807,1.701,1.333,1.721,0.872,3.018,2.129,2.078,2.996,2.929,4.830,4.755,2.534,3.801,2.308,2.446,1.962,1.209,2.600,3.322,2.148,1.627,2.069,2.830,3.087,4.502,3.817,2.935,3.409,2.903,1.901,2.549,2.203,2.824,3.004,2.861,3.543,3.679,3.413,3.006,3.111,3.408,4.227,4.855,4.487,4.440,3.327,3.182,3.919,3.602,3.164,3.039,3.980,3.698,4.234,4.463,4.016,4.408,4.176,4.869,4.462,4.094,5.034
+-36.259,2.501,-0.207,0.144,1.232,-36.259,1.140,-0.662,-0.468,0.464,1.829,2.001,2.176,-0.393,1.663,1.878,2.212,0.588,-0.853,1.511,1.776,0.646,0.760,2.445,1.288,1.089,2.760,4.599,4.439,2.269,2.717,2.199,2.828,1.294,0.662,1.622,2.678,2.141,1.818,1.239,1.686,4.093,4.352,4.193,3.930,2.986,2.342,3.270,4.065,2.443,2.501,3.470,4.564,4.691,3.580,2.669,4.645,4.211,3.706,4.114,4.609,4.354,4.218,4.256,4.072,4.238,4.566,4.260,3.367,3.511,3.833,4.266,4.262,3.786,3.486,4.400,4.830,4.395,5.108,5.004
+-36.259,1.105,2.168,0.800,2.945,-36.259,2.240,2.023,1.978,-0.762,2.396,1.170,0.991,1.023,0.521,2.441,1.984,2.043,2.103,1.445,1.280,-2.106,1.719,2.373,2.175,1.734,2.245,4.364,4.299,2.334,2.115,2.341,2.740,1.691,1.732,3.283,3.923,3.562,1.540,2.383,1.967,2.212,4.118,3.538,2.424,1.936,3.099,3.532,4.540,3.791,1.775,2.656,4.271,4.137,2.785,3.025,3.778,3.143,3.740,3.896,4.676,3.844,3.840,3.761,4.003,2.801,3.504,3.679,3.655,4.466,3.932,5.449,5.680,4.311,4.242,3.690,4.931,4.245,3.497,3.681
+-36.259,-0.567,1.245,1.823,1.937,-36.259,2.212,-1.468,1.837,1.620,0.371,0.236,2.458,0.878,1.635,2.523,2.849,3.250,1.859,2.847,2.289,-0.948,1.544,1.907,1.166,3.297,2.825,4.479,4.502,2.526,2.063,1.342,1.653,0.844,2.289,2.124,2.395,2.205,1.776,1.935,2.092,3.082,4.497,4.279,2.669,2.992,2.036,1.801,1.619,0.638,2.282,3.170,4.521,4.716,3.534,2.858,4.241,4.080,3.018,3.132,4.705,4.315,3.110,3.364,3.686,4.223,4.859,4.516,3.028,2.562,3.672,4.568,4.673,3.989,3.556,3.730,5.532,5.386,4.483,4.598
+-36.259,0.930,-1.157,1.447,2.939,-36.259,1.064,-0.716,1.204,2.546,1.491,0.812,0.438,0.857,1.357,2.475,2.476,2.910,-0.144,1.719,2.004,-0.025,2.066,2.319,2.405,2.130,2.879,4.605,4.859,2.893,3.925,3.596,1.944,1.442,0.723,1.974,2.087,2.101,1.004,3.114,2.776,2.864,4.001,3.354,2.688,3.429,3.835,3.087,2.658,2.534,2.775,3.394,4.280,4.092,2.340,2.365,2.849,2.337,3.057,3.594,3.092,4.088,3.610,3.347,3.570,3.212,4.062,4.225,3.353,3.531,4.272,5.108,5.360,4.010,4.355,3.928,4.884,4.460,4.596,4.433
+-36.259,0.834,0.559,1.540,2.715,-36.259,-0.563,0.702,1.925,2.436,1.140,0.910,0.762,1.540,1.549,1.922,2.745,2.399,2.327,2.003,1.627,0.554,2.505,2.914,1.986,3.134,1.864,4.368,4.585,2.869,3.321,2.575,2.837,2.610,1.556,2.234,2.680,2.392,2.058,2.324,2.027,3.624,4.094,3.468,2.727,3.227,3.804,2.572,3.504,2.911,2.068,2.673,3.915,4.383,3.573,3.770,3.628,3.424,3.910,4.024,4.761,5.191,4.614,4.167,4.591,3.679,4.310,3.800,3.535,4.142,4.448,4.931,5.190,4.549,5.006,3.659,4.611,4.415,4.619,4.437
+-36.259,1.990,0.403,1.281,2.593,-36.259,1.260,1.039,-1.538,2.141,2.366,0.812,1.391,1.311,0.131,2.139,0.746,-0.060,0.080,1.506,1.979,0.324,2.727,2.179,2.066,2.704,2.609,4.641,4.718,2.562,2.548,2.505,1.034,1.461,2.321,3.047,3.489,3.094,1.521,3.026,2.129,2.121,3.768,3.252,3.143,2.825,3.069,2.849,3.897,2.635,2.678,3.014,3.571,3.797,2.645,2.533,3.443,3.641,3.255,3.397,3.569,3.899,3.267,4.255,4.185,4.559,4.181,3.704,3.061,3.753,3.976,4.299,4.218,4.105,3.953,4.114,4.626,4.740,4.630,4.518
+-36.259,2.349,2.944,1.249,0.780,-36.259,1.143,1.994,1.540,2.071,2.447,1.145,0.547,0.265,0.217,1.692,3.201,2.808,0.526,1.714,1.615,0.078,-1.243,1.219,1.384,1.030,1.620,3.686,3.906,2.616,0.420,0.925,2.347,1.394,1.478,2.318,2.867,2.852,1.554,2.420,2.245,3.562,4.565,4.284,3.644,3.355,2.568,2.176,2.735,2.183,3.410,2.854,4.179,4.578,3.974,3.091,3.545,3.649,3.915,3.264,2.998,4.099,3.328,4.456,3.947,2.813,4.142,3.543,2.635,4.227,3.596,3.861,4.266,3.644,4.505,3.562,3.844,4.108,4.643,5.281
+-36.259,0.608,1.509,-1.647,1.604,-36.259,1.383,1.363,1.918,0.040,1.400,1.309,1.872,1.042,0.897,0.272,1.611,0.114,0.017,1.139,1.579,0.818,1.613,2.654,2.360,1.186,2.234,4.503,4.425,2.555,2.615,1.202,2.704,0.888,1.827,1.445,2.383,2.925,2.056,2.286,1.505,2.219,4.454,3.835,2.764,3.258,3.258,2.803,3.749,2.745,2.671,2.998,3.511,4.026,3.583,2.464,4.308,4.200,3.463,3.747,3.921,3.947,3.652,4.043,3.884,4.196,3.875,3.934,3.191,3.733,3.355,4.513,4.690,3.797,4.480,4.292,5.225,4.939,5.385,5.146
+-36.259,1.326,1.272,0.769,3.403,-36.259,2.447,-0.892,1.484,0.280,2.725,1.024,0.272,2.706,0.122,3.010,2.935,3.120,2.062,2.958,1.686,1.264,2.270,0.636,1.678,2.687,2.812,4.611,4.651,2.857,1.712,1.945,3.065,2.156,2.021,1.778,2.912,2.717,2.171,2.766,2.817,2.537,4.521,3.756,2.889,3.689,3.506,1.972,1.882,2.652,3.585,2.702,4.160,4.569,3.423,3.278,3.345,3.024,2.829,3.563,4.220,3.970,4.623,3.461,3.485,3.749,4.303,3.775,3.621,2.977,3.450,3.721,4.302,3.565,4.027,3.973,5.244,4.743,5.333,5.613
+-36.259,1.548,1.796,0.683,-3.352,-36.259,1.170,1.747,0.719,1.119,2.389,1.958,0.050,0.384,-0.339,1.138,1.095,1.941,2.760,2.464,1.745,-0.412,2.388,2.450,2.654,2.796,2.041,4.043,4.554,3.269,2.510,3.291,1.986,2.284,1.226,2.031,2.202,2.171,1.972,3.101,3.123,3.313,3.607,3.694,3.492,3.357,3.109,2.650,3.833,2.905,2.344,3.184,3.941,3.663,3.586,3.360,3.889,3.793,3.943,4.008,4.411,3.834,3.460,3.640,2.740,2.683,3.238,2.855,2.732,3.461,3.192,4.915,5.193,4.202,3.052,4.106,4.647,4.321,4.844,5.207
+-36.259,3.480,3.846,2.056,1.445,-36.259,2.506,2.376,2.446,1.503,2.422,1.774,-0.430,-0.380,1.252,1.195,1.814,0.308,1.922,1.086,1.038,0.156,1.579,2.045,1.600,1.919,1.837,4.383,4.573,2.685,1.950,2.116,2.435,1.187,0.148,2.182,1.949,2.079,2.315,3.339,3.071,3.405,5.231,5.213,3.572,3.779,3.599,3.149,3.317,2.476,2.919,2.973,3.361,3.245,3.039,2.806,3.922,3.779,2.750,3.762,4.150,4.493,3.664,4.002,4.002,3.812,3.328,3.818,3.372,4.007,3.612,4.396,4.376,3.294,3.235,3.265,4.611,4.416,4.092,4.460
+-36.259,2.163,2.771,1.748,-1.272,-36.259,0.153,1.460,1.350,0.029,1.635,1.420,1.099,0.359,2.118,0.748,0.915,1.948,1.161,1.753,1.223,0.396,0.466,1.360,1.455,1.411,2.470,4.414,4.522,2.550,1.831,2.540,1.036,0.281,0.073,2.416,3.237,2.425,2.049,3.024,2.311,2.483,3.783,3.881,3.453,2.918,1.609,2.267,3.059,2.333,2.272,2.810,3.729,3.736,3.089,2.776,3.545,3.751,3.554,3.884,4.889,4.479,3.693,4.047,3.981,4.367,4.006,4.140,3.613,3.865,3.700,4.118,4.161,3.571,3.689,4.249,4.822,4.424,4.262,5.486
+-36.259,2.929,2.461,1.792,2.751,-36.259,1.161,0.472,1.867,2.310,2.743,2.364,0.537,1.926,2.161,1.265,1.446,1.622,0.708,2.515,2.076,1.209,2.124,2.595,1.604,2.766,2.644,4.436,4.415,2.700,2.191,2.691,3.326,1.624,2.018,2.721,3.410,3.045,2.074,1.821,2.498,3.128,4.533,3.848,3.440,3.344,3.345,3.004,3.845,3.229,3.079,3.212,3.704,3.273,2.425,2.484,4.124,4.043,3.078,3.888,4.683,4.069,3.124,4.663,4.620,2.528,3.454,3.644,3.194,3.938,4.014,4.887,5.145,4.553,4.514,4.023,4.418,4.560,4.082,4.615
+-36.259,0.349,0.174,0.941,2.028,-36.259,0.634,0.539,0.815,0.619,2.093,-3.288,0.632,1.113,2.261,3.448,2.865,2.709,2.685,1.995,2.020,0.869,1.647,2.571,2.448,2.004,2.903,4.453,4.465,2.822,3.417,2.788,2.745,1.976,1.530,3.108,3.457,3.013,1.988,4.055,3.211,2.980,4.663,3.931,3.106,3.718,2.852,2.708,3.760,3.066,2.237,3.029,3.446,3.528,2.984,2.467,3.105,3.717,3.459,3.766,4.270,4.555,3.845,4.719,4.360,3.326,3.943,3.742,3.045,3.274,4.026,4.267,4.609,4.907,4.688,4.381,4.842,5.116,5.846,5.276
+-36.259,1.448,0.698,1.233,-1.741,-36.259,-0.057,-0.940,-0.059,-0.287,1.806,1.068,-0.140,-0.250,1.488,1.916,1.916,2.610,2.734,1.787,2.540,1.349,1.924,-0.337,1.557,1.761,3.448,4.515,4.141,2.182,2.829,1.738,2.507,1.917,1.118,2.079,2.094,2.343,2.055,2.896,2.940,2.032,3.821,3.281,2.755,3.650,3.640,1.917,2.431,2.689,3.023,3.219,3.683,3.307,2.791,3.061,4.005,2.738,3.997,4.111,3.932,4.472,4.481,3.454,3.767,3.996,3.803,3.671,3.355,2.690,4.251,4.325,4.543,4.899,4.446,4.463,5.301,5.170,5.079,5.403
+-36.259,3.052,-0.811,2.186,-1.842,-36.259,0.344,0.886,2.758,-1.383,2.788,2.105,1.678,-1.503,0.747,2.822,1.910,2.545,2.870,1.171,1.042,-0.287,1.795,1.097,1.794,2.519,2.481,4.427,4.473,2.236,2.555,1.921,2.287,2.218,1.608,2.474,3.161,2.994,2.205,3.629,2.432,3.340,5.001,4.926,4.018,4.028,3.609,3.153,3.289,2.302,2.269,2.260,3.493,3.973,2.834,2.705,4.190,4.232,3.565,3.597,4.201,3.940,3.906,4.394,4.377,3.608,3.960,3.792,3.271,3.286,4.180,5.302,5.366,4.463,3.808,4.185,4.907,4.492,5.155,5.065
+-36.259,1.604,1.576,1.656,2.351,-36.259,2.214,2.190,3.142,-3.928,2.009,1.475,1.793,1.152,0.614,1.431,0.456,-1.846,0.536,0.464,1.564,1.074,2.221,2.574,1.917,1.488,2.492,4.594,4.669,2.595,1.023,1.872,2.035,2.028,1.576,1.898,2.544,2.408,1.829,3.431,2.994,3.239,4.654,4.383,3.817,3.669,3.407,3.324,3.555,2.120,1.761,2.808,4.040,4.283,3.328,3.039,3.052,2.876,3.667,3.432,4.498,4.833,4.045,4.463,4.189,4.091,4.187,3.921,3.276,4.485,4.303,4.299,4.592,5.193,5.209,3.748,4.802,4.828,4.973,4.371
+-36.259,0.569,1.925,0.910,-0.608,-36.259,1.611,1.246,0.232,-1.068,1.852,1.623,1.048,0.712,0.469,2.852,3.031,2.598,2.148,1.046,1.007,0.040,2.303,2.936,1.596,1.874,2.654,3.788,3.833,1.446,3.766,2.373,2.373,1.648,1.488,1.347,2.312,2.203,1.425,2.278,2.333,2.937,3.280,3.280,3.811,2.586,2.860,2.553,1.661,2.241,2.483,3.715,2.946,3.070,4.069,3.901,3.462,3.456,3.525,3.651,4.696,4.496,3.859,3.954,3.322,4.266,3.556,3.315,2.561,3.320,3.404,3.377,3.754,3.925,3.292,2.681,4.560,4.488,4.982,4.802
+-36.259,1.743,0.612,1.050,0.317,-36.259,1.064,-0.754,2.143,1.110,2.973,1.909,0.581,-1.132,1.350,2.196,2.521,1.339,1.439,1.753,2.335,0.315,1.157,0.938,1.532,2.422,1.774,3.444,3.699,2.561,2.498,1.768,2.228,1.998,1.192,1.944,2.354,2.279,1.717,3.103,2.468,1.768,3.868,3.416,2.461,2.894,2.828,1.894,3.148,1.557,2.942,3.079,3.727,4.088,3.933,3.944,3.995,2.884,4.069,3.638,3.837,3.703,4.559,3.572,3.988,3.732,4.182,4.039,3.614,3.056,3.255,3.626,3.707,3.613,3.521,3.354,4.752,4.489,3.898,3.396
+-36.259,1.625,2.204,0.658,-0.583,-36.259,1.737,1.093,2.318,0.804,-0.188,-1.847,-0.129,-0.159,0.679,2.569,3.190,2.627,1.490,2.267,2.644,1.508,2.022,0.909,2.079,2.026,3.333,4.811,4.815,3.514,2.479,1.289,1.147,2.023,1.469,1.711,2.650,2.309,0.501,2.224,2.323,2.846,2.955,2.690,3.260,2.703,2.090,2.949,3.551,2.358,3.196,2.709,4.225,4.424,2.917,2.929,3.134,3.472,3.786,3.878,3.514,3.596,3.482,3.280,3.333,3.058,4.849,4.539,3.285,3.207,4.428,4.120,4.277,4.909,3.909,4.049,4.357,4.619,5.417,5.263
+-36.259,2.187,0.948,0.256,3.025,-36.259,1.114,1.253,0.888,0.129,1.546,1.139,-1.138,1.807,0.692,2.503,2.961,0.454,0.665,0.539,1.104,-0.281,1.897,1.667,1.037,2.367,2.151,4.206,4.283,2.219,1.850,1.042,1.642,0.697,1.153,1.535,1.388,1.683,0.578,2.748,2.260,3.206,3.930,2.659,2.634,2.695,2.474,1.311,1.933,1.365,3.264,3.825,3.904,4.188,4.058,3.577,2.898,3.051,3.865,3.715,3.469,3.668,4.403,3.399,3.434,3.769,3.574,3.618,3.526,2.958,3.196,3.889,3.795,3.134,3.614,4.244,4.716,4.897,5.810,4.743
+-36.259,-0.361,0.761,-0.042,1.690,-36.259,2.726,0.545,1.044,2.755,3.673,1.434,1.315,1.867,1.514,1.431,1.026,2.764,1.918,1.920,2.315,-0.951,3.036,1.823,-0.486,1.218,2.445,4.437,4.485,2.618,2.263,1.437,2.798,1.555,2.181,3.729,3.879,2.866,1.358,3.454,2.468,3.423,4.271,3.371,3.308,3.752,2.618,2.263,3.933,2.818,3.767,2.842,3.942,4.153,3.566,3.133,4.136,4.280,3.836,4.041,4.429,4.475,4.719,4.640,4.207,4.243,4.420,4.267,4.056,2.921,3.700,4.725,4.742,4.209,4.365,4.376,5.279,4.809,4.761,4.386
+-36.259,0.685,-0.263,0.001,1.543,-36.259,-0.471,0.769,1.985,2.199,0.428,-0.279,-2.037,0.020,2.046,0.206,2.862,2.471,0.405,2.381,0.870,0.821,2.010,0.972,1.863,2.606,2.257,4.267,4.262,1.987,2.294,1.598,2.762,2.317,1.712,3.090,3.279,3.244,2.466,1.800,1.966,3.424,4.525,3.454,3.095,2.565,3.270,3.220,3.381,2.125,2.471,2.889,3.854,4.027,3.690,3.811,4.161,4.108,4.038,4.396,3.941,4.362,4.535,4.005,4.217,3.676,3.516,3.319,2.511,3.119,3.660,3.925,4.079,4.260,3.991,3.814,4.422,4.271,4.432,4.698
+-36.259,1.764,1.260,-0.232,1.901,-36.259,1.071,0.284,1.101,1.802,0.955,1.911,0.204,0.490,-0.665,2.382,2.773,-0.894,1.782,-1.332,0.083,-0.024,0.943,1.572,1.017,0.323,1.850,4.421,4.411,1.802,1.546,0.860,1.225,0.885,2.011,3.299,3.525,2.919,1.360,1.974,2.966,2.995,4.065,3.697,3.001,3.248,1.679,1.149,2.491,3.147,1.900,1.094,3.229,3.591,3.572,2.909,3.365,3.156,2.178,2.825,3.986,3.801,4.000,3.635,3.980,3.329,3.393,3.632,3.252,3.927,3.556,4.194,4.545,4.070,4.618,4.381,5.134,5.354,5.536,5.452
+-36.259,2.699,2.691,2.141,1.271,-36.259,2.516,0.679,-0.338,2.467,3.467,2.448,2.367,1.536,0.121,2.343,1.271,0.585,1.006,-0.014,2.019,0.860,2.727,2.647,2.484,3.146,2.723,4.940,4.802,2.309,2.284,2.312,2.389,1.462,1.965,1.683,2.808,1.583,1.951,3.250,2.195,3.453,2.758,2.610,3.543,3.062,3.605,1.817,3.719,3.285,2.680,2.804,3.665,3.827,2.926,2.946,3.572,3.091,3.425,3.466,4.080,3.607,3.603,3.988,4.005,3.412,3.674,3.431,2.897,3.169,4.016,4.516,4.775,4.099,3.756,3.994,4.696,4.644,4.673,4.979
+-36.259,-0.792,2.248,0.231,0.550,-36.259,1.514,0.986,1.306,-0.532,2.580,0.640,0.828,-1.644,1.702,2.339,2.024,1.709,0.669,1.541,2.122,-1.539,1.204,1.506,1.509,2.304,2.473,4.571,4.565,2.497,2.277,2.077,2.679,2.664,1.246,3.371,3.367,2.739,1.679,2.405,2.149,2.909,4.160,3.511,2.895,3.025,3.520,2.419,3.314,2.195,2.480,2.970,3.880,3.881,3.364,3.258,3.838,3.981,3.213,3.602,3.955,3.777,3.750,4.537,4.516,3.427,4.160,4.277,3.393,3.288,3.680,4.195,4.256,3.971,4.628,3.878,4.435,3.954,3.984,4.999
+-36.259,1.612,2.339,-0.859,2.698,-36.259,1.931,1.339,0.031,-0.353,0.643,1.712,-0.047,1.869,1.326,1.536,1.030,2.021,3.286,1.286,1.731,-0.107,1.740,1.825,1.709,1.461,2.625,4.231,4.383,2.844,3.004,2.401,0.988,1.953,0.404,1.171,1.597,1.060,1.253,3.502,2.937,2.834,3.400,3.349,3.073,2.678,2.467,2.240,3.114,2.703,1.665,3.335,4.258,4.226,3.491,2.070,2.597,3.014,3.763,3.508,4.858,4.506,3.517,3.194,2.921,2.780,3.741,3.761,2.881,3.312,4.107,4.561,4.490,4.463,2.654,3.610,4.622,4.094,3.969,4.251
+-36.259,2.569,1.412,1.146,0.475,-36.259,-0.144,-0.147,0.952,2.531,1.295,0.213,-1.383,0.822,0.082,1.786,1.924,2.925,1.941,0.632,1.742,2.029,3.973,3.828,1.921,2.055,2.381,3.760,3.960,2.216,2.854,2.261,3.604,3.200,0.697,1.491,2.171,2.113,1.952,3.124,2.342,3.605,3.098,3.463,4.469,3.250,3.669,3.532,2.829,2.425,2.156,2.054,3.115,3.226,2.753,2.641,2.824,2.796,2.924,3.380,4.550,3.811,3.425,3.398,3.474,3.151,3.808,3.782,3.167,4.023,4.202,4.714,4.564,3.738,3.790,3.770,4.962,4.079,4.265,4.493
+-36.259,1.858,2.885,0.868,0.611,-36.259,2.033,2.085,0.322,1.518,0.895,0.833,1.430,1.337,1.123,2.789,1.454,2.638,2.201,2.057,1.457,1.083,1.701,1.866,2.413,1.810,2.483,4.594,4.793,3.131,2.605,2.860,2.034,0.865,1.877,2.830,3.299,3.041,2.267,2.792,2.421,3.033,4.170,3.185,2.960,3.553,1.920,1.670,2.373,2.335,1.750,2.420,3.711,3.540,3.482,3.231,3.934,4.073,3.499,3.534,3.584,3.723,3.224,4.332,4.337,3.098,3.520,3.519,3.340,3.679,3.452,4.077,4.139,3.867,4.261,4.018,4.769,4.655,4.734,4.602
+-36.259,2.165,1.458,0.661,1.307,-36.259,2.106,-0.280,1.013,1.584,1.698,1.202,1.663,0.790,0.791,2.582,2.013,2.022,1.702,1.019,1.174,1.502,2.670,2.232,1.709,1.007,2.031,4.739,4.724,1.884,-0.236,1.153,1.213,0.869,0.581,2.209,2.524,2.498,1.618,3.555,3.667,3.778,3.975,4.341,4.190,3.815,3.097,2.572,2.819,2.201,2.725,2.628,3.785,3.749,2.933,3.464,3.973,4.183,3.762,3.739,4.663,4.434,4.080,4.894,4.544,3.016,4.126,4.102,3.367,3.401,4.021,4.665,4.723,4.364,4.493,4.237,4.858,4.286,5.070,4.729
+-36.259,1.263,0.700,-1.004,3.231,-36.259,1.133,1.704,2.500,2.121,2.782,-0.227,1.320,2.313,1.085,2.782,2.185,2.406,2.109,2.444,1.730,-0.271,2.117,0.332,1.393,2.048,1.678,4.464,4.612,2.425,3.374,2.162,1.897,0.091,2.016,2.278,2.815,2.343,1.458,3.071,3.002,3.492,4.974,4.441,3.946,3.652,2.319,2.664,2.712,1.939,2.951,2.900,3.738,3.890,2.933,3.164,3.497,2.812,2.875,3.254,4.336,4.040,4.259,3.095,3.011,3.805,4.789,4.774,3.338,3.658,3.760,4.246,4.273,4.070,3.852,3.816,4.740,5.069,5.790,5.855
+-36.259,0.921,0.834,0.112,1.951,-36.259,-0.158,-0.077,0.863,2.428,0.135,1.890,-0.763,0.393,1.537,2.058,2.627,1.808,2.156,2.262,1.048,1.734,2.694,2.388,1.439,1.816,2.349,4.556,4.684,3.064,1.996,2.439,3.550,3.171,2.336,3.307,3.027,2.836,1.917,2.536,3.226,3.877,4.689,4.177,4.413,3.707,3.423,2.652,3.581,3.173,3.162,3.137,3.483,4.110,3.432,2.764,3.575,3.168,3.297,3.831,4.369,4.023,3.313,3.782,4.069,2.810,3.670,3.564,3.607,4.032,4.063,4.648,4.586,4.463,3.867,3.307,4.932,4.369,4.688,5.336
+-36.259,3.055,2.754,2.541,0.218,-36.259,1.142,1.599,2.097,1.826,0.831,0.816,-0.976,0.732,1.956,2.770,3.070,2.430,1.858,1.909,1.664,0.961,2.283,1.826,1.361,2.522,3.018,4.775,4.701,2.153,3.051,1.886,2.950,1.690,2.334,3.087,3.144,3.255,2.693,2.885,1.974,3.325,4.547,3.130,2.876,3.464,3.097,2.604,2.799,2.467,2.837,3.449,4.100,4.100,2.873,1.748,2.856,2.688,3.406,3.553,3.727,3.689,2.998,3.657,3.326,3.092,4.038,3.829,3.395,2.878,3.707,4.335,4.414,4.182,4.658,4.726,5.745,5.232,4.901,5.523
+-36.259,2.295,2.378,-1.544,2.394,-36.259,1.360,1.012,0.468,1.376,1.487,1.605,-0.085,2.026,2.002,0.409,1.433,-0.800,2.303,1.040,1.563,0.394,1.618,0.989,1.579,3.318,3.018,4.834,4.855,2.841,3.339,3.102,3.138,3.297,1.576,0.385,2.113,2.306,2.483,2.277,2.646,3.043,3.682,3.432,1.953,2.773,3.223,2.907,2.799,2.531,3.336,3.746,4.615,4.510,3.346,3.032,3.330,3.121,2.476,3.291,3.181,3.443,3.650,3.507,3.378,3.444,4.349,4.418,3.495,2.906,3.586,4.470,4.632,4.359,3.655,4.247,5.265,4.733,4.426,4.263
+-36.259,0.973,0.454,1.722,1.556,-36.259,1.744,0.741,1.593,2.510,1.065,-0.453,1.555,-1.344,0.637,2.051,1.283,-1.622,1.527,1.976,1.073,-0.160,2.620,2.204,1.991,3.117,3.317,5.062,5.035,3.106,2.418,1.826,1.967,1.110,1.588,1.717,1.687,1.737,1.147,1.929,2.395,2.604,4.294,4.005,3.287,2.198,2.326,1.870,3.027,2.168,2.815,3.189,4.354,4.394,3.247,2.851,3.988,3.896,3.457,3.265,4.255,3.927,3.306,3.783,3.736,3.326,4.277,4.265,3.375,3.718,4.164,5.152,5.331,4.612,4.298,4.072,4.740,4.363,3.910,4.749
+-36.259,-0.867,1.994,-0.716,1.307,-36.259,1.483,0.914,1.420,1.343,2.123,0.709,0.693,0.314,2.115,1.599,0.997,2.327,1.086,2.239,0.753,1.502,2.808,2.521,1.257,2.660,2.831,4.745,4.909,3.298,2.533,2.250,2.496,2.400,1.030,1.718,2.188,2.559,2.597,1.771,2.190,2.919,3.489,3.075,2.803,2.422,2.948,2.367,2.912,2.719,2.990,3.000,2.955,2.840,2.966,2.994,3.811,3.442,3.659,3.631,4.378,3.718,4.171,4.644,5.017,4.701,3.857,3.038,3.368,2.673,3.588,4.842,5.106,4.023,3.006,4.007,5.540,4.464,4.365,4.695
+-36.259,2.002,-1.350,1.956,2.232,-36.259,0.479,-1.149,0.241,2.554,2.110,1.223,-0.776,1.370,1.109,1.595,2.700,2.377,1.902,1.945,1.231,1.275,1.531,1.889,0.537,0.804,2.404,4.391,4.492,2.665,1.579,2.142,2.093,2.673,2.367,3.252,3.509,3.135,2.894,2.815,2.538,2.641,3.818,3.504,3.890,2.721,3.197,3.694,3.270,2.491,2.401,2.723,4.278,4.452,3.327,2.480,3.161,3.553,2.778,3.231,5.022,4.574,3.419,3.947,3.789,3.021,4.051,3.942,3.117,3.208,3.981,4.459,4.217,4.388,3.544,3.914,5.321,4.510,5.063,4.751
+-36.259,3.097,2.353,0.814,1.359,-36.259,1.992,0.798,-0.839,3.502,1.061,-0.098,0.416,-1.979,-1.369,1.236,2.704,2.579,2.086,1.302,0.694,0.708,2.247,2.712,1.589,2.316,1.444,4.371,4.413,2.322,3.013,2.728,3.056,2.826,1.223,2.130,3.080,1.392,1.194,2.789,2.440,1.921,3.872,3.421,2.726,2.635,2.397,2.599,2.730,3.195,3.726,3.767,3.859,4.056,3.718,3.213,3.481,2.920,2.783,3.498,4.046,3.851,3.526,4.323,4.376,3.452,3.671,3.755,3.251,3.900,3.160,4.280,4.436,4.053,3.745,3.538,4.724,4.558,5.802,4.639
+-36.259,3.124,3.079,2.308,2.052,-36.259,2.340,2.455,1.395,3.122,2.856,1.900,1.004,0.678,1.638,1.902,1.267,3.773,3.071,2.777,2.668,0.809,2.701,2.042,1.552,1.515,3.415,4.601,4.460,2.543,3.018,1.778,2.846,2.072,2.726,3.544,3.469,2.242,1.522,3.072,2.548,3.587,4.836,4.265,3.024,2.713,1.991,2.918,3.437,2.280,2.036,2.932,3.457,3.913,3.383,3.073,3.025,3.461,3.058,3.378,4.249,3.968,3.184,3.476,3.188,3.050,3.870,2.992,3.023,4.572,3.253,3.545,3.724,3.590,4.620,4.378,5.492,4.721,5.048,4.562
+-36.259,-0.203,0.834,0.926,1.032,-36.259,-1.288,1.025,1.384,1.247,2.276,0.643,-0.612,-1.152,1.858,1.112,1.871,2.551,1.037,2.024,2.047,-0.973,1.346,1.457,0.806,2.008,2.608,4.714,4.798,2.883,3.420,1.453,2.607,0.726,2.538,2.164,3.477,3.272,1.130,1.195,1.524,2.716,3.166,3.674,3.326,2.855,1.834,2.744,3.472,3.168,2.605,3.306,4.718,4.881,3.435,2.710,3.554,2.865,3.684,4.138,5.106,4.220,4.148,3.892,4.074,3.498,4.346,4.399,3.487,4.525,3.670,4.492,4.411,3.907,4.697,3.394,4.520,4.585,4.429,4.068
+-36.259,2.203,2.247,0.016,2.755,-36.259,2.670,1.455,-1.020,-0.116,2.840,2.457,0.069,1.456,-0.842,2.117,2.393,1.979,2.223,1.020,1.725,0.793,1.207,0.554,1.814,3.080,3.011,4.479,4.345,2.296,2.948,1.689,1.409,0.967,0.719,1.793,2.491,2.925,2.375,2.741,2.490,3.215,3.505,2.878,3.152,2.408,3.115,2.305,3.426,1.911,2.586,2.774,4.145,4.086,2.729,3.066,3.170,3.240,3.341,3.406,3.251,3.724,4.443,3.755,3.250,2.917,3.412,3.182,2.426,4.280,3.991,4.885,4.907,4.592,4.642,3.718,5.317,4.224,4.243,4.581
+-36.259,3.114,2.775,2.019,0.950,-36.259,1.310,-0.309,0.549,1.793,1.271,-0.832,0.806,1.514,1.715,2.061,2.742,1.305,1.001,0.850,1.380,0.150,1.915,0.823,1.706,0.994,2.526,4.149,4.074,2.230,3.164,1.870,2.043,2.180,0.471,1.969,2.655,2.617,1.925,1.849,1.270,2.216,3.457,3.090,3.414,2.296,2.582,3.063,3.934,2.835,1.960,2.636,3.378,3.497,3.038,3.086,2.867,3.324,4.616,4.973,4.316,4.720,4.443,3.419,3.167,3.159,3.664,3.399,3.406,3.727,3.673,3.731,4.264,4.132,3.762,3.871,3.887,4.684,5.062,5.178
+-36.259,0.495,1.359,-0.693,3.254,-36.259,1.523,1.176,0.829,0.303,1.472,0.174,0.481,0.955,0.055,2.933,2.728,3.359,2.902,2.791,2.661,-0.144,2.168,2.772,1.856,0.647,2.635,4.409,4.556,2.559,3.743,3.242,1.541,0.054,2.264,3.742,4.117,3.673,1.833,2.848,2.382,3.514,2.582,2.437,3.285,2.149,2.150,3.186,4.431,3.587,3.384,4.326,4.622,4.623,3.535,2.695,2.799,2.360,3.608,3.758,4.779,3.434,3.137,4.089,4.348,3.339,4.385,4.554,3.568,4.059,4.002,4.002,3.836,4.471,4.282,3.795,4.684,4.285,4.735,4.633
+-36.259,2.860,1.507,1.757,1.447,-36.259,0.405,1.557,1.925,0.617,0.724,0.172,0.701,0.988,2.264,2.683,2.632,3.282,3.074,2.590,1.362,-0.156,1.888,0.880,2.012,2.095,2.787,4.822,4.829,2.789,2.173,1.988,2.563,1.931,1.257,3.072,2.775,3.191,0.915,3.394,3.230,3.669,4.098,3.894,3.189,2.885,3.052,3.020,3.691,2.773,3.159,3.623,3.951,4.098,3.175,2.661,3.553,3.876,3.635,4.011,4.281,3.884,3.918,3.353,3.056,3.826,3.226,3.237,3.690,3.502,3.942,4.068,4.287,4.503,4.057,4.349,4.218,3.915,4.225,4.228
+-36.259,1.855,-2.475,1.011,1.711,-36.259,1.112,-0.558,2.071,2.705,1.928,1.794,-0.615,0.685,0.133,2.199,2.766,1.850,2.351,1.703,0.847,0.455,1.290,1.527,0.371,1.481,1.679,4.652,4.711,2.530,2.755,1.954,2.799,0.495,1.769,1.834,3.353,2.551,2.027,3.063,3.308,2.471,3.798,3.548,3.015,2.835,2.728,1.982,3.237,2.219,2.400,2.785,3.058,3.539,2.832,2.470,4.008,3.355,3.422,3.956,3.640,4.676,4.030,3.222,3.848,4.239,4.512,4.289,3.143,3.671,3.500,4.322,4.516,4.242,4.462,3.652,3.942,3.759,4.736,5.397
+-36.259,1.513,0.313,0.410,2.014,-36.259,1.122,1.429,1.899,2.443,1.134,1.313,2.376,1.308,0.028,1.635,1.328,-0.247,0.303,1.576,1.561,-2.016,1.888,2.354,1.762,1.978,2.909,4.835,5.149,3.883,2.153,3.000,2.495,0.823,0.989,1.391,2.541,2.712,2.180,3.234,2.707,2.586,3.710,3.482,2.926,3.014,3.101,3.475,3.591,2.701,2.238,2.327,3.199,3.080,2.149,2.351,3.821,3.273,3.105,3.899,4.447,4.108,3.602,3.844,3.509,2.633,3.611,4.041,2.564,3.890,3.335,4.378,4.717,4.168,3.507,4.362,5.410,4.909,4.480,4.067
+-36.259,2.357,0.502,0.785,2.079,-36.259,0.994,-0.570,0.168,1.516,1.867,1.309,1.178,1.657,1.029,2.437,1.916,1.120,0.901,1.538,0.954,0.291,0.625,1.426,1.027,2.487,1.411,3.763,4.234,2.818,2.454,2.678,2.662,2.381,2.269,2.217,2.331,2.172,2.070,3.035,2.627,1.967,3.271,3.768,2.669,3.173,3.595,2.859,2.927,2.521,2.535,3.010,3.867,3.875,3.276,3.102,2.802,2.657,3.239,3.831,4.351,4.069,3.689,3.490,3.028,3.788,3.510,3.736,3.144,3.795,4.194,5.124,5.298,4.462,4.660,4.330,4.621,4.983,4.577,4.492
+-36.259,1.268,2.262,1.754,2.079,-36.259,2.163,1.132,2.178,0.052,2.734,2.656,0.029,-0.211,-1.292,2.223,2.976,1.994,0.738,0.705,1.595,-0.358,-0.723,1.131,2.197,1.492,2.554,4.638,4.724,3.037,2.269,2.457,2.641,2.454,1.594,1.471,2.290,2.079,2.076,3.558,2.555,2.977,3.972,3.555,2.979,3.615,3.143,2.781,3.616,2.136,2.422,2.682,3.295,3.773,3.581,2.782,3.210,3.128,2.540,3.989,4.792,4.872,4.316,3.906,3.762,2.684,4.324,3.954,2.304,3.291,3.895,4.213,4.506,4.360,3.454,3.388,5.016,4.118,4.428,4.866
+-36.259,1.728,2.878,1.337,2.209,-36.259,1.123,2.443,1.537,-0.126,1.761,1.753,1.196,0.124,0.523,1.562,1.651,0.951,1.559,1.983,0.764,-0.233,1.899,1.050,1.832,1.391,2.431,4.016,4.148,2.970,2.295,2.097,3.257,2.476,1.661,2.513,3.158,2.574,2.577,2.123,2.811,2.485,4.233,4.071,3.282,4.311,2.908,2.314,3.376,2.206,2.093,2.747,3.426,3.452,3.455,3.556,3.108,3.168,2.854,3.353,4.424,3.955,3.558,3.935,4.177,4.421,3.907,4.315,3.298,3.928,3.999,4.178,3.804,4.193,4.272,4.237,4.749,4.195,4.566,4.705
+-36.259,1.118,1.267,0.395,3.914,-36.259,1.919,1.186,1.353,-0.230,2.247,0.705,0.338,2.544,0.349,2.248,1.471,1.093,2.607,1.004,0.797,0.709,1.880,0.355,2.195,2.755,2.274,4.304,4.841,3.743,1.656,2.387,2.223,1.805,2.135,3.440,3.463,3.706,2.419,3.416,3.384,3.061,4.248,3.410,2.264,2.760,1.998,2.755,2.736,2.426,1.116,2.849,4.470,4.028,2.183,3.180,3.525,3.093,3.033,3.309,4.590,4.106,3.424,3.401,3.084,3.745,3.482,3.486,2.520,3.616,3.226,3.981,4.091,4.147,4.137,3.717,3.910,3.754,4.388,4.680
+-36.259,0.199,1.723,0.395,1.294,-36.259,2.085,-3.444,1.575,0.701,2.238,1.626,1.244,-0.379,1.472,2.031,2.281,2.581,2.616,1.775,1.964,0.091,2.658,3.143,2.292,1.970,2.393,4.747,4.914,3.021,2.420,3.256,2.532,1.681,1.791,2.765,2.453,2.564,1.661,3.298,2.649,2.140,4.580,3.650,2.483,3.283,3.064,2.078,2.557,2.281,3.062,3.335,4.333,4.090,3.116,3.126,3.715,3.516,3.049,3.689,5.276,4.705,3.875,3.543,3.862,3.154,3.512,3.770,3.253,3.136,4.221,4.162,4.525,4.514,3.663,3.717,4.879,4.145,3.935,4.816
+-36.259,2.429,0.817,1.513,-0.019,-36.259,1.239,-0.667,2.158,1.467,2.897,1.711,-0.121,0.513,2.226,1.572,1.062,1.271,2.095,0.972,1.184,1.753,2.914,3.075,2.006,0.509,2.278,4.490,4.235,1.449,2.891,2.310,2.324,1.471,1.178,2.630,3.262,2.935,2.004,3.805,3.134,3.246,4.205,3.569,3.166,3.032,3.047,2.364,3.069,2.714,1.897,2.852,3.959,4.368,3.180,2.831,3.415,2.580,3.137,4.196,4.586,4.639,3.484,3.354,3.227,3.273,3.696,3.360,2.637,2.803,3.702,5.398,5.713,4.377,4.224,3.815,4.595,3.846,5.077,4.941
+-36.259,1.828,0.593,1.170,3.018,-36.259,1.560,1.457,2.498,1.720,-0.206,1.151,1.004,1.812,0.806,0.599,0.814,2.271,0.961,1.805,3.275,-2.419,2.281,2.089,1.655,2.210,3.000,4.569,4.730,3.254,3.499,2.215,3.307,2.693,2.660,0.661,2.250,2.019,1.795,3.536,3.213,2.517,3.968,3.576,2.830,3.095,2.243,2.246,3.361,2.948,3.076,3.588,4.346,3.643,3.305,3.648,2.540,2.720,3.544,3.590,3.642,3.436,3.396,3.695,3.490,3.997,3.076,3.068,3.051,3.328,2.768,3.987,4.340,3.945,4.147,3.433,4.411,3.851,5.410,4.878
+-36.259,2.481,2.926,1.371,2.922,-36.259,3.144,2.426,1.338,2.368,2.434,2.690,1.420,1.138,-0.170,3.287,2.768,2.563,2.610,1.407,1.768,1.328,3.269,2.847,1.731,1.109,1.797,4.431,4.540,2.495,2.974,2.839,2.667,2.112,2.190,2.844,3.075,2.969,0.876,2.838,2.169,1.663,3.798,3.350,3.086,3.130,2.565,2.832,3.043,3.274,2.471,3.083,3.479,3.755,3.742,4.019,3.960,4.056,2.905,3.368,4.326,4.515,3.708,4.156,4.460,4.367,4.259,3.815,3.267,3.457,3.853,4.715,4.727,4.418,3.265,4.214,4.984,4.268,5.046,4.632
+-36.259,-0.898,2.148,-0.039,2.250,-36.259,-2.129,1.485,1.899,2.066,2.791,0.784,0.807,-0.335,-1.282,0.916,2.835,0.378,0.190,0.723,1.541,1.636,2.354,2.878,1.305,1.714,2.439,3.975,4.142,3.023,2.060,1.757,2.307,2.173,1.432,2.292,3.049,2.520,2.378,3.899,2.830,2.630,4.490,3.961,3.162,3.292,2.764,2.026,3.662,2.804,2.148,2.219,4.357,4.387,2.879,2.314,4.287,3.781,3.151,3.466,5.059,4.885,3.752,3.574,3.608,2.779,3.861,3.609,3.307,3.815,3.533,4.173,4.392,4.259,4.449,4.274,4.298,4.073,4.102,4.109
+-36.259,1.252,1.249,0.971,2.029,-36.259,1.484,0.344,0.285,-0.065,1.364,0.103,1.859,0.348,-0.893,2.953,1.946,2.841,2.596,1.663,2.286,-0.524,2.008,1.018,1.667,2.403,1.752,3.886,3.946,1.579,2.890,2.626,3.114,2.568,1.813,2.365,2.846,3.062,2.189,2.668,2.825,3.255,4.386,4.464,3.211,3.177,2.465,2.490,2.673,2.429,3.747,3.577,3.286,3.195,3.576,3.300,3.298,3.302,4.075,4.181,4.777,4.655,4.562,4.199,3.144,4.124,3.144,3.229,4.010,2.973,3.880,4.729,4.924,4.416,3.604,4.552,4.242,4.843,4.572,4.859
+-36.259,2.435,0.716,1.929,-0.658,-36.259,-0.701,-0.153,1.762,0.611,-1.046,-0.113,1.738,-0.855,-0.976,2.295,2.258,1.360,1.648,0.798,1.907,1.554,2.303,1.145,1.779,2.468,2.217,4.191,4.205,1.975,1.707,1.408,2.044,2.233,1.299,3.024,3.921,3.411,3.080,3.533,2.725,3.767,4.743,4.638,4.040,3.603,3.466,2.915,3.118,1.392,2.880,3.683,3.429,4.127,4.153,3.275,3.882,4.251,3.358,3.628,4.532,4.345,4.151,3.810,3.910,3.746,4.198,3.772,3.667,3.440,4.226,4.791,4.796,4.341,3.892,4.390,5.050,4.683,4.155,5.226
+-36.259,2.249,1.627,2.489,2.451,-36.259,2.631,0.320,1.027,1.206,2.102,1.896,1.190,1.128,1.196,2.586,2.652,2.414,2.058,1.205,1.285,1.520,1.860,1.555,1.867,0.844,1.904,4.219,4.341,2.407,1.463,2.088,1.402,1.068,0.859,1.781,2.943,2.084,1.396,2.193,2.317,2.100,4.203,3.981,2.574,3.353,3.277,0.701,3.429,2.706,2.397,3.728,4.247,4.598,4.031,3.126,4.292,4.018,2.959,3.253,4.760,4.450,3.619,3.678,4.246,3.747,4.276,3.993,3.254,3.010,3.987,3.897,4.006,4.306,3.767,3.884,4.990,4.468,4.401,5.063
+-36.259,2.501,1.988,0.903,1.821,-36.259,1.551,1.489,1.043,-1.343,2.744,-1.144,0.161,-0.008,0.254,0.751,2.828,1.876,0.786,1.769,1.917,0.239,2.544,2.426,1.968,2.523,2.662,4.530,4.570,2.099,2.421,2.379,2.102,1.205,1.237,1.154,3.058,2.097,0.802,3.824,2.720,2.867,4.601,3.994,2.716,2.955,2.606,2.451,3.286,2.388,2.641,2.958,4.551,4.657,3.264,2.913,3.129,3.022,3.753,3.650,4.985,4.689,4.245,4.128,3.547,3.972,4.862,4.746,3.449,3.363,3.434,4.731,4.847,3.807,3.834,4.589,5.790,4.875,4.362,5.206
+-36.259,0.817,2.664,-0.327,2.990,-36.259,1.782,1.956,1.905,-1.066,2.174,1.469,-0.334,1.592,-2.202,2.336,2.233,2.332,0.507,1.391,0.897,-0.372,2.668,2.792,1.173,2.395,0.595,3.939,4.084,2.354,2.124,1.853,1.514,0.321,1.761,2.628,3.212,2.619,2.014,3.727,3.169,2.226,4.584,3.796,2.925,3.558,3.099,3.000,3.883,2.202,2.285,3.458,4.165,4.544,4.346,3.680,4.477,4.132,3.720,4.700,4.001,4.218,5.371,4.047,4.248,3.556,4.311,4.192,2.563,4.318,3.441,4.130,4.333,3.534,3.487,3.997,5.185,4.301,4.179,3.985
+-36.259,1.495,3.127,1.266,1.884,-36.259,2.054,2.365,1.093,1.717,2.851,2.178,0.049,0.832,0.520,2.131,1.639,1.899,2.772,1.849,0.402,0.650,1.545,1.450,1.861,2.425,2.624,4.343,4.221,2.501,3.384,2.333,2.858,2.741,1.607,2.856,3.203,2.617,2.122,3.754,3.602,3.037,3.762,3.377,3.181,3.623,3.218,2.165,3.984,3.545,2.986,3.019,2.927,3.276,3.605,3.190,4.481,3.872,3.390,4.134,5.115,4.868,4.873,4.349,4.264,3.276,3.509,3.686,3.689,4.165,3.693,4.415,4.569,3.948,3.615,3.963,5.067,4.095,4.502,4.627
+-36.259,2.469,2.162,1.801,-0.092,-36.259,2.696,0.958,-2.322,1.799,3.410,2.466,2.410,-1.436,1.138,0.688,2.349,2.421,2.738,-0.342,2.198,1.763,1.617,1.488,0.136,1.142,2.689,4.688,4.699,2.656,2.295,2.379,1.577,2.203,2.325,2.918,3.036,2.946,2.541,4.043,3.493,3.462,4.634,4.579,3.551,3.984,3.585,3.110,3.594,3.170,3.549,3.139,2.871,3.397,3.580,3.143,2.788,3.193,3.540,3.089,4.115,4.278,4.240,3.858,3.150,4.147,3.628,2.685,2.897,2.900,3.676,4.534,4.614,4.230,3.893,4.070,4.499,4.311,4.298,3.862
+-36.259,1.790,1.879,0.167,2.866,-36.259,2.397,1.789,0.726,0.469,2.078,-3.096,1.454,2.057,1.429,1.412,1.405,1.736,0.852,1.527,1.440,0.572,1.679,0.668,0.988,2.553,2.563,4.715,4.713,2.759,3.260,1.819,2.020,1.755,1.894,2.812,3.316,2.908,1.942,4.030,2.870,2.861,3.794,3.489,2.175,3.020,2.880,2.977,3.265,2.366,2.062,3.584,3.104,3.456,3.266,2.185,3.497,3.653,3.586,3.754,4.536,3.986,3.501,3.513,3.769,3.289,3.851,3.479,2.869,4.708,4.181,5.240,5.581,5.016,4.927,3.871,4.246,3.986,5.254,4.937
+-36.259,1.011,1.189,0.831,2.153,-36.259,1.092,0.999,-1.218,1.543,2.008,-0.518,1.255,0.965,-0.022,0.576,0.260,1.490,1.156,1.628,2.396,0.942,2.499,1.635,1.656,2.040,2.553,4.425,4.337,2.361,1.882,1.286,2.808,2.306,0.727,3.386,3.474,3.800,2.935,3.473,2.580,1.432,3.266,3.428,3.213,3.624,3.212,3.261,3.300,2.813,2.716,3.208,4.371,4.286,2.868,3.202,4.007,3.503,4.051,4.103,3.817,3.875,4.223,4.391,4.185,3.518,3.896,3.115,2.663,3.657,3.944,4.326,4.862,4.903,5.087,4.350,4.576,4.580,5.288,5.523
+-36.259,1.508,-0.072,1.000,2.418,-36.259,0.428,-0.412,1.689,2.441,1.153,0.866,-1.017,-0.146,1.176,1.316,2.775,2.065,1.725,2.185,1.462,-1.057,1.823,0.698,1.093,0.396,2.246,4.486,4.580,2.288,1.494,2.897,1.972,1.398,1.395,2.020,2.236,1.807,0.461,2.828,2.655,3.004,4.617,4.643,3.474,3.589,2.999,1.977,2.539,2.543,2.401,3.179,3.550,3.437,3.135,2.531,3.037,2.976,3.046,3.792,3.818,4.435,5.026,3.282,3.312,3.594,3.762,3.230,3.070,3.091,3.722,4.444,4.859,4.174,3.652,3.817,4.894,4.069,4.498,4.838
+-36.259,-0.466,0.107,1.401,-3.299,-36.259,-0.169,0.398,2.170,2.786,2.303,1.050,0.988,-0.532,1.645,1.710,2.158,1.725,1.984,1.356,1.227,0.622,1.458,1.010,1.881,1.918,2.337,4.073,4.201,2.790,1.939,2.070,2.999,1.855,0.879,2.218,2.552,3.031,2.808,3.349,2.852,2.703,4.123,4.110,3.909,3.457,1.713,1.754,2.733,1.811,2.503,3.220,4.320,4.193,3.110,2.706,3.396,2.781,3.819,3.399,3.474,4.067,4.244,3.322,3.197,3.503,4.338,4.519,3.684,3.047,4.032,4.875,5.072,4.521,3.462,3.780,5.319,5.255,4.443,4.530
+-36.259,0.267,0.292,0.284,-0.177,-36.259,0.605,1.001,2.283,1.783,2.554,0.649,-1.624,0.174,0.921,2.025,2.235,2.882,0.569,1.505,1.595,-0.035,1.467,1.008,1.449,2.316,2.698,4.647,4.535,2.470,2.971,2.606,2.657,1.725,1.798,2.638,2.709,2.331,1.236,3.226,3.396,2.342,3.050,2.747,2.673,2.538,2.807,1.801,2.825,1.889,2.506,2.847,3.308,3.910,3.101,2.468,4.363,4.131,3.390,3.804,3.969,3.725,3.961,4.065,4.027,3.181,3.515,3.840,3.372,3.624,4.029,3.998,3.735,4.463,5.299,4.061,4.886,4.214,4.309,3.897
+-36.259,3.556,2.581,2.699,2.136,-36.259,2.510,1.399,2.053,2.915,1.552,-0.303,2.136,0.666,1.531,1.284,3.067,1.677,2.234,1.760,0.897,-2.450,2.235,2.651,2.223,3.110,1.604,3.862,3.643,0.878,3.785,2.990,2.957,2.756,2.032,1.871,3.145,2.675,2.481,2.707,2.343,2.528,3.452,3.300,2.527,2.514,3.091,1.926,3.471,1.711,2.196,2.442,3.621,3.557,2.787,2.527,4.043,3.792,2.848,3.402,4.075,3.993,3.719,3.505,3.125,2.865,4.001,4.058,3.222,2.845,3.709,4.056,4.017,4.077,4.384,4.632,4.547,4.495,4.724,4.509
+-36.259,1.006,0.511,0.671,-1.057,-36.259,1.519,-0.168,-1.902,2.271,-0.210,0.435,-0.215,-0.160,1.172,0.902,2.939,2.334,-0.780,0.999,-0.801,0.632,2.132,1.539,0.964,0.647,3.364,4.196,4.166,2.398,1.753,2.992,2.507,2.273,2.480,2.029,2.996,2.414,0.989,3.884,3.282,3.031,4.221,3.621,3.320,3.657,2.488,2.466,3.417,2.147,2.800,3.663,4.196,4.237,3.651,3.373,3.481,3.586,3.244,3.358,3.956,4.084,4.243,3.533,3.661,3.960,3.995,3.723,2.633,2.885,3.492,4.112,3.905,3.922,4.020,4.703,5.191,4.987,4.352,5.100
+-36.259,0.794,1.110,1.205,2.545,-36.259,1.770,0.220,2.416,2.677,2.467,1.882,-1.832,0.991,1.651,2.015,1.017,2.166,2.442,1.864,1.876,-0.185,1.534,0.445,1.937,1.726,2.051,4.263,4.588,3.105,3.159,2.921,3.064,2.719,1.848,2.237,3.029,3.083,2.179,3.060,2.154,3.077,4.278,3.554,3.344,2.827,3.843,2.925,2.708,2.321,3.111,3.372,4.190,4.027,3.508,3.738,3.898,2.884,4.223,4.270,4.339,4.296,4.286,3.394,4.005,4.040,4.010,3.217,2.637,3.433,3.076,4.296,5.055,4.542,4.349,4.143,4.723,4.417,4.583,5.069
+-36.259,1.274,-0.612,0.790,2.323,-36.259,1.766,1.535,1.478,2.095,2.381,2.327,1.262,1.911,-0.167,3.315,1.871,2.318,2.185,0.358,0.514,1.241,1.960,1.180,0.477,1.572,1.806,3.965,4.028,2.390,1.461,0.623,1.668,1.229,1.178,1.895,1.144,1.976,1.564,3.272,2.790,2.793,3.717,3.410,2.720,2.677,2.622,2.198,3.263,3.027,3.166,2.582,3.813,4.046,2.875,2.983,3.017,3.327,3.527,3.156,3.949,3.716,3.383,3.586,3.683,3.858,3.286,3.014,2.999,3.133,2.786,3.833,4.116,3.800,4.016,4.030,5.498,4.706,4.587,4.620
+-36.259,-0.661,0.503,-3.183,2.412,-36.259,1.136,0.674,-0.890,-1.153,2.216,1.829,-0.522,1.020,0.589,0.453,1.673,1.031,1.374,0.409,1.280,-4.205,1.092,-0.925,-0.202,-1.349,2.124,3.729,3.928,2.602,-0.557,0.970,0.539,1.565,1.677,2.711,3.464,3.025,2.571,2.249,2.730,1.841,4.192,3.611,3.799,2.774,3.427,3.007,4.321,2.708,0.951,1.906,2.954,3.126,3.277,2.818,3.677,3.398,3.673,4.090,4.043,4.128,4.013,3.893,4.303,4.108,4.128,3.966,3.041,4.001,3.840,4.368,4.103,3.995,3.663,3.906,4.646,4.704,5.272,5.055
+-36.259,3.558,3.320,2.371,2.586,-36.259,2.666,1.795,2.671,-3.945,1.607,1.204,-0.496,0.355,2.016,0.175,0.753,2.267,-0.565,2.684,1.938,1.402,-0.106,1.518,2.085,2.254,1.653,3.846,3.906,2.142,2.839,1.127,0.953,1.382,1.695,1.826,2.404,2.317,2.641,3.453,2.741,2.814,3.655,3.970,2.993,3.550,2.014,1.863,3.111,3.082,1.697,3.330,4.510,4.605,3.290,3.023,3.104,3.226,3.743,3.491,4.040,3.760,4.249,3.379,3.491,3.084,3.941,3.839,3.467,3.005,3.362,4.478,4.623,4.405,3.857,4.011,4.868,5.255,4.469,5.062
+-36.259,2.947,2.631,3.020,2.459,-36.259,2.011,2.004,2.488,2.198,2.108,-0.475,0.778,0.319,2.560,3.437,3.694,3.316,2.915,2.300,1.741,1.497,2.582,3.223,2.668,1.473,2.104,4.321,4.288,2.313,3.224,1.749,2.198,2.092,2.543,2.032,2.818,3.014,2.418,1.501,1.434,2.868,2.323,2.435,2.757,2.826,2.109,1.727,3.023,2.514,2.616,2.021,3.243,3.440,3.558,3.236,3.000,2.526,3.318,3.511,3.723,3.908,3.726,2.941,3.118,3.908,3.919,3.583,2.767,3.519,3.654,4.841,4.916,4.193,4.379,3.572,4.693,4.380,4.759,4.529
+-36.259,2.834,-0.726,2.107,2.579,-36.259,0.833,1.186,1.683,1.781,0.002,-0.291,1.642,1.897,1.467,3.163,3.233,2.184,2.352,2.086,1.783,1.337,2.065,0.327,2.141,1.800,3.260,4.917,4.996,3.436,1.675,2.425,2.197,2.136,-0.372,1.115,2.258,0.762,1.144,2.711,2.447,2.125,3.978,3.765,2.287,3.053,3.344,2.091,2.821,2.216,1.721,2.048,3.445,3.660,2.718,2.748,3.383,2.942,2.579,3.338,3.908,3.831,4.269,3.243,3.971,3.224,3.637,3.253,2.932,3.475,3.500,3.872,4.163,4.145,4.216,3.835,5.101,4.681,5.259,5.131
+-36.259,2.293,1.260,2.149,0.301,-36.259,1.684,0.101,2.526,1.757,2.450,0.852,0.158,-1.279,-0.114,2.419,2.167,2.669,0.152,1.741,0.646,-1.284,3.122,3.194,1.231,1.805,1.854,3.918,4.351,2.681,2.652,2.062,2.473,1.735,2.192,1.436,1.751,2.331,1.876,3.628,3.309,2.206,3.477,2.965,2.052,3.268,2.343,1.884,3.958,2.874,2.345,2.080,3.098,3.502,2.119,2.445,2.872,2.705,2.709,3.091,3.410,3.028,3.476,3.586,3.599,3.145,3.562,3.660,2.637,3.521,4.005,4.434,4.689,4.769,4.183,3.837,5.095,4.099,4.747,4.141
+-36.259,2.834,2.589,2.361,1.613,-36.259,-0.025,1.652,1.814,0.648,1.431,1.088,-0.114,1.373,0.685,0.589,2.391,1.769,1.116,-0.357,0.332,1.181,1.817,2.182,2.282,1.507,1.689,3.906,4.137,1.836,2.738,2.218,2.243,1.132,0.497,1.347,1.814,2.143,2.171,2.158,2.352,3.298,4.201,3.858,2.810,3.733,2.439,1.554,3.674,3.146,2.318,2.722,2.918,3.498,2.945,3.049,3.437,3.679,3.124,3.192,3.807,4.002,3.312,3.870,4.066,3.167,3.433,3.825,3.181,3.534,3.388,4.050,4.207,3.926,4.960,4.369,4.352,4.889,5.990,5.582
+-36.259,3.188,1.957,0.638,1.346,-36.259,1.639,1.055,0.562,0.126,2.062,-0.188,1.219,1.067,0.635,1.212,2.954,2.512,0.827,1.344,2.309,0.394,1.404,1.750,1.750,3.080,2.792,4.768,4.731,2.749,3.082,1.916,2.786,3.284,2.643,2.285,2.635,2.816,1.872,3.220,2.525,2.660,2.939,2.749,2.700,3.939,4.122,2.822,4.519,4.003,3.969,3.493,3.610,3.685,3.473,3.217,3.805,3.774,3.515,3.735,3.793,3.877,3.631,3.814,4.008,4.199,4.330,4.025,3.702,4.306,4.452,4.681,4.705,4.411,4.132,4.000,4.735,4.794,4.644,4.999
+-36.259,3.073,-1.156,2.400,2.723,-36.259,0.139,1.323,0.992,1.180,-0.377,1.095,0.546,1.311,0.740,1.309,2.089,2.268,1.128,1.480,1.656,0.654,1.408,1.629,1.315,2.732,2.472,4.638,4.802,3.329,2.608,2.029,2.282,2.274,2.189,2.082,3.027,2.614,1.228,3.393,3.046,2.300,3.321,3.890,3.222,4.100,3.161,2.280,3.150,2.341,2.341,2.985,4.159,4.060,3.279,3.058,3.241,3.411,3.828,3.511,3.756,3.764,3.970,3.506,2.449,3.294,4.949,4.858,2.991,3.228,3.920,4.124,3.833,3.881,3.802,3.634,4.243,3.673,4.894,4.690
+-36.259,1.216,2.709,-0.286,1.897,-36.259,0.200,0.920,0.774,1.644,0.984,-1.018,1.937,-0.961,-0.945,2.086,1.750,2.402,2.109,1.703,1.675,-1.104,1.114,2.846,2.274,2.048,2.315,3.979,4.239,2.956,1.716,3.460,2.652,1.184,1.045,3.914,4.111,3.500,2.731,3.723,3.289,2.677,4.599,3.994,2.859,3.313,3.387,2.780,3.801,3.079,2.962,3.465,3.861,3.953,3.008,2.511,4.065,3.321,3.189,3.456,4.375,4.175,4.518,3.540,3.909,3.801,3.387,3.413,2.845,3.374,3.831,4.343,4.482,4.439,3.679,3.404,4.436,4.058,3.647,3.817
+-36.259,1.046,-1.969,1.098,2.900,-36.259,1.392,-0.563,1.122,0.011,2.250,1.946,0.011,1.029,2.462,1.153,3.127,3.157,0.935,3.112,1.307,-0.184,2.564,1.677,2.170,2.177,3.199,4.654,4.641,2.734,1.469,2.093,1.806,1.446,2.843,2.979,3.900,3.388,3.154,3.385,2.110,1.971,4.016,3.465,2.179,2.704,2.970,2.721,3.152,3.397,2.765,1.688,3.278,3.685,2.733,2.951,3.641,3.309,3.514,3.731,4.445,3.858,4.017,3.349,3.412,3.388,3.870,3.849,3.453,2.939,3.730,4.311,4.416,3.955,3.723,3.743,4.242,3.809,3.751,3.676
+-36.259,2.315,1.269,2.120,2.408,-36.259,-1.155,-0.546,2.469,1.592,1.704,1.562,-2.687,1.659,2.512,1.985,2.992,0.420,0.927,2.384,1.155,-0.366,1.810,1.932,1.528,1.360,2.877,4.484,4.503,2.569,1.989,1.784,2.743,1.536,1.394,2.813,3.257,2.888,2.226,2.983,2.575,3.446,4.454,4.126,2.968,2.477,3.017,3.001,4.082,3.444,2.235,3.690,4.611,5.010,4.186,3.564,3.432,4.038,3.659,3.658,4.914,4.310,4.154,3.836,4.272,3.479,4.217,4.040,3.154,3.467,3.581,4.932,5.255,3.652,3.449,4.053,4.086,4.304,4.365,4.677
+-36.259,1.130,2.187,-0.163,2.022,-36.259,1.025,0.053,1.339,3.403,-1.185,1.624,0.999,1.159,2.389,2.967,3.445,3.080,0.611,2.111,1.514,0.125,2.143,2.922,0.123,1.694,1.871,4.372,4.364,2.052,2.732,2.128,2.818,1.753,1.794,2.145,2.580,2.427,2.701,3.211,1.981,2.055,3.449,2.735,3.011,2.518,2.699,2.886,2.716,2.841,3.642,3.303,3.187,3.502,3.492,3.058,4.075,4.023,3.637,4.371,4.800,4.589,4.316,3.139,3.931,4.067,3.573,3.391,3.427,3.248,3.988,4.917,4.987,4.724,4.007,3.783,4.918,4.290,5.005,4.629
+-36.259,3.700,3.385,2.299,1.653,-36.259,1.576,1.478,1.983,0.440,0.863,0.331,0.790,-0.138,1.594,1.649,3.357,0.915,1.865,1.346,1.738,0.616,1.594,2.053,2.071,1.185,2.262,4.566,4.643,2.529,1.675,1.660,1.785,1.840,0.960,3.152,3.875,3.041,1.850,2.514,2.320,2.576,4.323,3.619,1.924,3.575,3.688,2.660,3.790,1.812,2.968,3.423,3.394,3.588,3.753,3.685,3.670,3.422,2.289,2.805,3.976,4.061,3.216,3.625,3.755,3.824,3.667,3.607,3.244,3.753,3.965,4.311,4.666,4.139,3.931,4.258,4.965,4.472,3.773,4.504
+-36.259,1.242,2.326,-0.140,1.564,-36.259,1.172,1.273,0.007,2.191,1.678,0.181,1.070,1.411,0.170,1.955,3.218,2.592,1.375,1.735,1.545,0.510,2.684,2.893,1.410,2.151,2.623,4.563,4.506,2.819,2.436,2.633,2.731,1.136,1.175,1.842,2.738,2.163,0.990,2.621,2.426,3.134,3.331,3.501,3.467,2.889,3.605,2.586,3.649,3.352,2.979,3.102,3.380,3.457,3.109,2.574,3.454,3.944,3.713,3.354,3.950,4.151,4.315,4.339,3.151,4.094,3.590,4.001,3.651,3.475,3.361,4.175,4.316,3.677,4.457,3.520,4.760,4.255,5.639,4.625
+-36.259,2.344,1.337,-0.234,1.726,-36.259,-2.251,0.756,1.057,-1.069,1.099,-0.125,0.867,2.093,-0.613,0.980,1.661,-0.175,-0.319,-0.326,0.962,0.949,2.317,1.092,1.663,1.706,2.608,4.467,4.460,2.454,1.334,2.313,1.722,1.979,1.235,2.683,3.439,3.240,1.342,2.259,2.215,2.248,3.320,2.959,3.462,2.777,3.088,2.832,3.290,2.505,1.983,3.039,3.818,3.939,2.846,2.678,3.343,3.999,3.857,4.040,4.931,4.613,4.021,3.774,3.632,2.995,3.136,2.978,2.342,3.719,3.984,5.007,5.058,4.123,3.732,3.301,5.046,4.393,5.214,4.756
+-36.259,-0.619,1.469,0.852,1.656,-36.259,2.034,1.455,2.123,-1.077,-0.792,1.409,-0.172,-1.314,2.074,1.298,0.793,2.658,2.278,1.581,0.200,-0.549,2.871,3.521,3.213,2.485,3.172,5.108,5.115,3.510,1.990,3.053,2.362,2.028,0.215,2.234,2.285,2.751,1.175,3.312,2.609,2.358,4.375,3.454,1.780,2.068,3.198,2.749,3.901,2.921,2.454,3.610,4.772,5.007,4.291,3.502,3.181,2.590,3.655,3.828,4.213,3.657,4.186,3.540,4.008,3.421,4.387,3.774,2.874,3.107,3.667,4.072,3.970,4.057,4.397,4.092,4.658,4.534,4.944,4.117
+-36.259,2.352,2.386,0.524,1.496,-36.259,-1.335,1.479,0.534,1.482,0.519,2.270,0.186,1.187,0.087,1.346,0.976,1.261,3.086,1.091,0.735,1.379,0.776,1.391,0.523,0.038,2.218,3.976,4.277,2.405,1.694,2.536,2.387,1.545,2.051,2.915,2.621,2.249,1.506,2.940,1.904,2.983,4.903,4.194,2.719,3.135,2.654,3.042,4.347,3.167,3.137,3.632,4.423,4.139,3.654,3.424,3.729,3.735,4.407,4.033,4.568,4.345,4.412,3.704,4.157,3.967,3.109,3.548,2.923,3.367,2.759,4.258,4.417,3.638,3.572,3.463,3.686,3.716,4.834,4.453
+-36.259,1.471,2.758,1.450,0.450,-36.259,0.836,2.065,2.423,1.064,2.164,1.776,0.451,0.662,1.233,2.345,2.169,1.605,1.841,1.105,1.502,-0.269,1.297,1.025,-1.194,1.496,1.959,4.123,4.112,2.181,2.721,1.770,0.857,1.490,2.876,3.078,3.326,2.158,1.973,3.397,2.535,2.917,4.907,3.858,2.970,3.587,2.540,1.871,2.989,3.065,2.024,1.888,3.135,3.628,3.420,3.370,3.456,3.461,3.697,3.946,4.465,4.327,4.119,3.266,3.719,3.472,4.141,3.567,3.117,3.287,3.392,4.446,4.452,3.517,3.697,3.815,3.959,4.109,4.890,4.471
+-36.259,-3.191,0.421,0.652,1.827,-36.259,1.532,0.984,1.645,1.174,0.933,1.132,1.431,0.440,1.169,2.571,1.846,2.586,0.136,1.458,0.823,0.809,2.683,2.504,1.433,0.932,2.077,3.649,3.830,2.185,0.970,2.513,3.054,1.478,1.288,2.627,3.580,2.809,0.699,2.989,2.398,2.509,3.770,3.283,2.553,2.632,2.273,2.964,3.191,2.681,2.409,2.947,3.212,3.336,3.404,2.801,2.595,3.200,3.434,3.140,5.177,4.301,3.694,3.387,2.838,3.692,4.499,4.214,3.258,4.157,3.674,3.756,4.299,3.882,4.459,4.037,4.522,4.374,4.815,4.440
+-36.259,0.672,1.305,-0.539,1.796,-36.259,-0.475,1.715,2.153,3.398,2.684,1.316,0.730,1.899,0.179,2.151,3.071,2.305,2.435,1.650,0.952,-1.256,2.924,3.006,1.503,2.744,2.165,4.475,4.422,2.130,2.948,2.000,2.929,1.920,1.017,0.700,2.013,2.387,1.687,3.206,3.266,3.075,3.837,2.807,3.732,2.752,2.409,2.754,2.328,2.089,2.530,2.757,4.121,4.308,3.273,3.325,4.430,4.105,3.081,3.142,2.537,2.605,3.111,3.226,3.062,3.418,4.263,4.034,2.714,3.398,4.323,5.048,5.292,4.820,4.218,4.278,4.919,4.723,5.345,4.689
+-36.259,1.630,0.172,0.874,0.642,-36.259,-0.290,0.221,0.323,2.580,0.423,1.187,0.567,1.621,0.932,2.166,2.473,-0.678,1.449,1.569,0.492,0.182,1.764,1.498,0.837,1.737,2.398,3.909,3.856,2.756,2.794,2.398,3.711,2.945,1.446,2.232,2.527,2.211,2.828,3.615,3.055,2.890,3.893,3.751,3.296,3.563,3.170,1.648,3.380,2.830,3.235,3.743,3.886,3.828,3.467,2.951,3.029,2.948,3.376,3.369,3.968,4.018,4.387,3.903,3.941,3.643,4.047,3.768,2.731,3.424,3.933,4.657,4.619,4.351,4.589,4.013,3.509,3.904,4.745,4.486
+-36.259,1.840,-0.218,-1.500,2.492,-36.259,1.058,0.768,0.009,1.488,1.369,0.872,-0.408,0.864,-0.843,2.359,0.230,2.190,2.903,1.955,2.043,1.778,2.129,1.467,2.598,2.436,2.368,4.387,4.405,2.392,2.264,3.119,2.854,2.888,2.184,2.005,0.936,0.111,2.149,4.097,3.350,2.445,3.661,4.012,2.542,3.172,3.444,1.799,2.977,2.186,1.990,2.716,3.294,3.868,4.062,3.644,4.134,4.218,3.794,3.653,4.836,4.704,4.472,4.684,4.196,3.175,3.491,3.134,2.877,3.261,3.516,3.981,4.406,4.542,4.785,4.326,4.592,4.981,4.435,4.682
+-36.259,-0.704,0.239,0.364,0.987,-36.259,1.453,-0.056,-0.165,3.032,1.081,0.597,1.444,-0.631,-0.121,2.591,2.669,2.620,0.559,1.626,2.316,1.004,2.979,3.159,2.042,1.782,3.017,4.824,4.725,3.239,2.465,1.924,3.023,1.951,1.868,2.305,2.798,2.548,2.618,2.750,3.269,2.216,2.997,3.339,3.348,3.450,3.160,2.802,2.899,2.065,2.608,2.755,4.173,4.192,3.534,3.619,3.925,3.486,3.397,3.892,4.437,4.086,4.012,3.797,3.716,4.078,4.402,4.079,2.817,3.046,2.903,4.238,4.407,4.317,3.959,3.333,5.241,4.316,3.635,4.629
+-36.259,1.447,-0.635,1.662,1.003,-36.259,-0.044,0.599,0.919,1.890,2.385,1.821,-0.776,0.492,2.094,0.326,2.236,2.154,1.520,0.736,0.073,0.860,0.993,1.561,0.983,1.711,2.154,4.752,4.763,2.800,1.819,1.917,2.316,0.908,-0.101,2.857,3.496,2.804,2.301,3.623,3.252,3.391,3.276,4.076,3.685,3.985,2.961,2.470,3.664,2.740,2.402,2.874,4.517,4.494,3.506,3.204,3.132,3.015,2.774,3.214,3.487,4.073,3.796,3.467,3.371,3.721,3.606,3.153,2.311,2.816,4.076,4.792,4.835,3.942,3.674,3.837,5.046,4.932,4.904,4.849
+-36.259,1.918,1.664,1.862,1.575,-36.259,2.293,0.282,1.342,1.982,3.414,2.415,1.569,0.909,0.966,2.075,0.822,1.366,2.288,1.978,2.195,1.950,3.225,1.653,1.752,2.592,2.763,4.561,4.572,2.529,1.667,1.620,2.520,0.899,1.422,2.519,2.168,1.792,1.843,2.606,2.990,2.286,3.784,3.207,1.709,2.489,1.751,2.486,3.140,3.243,1.506,1.688,3.518,3.822,2.488,2.611,3.043,3.789,3.559,3.512,5.232,4.632,3.547,3.859,3.755,3.511,3.519,2.991,2.906,3.455,3.448,4.026,4.047,3.969,4.129,3.831,5.086,4.606,4.528,5.088
+-36.259,2.589,1.108,-0.064,2.218,-36.259,1.965,-0.251,1.435,1.084,1.938,1.812,1.307,1.057,1.975,0.792,1.327,1.871,1.224,1.882,0.813,1.381,2.035,1.312,1.461,2.612,2.894,4.724,4.800,2.887,2.010,1.601,2.047,0.793,1.307,2.419,3.384,3.051,1.548,3.620,2.943,2.909,4.011,3.486,3.072,3.478,3.373,2.463,1.683,2.102,2.590,3.384,4.243,4.392,3.851,3.394,4.092,4.141,2.960,3.728,4.932,4.959,3.902,3.704,4.056,4.164,4.514,3.737,3.094,3.278,3.304,3.919,3.955,3.620,3.669,3.650,4.777,4.358,4.366,4.547
+-36.259,1.379,1.068,1.569,-1.951,-36.259,2.224,-2.306,0.271,0.698,0.736,0.445,0.148,0.553,-0.827,0.080,-0.429,0.380,0.294,1.798,1.498,0.014,1.598,1.835,-0.811,-0.909,1.859,4.472,4.435,2.131,0.203,1.753,1.730,1.742,0.918,2.642,3.151,2.959,1.208,3.205,2.806,3.216,4.334,3.697,2.854,3.316,1.687,2.211,2.745,2.210,3.335,3.501,4.651,5.009,3.889,3.848,3.764,3.400,3.662,3.484,4.051,3.947,3.880,4.414,4.642,4.486,4.948,4.183,3.019,4.137,3.501,3.794,4.177,4.682,4.619,3.998,4.793,4.467,4.738,5.258
+-36.259,2.295,2.445,0.142,3.083,-36.259,0.429,1.661,1.428,2.680,0.640,1.212,0.624,1.463,0.734,2.658,1.800,3.001,2.832,0.900,1.759,1.312,2.500,3.082,1.605,1.720,2.212,4.770,4.784,2.864,2.772,3.396,2.257,1.146,0.388,1.391,2.339,2.004,1.263,2.988,2.761,2.739,4.363,4.458,2.856,4.089,3.796,3.160,3.201,2.525,2.491,3.351,3.392,3.429,3.058,2.870,4.165,3.581,3.096,3.701,3.754,4.173,3.973,4.122,3.993,3.499,3.100,3.614,2.798,3.450,4.109,4.908,4.932,4.155,3.571,3.604,4.948,4.291,4.521,4.444
+-36.259,1.954,3.132,1.417,0.310,-36.259,1.419,1.867,0.233,2.959,2.082,1.802,1.218,1.350,0.225,1.262,1.422,1.070,1.990,1.298,1.277,0.306,2.117,1.271,2.150,1.829,2.023,4.495,4.604,2.470,3.066,1.787,2.112,2.158,1.833,2.622,3.047,2.901,1.919,2.835,1.485,2.328,3.432,3.787,3.796,2.551,3.233,2.955,3.441,2.222,3.003,2.921,4.040,4.083,3.758,3.624,3.453,3.651,4.336,4.380,4.496,4.485,4.458,4.242,4.234,4.274,3.876,3.527,2.923,3.526,3.885,4.450,4.604,4.337,3.567,3.517,4.230,3.963,4.575,4.545
+-36.259,1.618,0.925,-0.016,1.323,-36.259,1.448,2.077,2.497,2.859,2.679,1.483,-0.649,-0.346,0.972,0.795,1.240,1.380,1.123,1.557,0.728,-1.052,2.339,2.070,1.692,2.895,1.579,3.960,4.158,2.278,3.070,2.557,2.185,1.384,0.646,2.236,3.136,2.348,2.072,3.456,2.294,3.012,4.173,3.336,3.718,3.186,3.272,2.729,3.047,2.123,2.265,2.775,2.996,3.527,3.371,3.529,4.125,3.304,3.994,4.319,4.951,5.225,4.038,3.677,3.877,3.602,3.796,3.162,2.557,4.392,3.757,4.779,4.680,4.244,5.052,3.990,4.407,4.148,4.170,3.853
+-36.259,2.413,2.523,0.608,2.311,-36.259,1.231,1.204,1.170,0.549,2.288,2.144,0.716,0.937,-2.617,2.343,2.355,2.226,1.589,0.886,1.585,1.978,2.354,2.128,1.723,1.830,2.631,4.371,4.048,1.707,2.311,1.630,1.315,1.728,1.745,1.800,2.852,2.819,2.454,2.653,2.838,2.905,4.083,3.484,3.086,3.283,2.299,3.016,3.103,2.109,1.670,2.515,3.490,3.423,2.994,2.333,3.461,3.766,3.441,3.604,4.762,4.050,3.862,3.930,3.667,3.804,3.543,3.731,2.878,3.592,4.092,4.464,4.825,4.549,3.832,3.686,5.114,4.231,4.901,4.550
+-36.259,-0.387,2.565,0.661,-0.884,-36.259,0.516,0.662,0.029,2.265,-0.729,1.541,-0.070,-1.657,1.293,1.585,1.214,2.295,1.336,1.684,1.530,-0.037,1.283,1.962,1.183,1.896,0.886,4.189,4.100,1.278,1.553,1.574,0.363,0.742,0.768,3.512,4.286,3.526,2.103,2.736,2.675,2.144,3.871,3.019,1.966,2.627,2.547,2.580,3.745,2.626,2.021,2.539,4.601,4.696,2.805,1.883,3.430,3.673,3.285,3.286,4.748,3.639,3.584,3.423,3.233,3.498,4.673,3.876,2.399,3.156,2.962,3.628,3.683,3.949,5.024,3.543,4.830,5.023,5.282,4.824
+-36.259,3.906,3.311,2.628,-0.160,-36.259,2.446,1.751,2.274,3.257,2.476,1.850,-0.759,-0.403,0.306,1.446,0.594,1.782,2.117,0.849,1.590,1.215,2.432,2.282,2.116,1.377,2.650,4.522,4.235,1.958,1.159,1.376,1.710,1.920,0.471,2.021,1.882,2.059,1.226,2.823,2.359,3.115,3.962,3.313,3.066,3.128,2.968,2.145,3.024,1.950,1.642,2.266,4.345,4.635,3.392,2.496,3.854,3.925,3.553,3.447,4.712,4.095,4.311,3.621,3.717,3.057,4.228,3.661,2.700,3.543,3.534,4.099,4.414,3.687,4.186,3.814,4.222,3.947,4.326,4.508
+-36.259,2.827,2.796,1.372,2.045,-36.259,1.894,0.508,1.442,2.592,0.631,1.341,0.984,0.891,0.115,2.340,2.062,3.005,1.940,1.801,0.673,0.511,1.495,2.444,0.996,0.950,2.623,4.521,4.855,3.609,2.079,2.589,1.763,1.867,1.354,1.605,1.213,0.934,1.507,4.113,3.401,2.628,3.885,3.658,2.191,3.559,3.188,1.814,2.709,1.394,2.324,2.835,4.452,4.498,2.754,1.886,3.407,3.513,3.132,3.837,4.677,4.678,3.726,3.675,3.189,3.334,4.126,3.539,2.456,2.572,3.955,4.007,4.026,4.037,3.674,3.588,5.012,4.035,4.023,4.220
+-36.259,1.464,-1.702,0.887,1.709,-36.259,0.422,-1.065,-0.946,-0.531,-4.348,-0.211,1.005,0.698,0.226,1.473,1.558,2.321,2.320,1.544,1.476,2.323,2.556,2.032,1.380,2.293,1.578,4.525,4.614,2.299,1.727,1.417,1.611,1.642,1.756,1.950,2.529,2.741,2.807,2.389,2.374,2.071,3.877,3.883,3.018,3.556,3.017,2.330,3.272,2.899,2.421,2.823,3.743,3.921,3.231,3.524,4.653,4.123,4.030,3.312,3.445,3.894,4.187,4.619,4.617,3.830,4.258,3.954,2.622,3.566,3.958,5.009,5.178,4.516,3.752,3.802,4.548,4.294,4.791,5.022
+-36.259,-0.188,-2.350,-0.951,1.430,-36.259,1.464,-0.145,2.396,2.247,2.751,-0.403,0.708,-0.082,1.705,0.906,1.832,0.844,1.737,0.717,-0.011,1.853,3.631,3.803,3.014,2.415,2.573,4.494,4.259,2.363,2.889,2.230,1.348,1.639,1.370,2.551,2.795,2.252,1.491,2.931,2.310,3.313,4.863,4.740,3.240,3.253,2.674,2.088,3.171,2.463,2.030,2.132,3.550,4.022,3.498,3.226,3.166,3.036,3.178,3.566,4.423,3.554,3.169,3.915,3.735,3.568,3.738,3.436,2.413,3.144,3.992,5.448,5.705,4.596,4.154,3.676,5.195,4.912,4.936,4.896
+-36.259,2.404,1.016,1.428,-2.553,-36.259,-0.539,1.616,1.616,1.557,0.560,0.146,-1.493,-0.791,2.246,2.280,2.281,1.566,1.705,0.841,0.140,0.643,1.925,2.338,1.690,1.179,2.825,4.311,4.353,2.984,1.831,2.180,0.901,0.535,0.941,3.042,3.473,2.729,1.372,3.050,1.340,1.410,3.988,3.907,2.445,2.495,2.446,3.115,2.983,2.314,2.214,2.736,4.415,4.584,3.554,3.548,4.465,4.213,3.314,3.656,4.663,4.255,4.150,4.402,4.086,3.236,3.309,2.848,1.861,3.403,4.077,5.224,5.290,4.049,3.668,3.287,5.127,4.652,4.938,5.148
+-36.259,2.543,2.845,1.106,1.333,-36.259,1.708,1.662,1.174,-2.369,1.200,0.165,-1.018,1.576,-0.605,2.446,2.384,2.557,0.618,0.677,-0.807,0.292,0.905,1.508,1.109,0.337,2.772,4.462,4.565,2.822,2.580,2.507,2.520,1.476,0.584,3.059,4.035,3.594,1.518,2.930,3.275,2.886,4.225,3.702,3.813,2.461,3.476,2.301,3.303,3.055,1.946,2.329,3.029,3.030,3.181,3.072,3.782,3.436,3.647,3.593,4.169,3.774,4.516,4.162,4.253,3.640,3.510,3.128,3.145,3.832,3.683,4.132,4.419,4.326,4.497,4.068,5.073,4.482,5.451,4.840
+-36.259,1.892,0.401,-1.766,0.829,-36.259,0.994,-0.083,0.012,1.682,2.854,1.535,-1.743,0.470,1.441,3.005,1.658,2.918,2.080,2.491,2.454,-0.787,2.047,0.299,1.373,2.070,2.356,3.932,3.960,2.325,3.182,2.139,1.512,1.392,0.861,2.012,2.440,2.164,1.821,2.329,1.956,1.506,3.751,3.115,2.628,2.929,1.585,2.465,3.243,2.513,1.330,2.283,3.506,3.867,2.880,3.040,3.708,4.195,4.158,4.109,4.548,4.292,5.141,4.163,3.808,3.374,3.232,3.164,2.072,3.307,3.923,5.350,5.215,4.222,3.722,3.735,4.677,4.671,4.345,4.936
+-36.259,2.129,-0.988,1.058,1.264,-36.259,1.443,-0.651,1.381,1.172,1.331,-0.310,0.254,0.850,-0.501,1.422,1.849,1.834,1.395,0.607,1.322,0.316,0.475,1.010,1.276,2.421,1.041,3.621,4.063,2.581,1.929,0.762,1.898,1.489,1.651,3.461,3.405,3.626,2.337,2.767,1.874,2.654,4.166,3.867,2.565,3.477,2.675,2.981,3.731,3.006,2.040,3.133,4.445,4.144,2.957,2.024,3.598,3.395,3.034,2.743,3.406,4.190,4.829,4.176,3.576,3.709,3.531,3.143,3.184,3.156,3.596,4.543,4.789,4.122,4.789,4.451,4.829,4.429,5.167,4.513
+-36.259,2.607,2.158,1.492,-1.180,-36.259,0.907,0.813,-0.912,2.011,2.011,-0.092,1.184,-3.584,0.924,1.666,1.666,2.767,-0.366,1.666,1.491,0.121,2.549,2.269,1.924,1.390,1.678,4.544,4.656,2.699,1.392,1.849,2.608,1.583,1.849,1.589,2.507,1.589,0.949,2.866,2.783,2.536,4.487,4.235,2.691,3.058,1.920,0.852,1.852,1.503,3.018,3.387,4.645,5.016,3.835,3.046,3.046,3.684,3.780,3.928,4.576,4.906,4.521,4.194,4.110,4.278,4.431,3.918,3.203,3.613,3.506,4.085,4.206,3.515,4.145,3.863,4.873,4.660,4.157,3.995
+-36.259,2.756,2.371,0.200,2.301,-36.259,2.168,0.492,1.574,1.920,1.082,0.621,1.471,0.635,1.745,2.208,1.755,1.898,1.599,1.852,1.434,0.835,0.287,0.940,0.570,1.632,2.271,4.330,4.476,2.602,2.058,1.864,2.328,1.696,1.825,1.688,2.476,1.221,0.935,3.054,2.715,3.084,4.105,3.796,2.650,3.085,2.558,2.356,3.463,2.744,1.752,1.812,3.685,3.952,3.564,2.889,3.864,4.050,3.150,3.608,4.033,3.572,4.080,4.154,3.813,3.496,3.725,3.405,2.901,3.312,3.691,4.990,5.034,4.243,3.919,3.780,3.868,3.898,4.267,5.115
+-36.259,3.358,1.161,1.265,0.675,-36.259,1.112,-0.956,2.294,-0.855,2.100,2.074,1.428,-0.222,0.667,1.422,1.580,2.584,2.606,1.526,0.507,2.184,2.830,3.134,1.683,1.031,1.752,4.238,4.389,3.191,2.679,2.201,2.220,2.670,1.474,1.701,1.741,2.507,2.265,3.069,2.616,2.766,4.384,4.376,3.267,3.081,3.441,2.772,3.089,2.894,3.542,4.243,5.017,5.065,4.259,3.681,4.166,3.750,3.247,3.079,3.674,3.753,3.472,4.013,4.178,4.277,4.716,4.449,3.683,3.625,3.854,4.531,4.356,4.153,3.846,4.001,4.756,4.898,4.992,4.931
+-36.259,0.950,1.864,-1.583,0.480,-36.259,1.412,0.945,2.720,2.148,2.937,2.160,-0.974,1.079,1.301,1.722,0.983,2.206,1.648,2.090,1.440,1.584,1.924,1.963,2.096,1.523,2.883,4.620,4.614,2.790,1.785,1.986,1.748,2.282,1.026,2.613,2.689,1.882,2.675,4.003,3.302,3.274,3.734,3.143,3.399,4.112,4.136,2.328,2.852,2.894,2.621,2.782,3.593,3.885,2.958,2.775,3.484,3.702,3.932,3.804,3.953,4.062,4.580,4.514,4.012,3.256,3.928,3.731,3.402,3.276,3.383,3.957,4.549,4.161,4.277,4.705,5.092,4.242,4.820,4.854
+-36.259,1.086,1.822,0.318,1.958,-36.259,0.906,-2.539,2.140,1.354,1.808,2.228,2.248,-0.196,-0.272,2.578,2.029,1.660,2.108,0.908,2.320,2.265,3.435,3.656,3.258,2.536,2.554,4.302,4.301,2.624,2.963,3.078,2.386,2.413,1.289,2.057,2.531,2.488,1.723,2.303,2.556,3.169,4.507,3.932,3.022,2.803,3.099,2.347,2.565,2.545,2.500,3.245,3.632,4.043,3.410,3.081,3.724,3.664,3.671,4.339,3.561,3.821,4.774,3.752,3.659,3.647,3.608,3.594,3.608,3.052,3.474,4.849,5.397,4.025,3.117,3.504,4.246,4.864,4.156,4.697
+-36.259,2.056,-0.405,-0.048,0.152,-36.259,1.749,-0.944,1.591,-0.557,-1.097,0.772,1.067,-0.725,-4.173,0.928,2.051,1.908,0.899,0.464,1.261,0.798,2.712,3.057,2.455,2.226,3.138,4.548,4.357,2.659,2.263,1.881,1.228,0.045,1.541,1.951,2.135,2.650,1.122,3.394,3.047,2.844,3.222,3.244,3.336,3.831,2.639,1.912,1.894,1.737,2.279,3.730,4.646,4.772,3.889,2.936,2.883,2.656,3.254,3.719,3.743,4.219,4.398,3.409,3.445,4.045,4.820,3.998,2.012,2.629,3.881,4.107,3.699,4.022,3.455,4.606,5.844,5.036,3.855,5.192
+-36.259,1.714,1.176,1.135,-2.244,-36.259,1.257,1.003,1.641,-0.514,-0.905,-0.100,-0.032,-2.069,1.796,1.339,1.057,0.718,-2.018,-0.874,-0.035,-1.600,2.116,2.140,1.106,1.707,3.365,4.705,4.819,3.332,1.960,2.283,3.229,1.475,1.180,1.748,2.510,2.676,0.765,2.443,2.941,2.647,3.665,3.118,2.987,3.844,3.151,2.609,2.845,2.292,2.081,2.736,4.303,4.588,3.317,2.764,2.605,2.613,2.790,2.659,3.847,4.226,3.175,3.279,3.518,3.374,3.421,3.461,2.871,3.210,4.820,5.291,5.505,5.320,4.149,4.150,5.476,5.254,4.498,5.042
+-36.259,1.229,2.258,0.597,1.745,-36.259,0.251,1.242,1.745,0.920,2.577,1.620,0.839,-0.250,-0.019,2.316,2.329,1.930,2.400,1.149,0.826,0.071,1.625,2.463,1.317,2.666,2.424,4.478,4.330,1.952,1.656,2.497,3.295,2.189,1.670,3.516,3.813,3.280,2.312,3.351,3.238,2.474,3.578,3.198,2.611,2.949,2.714,3.554,3.468,1.688,3.759,4.045,4.637,4.670,3.983,3.562,4.016,3.334,3.243,3.817,3.493,3.702,3.737,3.658,3.819,4.579,4.231,4.221,3.746,3.890,4.017,4.773,5.057,4.085,3.963,3.564,4.647,4.326,4.564,4.806
+-36.259,0.971,0.052,1.378,-0.332,-36.259,0.949,0.591,0.271,2.156,1.539,1.427,-1.635,-1.203,0.594,1.674,1.760,1.465,-1.192,0.269,0.586,0.710,2.559,2.461,1.413,1.659,1.682,4.493,4.340,1.719,2.461,2.050,2.591,2.549,0.712,3.299,3.587,3.245,1.992,3.503,3.044,2.675,4.334,3.734,3.222,3.366,3.072,2.997,2.999,2.200,2.621,1.943,3.072,3.200,2.717,2.712,3.252,3.324,2.867,2.637,4.094,2.881,3.383,3.682,3.890,3.479,3.139,3.264,3.187,3.251,4.144,4.240,3.983,4.361,4.668,3.853,4.389,3.679,4.276,4.648
+-36.259,2.540,1.243,0.422,0.417,-36.259,0.098,1.826,1.718,0.055,2.128,1.932,1.638,1.916,-0.208,2.489,2.016,2.569,3.080,2.597,2.152,1.549,1.631,2.113,1.372,2.160,1.410,4.139,4.153,1.822,2.349,2.352,2.004,1.633,1.847,1.857,2.525,1.617,0.994,4.230,3.229,3.741,4.449,4.043,3.911,3.991,3.837,2.065,3.078,3.015,2.047,2.124,3.523,3.405,2.571,2.493,2.211,2.497,2.685,3.104,4.607,4.178,3.566,3.707,3.662,4.122,3.483,3.497,2.821,3.332,3.595,4.552,4.881,4.687,4.722,3.907,4.765,4.940,5.106,4.485
+-36.259,3.270,1.959,2.464,0.798,-36.259,1.111,0.354,1.673,-0.614,1.806,0.027,1.758,1.178,0.950,3.024,2.217,2.662,2.548,1.857,1.725,-0.730,2.067,3.007,2.408,0.994,2.017,4.143,4.182,2.529,1.856,2.309,1.239,2.390,1.877,1.192,2.600,2.513,2.522,2.858,1.609,3.114,4.679,4.637,3.070,2.931,2.834,2.447,2.655,2.680,3.097,3.088,3.891,4.220,2.412,2.412,2.868,2.675,3.612,4.011,4.494,4.058,3.864,3.709,3.033,3.765,4.264,3.820,3.315,2.812,3.566,4.615,4.521,4.071,4.274,4.119,5.525,4.815,4.567,4.667
diff --git a/ahakeyconfig-win-java/pom.xml b/ahakeyconfig-win-java/pom.xml
new file mode 100644
index 00000000..d6677ef0
--- /dev/null
+++ b/ahakeyconfig-win-java/pom.xml
@@ -0,0 +1,167 @@
+
+
+ 4.0.0
+
+ com.example
+ ahakey-studio
+ 1.0.0
+ jar
+
+
+ 17
+ 17.0.10
+ ${java.version}
+ ${java.version}
+ UTF-8
+
+
+
+
+
+ org.openjfx
+ javafx-controls
+ ${javafx.version}
+
+
+ org.openjfx
+ javafx-fxml
+ ${javafx.version}
+
+
+ org.openjfx
+ javafx-graphics
+ ${javafx.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.16.0
+
+
+
+
+ net.java.dev.jna
+ jna
+ 5.14.0
+
+
+ net.java.dev.jna
+ jna-platform
+ 5.14.0
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.9
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.11
+
+
+ ch.qos.logback
+ logback-core
+ 1.4.11
+
+
+
+
+ com.microsoft.onnxruntime
+ onnxruntime
+ 1.16.3
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+ org.openjfx
+ javafx-maven-plugin
+ 0.0.8
+
+ com.example.ahakey.App
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+
+ copy-resources
+ process-resources
+
+ copy-resources
+
+
+ ${project.build.directory}/classes
+
+
+ src/main/resources
+
+ **/*
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ com.example.ahakey.App
+ true
+ lib/
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.6.0
+
+
+ copy-dependencies
+ package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/lib
+ runtime
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ahakeyconfig-win-java/sources.txt b/ahakeyconfig-win-java/sources.txt
new file mode 100644
index 00000000..43889eba
--- /dev/null
+++ b/ahakeyconfig-win-java/sources.txt
@@ -0,0 +1,17 @@
+src/main/java/com/example/ahakey/util/ConfigStore.java
+src/main/java/com/example/ahakey/util/Icons.java
+src/main/java/com/example/ahakey/protocol/AhaKeyProtocol.java
+src/main/java/com/example/ahakey/Main.java
+src/main/java/com/example/ahakey/model/IDEState.java
+src/main/java/com/example/ahakey/model/KeyConfig.java
+src/main/java/com/example/ahakey/model/DeviceStatus.java
+src/main/java/com/example/ahakey/model/HIDUsage.java
+src/main/java/com/example/ahakey/view/CanvasPane.java
+src/main/java/com/example/ahakey/view/InfoPill.java
+src/main/java/com/example/ahakey/view/StatusBar.java
+src/main/java/com/example/ahakey/view/InspectorPane.java
+src/main/java/com/example/ahakey/view/TopBar.java
+src/main/java/com/example/ahakey/App.java
+src/main/java/com/example/ahakey/service/BleManager.java
+src/main/java/com/example/ahakey/service/HookClient.java
+src/main/java/com/example/ahakey/service/SocketServer.java
diff --git a/ahakeyconfig-win-java/src/main/java/com/example/ahakey/App.java b/ahakeyconfig-win-java/src/main/java/com/example/ahakey/App.java
new file mode 100644
index 00000000..6690a891
--- /dev/null
+++ b/ahakeyconfig-win-java/src/main/java/com/example/ahakey/App.java
@@ -0,0 +1,463 @@
+package com.example.ahakey;
+
+import com.example.ahakey.app.StudioController;
+import com.example.ahakey.platform.windows.WindowsVoiceRelayService;
+import com.example.ahakey.platform.windows.WindowsVoiceTyping;
+import com.example.ahakey.service.VoiceInputManager;
+import com.example.ahakey.view.CanvasPane;
+import com.example.ahakey.view.InspectorPane;
+import com.example.ahakey.view.StatusBar;
+import com.example.ahakey.view.TopBar;
+import javafx.animation.PauseTransition;
+import javafx.util.Duration;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Scene;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.image.Image;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.stage.Screen;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.awt.SystemTray;
+import java.awt.TrayIcon;
+import java.awt.MenuItem;
+import java.awt.PopupMenu;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.Toolkit;
+import java.awt.Font;
+
+/**
+ * AhaKey Studio 主应用类
+ *
+ * JavaFX 桌面应用的入口,类比于 Java Web 中的 Servlet 或 Spring Boot 的 Application 类。
+ *
+ * JavaFX 应用结构说明(类比 Web 开发):
+ * - Stage(舞台):相当于浏览器窗口,是整个应用的顶层容器
+ * - Scene(场景):相当于 HTML 的 body,包含所有 UI 元素
+ * - Pane(面板):相当于 HTML 的 div,用于布局管理
+ * - Node(节点):相当于 HTML 的各种标签(button、label 等)
+ *
+ * 生命周期方法:
+ * 1. main() - 程序入口,调用 launch() 启动 JavaFX 运行时
+ * 2. init() - 可选,在 start() 之前调用,用于初始化资源(本项目未使用)
+ * 3. start() - 核心方法,构建 UI 界面
+ * 4. stop() - 可选,应用关闭时调用(本项目通过 setOnCloseRequest 处理)
+ */
+public class App extends Application {
+
+ private static final Logger logger = LoggerFactory.getLogger(App.class);
+
+ /**
+ * 主控制器,负责协调各个组件之间的通信
+ * 类比于 Spring MVC 中的 Controller,管理应用的业务逻辑和状态
+ */
+ private StudioController controller;
+
+ /**
+ * 语音输入管理器,负责管理语音识别和键盘注入功能
+ */
+ private VoiceInputManager voiceInputManager;
+ private boolean shuttingDown;
+
+ /**
+ * 系统托盘图标
+ */
+ private TrayIcon trayIcon;
+ private Stage primaryStage;
+
+ /**
+ * 保存窗口原始尺寸,用于从托盘恢复时恢复尺寸
+ */
+ private double savedWidth = 1280;
+ private double savedHeight = 820;
+
+ /**
+ * JavaFX 应用的核心方法,负责初始化和显示主界面
+ * @param primaryStage 主舞台(窗口),由 JavaFX 运行时自动创建
+ */
+ @Override
+ public void start(Stage primaryStage) {
+ this.primaryStage = primaryStage;
+
+ // 禁用隐式退出:当窗口隐藏时不会自动关闭应用,只有明确调用 Platform.exit() 才会退出
+ Platform.setImplicitExit(false);
+
+ // 1. 创建主控制器,作为整个应用的核心协调者
+ controller = new StudioController();
+
+ // 2. 条件初始化语音输入管理器(根据 model.enabled 配置)
+ if (com.example.ahakey.config.ModelConfig.getInstance().isEnabled()) {
+ initVoiceInputManager();
+ } else {
+ logger.info("本地模型已禁用 (model.enabled=false),跳过语音输入初始化");
+ }
+
+ // 3. 初始化系统托盘
+ initSystemTray();
+
+ // 4. 配置主窗口(Stage)属性
+ primaryStage.setTitle("AhaKey Studio"); // 窗口标题
+ primaryStage.setMinWidth(800); // 最小宽度(分屏/多屏适配)
+ primaryStage.setMinHeight(600); // 最小高度
+ primaryStage.setWidth(1280); // 默认宽度
+ primaryStage.setHeight(820); // 默认高度
+ primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("/ahakey.jpg")));
+
+ // 3. 创建根布局容器
+ // BorderPane 是一个边界布局,分为五个区域:top、bottom、left、right、center
+ // 类比于 Web 中的 +