Files

955 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AIC Pico Configuration</title>
<style>
/* 内联CSS以避免外部文件问题 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 {
color: #2c3e50;
font-size: 2em;
}
.connection-panel {
display: flex;
align-items: center;
gap: 15px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
text-transform: uppercase;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:disabled,
.btn-secondary.saved {
background: #bdc3c7;
cursor: not-allowed;
opacity: 0.7;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.status {
padding: 5px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status.connected {
background: #d5f4e6;
color: #27ae60;
}
.status.disconnected {
background: #fadbd8;
color: #e74c3c;
}
.config-panel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.hidden {
display: none !important;
}
.card {
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card h2 {
color: #2c3e50;
margin-bottom: 20px;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
.setting-group {
margin-bottom: 20px;
}
.setting-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #34495e;
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.radio-group label {
display: flex;
align-items: center;
margin-bottom: 0;
cursor: pointer;
gap: 4px;
}
.radio-group input[type="radio"] {
margin: 0;
}
select, input[type="checkbox"] {
padding: 8px;
border: 2px solid #bdc3c7;
border-radius: 5px;
font-size: 14px;
}
.slider-group {
display: flex;
flex-direction: column;
gap: 15px;
}
.slider-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.slider-item label {
display: flex;
justify-content: space-between;
align-items: center;
}
input[type="range"] {
width: 100%;
height: 8px;
border-radius: 5px;
background: #ddd;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #3498db;
cursor: pointer;
}
.info-display {
background: #ecf0f1;
padding: 10px;
border-radius: 5px;
border-left: 4px solid #3498db;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.debug-console {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.debug-output {
background: #2c3e50;
color: #ecf0f1;
padding: 15px;
border-radius: 5px;
height: 200px;
overflow-y: auto;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
margin-bottom: 10px;
white-space: pre-wrap;
}
.debug-input {
display: flex;
gap: 10px;
}
.debug-input input {
flex: 1;
padding: 10px;
border: 2px solid #bdc3c7;
border-radius: 5px;
}
.error-message {
background: #fadbd8;
color: #e74c3c;
padding: 20px;
border-radius: 10px;
margin: 20px;
text-align: center;
}
.checkbox-group {
display: flex;
gap: 15px;
}
.checkbox-group label {
display: flex;
align-items: center;
margin-bottom: 0;
cursor: pointer;
gap: 8px;
}
.checkbox-group input[type="checkbox"] {
margin: 0;
}
/* 新增样式:禁用状态 */
.card.disabled {
opacity: 0.6;
pointer-events: none;
background: rgba(200, 200, 200, 0.95);
}
.card.disabled h2 {
color: #7f8c8d;
border-bottom-color: #bdc3c7;
}
.card.disabled input,
.card.disabled select,
.card.disabled button {
opacity: 0.7;
cursor: not-allowed;
}
/* 新增样式:加载指示器 */
.loading-indicator {
color: #95a5a6;
font-style: italic;
margin-top: 10px;
}
</style>
</head>
<body>
<div id="app">
<!-- Web Serial API 支持检查 -->
<div id="unsupported" class="error-message" style="display: none;">
<h2>Web Serial API Not Supported</h2>
<p>This browser doesn't support Web Serial API.</p>
<p>Please use Chrome 89+ or Edge 89+ and access via HTTPS or localhost.</p>
</div>
<!-- 主界面 -->
<div id="mainApp" class="container">
<header>
<h1>AIC Pico Configuration</h1>
<div class="connection-panel">
<button id="connectBtn" class="btn btn-primary">CONNECT</button>
<span id="connectionStatus" class="status disconnected">Disconnected</span>
</div>
</header>
<main id="configPanel" class="config-panel hidden">
<!-- Light Settings -->
<section class="card">
<h2>Light Settings</h2>
<div class="setting-group">
<label>Light Mode:</label>
<div class="checkbox-group">
<label><input type="checkbox" id="rgbEnabled"> RGB</label>
<label><input type="checkbox" id="ledEnabled"> LED</label>
</div>
</div>
<div class="setting-group">
<label>RGB Order:</label>
<select id="rgbOrder">
<option value="grb">GRB</option>
<option value="brg">BRG</option>
<option value="rgb">RGB</option>
<option value="rbg">RBG</option>
</select>
</div>
<div class="setting-group">
<label>Brightness Levels:</label>
<div class="slider-group">
<div class="slider-item">
<label>Idle: <span id="idleValue">24</span></label>
<input type="range" id="idleSlider" min="0" max="255" value="24">
</div>
<div class="slider-item">
<label>Active: <span id="activeValue">100</span></label>
<input type="range" id="activeSlider" min="0" max="255" value="100">
</div>
</div>
</div>
</section>
<!-- Reader Settings -->
<section class="card">
<h2>Reader Settings</h2>
<div class="setting-group">
<label>
<input type="checkbox" id="virtualAic"> Virtual AIC
</label>
</div>
<div class="setting-group">
<label>Reader Mode:</label>
<select id="readerMode">
<option value="auto">Auto</option>
<option value="aime0">Aime 0</option>
<option value="aime1">Aime 1</option>
<option value="bana">Bandai Namco</option>
</select>
</div>
<div class="setting-group">
<label>Detected Mode:</label>
<span id="detectedMode" class="info-display">None</span>
</div>
</section>
<!-- LCD Settings -->
<section class="card">
<h2>LCD Settings</h2>
<div class="setting-group">
<label>Backlight: <span id="lcdValue">200</span></label>
<input type="range" id="lcdSlider" min="0" max="255" value="200">
</div>
</section>
<!-- NFC Module Info -->
<section class="card">
<h2>NFC Module</h2>
<div id="nfcInfo" class="info-display">Unknown ()</div>
</section>
<!-- Action Buttons -->
<section class="card">
<h2>Actions</h2>
<div class="button-group">
<button id="refreshBtn" class="btn btn-success">Refresh</button>
<button id="saveBtn" class="btn btn-success">Save to Flash</button>
</div>
<div class="button-group" style="margin-top: 10px;">
<button id="factoryResetBtn" class="btn btn-danger">Factory Reset</button>
<button id="updateBtn" class="btn btn-danger">Update Firmware</button>
</div>
</section>
</main>
<!-- Debug Console -->
<section class="debug-console">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3>Debug Console</h3>
<button id="clearBtn" class="btn btn-secondary">Clear</button>
</div>
<div id="debugOutput" class="debug-output"></div>
<div class="debug-input">
<input type="text" id="commandInput" placeholder="Enter command...">
<button id="sendBtn" class="btn btn-secondary">Send</button>
</div>
</section>
</div>
</div>
<script>
// 检查Web Serial API支持
if (!('serial' in navigator)) {
document.getElementById('unsupported').style.display = 'block';
document.getElementById('mainApp').style.display = 'none';
} else {
// 主控制器类
class AICPicoController {
constructor() {
this.port = null;
this.reader = null;
this.writer = null;
this.connected = false;
this.hasUnsavedChanges = false;
this.configParseRetries = 0;
this.maxConfigRetries = 3;
this.configLoaded = false;
this.initializeElements();
this.attachEventListeners();
}
initializeElements() {
// Connection elements
this.connectBtn = document.getElementById('connectBtn');
this.connectionStatus = document.getElementById('connectionStatus');
this.configPanel = document.getElementById('configPanel');
// Control elements
this.lightModeRadios = document.querySelectorAll('input[name="lightMode"]');
this.rgbOrderSelect = document.getElementById('rgbOrder');
this.idleSlider = document.getElementById('idleSlider');
this.activeSlider = document.getElementById('activeSlider');
this.idleValue = document.getElementById('idleValue');
this.activeValue = document.getElementById('activeValue');
this.lcdSlider = document.getElementById('lcdSlider');
this.lcdValue = document.getElementById('lcdValue');
this.virtualAicCheck = document.getElementById('virtualAic');
this.readerModeSelect = document.getElementById('readerMode');
// Info displays
this.nfcInfo = document.getElementById('nfcInfo');
this.detectedMode = document.getElementById('detectedMode');
// Action buttons
this.refreshBtn = document.getElementById('refreshBtn');
this.saveBtn = document.getElementById('saveBtn');
this.updateBtn = document.getElementById('updateBtn');
this.factoryResetBtn = document.getElementById('factoryResetBtn');
// Debug console
this.debugOutput = document.getElementById('debugOutput');
this.commandInput = document.getElementById('commandInput');
this.sendBtn = document.getElementById('sendBtn');
this.clearBtn = document.getElementById('clearBtn');
// Light mode checkboxes
this.rgbEnabled = document.getElementById('rgbEnabled');
this.ledEnabled = document.getElementById('ledEnabled');
}
attachEventListeners() {
// Connection
this.connectBtn.addEventListener('click', () => this.toggleConnection());
// Slider updates
this.idleSlider.addEventListener('input', (e) => {
this.idleValue.textContent = e.target.value;
this.updateLightLevel();
});
this.activeSlider.addEventListener('input', (e) => {
this.activeValue.textContent = e.target.value;
this.updateLightLevel();
});
this.lcdSlider.addEventListener('input', (e) => {
this.lcdValue.textContent = e.target.value;
this.updateLcdBacklight();
});
// Light mode
this.lightModeRadios.forEach(radio => {
radio.addEventListener('change', () => this.updateLightMode());
});
// RGB order
this.rgbOrderSelect.addEventListener('change', () => this.updateRgbOrder());
// Virtual AIC
this.virtualAicCheck.addEventListener('change', () => this.updateVirtualAic());
// Reader mode
this.readerModeSelect.addEventListener('change', () => this.updateReaderMode());
// Action buttons
this.refreshBtn.addEventListener('click', () => this.refreshSettings());
this.saveBtn.addEventListener('click', () => this.saveSettings());
this.updateBtn.addEventListener('click', () => this.enterUpdateMode());
this.factoryResetBtn.addEventListener('click', () => this.factoryReset());
// Debug console
this.sendBtn.addEventListener('click', () => this.sendCommand());
this.clearBtn.addEventListener('click', () => this.clearConsole());
this.commandInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.sendCommand();
});
// Light mode checkboxes
this.rgbEnabled.addEventListener('change', () => this.updateLightMode());
this.ledEnabled.addEventListener('change', () => this.updateLightMode());
}
async toggleConnection() {
if (this.connected) {
await this.disconnect();
} else {
await this.connect();
}
}
async connect() {
try {
// Request serial port
this.port = await navigator.serial.requestPort();
await this.port.open({ baudRate: 115200 });
this.reader = this.port.readable.getReader();
this.writer = this.port.writable.getWriter();
this.connected = true;
this.updateConnectionUI();
// Start reading responses
this.startReading();
// 增加延迟并确保配置获取成功
setTimeout(async () => {
await this.refreshSettings();
// 如果2秒后仍未正确解析配置,再次尝试
setTimeout(() => {
this.retryConfigIfNeeded();
}, 2000);
}, 1000);
} catch (error) {
this.log(`Connection failed: ${error.message}`);
}
}
async disconnect() {
try {
if (this.reader) {
await this.reader.cancel();
await this.reader.releaseLock();
}
if (this.writer) {
await this.writer.releaseLock();
}
if (this.port) {
await this.port.close();
}
this.connected = false;
this.updateConnectionUI();
} catch (error) {
this.log(`Disconnect failed: ${error.message}`);
}
}
updateConnectionUI() {
if (this.connected) {
this.connectBtn.textContent = 'DISCONNECT';
this.connectionStatus.textContent = 'Connected';
this.connectionStatus.className = 'status connected';
this.configPanel.classList.remove('hidden');
// 连接后先禁用所有面板
this.setConfigPanelsState(false);
this.updateSaveButtonState();
} else {
this.connectBtn.textContent = 'CONNECT';
this.connectionStatus.textContent = 'Disconnected';
this.connectionStatus.className = 'status disconnected';
this.configPanel.classList.add('hidden');
this.configLoaded = false;
}
}
setConfigPanelsState(enabled) {
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
if (enabled) {
card.classList.remove('disabled');
} else {
card.classList.add('disabled');
}
});
if (!enabled && this.connected) {
this.addLoadingIndicator();
} else {
this.removeLoadingIndicator();
}
}
addLoadingIndicator() {
const cards = document.querySelectorAll('.card h2');
cards.forEach(h2 => {
if (!h2.querySelector('.loading-indicator')) {
const indicator = document.createElement('span');
indicator.className = 'loading-indicator';
indicator.textContent = ' (Loading...)';
h2.appendChild(indicator);
}
});
}
removeLoadingIndicator() {
const indicators = document.querySelectorAll('.loading-indicator');
indicators.forEach(indicator => indicator.remove());
}
markConfigChanged() {
this.hasUnsavedChanges = true;
this.updateSaveButtonState();
}
markConfigSaved() {
this.hasUnsavedChanges = false;
this.updateSaveButtonState();
}
updateSaveButtonState() {
if (this.hasUnsavedChanges) {
this.saveBtn.textContent = 'Save to Flash';
this.saveBtn.className = 'btn btn-success';
this.saveBtn.disabled = false;
} else {
this.saveBtn.textContent = 'Saved';
this.saveBtn.className = 'btn btn-secondary saved';
this.saveBtn.disabled = true;
}
}
async startReading() {
try {
while (this.connected) {
const { value, done } = await this.reader.read();
if (done) break;
const text = new TextDecoder().decode(value);
this.log(text, 'response');
}
} catch (error) {
if (this.connected) {
this.log(`Read error: ${error.message}`);
}
}
}
async sendCommand(command = null) {
if (!this.connected) return;
const cmd = command || this.commandInput.value.trim();
if (!cmd) return;
try {
const data = new TextEncoder().encode(cmd + '\n');
await this.writer.write(data);
if (!command) {
this.commandInput.value = '';
}
} catch (error) {
this.log(`Send error: ${error.message}`);
}
}
parseAndUpdateConfig(message) {
let configParsed = false;
if (message.includes('RGB-')) {
const rgbMatch = message.match(/RGB-(\w+) \((\w+)\), LED-(\w+)/);
if (rgbMatch) {
this.rgbEnabled.checked = rgbMatch[1] === 'ON';
this.rgbOrderSelect.value = rgbMatch[2].toLowerCase();
this.ledEnabled.checked = rgbMatch[3] === 'ON';
configParsed = true;
}
}
if (message.includes('Level:')) {
const levelMatch = message.match(/Level: Idle-(\d+), Active-(\d+)/);
if (levelMatch) {
this.idleSlider.value = levelMatch[1];
this.idleValue.textContent = levelMatch[1];
this.activeSlider.value = levelMatch[2];
this.activeValue.textContent = levelMatch[2];
}
}
if (message.includes('Virtual AIC:')) {
const virtualMatch = message.match(/Virtual AIC: (ON|OFF)/);
if (virtualMatch) {
this.virtualAicCheck.checked = virtualMatch[1] === 'ON';
configParsed = true;
}
}
if (message.includes('Mode:')) {
const modeMatch = message.match(/Mode: (\w+)/);
if (modeMatch) {
const mode = modeMatch[1].toLowerCase();
if (['auto', 'aime0', 'aime1', 'bana'].includes(mode)) {
this.readerModeSelect.value = mode;
}
}
}
if (message.includes('Detected:')) {
const detectedMatch = message.match(/Detected: (\w+)/);
if (detectedMatch) {
this.detectedMode.textContent = detectedMatch[1];
}
}
if (message.includes('Backlight:')) {
const backlightMatch = message.match(/Backlight: (\d+)/);
if (backlightMatch) {
this.lcdSlider.value = backlightMatch[1];
this.lcdValue.textContent = backlightMatch[1];
configParsed = true;
}
}
if (configParsed && !this.configLoaded) {
this.configLoaded = true;
this.setConfigPanelsState(true);
}
}
log(message, type = 'info') {
// 检测保存完成消息
if (message.includes('Program Flash')) {
this.markConfigSaved();
}
// 简化配置解析触发条件 - 严格检测4个空格开头的配置行
if (message.includes(' RGB-') || message.includes(' Virtual AIC:') ||
message.includes(' Mode:') || message.includes(' Level:') ||
message.includes(' Backlight:') || message.includes(' Detected:') ||
message.includes('PN532') || message.includes('PN5180')) {
this.parseAndUpdateConfig(message);
}
// 对消息进行色彩处理
const coloredMessage = this.colorizeMessage(message);
this.debugOutput.innerHTML += coloredMessage;
this.debugOutput.scrollTop = this.debugOutput.scrollHeight;
}
colorizeMessage(message) {
// 转义HTML特殊字符,但保留原始格式
let escaped = message
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 添加色彩高亮
let colored = escaped
// [Section] 标题用黄色
.replace(/(\[.*?\])/g, '<span style="color: #f39c12;">$1</span>')
// 数字用青色
.replace(/\b(\d+)\b/g, '<span style="color: #3498db;">$1</span>')
// ON/OFF 状态用绿色/红色
.replace(/\b(ON)\b/g, '<span style="color: #27ae60;">$1</span>')
.replace(/\b(OFF)\b/g, '<span style="color: #e74c3c;">$1</span>')
// 冒号后的值用浅蓝色
.replace(/:\s*([A-Za-z0-9-]+)/g, ': <span style="color: #85c1e9;">$1</span>');
// 将换行符转换为<br>标签
return colored.replace(/\r?\n/g, '<br>');
}
// Settings update methods
async updateLightLevel() {
const idle = this.idleSlider.value;
const active = this.activeSlider.value;
await this.sendCommand(`level ${idle} ${active}`);
this.markConfigChanged();
}
async updateLcdBacklight() {
const backlight = this.lcdSlider.value;
await this.sendCommand(`lcd ${backlight}`);
this.markConfigChanged();
}
async updateLightMode() {
const rgbOn = this.rgbEnabled.checked;
const ledOn = this.ledEnabled.checked;
let mode;
if (rgbOn && ledOn) {
mode = 'both';
} else if (rgbOn) {
mode = 'rgb';
} else if (ledOn) {
mode = 'led';
} else {
mode = 'off';
}
await this.sendCommand(`light ${mode}`);
this.markConfigChanged();
}
async updateRgbOrder() {
const order = this.rgbOrderSelect.value;
await this.sendCommand(`rgb-order ${order}`);
this.markConfigChanged();
}
async updateVirtualAic() {
const enabled = this.virtualAicCheck.checked ? 'on' : 'off';
await this.sendCommand(`virtual ${enabled}`);
this.markConfigChanged();
}
async updateReaderMode() {
const mode = this.readerModeSelect.value;
await this.sendCommand(`mode ${mode}`);
this.markConfigChanged();
}
async refreshSettings() {
this.configParseRetries = 0;
this.configLoaded = false; // 重置配置加载状态
this.setConfigPanelsState(false); // 禁用面板
await this.sendCommand('display');
// 初始加载配置时标记为已保存状态
setTimeout(() => {
this.markConfigSaved();
}, 1000);
}
retryConfigIfNeeded() {
// 检查关键配置是否已加载
const hasLightConfig = this.rgbEnabled.checked || this.ledEnabled.checked ||
this.idleSlider.value !== '24' || this.activeSlider.value !== '100';
const hasReaderConfig = this.virtualAicCheck.checked || this.readerModeSelect.value !== 'auto';
const hasLcdConfig = this.lcdSlider.value !== '200';
// 如果配置看起来还是默认值,重新获取
if (!hasLightConfig && !hasReaderConfig && !hasLcdConfig) {
this.log('Configuration not loaded, retrying...');
this.refreshSettings();
}
}
async saveSettings() {
await this.sendCommand('save');
// 不要立即标记为已保存,等待设备确认
}
async enterUpdateMode() {
const confirmMessage = 'This will disconnect the device and enter update mode.\n\n' +
'You will need to manually copy the firmware file (.uf2) to the RPI-RP2 drive that appears.\n\n' +
'Do you want to proceed?';
if (confirm(confirmMessage)) {
try {
await this.sendCommand('update');
// 设备会自动断开连接进入bootloader模式
setTimeout(() => {
this.log('Device entered update mode. Look for RPI-RP2 drive and copy firmware file.');
}, 1000);
} catch (error) {
this.log(`Update mode failed: ${error.message}`);
}
}
}
clearConsole() {
this.debugOutput.innerHTML = '';
}
async factoryReset() {
const confirmMessage = 'This will reset all settings to factory defaults.\n\n' +
'All your custom configurations will be lost permanently.\n\n' +
'Are you sure you want to proceed?';
if (confirm(confirmMessage)) {
try {
await this.sendCommand('factory');
this.log('Factory reset initiated. Device will restart with default settings.');
// 重置后标记配置已保存(因为会恢复到默认值)
this.markConfigSaved();
// 等待设备重启后自动刷新配置显示
setTimeout(() => {
this.refreshSettings();
this.log('Refreshing settings after factory reset...');
}, 2000); // 等待2秒让设备完成重启
} catch (error) {
this.log(`Factory reset failed: ${error.message}`);
}
}
}
}
// 初始化控制器
document.addEventListener('DOMContentLoaded', () => {
window.aicController = new AICPicoController();
});
}
</script>
</body>
</html>