66import { EventEmitter } from 'events' ;
77import { SummitClient , FireDetection } from './SummitClient' ;
88import * as sharp from 'sharp' ;
9+ import { MultiLinkManager , LinkType , MessagePriority } from './communication' ;
910
1011export interface TowerConfig {
1112 towerId : string ;
@@ -17,6 +18,22 @@ export interface TowerConfig {
1718 } ;
1819 thermalCamera : ThermalCameraConfig ;
1920 acousticArray : AcousticArrayConfig ;
21+ communication : {
22+ multiLink : {
23+ enabled : boolean ;
24+ primaryLink : 'radio_mesh' | 'cellular' | 'satellite' | 'wifi' ;
25+ failoverOrder : Array < 'radio_mesh' | 'cellular' | 'satellite' | 'wifi' > ;
26+ autonomousMode : { enabled : boolean ; syncInterval : number ; bufferSizeMB : number } ;
27+ } ;
28+ radioMesh : {
29+ frequency : '900MHz' | '2.4GHz' | '5GHz' ;
30+ meshId : string ;
31+ powerLevel : 'low' | 'medium' | 'high' ;
32+ encryption : 'wpa3' | 'none' ;
33+ } ;
34+ cellular : { carriers : [ 'primary' , 'secondary' ] | string [ ] ; dataLimitMB : number } ;
35+ satellite : { provider : 'starlink' | string ; backupOnly : boolean } ;
36+ } ;
2037 summitIntegration : {
2138 apiUrl : string ;
2239 apiKey : string ;
@@ -79,6 +96,7 @@ export interface MultiSensorFusion {
7996export class SentryTowerAgent extends EventEmitter {
8097 private config : TowerConfig ;
8198 private summitClient : SummitClient ;
99+ private multiLink ?: MultiLinkManager ;
82100 private isRunning : boolean = false ;
83101 private detectionHistory : FireDetectionResult [ ] = [ ] ;
84102 private fusionEngine : MultiSensorFusion ;
@@ -105,6 +123,9 @@ export class SentryTowerAgent extends EventEmitter {
105123 try {
106124 console . log ( `Starting Sentry Tower Agent for tower ${ this . config . towerId } ` ) ;
107125
126+ // Initialize multi-link communications
127+ this . initializeMultiLink ( ) ;
128+
108129 // Connect to Summit.OS
109130 await this . summitClient . connect ( ) ;
110131
@@ -267,25 +288,41 @@ export class SentryTowerAgent extends EventEmitter {
267288 this . detectionHistory = this . detectionHistory . slice ( - 1000 ) ;
268289 }
269290
270- // Create Summit.OS detection
271- const summitDetection : Omit < FireDetection , 'id' > = {
291+ // Prepare alert payload
292+ const alert = {
272293 deviceId : this . config . towerId ,
273294 timestamp : detection . timestamp ,
274295 type : detection . type ,
275296 confidence : detection . confidence ,
276297 position : detection . position ,
277298 bearing : detection . bearing ,
278- mediaRef : `tower_${ this . config . towerId } _${ Date . now ( ) } ` ,
279- source : 'edge'
299+ source : 'edge' as const ,
280300 } ;
281-
301+
302+ // 1) Immediate mesh broadcast (<50ms)
303+ if ( this . multiLink ) {
304+ await this . multiLink . sendMessage (
305+ 'sentinel/alerts/fire' ,
306+ alert ,
307+ MessagePriority . CRITICAL ,
308+ { preferredLink : LinkType . RADIO_MESH }
309+ ) ;
310+ }
311+
282312 if ( this . offlineMode ) {
283313 // Store for later sync
284314 this . offlineData . push ( detection ) ;
285315 this . emit ( 'offlineDetection' , detection ) ;
286316 } else {
287- // Report to Summit.OS immediately
288- await this . summitClient . reportFireDetection ( summitDetection ) ;
317+ // 2) Send to command via best available link (cellular/satellite/wifi)
318+ if ( this . multiLink ) {
319+ await this . multiLink . sendMessage ( 'sentinel/alerts/fire/cloud' , alert , MessagePriority . CRITICAL ) ;
320+ }
321+ // Maintain existing Summit.OS reporting path
322+ await this . summitClient . reportFireDetection ( {
323+ ...alert ,
324+ mediaRef : `tower_${ this . config . towerId } _${ Date . now ( ) } ` ,
325+ } ) ;
289326 this . emit ( 'fireDetection' , detection ) ;
290327 }
291328
@@ -325,6 +362,92 @@ export class SentryTowerAgent extends EventEmitter {
325362 this . summitClient . on ( 'error' , ( error ) => {
326363 this . emit ( 'summitError' , error ) ;
327364 } ) ;
365+
366+ // Multi-link link status to drive autonomous mode
367+ this . on ( 'linkUpdated' , ( ) => {
368+ if ( ! this . multiLink ) return ;
369+ const wide = this . multiLink . hasWideAreaConnectivity ( ) ;
370+ if ( ! wide && ! this . offlineMode ) {
371+ // Enter autonomous mode
372+ this . enableOfflineMode ( ) ;
373+ }
374+ if ( wide && this . offlineMode ) {
375+ // Exit autonomous mode and trigger flush
376+ this . disableOfflineMode ( ) ;
377+ this . multiLink . flushBuffer ( ) ;
378+ }
379+ } ) ;
380+ }
381+
382+ private initializeMultiLink ( ) : void {
383+ if ( ! this . config . communication ?. multiLink ?. enabled ) return ;
384+ const ml = new MultiLinkManager ( {
385+ deviceId : this . config . towerId ,
386+ failoverOrder : this . config . communication . multiLink . failoverOrder . map ( ( t ) => t as unknown as LinkType ) ,
387+ autonomousMode : {
388+ enabled : this . config . communication . multiLink . autonomousMode . enabled ,
389+ syncIntervalSec : this . config . communication . multiLink . autonomousMode . syncInterval ,
390+ bufferSizeMB : this . config . communication . multiLink . autonomousMode . bufferSizeMB ,
391+ } ,
392+ } ) ;
393+
394+ // Seed link states (these would be updated by real adapters/health checks)
395+ ml . addOrUpdateLink ( {
396+ type : LinkType . RADIO_MESH ,
397+ id : this . config . communication . radioMesh . meshId ,
398+ enabled : true ,
399+ up : true ,
400+ preferred : this . config . communication . multiLink . primaryLink === 'radio_mesh' ,
401+ costScore : 1 ,
402+ quality : { latencyMs : 20 , throughputKbps : 1000 , lossPct : 0.5 } ,
403+ } ) ;
404+
405+ ml . addOrUpdateLink ( {
406+ type : LinkType . CELLULAR ,
407+ id : 'dual-sim-modem' ,
408+ enabled : true ,
409+ up : true ,
410+ preferred : this . config . communication . multiLink . primaryLink === 'cellular' ,
411+ costScore : 5 ,
412+ quality : { latencyMs : 80 , throughputKbps : 20000 , lossPct : 1.0 } ,
413+ } ) ;
414+
415+ ml . addOrUpdateLink ( {
416+ type : LinkType . SATELLITE ,
417+ id : this . config . communication . satellite . provider ,
418+ enabled : true ,
419+ up : ! this . config . communication . satellite . backupOnly , // still available but used as backup
420+ preferred : this . config . communication . multiLink . primaryLink === 'satellite' ,
421+ costScore : 9 ,
422+ quality : { latencyMs : 600 , throughputKbps : 10000 , lossPct : 1.5 } ,
423+ } ) ;
424+
425+ ml . addOrUpdateLink ( {
426+ type : LinkType . WIFI ,
427+ id : 'wifi-ap' ,
428+ enabled : true ,
429+ up : true ,
430+ preferred : this . config . communication . multiLink . primaryLink === 'wifi' ,
431+ costScore : 2 ,
432+ quality : { latencyMs : 30 , throughputKbps : 54000 , lossPct : 0.5 } ,
433+ } ) ;
434+
435+ ml . setFailoverOrder ( [
436+ LinkType . RADIO_MESH ,
437+ LinkType . CELLULAR ,
438+ LinkType . SATELLITE ,
439+ LinkType . WIFI ,
440+ ] ) ;
441+
442+ ml . start ( ) ;
443+
444+ // Bubble link events
445+ ml . on ( 'linkUpdated' , ( s ) => this . emit ( 'linkUpdated' , s ) ) ;
446+ ml . on ( 'sent' , ( s ) => this . emit ( 'linkSent' , s ) ) ;
447+ ml . on ( 'buffered' , ( s ) => this . emit ( 'linkBuffered' , s ) ) ;
448+ ml . on ( 'flushed' , ( s ) => this . emit ( 'linkFlushed' , s ) ) ;
449+
450+ this . multiLink = ml ;
328451 }
329452}
330453
0 commit comments