-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
181 lines (152 loc) · 8.32 KB
/
app.js
File metadata and controls
181 lines (152 loc) · 8.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
document.addEventListener('DOMContentLoaded', () => {
// --- Configuration ---
const SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
const CHAR_WRITE_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
const CHAR_NOTIFY_UUID = "498c599b-ad01-4148-8a6a-73c332854747";
let bleDevice, bleServer, writeChar, notifyChar;
let bleBuffer = ""; // Buffers incoming data to handle packet fragmentation
let state = {
boards: {}, // Stores: { 0: { name: 'Master', channels: { 1: 'disarmed', ... }, connected: true } }
selectedBoardId: 0
};
// --- DOM Elements ---
const connectBtn = document.getElementById('connect-button');
const mockBtn = document.getElementById('mock-button');
const refreshBtn = document.getElementById('refresh-button');
const boardList = document.getElementById('board-list');
const channelGrid = document.getElementById('channel-grid');
const boardTitle = document.getElementById('board-title');
// --- Connection Logic ---
async function connect() {
try {
bleDevice = await navigator.bluetooth.requestDevice({
filters: [{ services: [SERVICE_UUID] }]
});
bleServer = await bleDevice.gatt.connect();
const service = await bleServer.getPrimaryService(SERVICE_UUID);
writeChar = await service.getCharacteristic(CHAR_WRITE_UUID);
notifyChar = await service.getCharacteristic(CHAR_NOTIFY_UUID);
await notifyChar.startNotifications();
notifyChar.addEventListener('characteristicvaluechanged', handleNotify);
// Initial Sync
sendData("SYNC_BOARDS");
enterApp();
} catch (e) { console.error("Connection Failed", e); }
}
function enterApp() {
document.getElementById('connection-screen').classList.add('hidden');
document.getElementById('sidebar').classList.remove('hidden');
document.getElementById('control-panel').classList.remove('hidden');
}
// --- Simulation Logic ---
function startSimulation() {
console.log("SIMULATION MODE ACTIVE");
console.log("Use window.simulateMsg('MESSAGE') to test events.");
console.log("Example: window.simulateMsg('B1_DISCONNECTED')");
enterApp();
processMessage("BOARDS:2"); // Booster, Ignition, Nose Cone
}
window.simulateMsg = (msg) => processMessage(msg);
// --- Data Processing ---
function handleNotify(event) {
const decoder = new TextDecoder();
bleBuffer += decoder.decode(event.target.value);
if (bleBuffer.includes('\n')) {
const lines = bleBuffer.split('\n');
bleBuffer = lines.pop(); // Keep incomplete fragment
lines.forEach(line => processMessage(line.trim()));
}
}
function processMessage(msg) {
if (msg.startsWith("BOARDS:")) {
const count = parseInt(msg.split(":")[1]);
initBoards(count);
} else if (msg.endsWith("_DISCONNECTED")) {
const bId = parseInt(msg.substring(1, msg.indexOf('_')));
if (state.boards[bId]) {
state.boards[bId].connected = false;
render();
}
} else if (msg.includes("_ARMED") || msg.includes("_DISARMED")) {
const parts = msg.split("_"); // B0, CH1, ARMED
const bId = parseInt(parts[0].substring(1));
const cId = parseInt(parts[1].substring(2));
const status = parts[2].toLowerCase();
if(state.boards[bId]) {
state.boards[bId].channels[cId] = status;
render();
}
}
}
function initBoards(count) {
const boardNames = {
0: "Booster Bay",
1: "Ignition Bay",
2: "Nose Cone"
};
state.boards = {};
for (let i = 0; i <= count; i++) {
state.boards[i] = {
id: i,
name: boardNames[i] || `Aux Board ${i}`,
channels: {},
connected: true
};
for (let j = 1; j <= 6; j++) state.boards[i].channels[j] = 'disarmed';
}
render();
}
// --- UI Rendering ---
function render() {
if (Object.keys(state.boards).length === 0) return;
const board = state.boards[state.selectedBoardId];
const isDisconnected = !board.connected;
const isBoardArmed = Object.values(board.channels).some(s => s === 'armed');
boardTitle.innerHTML = `
<div class="w-4 h-4 rounded-full shadow-lg transition-colors duration-500 ${isBoardArmed ? 'bg-green-500 shadow-green-900/40' : 'bg-red-600 shadow-red-900/40'}"></div>
${board.name}
${isDisconnected ? '<span class="ml-4 text-[10px] bg-red-600/20 text-red-500 border border-red-500/50 px-3 py-1 rounded-full uppercase tracking-tighter">Link Severed</span>' : ''}
`;
// Render Sidebar
boardList.innerHTML = Object.values(state.boards).map(b => `
<li onclick="window.selectBoard(${b.id})" class="p-4 mb-3 rounded-lg cursor-pointer transition-all border border-transparent ${b.id === state.selectedBoardId ? 'bg-blue-600 border-blue-400 shadow-lg shadow-blue-900/40' : 'bg-slate-800 hover:bg-slate-700'} ${!b.connected ? 'opacity-40 grayscale' : ''}">
<div class="flex justify-between items-center">
<span class="font-black text-xs uppercase tracking-widest">${b.name}</span>
${!b.connected ? '<span class="text-[8px] font-black text-red-500">OFFLINE</span>' : '<span class="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_10px_#22c55e]"></span>'}
</div>
</li>
`).join('');
// Render Channels
channelGrid.innerHTML = Object.entries(board.channels).map(([id, status]) => `
<div class="channel-card p-10 rounded-3xl border-2 border-slate-800 flex flex-col items-center relative overflow-hidden group transition-all hover:translate-y-[-2px]">
${isDisconnected ? '<div class="absolute inset-0 bg-slate-950/70 z-20 flex items-center justify-center backdrop-blur-md"><span class="text-red-500 font-black text-xs tracking-[0.5em] rotate-[-15deg] border-4 border-red-500 px-4 py-2">MALFUNCTION</span></div>' : ''}
<div class="absolute top-6 left-6 text-[9px] text-slate-500 font-black uppercase tracking-widest opacity-50">Relay 0${id}</div>
<div class="absolute top-6 right-6 text-[9px] ${status === 'armed' ? 'text-red-500' : 'text-green-500'} font-black uppercase tracking-widest">${status}</div>
<div class="status-indicator w-20 h-1.5 rounded-full mb-12 ${status}"></div>
<h3 class="text-slate-400 text-[11px] font-black uppercase tracking-[0.5em] mb-12">System Control</h3>
<div class="flex gap-6 w-full px-2">
<button onclick="sendAction(${id}, 'ARM')" ${isDisconnected ? 'disabled' : ''}
class="btn-industrial flex-1 bg-red-600 border-red-900 hover:bg-red-500 disabled:bg-slate-800 disabled:border-slate-900 disabled:text-slate-600 py-5 rounded-2xl text-[11px] ${status === 'armed' ? 'active' : ''}">
ARM
</button>
<button onclick="sendAction(${id}, 'DISARM')" ${isDisconnected ? 'disabled' : ''}
class="btn-industrial flex-1 bg-emerald-600 border-emerald-900 hover:bg-emerald-500 disabled:bg-slate-800 disabled:border-slate-900 disabled:text-slate-600 py-5 rounded-2xl text-[11px] ${status === 'disarmed' ? 'active' : ''}">
DISARM
</button>
</div>
</div>
`).join('');
}
window.selectBoard = (id) => { state.selectedBoardId = id; render(); };
window.sendAction = (ch, action) => {
sendData(`B${state.selectedBoardId}_CH${ch}_${action}`);
};
async function sendData(str) {
console.log("OUTGOING:", str);
if (!writeChar) return;
await writeChar.writeValue(new TextEncoder().encode(str + "\n"));
}
connectBtn.addEventListener('click', connect);
mockBtn.addEventListener('click', startSimulation);
refreshBtn.addEventListener('click', () => sendData("SYNC_ALL"));
});