import { config } from "dotenv" ; import { connect , ConnectRequest , Emitter , EmitterMessage } from 'emitter-io' ; import LatLon from "geodesy/latlon-spherical.js" ; interface ControlEvent { type : "pause" | "resume" | "cancel" | "set_altitude" ; filter : { name ? : string [ ] ; warehouse ? : string [ ] ; hangar ? : string [ ] ; } ; data : { message : string ; altitude ? : number ; } ; } interface DroneEvent { type : "drone_deployed" | "drone_grounded" | "low_battery" | "package_loaded" | "package_delivered" | "system_warning" | "system_error" | "control_event_rx" ; data : { message : string ; name : string ; batteryPercent ? : number ; batteryConsumed ? : number ; event ? : string ; hangar ? : string ; location ? : Location ; payload ? : number ; warehouse ? : string ; } ; } interface DronePosition { altitude : number ; batteryPercent : number ; bearing : number ; destination : { lat : number ; lon : number ; } ; location : { lat : number ; lon : number ; } ; name : string ; payloadPercent : number ; speed : number ; status : string ; tempCelsius : number ; } interface Position { location : LatLon ; dest : LatLon ; speed : number ; bearing : number ; altitude : number ; } interface Options { host : string ; port : number ; channels : { [ channel ] : string ; } ; secure ? : boolean ; } const CHAN_CONTROL_EVENT = "control-event" ; const CHAN_DRONE_EVENT = "drone-event" ; const CHAN_DRONE_POSITION = "drone-position" ; export class TrafficController { private run : boolean ; private client : Emitter ; private drones : { [ name ] : Position ; } ; private channels : { [ channel ] : string ; } ; constructor ( options ) { this . run = true ; this . drones = { } ; this . channels = options . channels ; this . client = connect ( { host : options . host , port : options . port , secure : ! ! options . secure } , ( ) => { console . log ( "connected to data feed" ) ; this . client . on ( 'error' , ( error ) => console . log ( error . stack ) ) ; this . client . on ( 'offline' , ( ) => { this . run = false ; } ) ; this . client . on ( "message" , ( msg ) => { if ( msg . channel === "drone-position/" ) { const message = msg . asObject ( ) ; if ( ! this . drones [ message . name ] ) { this . drones [ message . name ] = { altitude : message . altitude , bearing : message . bearing , dest : new LatLon ( message . destination . lat , message . destination . lon ) , location : new LatLon ( message . location . lat , message . location . lon ) , speed : message . speed , } ; } else { this . drones [ message . name ] . bearing = message . bearing ; this . drones [ message . name ] . dest . lat = message . destination . lat ; this . drones [ message . name ] . dest . lon = message . destination . lon ; this . drones [ message . name ] . location . lat = message . location . lat ; this . drones [ message . name ] . location . lon = message . location . lon ; this . drones [ message . name ] . speed = message . speed ; } this . detectCollision ( message . name , this . drones [ message . name ] ) ; } else if ( msg . channel === "drone-event/" ) { const message = msg . asObject ( ) ; if ( message . type === "package_delivered" ) { console . log ( "%s delivered package - adjusting altitude" , message . data . name ) ; this . setAltitude ( message . data . name , 200 , "ATC adjusting target altitude to 200 for return to warehouse" ) ; } if ( message . type === "package_loaded" ) { console . log ( "%s loaded package - adjusting altitude" , message . data . name ) ; this . setAltitude ( message . data . name , 150 , "ATC adjusting target altitude to 150 for package delivery" ) ; } if ( message . type === "low_battery" ) { console . log ( "%s returning to hangar - adjusting altitude" , message . data . name ) ; this . setAltitude ( message . data . name , 300 , "ATC adjusting target altitude to 300 for return to hangar" ) ; } if ( message . type === "system_warning" ) { console . log ( "%s sent warning - adjusting for conditions" , message . data . name ) ; this . setAltitude ( message . data . name , 50 , "ATC reducing altitude to avoid windy conditions" ) ; } if ( message . type === "system_error" ) { console . log ( "%s sent error - cancelling" , message . data . name ) ; this . setAltitude ( message . data . name , 300 , "Adjusting target altitude to 300 for return to hangar" ) ; this . cancelDrone ( message . data . name , "ATC detected system error - returning drone to hangar" ) ; } } } ) ; } ) ; } public async setAltitude ( drone , alt , reason ) { this . drones [ drone ] . altitude = alt ; this . sendControlEvent ( { data : { altitude : alt , message : reason , } , filter : { name : [ drone ] } , type : "set_altitude" , } ) ; } public async pauseDrone ( drone , reason ) { this . sendControlEvent ( { data : { message : reason , } , filter : { name : [ drone ] } , type : "pause" , } ) ; } public async resumeDrone ( drone , reason ) { this . sendControlEvent ( { data : { message : reason , } , filter : { name : [ drone ] } , type : "resume" , } ) ; } public async cancelDrone ( drone , reason ) { this . sendControlEvent ( { data : { message : reason , } , filter : { name : [ drone ] } , type : "cancel" , } ) ; } public async monitor ( ) { this . client . subscribe ( { channel : CHAN_DRONE_POSITION , key : this . channels [ CHAN_DRONE_POSITION ] , } ) ; this . client . subscribe ( { channel : CHAN_DRONE_EVENT , key : this . channels [ CHAN_DRONE_EVENT ] , } ) ; while ( this . run ) { await new Promise ( ( resolve ) => setTimeout ( resolve , 5000 ) ) ; } } private async sendControlEvent ( msg ) { this . client && this . client . publish ( { channel : CHAN_CONTROL_EVENT , key : this . channels [ CHAN_CONTROL_EVENT ] , message : JSON . stringify ( msg ) , } ) ; } private async detectCollision ( name , position ) { const conflicts = [ ] ; const conflictNames = [ ] ; for ( const [ otherName , other ] of Object . entries ( this . drones ) ) { if ( otherName === name ) { continue ; } if ( position . dest . equals ( other . dest ) ) { continue ; } if ( position . location . equals ( other . location ) ) { continue ; } if ( position . location . equals ( position . dest ) || other . location . equals ( other . dest ) ) { continue ; } if ( Math . abs ( position . altitude - other . altitude ) >= 20 ) { continue ; } if ( position . location . distanceTo ( other . location ) > 600 ) { continue ; } const bear1To = position . location . initialBearingTo ( position . dest ) ; const bear1From = position . dest . initialBearingTo ( position . location ) ; const bear2To = other . location . initialBearingTo ( other . dest ) ; const bear2From = other . dest . initialBearingTo ( other . location ) ; const intersectionTo = LatLon . intersection ( position . location , bear1To , other . location , bear2To ) ; const intersectionFrom = LatLon . intersection ( position . dest , bear1From , other . dest , bear2From ) ; if ( intersectionTo && intersectionFrom && intersectionTo . distanceTo ( intersectionFrom ) < 100 ) { console . log ( "Potential path conflict between %s and %s" , name , otherName ) ; console . log ( position ) ; console . log ( other ) ; console . log ( intersectionTo ) ; console . log ( intersectionFrom ) ; conflicts . push ( other ) ; conflictNames . push ( otherName ) ; } } if ( 0 === conflicts . length ) { return ; } const options = [ ] ; conflicts . sort ( ( a , b ) => ( a . altitude > b . altitude ) ? 1 : - 1 ) ; let floor = 50 ; for ( const conflict of conflicts ) { if ( conflict . altitude - floor >= 40 ) { options . push ( conflict . altitude - 20 ) ; } floor = conflict . altitude ; } conflicts . sort ( ( a , b ) => ( b . altitude > a . altitude ) ? 1 : - 1 ) ; let ceil = 1000 ; for ( const conflict of conflicts ) { if ( ceil - conflict . altitude > 40 ) { options . push ( conflict . altitude + 20 ) ; } ceil = conflict . altitude ; } options . sort ( ( a , b ) => { return Math . abs ( position . altitude - a ) > Math . abs ( position . altitude - b ) ? 1 : - 1 ; } ) ; console . log ( options ) ; console . log ( "adjusting %s from %d to %d to avoid airspace violatioon" , name , position . altitude , options [ 0 ] ) ; this . setAltitude ( name , options [ 0 ] , "ATC adjusting altitude to " + options [ 0 ] + " to avoid collision(s) with " + conflictNames . toString ( ) ) ; } }