gameglue 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +275 -275
- package/babel.config.cjs +5 -5
- package/coverage/auth.js.html +525 -525
- package/coverage/base.css +224 -224
- package/coverage/block-navigation.js +87 -87
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +175 -175
- package/coverage/index.js.html +309 -309
- package/coverage/lcov-report/auth.js.html +525 -525
- package/coverage/lcov-report/base.css +224 -224
- package/coverage/lcov-report/block-navigation.js +87 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +175 -175
- package/coverage/lcov-report/index.js.html +309 -309
- package/coverage/lcov-report/listener.js.html +528 -528
- package/coverage/lcov-report/prettify.css +1 -1
- package/coverage/lcov-report/prettify.js +2 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -210
- package/coverage/lcov-report/user.js.html +117 -117
- package/coverage/lcov-report/utils.js.html +117 -117
- package/coverage/lcov.info +391 -391
- package/coverage/listener.js.html +528 -528
- package/coverage/prettify.css +1 -1
- package/coverage/prettify.js +2 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -210
- package/coverage/user.js.html +117 -117
- package/coverage/utils.js.html +117 -117
- package/dist/gg.cjs.js +1 -1
- package/dist/gg.cjs.js.map +1 -1
- package/dist/gg.esm.js +1 -1
- package/dist/gg.esm.js.map +1 -1
- package/dist/gg.umd.js +1 -1
- package/dist/gg.umd.js.map +1 -1
- package/examples/certs/cert.pem +19 -19
- package/examples/certs/key.pem +28 -28
- package/examples/flight-dashboard.html +431 -431
- package/examples/server.js +99 -99
- package/examples/telemetry-validator.html +1410 -1410
- package/jest.config.cjs +33 -33
- package/package.json +56 -56
- package/rollup.config.js +57 -57
- package/src/auth.js +255 -255
- package/src/auth.spec.js +481 -481
- package/src/index.js +168 -168
- package/src/listener.js +196 -193
- package/src/listener.spec.js +598 -598
- package/src/presence_listener.js +112 -112
- package/src/test/fixtures.js +106 -106
- package/src/test/setup.js +51 -51
- package/src/utils.js +63 -63
- package/src/utils.spec.js +78 -78
- package/types/index.d.ts +338 -338
- package/webpack.config.js +15 -15
|
@@ -1,431 +1,431 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>GameGlue Flight Dashboard Example</title>
|
|
7
|
-
<style>
|
|
8
|
-
* {
|
|
9
|
-
box-sizing: border-box;
|
|
10
|
-
margin: 0;
|
|
11
|
-
padding: 0;
|
|
12
|
-
}
|
|
13
|
-
body {
|
|
14
|
-
font-family: 'Segoe UI', -apple-system, sans-serif;
|
|
15
|
-
background: #0f172a;
|
|
16
|
-
color: #e2e8f0;
|
|
17
|
-
min-height: 100vh;
|
|
18
|
-
padding: 2rem;
|
|
19
|
-
}
|
|
20
|
-
.container {
|
|
21
|
-
max-width: 800px;
|
|
22
|
-
margin: 0 auto;
|
|
23
|
-
}
|
|
24
|
-
h1 {
|
|
25
|
-
font-size: 1.5rem;
|
|
26
|
-
margin-bottom: 0.5rem;
|
|
27
|
-
color: #3cff8f;
|
|
28
|
-
}
|
|
29
|
-
.subtitle {
|
|
30
|
-
color: #64748b;
|
|
31
|
-
margin-bottom: 2rem;
|
|
32
|
-
}
|
|
33
|
-
.header {
|
|
34
|
-
display: flex;
|
|
35
|
-
justify-content: space-between;
|
|
36
|
-
align-items: center;
|
|
37
|
-
margin-bottom: 1.5rem;
|
|
38
|
-
}
|
|
39
|
-
.status {
|
|
40
|
-
display: inline-block;
|
|
41
|
-
padding: 0.25rem 0.75rem;
|
|
42
|
-
border-radius: 9999px;
|
|
43
|
-
font-size: 0.875rem;
|
|
44
|
-
}
|
|
45
|
-
.status.disconnected { background: #7f1d1d; color: #fca5a5; }
|
|
46
|
-
.status.connecting { background: #78350f; color: #fcd34d; }
|
|
47
|
-
.status.connected { background: #14532d; color: #86efac; }
|
|
48
|
-
|
|
49
|
-
.logout-btn {
|
|
50
|
-
background: transparent;
|
|
51
|
-
border: 1px solid #334155;
|
|
52
|
-
color: #94a3b8;
|
|
53
|
-
padding: 0.5rem 1rem;
|
|
54
|
-
border-radius: 0.5rem;
|
|
55
|
-
font-size: 0.875rem;
|
|
56
|
-
cursor: pointer;
|
|
57
|
-
transition: all 0.15s;
|
|
58
|
-
}
|
|
59
|
-
.logout-btn:hover {
|
|
60
|
-
background: #334155;
|
|
61
|
-
color: #f1f5f9;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.card {
|
|
65
|
-
background: #1e293b;
|
|
66
|
-
border-radius: 0.75rem;
|
|
67
|
-
padding: 1.5rem;
|
|
68
|
-
margin-bottom: 1.5rem;
|
|
69
|
-
}
|
|
70
|
-
.card h2 {
|
|
71
|
-
font-size: 1rem;
|
|
72
|
-
color: #94a3b8;
|
|
73
|
-
margin-bottom: 1rem;
|
|
74
|
-
text-transform: uppercase;
|
|
75
|
-
letter-spacing: 0.05em;
|
|
76
|
-
}
|
|
77
|
-
.telemetry-grid {
|
|
78
|
-
display: grid;
|
|
79
|
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
80
|
-
gap: 1rem;
|
|
81
|
-
}
|
|
82
|
-
.telemetry-item {
|
|
83
|
-
background: #0f172a;
|
|
84
|
-
padding: 1rem;
|
|
85
|
-
border-radius: 0.5rem;
|
|
86
|
-
}
|
|
87
|
-
.telemetry-label {
|
|
88
|
-
font-size: 0.75rem;
|
|
89
|
-
color: #64748b;
|
|
90
|
-
text-transform: uppercase;
|
|
91
|
-
margin-bottom: 0.25rem;
|
|
92
|
-
}
|
|
93
|
-
.telemetry-value {
|
|
94
|
-
font-size: 1.5rem;
|
|
95
|
-
font-weight: 600;
|
|
96
|
-
font-family: 'SF Mono', 'Consolas', monospace;
|
|
97
|
-
}
|
|
98
|
-
.telemetry-unit {
|
|
99
|
-
font-size: 0.875rem;
|
|
100
|
-
color: #64748b;
|
|
101
|
-
margin-left: 0.25rem;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.controls-grid {
|
|
105
|
-
display: grid;
|
|
106
|
-
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
107
|
-
gap: 1rem;
|
|
108
|
-
}
|
|
109
|
-
.control-btn {
|
|
110
|
-
background: #334155;
|
|
111
|
-
border: none;
|
|
112
|
-
color: #e2e8f0;
|
|
113
|
-
padding: 1rem;
|
|
114
|
-
border-radius: 0.5rem;
|
|
115
|
-
font-size: 0.875rem;
|
|
116
|
-
cursor: pointer;
|
|
117
|
-
transition: all 0.15s;
|
|
118
|
-
display: flex;
|
|
119
|
-
flex-direction: column;
|
|
120
|
-
align-items: center;
|
|
121
|
-
gap: 0.5rem;
|
|
122
|
-
}
|
|
123
|
-
.control-btn:hover {
|
|
124
|
-
background: #475569;
|
|
125
|
-
}
|
|
126
|
-
.control-btn:active {
|
|
127
|
-
transform: scale(0.98);
|
|
128
|
-
}
|
|
129
|
-
.control-btn.active {
|
|
130
|
-
background: #166534;
|
|
131
|
-
color: #86efac;
|
|
132
|
-
}
|
|
133
|
-
.control-btn .icon {
|
|
134
|
-
font-size: 1.5rem;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.auth-section {
|
|
138
|
-
text-align: center;
|
|
139
|
-
padding: 3rem;
|
|
140
|
-
}
|
|
141
|
-
.auth-btn {
|
|
142
|
-
background: #3cff8f;
|
|
143
|
-
color: #0f172a;
|
|
144
|
-
border: none;
|
|
145
|
-
padding: 0.75rem 2rem;
|
|
146
|
-
border-radius: 0.5rem;
|
|
147
|
-
font-size: 1rem;
|
|
148
|
-
font-weight: 600;
|
|
149
|
-
cursor: pointer;
|
|
150
|
-
transition: all 0.15s;
|
|
151
|
-
}
|
|
152
|
-
.auth-btn:hover {
|
|
153
|
-
background: #22c55e;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.log {
|
|
157
|
-
background: #0f172a;
|
|
158
|
-
border-radius: 0.5rem;
|
|
159
|
-
padding: 1rem;
|
|
160
|
-
font-family: 'SF Mono', 'Consolas', monospace;
|
|
161
|
-
font-size: 0.75rem;
|
|
162
|
-
max-height: 200px;
|
|
163
|
-
overflow-y: auto;
|
|
164
|
-
}
|
|
165
|
-
.log-entry {
|
|
166
|
-
color: #64748b;
|
|
167
|
-
margin-bottom: 0.25rem;
|
|
168
|
-
}
|
|
169
|
-
.log-entry.command {
|
|
170
|
-
color: #fcd34d;
|
|
171
|
-
}
|
|
172
|
-
.log-entry.error {
|
|
173
|
-
color: #f87171;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.hidden { display: none; }
|
|
177
|
-
</style>
|
|
178
|
-
</head>
|
|
179
|
-
<body>
|
|
180
|
-
<div class="container">
|
|
181
|
-
<h1>/GameGlue Flight Dashboard</h1>
|
|
182
|
-
<p class="subtitle">Example app demonstrating bidirectional data flow</p>
|
|
183
|
-
|
|
184
|
-
<!-- Auth Section (shown when not authenticated) -->
|
|
185
|
-
<div id="auth-section" class="card auth-section">
|
|
186
|
-
<p style="margin-bottom: 1rem; color: #94a3b8;">Connect to GameGlue to view flight telemetry and send commands</p>
|
|
187
|
-
<button class="auth-btn" onclick="doLogin()">Connect with GameGlue</button>
|
|
188
|
-
</div>
|
|
189
|
-
|
|
190
|
-
<!-- Dashboard (shown when authenticated) -->
|
|
191
|
-
<div id="dashboard" class="hidden">
|
|
192
|
-
<div class="header">
|
|
193
|
-
<span id="status" class="status disconnected">Waiting for MSFS...</span>
|
|
194
|
-
<button class="logout-btn" onclick="doLogout()">Logout</button>
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
<!-- Telemetry Card -->
|
|
198
|
-
<div class="card">
|
|
199
|
-
<h2>Flight Data</h2>
|
|
200
|
-
<div class="telemetry-grid">
|
|
201
|
-
<div class="telemetry-item">
|
|
202
|
-
<div class="telemetry-label">Altitude</div>
|
|
203
|
-
<div>
|
|
204
|
-
<span id="altitude" class="telemetry-value">---</span>
|
|
205
|
-
<span class="telemetry-unit">ft</span>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
<div class="telemetry-item">
|
|
209
|
-
<div class="telemetry-label">Airspeed</div>
|
|
210
|
-
<div>
|
|
211
|
-
<span id="airspeed" class="telemetry-value">---</span>
|
|
212
|
-
<span class="telemetry-unit">kts</span>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
<div class="telemetry-item">
|
|
216
|
-
<div class="telemetry-label">Ground Speed</div>
|
|
217
|
-
<div>
|
|
218
|
-
<span id="ground-speed" class="telemetry-value">---</span>
|
|
219
|
-
<span class="telemetry-unit">kts</span>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
<div class="telemetry-item">
|
|
223
|
-
<div class="telemetry-label">Vertical Speed</div>
|
|
224
|
-
<div>
|
|
225
|
-
<span id="vertical-speed" class="telemetry-value">---</span>
|
|
226
|
-
<span class="telemetry-unit">fpm</span>
|
|
227
|
-
</div>
|
|
228
|
-
</div>
|
|
229
|
-
<div class="telemetry-item">
|
|
230
|
-
<div class="telemetry-label">Heading</div>
|
|
231
|
-
<div>
|
|
232
|
-
<span id="heading" class="telemetry-value">---</span>
|
|
233
|
-
<span class="telemetry-unit">°</span>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
<div class="telemetry-item">
|
|
237
|
-
<div class="telemetry-label">Autopilot</div>
|
|
238
|
-
<div>
|
|
239
|
-
<span id="autopilot-status" class="telemetry-value">---</span>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
<!-- Controls Card -->
|
|
246
|
-
<div class="card">
|
|
247
|
-
<h2>Aircraft Controls</h2>
|
|
248
|
-
<div class="controls-grid">
|
|
249
|
-
<button class="control-btn" onclick="sendCommand('AUTOPILOT_ON', true)">
|
|
250
|
-
<span class="icon">AP</span>
|
|
251
|
-
<span>Autopilot ON</span>
|
|
252
|
-
</button>
|
|
253
|
-
<button class="control-btn" onclick="sendCommand('AUTOPILOT_OFF', true)">
|
|
254
|
-
<span class="icon">AP</span>
|
|
255
|
-
<span>Autopilot OFF</span>
|
|
256
|
-
</button>
|
|
257
|
-
<button class="control-btn" onclick="sendCommand('TOGGLE_FLIGHT_DIRECTOR', true)">
|
|
258
|
-
<span class="icon">FD</span>
|
|
259
|
-
<span>Toggle Flight Director</span>
|
|
260
|
-
</button>
|
|
261
|
-
<button class="control-btn" onclick="sendCommand('AP_HDG_HOLD_ON', true)">
|
|
262
|
-
<span class="icon">HDG</span>
|
|
263
|
-
<span>Heading Hold ON</span>
|
|
264
|
-
</button>
|
|
265
|
-
<button class="control-btn" onclick="sendCommand('AP_ALT_HOLD_ON', true)">
|
|
266
|
-
<span class="icon">ALT</span>
|
|
267
|
-
<span>Altitude Hold ON</span>
|
|
268
|
-
</button>
|
|
269
|
-
<button class="control-btn" onclick="sendCommand('TOGGLE_NAV_LIGHTS', true)">
|
|
270
|
-
<span class="icon">NAV</span>
|
|
271
|
-
<span>Toggle Nav Lights</span>
|
|
272
|
-
</button>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
|
|
276
|
-
<!-- Log Card -->
|
|
277
|
-
<div class="card">
|
|
278
|
-
<h2>Event Log</h2>
|
|
279
|
-
<div id="log" class="log"></div>
|
|
280
|
-
</div>
|
|
281
|
-
</div>
|
|
282
|
-
</div>
|
|
283
|
-
|
|
284
|
-
<!-- Load GameGlue SDK from local build -->
|
|
285
|
-
<script src="../dist/gg.umd.js"></script>
|
|
286
|
-
<script>
|
|
287
|
-
// Configuration
|
|
288
|
-
const CLIENT_ID = 'gameglue-sdk-examples';
|
|
289
|
-
const REDIRECT_URI = window.location.href.split('?')[0].split('#')[0];
|
|
290
|
-
|
|
291
|
-
// Create a single SDK instance
|
|
292
|
-
const ggClient = new GameGlue({
|
|
293
|
-
clientId: CLIENT_ID,
|
|
294
|
-
redirect_uri: REDIRECT_URI,
|
|
295
|
-
scopes: ['msfs:read', 'msfs:write']
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
let listener = null;
|
|
299
|
-
|
|
300
|
-
// Login button handler
|
|
301
|
-
function doLogin() {
|
|
302
|
-
log('Redirecting to GameGlue login...');
|
|
303
|
-
ggClient.login();
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Logout button handler
|
|
307
|
-
function doLogout() {
|
|
308
|
-
log('Logging out...');
|
|
309
|
-
ggClient.logout({ redirect: false });
|
|
310
|
-
document.getElementById('auth-section').classList.remove('hidden');
|
|
311
|
-
document.getElementById('dashboard').classList.add('hidden');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Check authentication and set up listener
|
|
315
|
-
async function init() {
|
|
316
|
-
try {
|
|
317
|
-
log('Checking authentication...');
|
|
318
|
-
|
|
319
|
-
// isAuthenticated() handles OAuth callback automatically
|
|
320
|
-
const isAuthed = await ggClient.isAuthenticated();
|
|
321
|
-
|
|
322
|
-
if (!isAuthed) {
|
|
323
|
-
log('Not authenticated - please log in');
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Authenticated
|
|
328
|
-
const userId = ggClient.getUser();
|
|
329
|
-
log('Authenticated! User ID: ' + userId);
|
|
330
|
-
showDashboard();
|
|
331
|
-
await setupListener(userId);
|
|
332
|
-
} catch (err) {
|
|
333
|
-
log('Error: ' + err.message, 'error');
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Set up telemetry listener
|
|
338
|
-
async function setupListener(userId) {
|
|
339
|
-
try {
|
|
340
|
-
log('Creating listener for MSFS telemetry...');
|
|
341
|
-
|
|
342
|
-
listener = await ggClient.createListener({
|
|
343
|
-
userId: userId,
|
|
344
|
-
gameId: 'msfs'
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
listener.on('update', (evt) => {
|
|
348
|
-
updateTelemetry(evt.data);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
setStatus('connected', 'Connected to MSFS');
|
|
352
|
-
log('Listener active. Waiting for telemetry...');
|
|
353
|
-
} catch (err) {
|
|
354
|
-
log('Failed to create listener: ' + err.message, 'error');
|
|
355
|
-
setStatus('disconnected', 'Connection failed');
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Update telemetry display
|
|
360
|
-
function updateTelemetry(data) {
|
|
361
|
-
if (data.indicated_altitude !== undefined) {
|
|
362
|
-
document.getElementById('altitude').textContent = Math.round(data.indicated_altitude);
|
|
363
|
-
}
|
|
364
|
-
if (data.airspeed_indicated !== undefined) {
|
|
365
|
-
document.getElementById('airspeed').textContent = Math.round(data.airspeed_indicated);
|
|
366
|
-
}
|
|
367
|
-
if (data.ground_velocity !== undefined) {
|
|
368
|
-
document.getElementById('ground-speed').textContent = Math.round(data.ground_velocity);
|
|
369
|
-
}
|
|
370
|
-
if (data.vertical_speed !== undefined) {
|
|
371
|
-
document.getElementById('vertical-speed').textContent = Math.round(data.vertical_speed);
|
|
372
|
-
}
|
|
373
|
-
if (data.heading_indicator !== undefined) {
|
|
374
|
-
document.getElementById('heading').textContent = Math.round(data.heading_indicator);
|
|
375
|
-
}
|
|
376
|
-
if (data.autopilot_master !== undefined) {
|
|
377
|
-
document.getElementById('autopilot-status').textContent = data.autopilot_master ? 'ON' : 'OFF';
|
|
378
|
-
document.getElementById('autopilot-status').style.color = data.autopilot_master ? '#86efac' : '#f87171';
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Send command to aircraft
|
|
383
|
-
async function sendCommand(field, value) {
|
|
384
|
-
if (!listener) {
|
|
385
|
-
log('Not connected - cannot send command', 'error');
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
log(`Sending command: ${field} = ${value}`, 'command');
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
const result = await listener.sendCommand(field, value);
|
|
393
|
-
if (result.status === 'success') {
|
|
394
|
-
log(`Command sent successfully: ${field}`, 'command');
|
|
395
|
-
} else {
|
|
396
|
-
log(`Command failed: ${result.reason}`, 'error');
|
|
397
|
-
}
|
|
398
|
-
} catch (err) {
|
|
399
|
-
log(`Command error: ${err.message}`, 'error');
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// UI Helpers
|
|
404
|
-
function showDashboard() {
|
|
405
|
-
document.getElementById('auth-section').classList.add('hidden');
|
|
406
|
-
document.getElementById('dashboard').classList.remove('hidden');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function setStatus(state, text) {
|
|
410
|
-
const el = document.getElementById('status');
|
|
411
|
-
el.className = 'status ' + state;
|
|
412
|
-
el.textContent = text;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function log(message, type = '') {
|
|
416
|
-
const logEl = document.getElementById('log');
|
|
417
|
-
if (logEl) {
|
|
418
|
-
const entry = document.createElement('div');
|
|
419
|
-
entry.className = 'log-entry ' + type;
|
|
420
|
-
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
421
|
-
logEl.appendChild(entry);
|
|
422
|
-
logEl.scrollTop = logEl.scrollHeight;
|
|
423
|
-
}
|
|
424
|
-
console.log(message);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Initialize on page load
|
|
428
|
-
window.onload = init;
|
|
429
|
-
</script>
|
|
430
|
-
</body>
|
|
431
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>GameGlue Flight Dashboard Example</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family: 'Segoe UI', -apple-system, sans-serif;
|
|
15
|
+
background: #0f172a;
|
|
16
|
+
color: #e2e8f0;
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
padding: 2rem;
|
|
19
|
+
}
|
|
20
|
+
.container {
|
|
21
|
+
max-width: 800px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
}
|
|
24
|
+
h1 {
|
|
25
|
+
font-size: 1.5rem;
|
|
26
|
+
margin-bottom: 0.5rem;
|
|
27
|
+
color: #3cff8f;
|
|
28
|
+
}
|
|
29
|
+
.subtitle {
|
|
30
|
+
color: #64748b;
|
|
31
|
+
margin-bottom: 2rem;
|
|
32
|
+
}
|
|
33
|
+
.header {
|
|
34
|
+
display: flex;
|
|
35
|
+
justify-content: space-between;
|
|
36
|
+
align-items: center;
|
|
37
|
+
margin-bottom: 1.5rem;
|
|
38
|
+
}
|
|
39
|
+
.status {
|
|
40
|
+
display: inline-block;
|
|
41
|
+
padding: 0.25rem 0.75rem;
|
|
42
|
+
border-radius: 9999px;
|
|
43
|
+
font-size: 0.875rem;
|
|
44
|
+
}
|
|
45
|
+
.status.disconnected { background: #7f1d1d; color: #fca5a5; }
|
|
46
|
+
.status.connecting { background: #78350f; color: #fcd34d; }
|
|
47
|
+
.status.connected { background: #14532d; color: #86efac; }
|
|
48
|
+
|
|
49
|
+
.logout-btn {
|
|
50
|
+
background: transparent;
|
|
51
|
+
border: 1px solid #334155;
|
|
52
|
+
color: #94a3b8;
|
|
53
|
+
padding: 0.5rem 1rem;
|
|
54
|
+
border-radius: 0.5rem;
|
|
55
|
+
font-size: 0.875rem;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition: all 0.15s;
|
|
58
|
+
}
|
|
59
|
+
.logout-btn:hover {
|
|
60
|
+
background: #334155;
|
|
61
|
+
color: #f1f5f9;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.card {
|
|
65
|
+
background: #1e293b;
|
|
66
|
+
border-radius: 0.75rem;
|
|
67
|
+
padding: 1.5rem;
|
|
68
|
+
margin-bottom: 1.5rem;
|
|
69
|
+
}
|
|
70
|
+
.card h2 {
|
|
71
|
+
font-size: 1rem;
|
|
72
|
+
color: #94a3b8;
|
|
73
|
+
margin-bottom: 1rem;
|
|
74
|
+
text-transform: uppercase;
|
|
75
|
+
letter-spacing: 0.05em;
|
|
76
|
+
}
|
|
77
|
+
.telemetry-grid {
|
|
78
|
+
display: grid;
|
|
79
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
80
|
+
gap: 1rem;
|
|
81
|
+
}
|
|
82
|
+
.telemetry-item {
|
|
83
|
+
background: #0f172a;
|
|
84
|
+
padding: 1rem;
|
|
85
|
+
border-radius: 0.5rem;
|
|
86
|
+
}
|
|
87
|
+
.telemetry-label {
|
|
88
|
+
font-size: 0.75rem;
|
|
89
|
+
color: #64748b;
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
margin-bottom: 0.25rem;
|
|
92
|
+
}
|
|
93
|
+
.telemetry-value {
|
|
94
|
+
font-size: 1.5rem;
|
|
95
|
+
font-weight: 600;
|
|
96
|
+
font-family: 'SF Mono', 'Consolas', monospace;
|
|
97
|
+
}
|
|
98
|
+
.telemetry-unit {
|
|
99
|
+
font-size: 0.875rem;
|
|
100
|
+
color: #64748b;
|
|
101
|
+
margin-left: 0.25rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.controls-grid {
|
|
105
|
+
display: grid;
|
|
106
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
107
|
+
gap: 1rem;
|
|
108
|
+
}
|
|
109
|
+
.control-btn {
|
|
110
|
+
background: #334155;
|
|
111
|
+
border: none;
|
|
112
|
+
color: #e2e8f0;
|
|
113
|
+
padding: 1rem;
|
|
114
|
+
border-radius: 0.5rem;
|
|
115
|
+
font-size: 0.875rem;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
transition: all 0.15s;
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: 0.5rem;
|
|
122
|
+
}
|
|
123
|
+
.control-btn:hover {
|
|
124
|
+
background: #475569;
|
|
125
|
+
}
|
|
126
|
+
.control-btn:active {
|
|
127
|
+
transform: scale(0.98);
|
|
128
|
+
}
|
|
129
|
+
.control-btn.active {
|
|
130
|
+
background: #166534;
|
|
131
|
+
color: #86efac;
|
|
132
|
+
}
|
|
133
|
+
.control-btn .icon {
|
|
134
|
+
font-size: 1.5rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.auth-section {
|
|
138
|
+
text-align: center;
|
|
139
|
+
padding: 3rem;
|
|
140
|
+
}
|
|
141
|
+
.auth-btn {
|
|
142
|
+
background: #3cff8f;
|
|
143
|
+
color: #0f172a;
|
|
144
|
+
border: none;
|
|
145
|
+
padding: 0.75rem 2rem;
|
|
146
|
+
border-radius: 0.5rem;
|
|
147
|
+
font-size: 1rem;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
transition: all 0.15s;
|
|
151
|
+
}
|
|
152
|
+
.auth-btn:hover {
|
|
153
|
+
background: #22c55e;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.log {
|
|
157
|
+
background: #0f172a;
|
|
158
|
+
border-radius: 0.5rem;
|
|
159
|
+
padding: 1rem;
|
|
160
|
+
font-family: 'SF Mono', 'Consolas', monospace;
|
|
161
|
+
font-size: 0.75rem;
|
|
162
|
+
max-height: 200px;
|
|
163
|
+
overflow-y: auto;
|
|
164
|
+
}
|
|
165
|
+
.log-entry {
|
|
166
|
+
color: #64748b;
|
|
167
|
+
margin-bottom: 0.25rem;
|
|
168
|
+
}
|
|
169
|
+
.log-entry.command {
|
|
170
|
+
color: #fcd34d;
|
|
171
|
+
}
|
|
172
|
+
.log-entry.error {
|
|
173
|
+
color: #f87171;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.hidden { display: none; }
|
|
177
|
+
</style>
|
|
178
|
+
</head>
|
|
179
|
+
<body>
|
|
180
|
+
<div class="container">
|
|
181
|
+
<h1>/GameGlue Flight Dashboard</h1>
|
|
182
|
+
<p class="subtitle">Example app demonstrating bidirectional data flow</p>
|
|
183
|
+
|
|
184
|
+
<!-- Auth Section (shown when not authenticated) -->
|
|
185
|
+
<div id="auth-section" class="card auth-section">
|
|
186
|
+
<p style="margin-bottom: 1rem; color: #94a3b8;">Connect to GameGlue to view flight telemetry and send commands</p>
|
|
187
|
+
<button class="auth-btn" onclick="doLogin()">Connect with GameGlue</button>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<!-- Dashboard (shown when authenticated) -->
|
|
191
|
+
<div id="dashboard" class="hidden">
|
|
192
|
+
<div class="header">
|
|
193
|
+
<span id="status" class="status disconnected">Waiting for MSFS...</span>
|
|
194
|
+
<button class="logout-btn" onclick="doLogout()">Logout</button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<!-- Telemetry Card -->
|
|
198
|
+
<div class="card">
|
|
199
|
+
<h2>Flight Data</h2>
|
|
200
|
+
<div class="telemetry-grid">
|
|
201
|
+
<div class="telemetry-item">
|
|
202
|
+
<div class="telemetry-label">Altitude</div>
|
|
203
|
+
<div>
|
|
204
|
+
<span id="altitude" class="telemetry-value">---</span>
|
|
205
|
+
<span class="telemetry-unit">ft</span>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="telemetry-item">
|
|
209
|
+
<div class="telemetry-label">Airspeed</div>
|
|
210
|
+
<div>
|
|
211
|
+
<span id="airspeed" class="telemetry-value">---</span>
|
|
212
|
+
<span class="telemetry-unit">kts</span>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="telemetry-item">
|
|
216
|
+
<div class="telemetry-label">Ground Speed</div>
|
|
217
|
+
<div>
|
|
218
|
+
<span id="ground-speed" class="telemetry-value">---</span>
|
|
219
|
+
<span class="telemetry-unit">kts</span>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="telemetry-item">
|
|
223
|
+
<div class="telemetry-label">Vertical Speed</div>
|
|
224
|
+
<div>
|
|
225
|
+
<span id="vertical-speed" class="telemetry-value">---</span>
|
|
226
|
+
<span class="telemetry-unit">fpm</span>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="telemetry-item">
|
|
230
|
+
<div class="telemetry-label">Heading</div>
|
|
231
|
+
<div>
|
|
232
|
+
<span id="heading" class="telemetry-value">---</span>
|
|
233
|
+
<span class="telemetry-unit">°</span>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
<div class="telemetry-item">
|
|
237
|
+
<div class="telemetry-label">Autopilot</div>
|
|
238
|
+
<div>
|
|
239
|
+
<span id="autopilot-status" class="telemetry-value">---</span>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Controls Card -->
|
|
246
|
+
<div class="card">
|
|
247
|
+
<h2>Aircraft Controls</h2>
|
|
248
|
+
<div class="controls-grid">
|
|
249
|
+
<button class="control-btn" onclick="sendCommand('AUTOPILOT_ON', true)">
|
|
250
|
+
<span class="icon">AP</span>
|
|
251
|
+
<span>Autopilot ON</span>
|
|
252
|
+
</button>
|
|
253
|
+
<button class="control-btn" onclick="sendCommand('AUTOPILOT_OFF', true)">
|
|
254
|
+
<span class="icon">AP</span>
|
|
255
|
+
<span>Autopilot OFF</span>
|
|
256
|
+
</button>
|
|
257
|
+
<button class="control-btn" onclick="sendCommand('TOGGLE_FLIGHT_DIRECTOR', true)">
|
|
258
|
+
<span class="icon">FD</span>
|
|
259
|
+
<span>Toggle Flight Director</span>
|
|
260
|
+
</button>
|
|
261
|
+
<button class="control-btn" onclick="sendCommand('AP_HDG_HOLD_ON', true)">
|
|
262
|
+
<span class="icon">HDG</span>
|
|
263
|
+
<span>Heading Hold ON</span>
|
|
264
|
+
</button>
|
|
265
|
+
<button class="control-btn" onclick="sendCommand('AP_ALT_HOLD_ON', true)">
|
|
266
|
+
<span class="icon">ALT</span>
|
|
267
|
+
<span>Altitude Hold ON</span>
|
|
268
|
+
</button>
|
|
269
|
+
<button class="control-btn" onclick="sendCommand('TOGGLE_NAV_LIGHTS', true)">
|
|
270
|
+
<span class="icon">NAV</span>
|
|
271
|
+
<span>Toggle Nav Lights</span>
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<!-- Log Card -->
|
|
277
|
+
<div class="card">
|
|
278
|
+
<h2>Event Log</h2>
|
|
279
|
+
<div id="log" class="log"></div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<!-- Load GameGlue SDK from local build -->
|
|
285
|
+
<script src="../dist/gg.umd.js"></script>
|
|
286
|
+
<script>
|
|
287
|
+
// Configuration
|
|
288
|
+
const CLIENT_ID = 'gameglue-sdk-examples';
|
|
289
|
+
const REDIRECT_URI = window.location.href.split('?')[0].split('#')[0];
|
|
290
|
+
|
|
291
|
+
// Create a single SDK instance
|
|
292
|
+
const ggClient = new GameGlue({
|
|
293
|
+
clientId: CLIENT_ID,
|
|
294
|
+
redirect_uri: REDIRECT_URI,
|
|
295
|
+
scopes: ['msfs:read', 'msfs:write']
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
let listener = null;
|
|
299
|
+
|
|
300
|
+
// Login button handler
|
|
301
|
+
function doLogin() {
|
|
302
|
+
log('Redirecting to GameGlue login...');
|
|
303
|
+
ggClient.login();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Logout button handler
|
|
307
|
+
function doLogout() {
|
|
308
|
+
log('Logging out...');
|
|
309
|
+
ggClient.logout({ redirect: false });
|
|
310
|
+
document.getElementById('auth-section').classList.remove('hidden');
|
|
311
|
+
document.getElementById('dashboard').classList.add('hidden');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Check authentication and set up listener
|
|
315
|
+
async function init() {
|
|
316
|
+
try {
|
|
317
|
+
log('Checking authentication...');
|
|
318
|
+
|
|
319
|
+
// isAuthenticated() handles OAuth callback automatically
|
|
320
|
+
const isAuthed = await ggClient.isAuthenticated();
|
|
321
|
+
|
|
322
|
+
if (!isAuthed) {
|
|
323
|
+
log('Not authenticated - please log in');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Authenticated
|
|
328
|
+
const userId = ggClient.getUser();
|
|
329
|
+
log('Authenticated! User ID: ' + userId);
|
|
330
|
+
showDashboard();
|
|
331
|
+
await setupListener(userId);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
log('Error: ' + err.message, 'error');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Set up telemetry listener
|
|
338
|
+
async function setupListener(userId) {
|
|
339
|
+
try {
|
|
340
|
+
log('Creating listener for MSFS telemetry...');
|
|
341
|
+
|
|
342
|
+
listener = await ggClient.createListener({
|
|
343
|
+
userId: userId,
|
|
344
|
+
gameId: 'msfs'
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
listener.on('update', (evt) => {
|
|
348
|
+
updateTelemetry(evt.data);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
setStatus('connected', 'Connected to MSFS');
|
|
352
|
+
log('Listener active. Waiting for telemetry...');
|
|
353
|
+
} catch (err) {
|
|
354
|
+
log('Failed to create listener: ' + err.message, 'error');
|
|
355
|
+
setStatus('disconnected', 'Connection failed');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Update telemetry display
|
|
360
|
+
function updateTelemetry(data) {
|
|
361
|
+
if (data.indicated_altitude !== undefined) {
|
|
362
|
+
document.getElementById('altitude').textContent = Math.round(data.indicated_altitude);
|
|
363
|
+
}
|
|
364
|
+
if (data.airspeed_indicated !== undefined) {
|
|
365
|
+
document.getElementById('airspeed').textContent = Math.round(data.airspeed_indicated);
|
|
366
|
+
}
|
|
367
|
+
if (data.ground_velocity !== undefined) {
|
|
368
|
+
document.getElementById('ground-speed').textContent = Math.round(data.ground_velocity);
|
|
369
|
+
}
|
|
370
|
+
if (data.vertical_speed !== undefined) {
|
|
371
|
+
document.getElementById('vertical-speed').textContent = Math.round(data.vertical_speed);
|
|
372
|
+
}
|
|
373
|
+
if (data.heading_indicator !== undefined) {
|
|
374
|
+
document.getElementById('heading').textContent = Math.round(data.heading_indicator);
|
|
375
|
+
}
|
|
376
|
+
if (data.autopilot_master !== undefined) {
|
|
377
|
+
document.getElementById('autopilot-status').textContent = data.autopilot_master ? 'ON' : 'OFF';
|
|
378
|
+
document.getElementById('autopilot-status').style.color = data.autopilot_master ? '#86efac' : '#f87171';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Send command to aircraft
|
|
383
|
+
async function sendCommand(field, value) {
|
|
384
|
+
if (!listener) {
|
|
385
|
+
log('Not connected - cannot send command', 'error');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
log(`Sending command: ${field} = ${value}`, 'command');
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const result = await listener.sendCommand(field, value);
|
|
393
|
+
if (result.status === 'success') {
|
|
394
|
+
log(`Command sent successfully: ${field}`, 'command');
|
|
395
|
+
} else {
|
|
396
|
+
log(`Command failed: ${result.reason}`, 'error');
|
|
397
|
+
}
|
|
398
|
+
} catch (err) {
|
|
399
|
+
log(`Command error: ${err.message}`, 'error');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// UI Helpers
|
|
404
|
+
function showDashboard() {
|
|
405
|
+
document.getElementById('auth-section').classList.add('hidden');
|
|
406
|
+
document.getElementById('dashboard').classList.remove('hidden');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function setStatus(state, text) {
|
|
410
|
+
const el = document.getElementById('status');
|
|
411
|
+
el.className = 'status ' + state;
|
|
412
|
+
el.textContent = text;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function log(message, type = '') {
|
|
416
|
+
const logEl = document.getElementById('log');
|
|
417
|
+
if (logEl) {
|
|
418
|
+
const entry = document.createElement('div');
|
|
419
|
+
entry.className = 'log-entry ' + type;
|
|
420
|
+
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
421
|
+
logEl.appendChild(entry);
|
|
422
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
423
|
+
}
|
|
424
|
+
console.log(message);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Initialize on page load
|
|
428
|
+
window.onload = init;
|
|
429
|
+
</script>
|
|
430
|
+
</body>
|
|
431
|
+
</html>
|