@blueharford/scrypted-spatial-awareness 0.3.0 → 0.4.0

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/src/main.ts CHANGED
@@ -32,6 +32,8 @@ import { GlobalTrackerSensor } from './devices/global-tracker-sensor';
32
32
  import { TrackingZone } from './devices/tracking-zone';
33
33
  import { MqttPublisher, MqttConfig } from './integrations/mqtt-publisher';
34
34
  import { EDITOR_HTML } from './ui/editor-html';
35
+ import { TRAINING_HTML } from './ui/training-html';
36
+ import { TrainingConfig, TrainingLandmark } from './models/training';
35
37
 
36
38
  const { deviceManager, systemManager } = sdk;
37
39
 
@@ -432,8 +434,91 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
432
434
  async getSettings(): Promise<Setting[]> {
433
435
  const settings = await this.storageSettings.getSettings();
434
436
 
437
+ // Training Mode button that opens mobile-friendly training UI in modal
438
+ const trainingOnclickCode = `(function(){var e=document.getElementById('sa-training-modal');if(e)e.remove();var m=document.createElement('div');m.id='sa-training-modal';m.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:2147483647;display:flex;align-items:center;justify-content:center;';var c=document.createElement('div');c.style.cssText='width:min(420px,95vw);height:92vh;max-height:900px;background:#121212;border-radius:8px;overflow:hidden;position:relative;box-shadow:0 8px 32px rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.1);';var b=document.createElement('button');b.innerHTML='×';b.style.cssText='position:absolute;top:8px;right:8px;z-index:2147483647;background:rgba(255,255,255,0.1);color:white;border:none;width:32px;height:32px;border-radius:4px;font-size:18px;cursor:pointer;line-height:1;';b.onclick=function(){m.remove();};var f=document.createElement('iframe');f.src='/endpoint/@blueharford/scrypted-spatial-awareness/ui/training';f.style.cssText='width:100%;height:100%;border:none;';c.appendChild(b);c.appendChild(f);m.appendChild(c);m.onclick=function(ev){if(ev.target===m)m.remove();};document.body.appendChild(m);})()`;
439
+
440
+ settings.push({
441
+ key: 'trainingMode',
442
+ title: 'Training Mode',
443
+ type: 'html' as any,
444
+ value: `
445
+ <style>
446
+ .sa-training-container {
447
+ padding: 16px;
448
+ background: rgba(255,255,255,0.03);
449
+ border-radius: 4px;
450
+ border: 1px solid rgba(255,255,255,0.08);
451
+ }
452
+ .sa-training-title {
453
+ color: #4fc3f7;
454
+ font-size: 14px;
455
+ font-weight: 500;
456
+ margin-bottom: 8px;
457
+ font-family: inherit;
458
+ }
459
+ .sa-training-desc {
460
+ color: rgba(255,255,255,0.6);
461
+ margin-bottom: 12px;
462
+ font-size: 13px;
463
+ line-height: 1.5;
464
+ font-family: inherit;
465
+ }
466
+ .sa-training-btn {
467
+ background: #4fc3f7;
468
+ color: #000;
469
+ border: none;
470
+ padding: 10px 20px;
471
+ border-radius: 4px;
472
+ font-size: 14px;
473
+ font-weight: 500;
474
+ cursor: pointer;
475
+ display: inline-flex;
476
+ align-items: center;
477
+ gap: 8px;
478
+ transition: background 0.2s;
479
+ font-family: inherit;
480
+ }
481
+ .sa-training-btn:hover {
482
+ background: #81d4fa;
483
+ }
484
+ .sa-training-steps {
485
+ color: rgba(255,255,255,0.5);
486
+ font-size: 12px;
487
+ margin-top: 12px;
488
+ padding-top: 12px;
489
+ border-top: 1px solid rgba(255,255,255,0.05);
490
+ font-family: inherit;
491
+ }
492
+ .sa-training-steps ol {
493
+ margin: 6px 0 0 16px;
494
+ padding: 0;
495
+ }
496
+ .sa-training-steps li {
497
+ margin-bottom: 2px;
498
+ }
499
+ </style>
500
+ <div class="sa-training-container">
501
+ <div class="sa-training-title">Guided Property Training</div>
502
+ <p class="sa-training-desc">Walk your property while the system learns your camera layout, transit times, and landmarks automatically.</p>
503
+ <button class="sa-training-btn" onclick="${trainingOnclickCode}">
504
+ Start Training Mode
505
+ </button>
506
+ <div class="sa-training-steps">
507
+ <strong>How it works:</strong>
508
+ <ol>
509
+ <li>Start training and walk to each camera</li>
510
+ <li>System auto-detects you and records transit times</li>
511
+ <li>Mark landmarks as you encounter them</li>
512
+ <li>Apply results to generate your topology</li>
513
+ </ol>
514
+ </div>
515
+ </div>
516
+ `,
517
+ group: 'Getting Started',
518
+ });
519
+
435
520
  // Topology editor button that opens modal overlay (appended to body for proper z-index)
436
- const onclickCode = `(function(){var e=document.getElementById('sa-topology-modal');if(e)e.remove();var m=document.createElement('div');m.id='sa-topology-modal';m.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);z-index:2147483647;display:flex;align-items:center;justify-content:center;';var c=document.createElement('div');c.style.cssText='width:95vw;height:92vh;max-width:1800px;background:#1a1a2e;border-radius:12px;overflow:hidden;position:relative;box-shadow:0 25px 50px rgba(0,0,0,0.5);';var b=document.createElement('button');b.innerHTML='×';b.style.cssText='position:absolute;top:15px;right:15px;z-index:2147483647;background:#e94560;color:white;border:none;width:40px;height:40px;border-radius:50%;font-size:24px;cursor:pointer;';b.onclick=function(){m.remove();};var f=document.createElement('iframe');f.src='/endpoint/@blueharford/scrypted-spatial-awareness/ui/editor';f.style.cssText='width:100%;height:100%;border:none;';c.appendChild(b);c.appendChild(f);m.appendChild(c);m.onclick=function(ev){if(ev.target===m)m.remove();};document.body.appendChild(m);})()`;
521
+ const onclickCode = `(function(){var e=document.getElementById('sa-topology-modal');if(e)e.remove();var m=document.createElement('div');m.id='sa-topology-modal';m.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:2147483647;display:flex;align-items:center;justify-content:center;';var c=document.createElement('div');c.style.cssText='width:95vw;height:92vh;max-width:1800px;background:#121212;border-radius:8px;overflow:hidden;position:relative;box-shadow:0 8px 32px rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.1);';var b=document.createElement('button');b.innerHTML='×';b.style.cssText='position:absolute;top:8px;right:8px;z-index:2147483647;background:rgba(255,255,255,0.1);color:white;border:none;width:32px;height:32px;border-radius:4px;font-size:18px;cursor:pointer;line-height:1;';b.onclick=function(){m.remove();};var f=document.createElement('iframe');f.src='/endpoint/@blueharford/scrypted-spatial-awareness/ui/editor';f.style.cssText='width:100%;height:100%;border:none;';c.appendChild(b);c.appendChild(f);m.appendChild(c);m.onclick=function(ev){if(ev.target===m)m.remove();};document.body.appendChild(m);})()`;
437
522
 
438
523
  settings.push({
439
524
  key: 'topologyEditor',
@@ -442,39 +527,41 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
442
527
  value: `
443
528
  <style>
444
529
  .sa-open-btn {
445
- background: linear-gradient(135deg, #e94560 0%, #0f3460 100%);
446
- color: white;
530
+ background: #4fc3f7;
531
+ color: #000;
447
532
  border: none;
448
- padding: 14px 28px;
449
- border-radius: 8px;
450
- font-size: 15px;
451
- font-weight: 600;
533
+ padding: 10px 20px;
534
+ border-radius: 4px;
535
+ font-size: 14px;
536
+ font-weight: 500;
452
537
  cursor: pointer;
453
538
  display: inline-flex;
454
539
  align-items: center;
455
- gap: 10px;
456
- transition: transform 0.2s, box-shadow 0.2s;
540
+ gap: 8px;
541
+ transition: background 0.2s;
542
+ font-family: inherit;
457
543
  }
458
544
  .sa-open-btn:hover {
459
- transform: translateY(-2px);
460
- box-shadow: 0 8px 20px rgba(233, 69, 96, 0.4);
545
+ background: #81d4fa;
461
546
  }
462
547
  .sa-btn-container {
463
- padding: 20px;
464
- background: #16213e;
465
- border-radius: 8px;
548
+ padding: 16px;
549
+ background: rgba(255,255,255,0.03);
550
+ border-radius: 4px;
466
551
  text-align: center;
552
+ border: 1px solid rgba(255,255,255,0.08);
467
553
  }
468
554
  .sa-btn-desc {
469
- color: #888;
470
- margin-bottom: 15px;
471
- font-size: 14px;
555
+ color: rgba(255,255,255,0.6);
556
+ margin-bottom: 12px;
557
+ font-size: 13px;
558
+ font-family: inherit;
472
559
  }
473
560
  </style>
474
561
  <div class="sa-btn-container">
475
562
  <p class="sa-btn-desc">Configure camera positions, connections, and transit times</p>
476
563
  <button class="sa-open-btn" onclick="${onclickCode}">
477
- <span>&#9881;</span> Open Topology Editor
564
+ Open Topology Editor
478
565
  </button>
479
566
  </div>
480
567
  `,
@@ -736,11 +823,38 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
736
823
  return this.handleJourneyPathRequest(globalId, response);
737
824
  }
738
825
 
826
+ // Training Mode endpoints
827
+ if (path.endsWith('/api/training/start')) {
828
+ return this.handleTrainingStartRequest(request, response);
829
+ }
830
+ if (path.endsWith('/api/training/pause')) {
831
+ return this.handleTrainingPauseRequest(response);
832
+ }
833
+ if (path.endsWith('/api/training/resume')) {
834
+ return this.handleTrainingResumeRequest(response);
835
+ }
836
+ if (path.endsWith('/api/training/end')) {
837
+ return this.handleTrainingEndRequest(response);
838
+ }
839
+ if (path.endsWith('/api/training/status')) {
840
+ return this.handleTrainingStatusRequest(response);
841
+ }
842
+ if (path.endsWith('/api/training/landmark')) {
843
+ return this.handleTrainingLandmarkRequest(request, response);
844
+ }
845
+ if (path.endsWith('/api/training/apply')) {
846
+ return this.handleTrainingApplyRequest(response);
847
+ }
848
+
739
849
  // UI Routes
740
850
  if (path.endsWith('/ui/editor') || path.endsWith('/ui/editor/')) {
741
851
  return this.serveEditorUI(response);
742
852
  }
743
853
 
854
+ if (path.endsWith('/ui/training') || path.endsWith('/ui/training/')) {
855
+ return this.serveTrainingUI(response);
856
+ }
857
+
744
858
  if (path.includes('/ui/')) {
745
859
  return this.serveStaticFile(path, response);
746
860
  }
@@ -748,7 +862,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
748
862
  // Default: return info page
749
863
  response.send(JSON.stringify({
750
864
  name: 'Spatial Awareness Plugin',
751
- version: '0.3.0',
865
+ version: '0.4.0',
752
866
  endpoints: {
753
867
  api: {
754
868
  trackedObjects: '/api/tracked-objects',
@@ -760,9 +874,19 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
760
874
  liveTracking: '/api/live-tracking',
761
875
  connectionSuggestions: '/api/connection-suggestions',
762
876
  landmarkSuggestions: '/api/landmark-suggestions',
877
+ training: {
878
+ start: '/api/training/start',
879
+ pause: '/api/training/pause',
880
+ resume: '/api/training/resume',
881
+ end: '/api/training/end',
882
+ status: '/api/training/status',
883
+ landmark: '/api/training/landmark',
884
+ apply: '/api/training/apply',
885
+ },
763
886
  },
764
887
  ui: {
765
888
  editor: '/ui/editor',
889
+ training: '/ui/training',
766
890
  },
767
891
  },
768
892
  }), {
@@ -1228,12 +1352,187 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1228
1352
  }
1229
1353
  }
1230
1354
 
1355
+ // ==================== Training Mode Handlers ====================
1356
+
1357
+ private handleTrainingStartRequest(request: HttpRequest, response: HttpResponse): void {
1358
+ if (!this.trackingEngine) {
1359
+ response.send(JSON.stringify({ error: 'Tracking engine not running. Configure topology first.' }), {
1360
+ code: 500,
1361
+ headers: { 'Content-Type': 'application/json' },
1362
+ });
1363
+ return;
1364
+ }
1365
+
1366
+ try {
1367
+ let config: Partial<TrainingConfig> | undefined;
1368
+ let trainerName: string | undefined;
1369
+
1370
+ if (request.body) {
1371
+ const body = JSON.parse(request.body);
1372
+ trainerName = body.trainerName;
1373
+ config = body.config;
1374
+ }
1375
+
1376
+ const session = this.trackingEngine.startTrainingSession(trainerName, config);
1377
+ response.send(JSON.stringify(session), {
1378
+ headers: { 'Content-Type': 'application/json' },
1379
+ });
1380
+ } catch (e) {
1381
+ response.send(JSON.stringify({ error: (e as Error).message }), {
1382
+ code: 500,
1383
+ headers: { 'Content-Type': 'application/json' },
1384
+ });
1385
+ }
1386
+ }
1387
+
1388
+ private handleTrainingPauseRequest(response: HttpResponse): void {
1389
+ if (!this.trackingEngine) {
1390
+ response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
1391
+ code: 500,
1392
+ headers: { 'Content-Type': 'application/json' },
1393
+ });
1394
+ return;
1395
+ }
1396
+
1397
+ const success = this.trackingEngine.pauseTrainingSession();
1398
+ if (success) {
1399
+ response.send(JSON.stringify({ success: true }), {
1400
+ headers: { 'Content-Type': 'application/json' },
1401
+ });
1402
+ } else {
1403
+ response.send(JSON.stringify({ error: 'No active training session to pause' }), {
1404
+ code: 400,
1405
+ headers: { 'Content-Type': 'application/json' },
1406
+ });
1407
+ }
1408
+ }
1409
+
1410
+ private handleTrainingResumeRequest(response: HttpResponse): void {
1411
+ if (!this.trackingEngine) {
1412
+ response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
1413
+ code: 500,
1414
+ headers: { 'Content-Type': 'application/json' },
1415
+ });
1416
+ return;
1417
+ }
1418
+
1419
+ const success = this.trackingEngine.resumeTrainingSession();
1420
+ if (success) {
1421
+ response.send(JSON.stringify({ success: true }), {
1422
+ headers: { 'Content-Type': 'application/json' },
1423
+ });
1424
+ } else {
1425
+ response.send(JSON.stringify({ error: 'No paused training session to resume' }), {
1426
+ code: 400,
1427
+ headers: { 'Content-Type': 'application/json' },
1428
+ });
1429
+ }
1430
+ }
1431
+
1432
+ private handleTrainingEndRequest(response: HttpResponse): void {
1433
+ if (!this.trackingEngine) {
1434
+ response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
1435
+ code: 500,
1436
+ headers: { 'Content-Type': 'application/json' },
1437
+ });
1438
+ return;
1439
+ }
1440
+
1441
+ const session = this.trackingEngine.endTrainingSession();
1442
+ if (session) {
1443
+ response.send(JSON.stringify(session), {
1444
+ headers: { 'Content-Type': 'application/json' },
1445
+ });
1446
+ } else {
1447
+ response.send(JSON.stringify({ error: 'No training session to end' }), {
1448
+ code: 400,
1449
+ headers: { 'Content-Type': 'application/json' },
1450
+ });
1451
+ }
1452
+ }
1453
+
1454
+ private handleTrainingStatusRequest(response: HttpResponse): void {
1455
+ if (!this.trackingEngine) {
1456
+ response.send(JSON.stringify({ state: 'idle', stats: null }), {
1457
+ headers: { 'Content-Type': 'application/json' },
1458
+ });
1459
+ return;
1460
+ }
1461
+
1462
+ const status = this.trackingEngine.getTrainingStatus();
1463
+ if (status) {
1464
+ response.send(JSON.stringify(status), {
1465
+ headers: { 'Content-Type': 'application/json' },
1466
+ });
1467
+ } else {
1468
+ response.send(JSON.stringify({ state: 'idle', stats: null }), {
1469
+ headers: { 'Content-Type': 'application/json' },
1470
+ });
1471
+ }
1472
+ }
1473
+
1474
+ private handleTrainingLandmarkRequest(request: HttpRequest, response: HttpResponse): void {
1475
+ if (!this.trackingEngine) {
1476
+ response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
1477
+ code: 500,
1478
+ headers: { 'Content-Type': 'application/json' },
1479
+ });
1480
+ return;
1481
+ }
1482
+
1483
+ try {
1484
+ const body = JSON.parse(request.body!) as Omit<TrainingLandmark, 'id' | 'markedAt'>;
1485
+ const landmark = this.trackingEngine.markTrainingLandmark(body);
1486
+ if (landmark) {
1487
+ response.send(JSON.stringify({ success: true, landmark }), {
1488
+ headers: { 'Content-Type': 'application/json' },
1489
+ });
1490
+ } else {
1491
+ response.send(JSON.stringify({ error: 'No active training session' }), {
1492
+ code: 400,
1493
+ headers: { 'Content-Type': 'application/json' },
1494
+ });
1495
+ }
1496
+ } catch (e) {
1497
+ response.send(JSON.stringify({ error: 'Invalid request body' }), {
1498
+ code: 400,
1499
+ headers: { 'Content-Type': 'application/json' },
1500
+ });
1501
+ }
1502
+ }
1503
+
1504
+ private handleTrainingApplyRequest(response: HttpResponse): void {
1505
+ if (!this.trackingEngine) {
1506
+ response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
1507
+ code: 500,
1508
+ headers: { 'Content-Type': 'application/json' },
1509
+ });
1510
+ return;
1511
+ }
1512
+
1513
+ const result = this.trackingEngine.applyTrainingToTopology();
1514
+ if (result.success) {
1515
+ // Save the updated topology
1516
+ const topology = this.trackingEngine.getTopology();
1517
+ this.storage.setItem('topology', JSON.stringify(topology));
1518
+ }
1519
+ response.send(JSON.stringify(result), {
1520
+ headers: { 'Content-Type': 'application/json' },
1521
+ });
1522
+ }
1523
+
1231
1524
  private serveEditorUI(response: HttpResponse): void {
1232
1525
  response.send(EDITOR_HTML, {
1233
1526
  headers: { 'Content-Type': 'text/html' },
1234
1527
  });
1235
1528
  }
1236
1529
 
1530
+ private serveTrainingUI(response: HttpResponse): void {
1531
+ response.send(TRAINING_HTML, {
1532
+ headers: { 'Content-Type': 'text/html' },
1533
+ });
1534
+ }
1535
+
1237
1536
  private serveStaticFile(path: string, response: HttpResponse): void {
1238
1537
  // Serve static files for the UI
1239
1538
  response.send('Not found', { code: 404 });