From 1ec48a966c1181bb8fce8684ea6e2b986f387875 Mon Sep 17 00:00:00 2001 From: Richard Harrison Date: Sat, 9 Apr 2016 21:39:57 +0200 Subject: [PATCH] Added Emesary Multiplayer bridge --- Aircraft/Generic/an_spn_46.nas | 52 ++++-- Nasal/emesary.nas | 81 ++++++++- Nasal/emesary_mp_bridge.nas | 319 +++++++++++++++++++++++++++++++++ Nasal/notifications.nas | 49 +++++ 4 files changed, 485 insertions(+), 16 deletions(-) create mode 100644 Nasal/emesary_mp_bridge.nas create mode 100644 Nasal/notifications.nas diff --git a/Aircraft/Generic/an_spn_46.nas b/Aircraft/Generic/an_spn_46.nas index 537f857d3..9240b8b09 100644 --- a/Aircraft/Generic/an_spn_46.nas +++ b/Aircraft/Generic/an_spn_46.nas @@ -65,21 +65,53 @@ var ANSPN46ActiveNotification = # param(_anspn46_system): instance of ANSPN46_System which will send the notification new: func(_anspn46_system) { - var new_class = emesary.Notification.new("ANSPN46ActiveNotification", _anspn46_system.Ident); + var ident="none"; + if (_anspn46_system != nil) + ident=_anspn46_system.Ident; + + var new_class = emesary.Notification.new("ANSPN46ActiveNotification", ident); + new_class.ANSPN46_system = _anspn46_system; + new_class.Position = nil; + new_class.BeamPosition = nil; + + new_class.BeamAngle = 35; + new_class.Channel = 2; + new_class.BeamRange = 35; ##nm + new_class.BeamPower = 999; ## mw ??? # # Set notification properties from the ANSPN46_System. new_class.set_from = func(_anspn) { - me.Position = _anspn.GetCarrierPosition(); - me.BeamPosition = _anspn.GetTDZPosition(); - me.BeamAngle = 35; - me.Channel = _anspn.GetChannel(); - me.BeamRange = 35; ##nm - me.BeamPower = 999; ## mw ??? + if (_anspn != nil) + { + me.Ident = _anspn.Ident; + me.Position = _anspn.GetCarrierPosition(); + me.BeamPosition = _anspn.GetTDZPosition(); + + me.BeamAngle = 35; + me.Channel = _anspn.GetChannel(); + me.BeamRange = 35; ##nm + me.BeamPower = 999; ## mw ??? + } +print("\nANSPN46ActiveNotification::set_from: ", me.Ident); }; - new_class.set_from(_anspn46_system); + new_class.bridgeProperties = func + { + return + [ + { + getValue:func{return emesary.TransferCoord.encode(new_class.Position);}, + setValue:func(v){new_class.Position=emesary.TransferCoord.decode(v);}, + }, + { + getValue:func{return emesary.TransferByte.encode(new_class.Channel);}, + setValue:func(v){new_class.Channel=emesary.TransferByte.decode(v);}, + }, + ]; + }; + new_class.set_from(new_class.ANSPN46_system); return new_class; }, }; @@ -228,7 +260,7 @@ var ANSPN46_System = { print("AN/SNP46 created for "~_ident); - var new_class = emesary.Recipient.new("ANSPN46_System "~_ident); + var new_class = emesary.Recipient.new(_ident~".ANSPN46"); new_class.ara_63_position = geo.Coord.new(); new_class.Model = _model; @@ -315,7 +347,7 @@ var ANSPN46_System = # this will be reset if we receive something back from the aircraft. if (me.UpdateRate < 10) me.UpdateRate = me.UpdateRate+1; -# print("AN/SPN 46 : update from",me.Ident," rate=",me.UpdateRate); + print("AN/SPN 46 : update msg: ",me.msg.Ident," sys.rate=",me.UpdateRate); return emesary.GlobalTransmitter.NotifyAll(me.msg); }; diff --git a/Nasal/emesary.nas b/Nasal/emesary.nas index 82ed98aa2..dc065e7c4 100644 --- a/Nasal/emesary.nas +++ b/Nasal/emesary.nas @@ -24,6 +24,8 @@ # #---------------------------------------------------------------------------*/ +var __emesaryUniqueId = 14; # 0-15 are reserved, this way the global transmitter will be 15. + # Transmitters send notifications to all recipients that are registered. var Transmitter = { @@ -41,6 +43,8 @@ var Transmitter = var new_class = { parents: [Transmitter]}; new_class.Recipients = []; new_class.Ident = _ident; + __emesaryUniqueId += 1; + new_class.UniqueId = __emesaryUniqueId; return new_class; }, @@ -78,9 +82,9 @@ var Transmitter = PrintRecipients: func { - print("Recpient list"); + print("Recpient list for ",me.Ident,"(",me.UniqueId,")"); for (var idx = 0; idx < size(me.Recipients); idx += 1) - print("Recpient ",idx," ",me.Recipients[idx].Ident); + print("Recpient ",idx," ",me.Recipients[idx].Ident," (",me.Recipients[idx].UniqueId,")"); }, # Notify all registered recipients. Stop when receipt status of abort or finished are received. @@ -94,7 +98,7 @@ var Transmitter = var return_status = Transmitter.ReceiptStatus_NotProcessed; foreach (var recipient; me.Recipients) { - if (recipient.Active) + if (recipient.RecipientActive) { var rstat = recipient.Receive(message); @@ -147,13 +151,27 @@ var Transmitter = # # Base class for Notifications. By convention a Notification has a type and a value. # SubClasses can add extra properties or methods. +# Properties: +# Ident : Generic message identity. Can be an ident, or for simple messages a value that needs transmitting. +# Type : Message Type +# IsDistinct : non zero if this message supercedes previous messages of this type. +# Distinct messages are usually sent often and self contained +# (i.e. no relative state changes such as toggle value) +# Messages that indicate an event (such as after a pilot action) +# will usually be non-distinct. So an example would be gear/up down +# or ATC acknowledgements that all need to be transmitted +# The IsDistinct is important for any messages that are bridged over MP as +# only the most recently sent distinct message will be transmitted over MP var Notification = { - new: func(_type, _value) + new: func(_type, _ident) { var new_class = { parents: [Notification]}; - new_class.Value = _value; + new_class.Ident = _ident; new_class.Type = _type; + new_class.IsDistinct = 1; + new_class.FromIncomingBridge = 0; + new_class.Callsign = nil; return new_class; }, }; @@ -174,7 +192,9 @@ var Recipient = construct: func(_ident, new_class) { new_class.Ident = _ident; - new_class.Active = 1; + new_class.RecipientActive = 1; + __emesaryUniqueId += 1; + new_class.UniqueId = __emesaryUniqueId; new_class.Receive = func(notification) { # warning if required function not @@ -189,3 +209,52 @@ var Recipient = # Instantiate a Global Transmitter, this is a convenience and a known starting point. Generally most classes will # use this transmitters, however other transmitters can be created and merely use the global transmitter to discover each other var GlobalTransmitter = Transmitter.new("GlobalTransmitter"); + +var TransferCoord = +{ + encode : func(v) + { + return mp_broadcast.Binary.encodeCoord(v); + }, + decode : func(v) + { + return mp_broadcast.Binary.decodeCoord(v); + } +}; + +var TransferByte = +{ + encode : func(v) + { + return mp_broadcast.Binary.encodeByte(v); + }, + decode : func(v) + { + return mp_broadcast.Binary.decodeByte(v); + } +}; + +var TransferInt = +{ + encode : func(v) + { + return mp_broadcast.Binary.encodeInt(v); + }, + decode : func(v) + { + return mp_broadcast.Binary.decodeInt(v); + } +}; + +var TransferDouble = +{ + encode : func(v) + { + return mp_broadcast.Binary.encodeDouble(v); + }, + decode : func(v) + { + return mp_broadcast.Binary.decodeDouble(v); + } +}; + diff --git a/Nasal/emesary_mp_bridge.nas b/Nasal/emesary_mp_bridge.nas new file mode 100644 index 000000000..e8d0f3e8a --- /dev/null +++ b/Nasal/emesary_mp_bridge.nas @@ -0,0 +1,319 @@ +# +# bridge message rules; +# - if distinct must be absolute and self contained as a later message will supercede any earlier ones in the outgoing queue. +# - use the message type and ident to identify distinct messages +# The outgoing 'port' is a multiplay/generic/string index. +# - ! is used as a seperator between the elements that are used to send the notification (typeid, sequence, notification) +# - ; is used to seperate serialzied elemnts of the notification +# +# Outgoing messages are sent in a scheduled manner, usually once per second, and each message has a lifetime (to allow for propogation to +# all clients over UDP). Clients will ignore messages that they have already received (based on the sequence id). +# +# The incoming bridge will usually be created part of the aircraft model file; it is important to understand that each AI/MP model will have an incoming bridge +# as each element in /ai/models needs its own bridge to keep up with the incoming sequence id. This scheme may not work properly as it relies on the model being +# loaded which may only happen when visible so it may be necessary to track AI models in a seperate instantiatable incoming bridge manager. +# +# The outgoing bridge would usually be created within the aircraft loading Nasal. +var OutgoingMPBridge = +{ + new: func(_ident, _notifications_to_bridge=nil, _mpidx=19, _root="", _transmitter=nil) + { + if (_transmitter == nil) + _transmitter = emesary.GlobalTransmitter; + + print("OutgoingMPBridge created for "~_ident); + + var new_class = emesary.Recipient.new("OutgoingMPBridge "~_ident); + + new_class.MessageIndex = 100; +# foreach (var notification; _notifications_to_bridge) +# new_class.NotificationsToBridge = notification.new(; + if(_notifications_to_bridge == nil) + new_class.NotificationsToBridge = []; + else + new_class.NotificationsToBridge = _notifications_to_bridge; + + new_class.MPout = ""; + new_class.MPidx = _mpidx; + new_class.MessageLifeTime = 10; # seconds + new_class.OutgoingList = []; + new_class.Transmitter = _transmitter; + new_class.TransmitRequired=0; + new_class.Transmitter.Register(new_class); + new_class.MpVariable = _root~"sim/multiplay/generic/string["~new_class.MPidx~"]"; + new_class.TransmitterActive = 0; + + new_class.TransmitTimer = + maketimer(6, func + { + if(new_class.TransmitterActive) + new_class.Transmit(); + + new_class.TransmitTimer.restart(1); + }); + new_class.TransmitTimer.restart(1); + + new_class.Delete = func + { + if (me.Transmitter != nil) { + me.Transmitter.DeRegister(me); + me.Transmitter = nil; + } + }; + new_class.AddMessage = func(m) + { + append(me.NotificationsToBridge, m); + }; + + #------------------------------------------- + # Receive override: + new_class.Receive = func(notification) + { + if (notification.FromIncomingBridge) + return emesary.Transmitter.ReceiptStatus_NotProcessed; + +#print("Receive ",notification.Type," (",notification.Ident); + for (var idx = 0; idx < size(me.NotificationsToBridge); idx += 1) + { + if(me.NotificationsToBridge[idx].Type == notification.Type) + { + me.MessageIndex += 1; + notification.MessageExpiryTime = systime()+me.MessageLifeTime; + notification.BridgeMessageId = me.MessageIndex; + notification.BridgeMessageTypeId = idx; + # + # The message key is a composite of the type and ident to allow for multiple senders + # of the same message type. + notification.BridgeMessageTypeKey = notification.Type~"."~notification.Ident; +#print("Received ",notification.BridgeMessageTypeKey," expire=",notification.MessageExpiryTime); + me.AddToOutgoing(notification); + return emesary.Transmitter.ReceiptStatus_Pending; + } + } + return emesary.Transmitter.ReceiptStatus_NotProcessed; + }; + new_class.AddToOutgoing = func(notification) + { + if (notification.IsDistinct) + { + for (var idx = 0; idx < size(me.OutgoingList); idx += 1) + { + if(me.OutgoingList[idx].BridgeMessageTypeKey == notification.BridgeMessageTypeKey) + { +#print("Update ",me.OutgoingList[idx].BridgeMessageTypeKey); + me.OutgoingList[idx]= notification; + me.TransmitterActive = size(me.OutgoingList); + return; + } + } + } +#print("Added ",notification.BridgeMessageTypeKey); + append(me.OutgoingList, notification); + me.TransmitterActive = size(me.OutgoingList); + }; + new_class.Transmit = func + { + var outgoing = ""; + var cur_time=systime(); + var out_idx = 0; + for (var idx = 0; idx < size(me.OutgoingList); idx += 1) + { + var sect = ""; + var notification = me.OutgoingList[idx]; + if (notification.MessageExpiryTime > cur_time) + { + var encval=""; + var first_time = 1; + var eidx = 0; + foreach(var p ; notification.bridgeProperties()) + { + if (encval != "") + encval = encval ~ ";"; + encval = encval ~ p.getValue(); +#print("Encode ",eidx,"=",encval); + eidx += 1; + } + sect = sprintf("%d!%d!%s",notification.BridgeMessageId, notification.BridgeMessageTypeId, encval); + outgoing = outgoing~sect; + me.OutgoingList[out_idx] = me.OutgoingList[idx]; +# print("xmit ",idx," out=",out_idx); + out_idx += 1; + } +# else +# printf("expired ",idx,out_idx); + } + me.TransmitterActive = size(me.OutgoingList); + var del_count = size(me.OutgoingList)-out_idx; + setprop(me.MpVariable,outgoing); +# print("Set ",me.MpVariable," to ",outgoing); +# print("outgoingList : ",out_idx, " ", size(me.OutgoingList), " del count=",del_count); + for(var del_i=0; del_i < del_count; del_i += 1) + pop(me.OutgoingList); + }; + return new_class; + }, +}; + +# +# +# one of these for each model instantiated in the model XML - it will +# route messages to +var IncomingMPBridge = +{ + new: func(_ident, _notifications_to_bridge=nil, _mpidx=19, _transmitter=nil) + { + if (_transmitter == nil) + _transmitter = emesary.GlobalTransmitter; + + print("IncominggMPBridge created for "~_ident); + + var new_class = emesary.Transmitter.new("IncominggMPBridge "~_ident); + + new_class.IncomingMessageIndex = 100; + if(_notifications_to_bridge == nil) + new_class.NotificationsToBridge = []; + else + new_class.NotificationsToBridge = _notifications_to_bridge; + + new_class.MPout = ""; + new_class.MPidx = _mpidx; + new_class.MessageLifeTime = 10; # seconds + new_class.OutgoingList = []; + new_class.Transmitter = _transmitter; + new_class.MpVariable = ""; + + new_class.Connect = func(_root) + { + me.MpVariable = _root~"sim/multiplay/generic/string["~new_class.MPidx~"]"; + me.Callsign = getprop(_root~"callsign"); + print("bridge ",me.MpVariable, " callsign ",me.Callsign); + setlistener(me.MpVariable, func(v) + { + me.ProcessIncoming(v.getValue()); + }); + }; + + new_class.AddMessage = func(m) + { + append(me.NotificationsToBridge, m); + }; + + new_class.Remove = func + { + print("Emesary IncomingMPBridge Remove() ",me.Ident); + me.Transmitter.DeRegister(me); + }; + + #------------------------------------------- +# Receive override: + new_class.ProcessIncoming = func(encoded_val) + { + if (encoded_val == "") + return; +# print("ProcessIncoming ", encoded_val); + var encoded_notifications = split(";", encoded_val); + for (var idx = 0; idx < size(encoded_notifications); idx += 1) + { +# get the message parts + var encoded_notification = split("!", encoded_val); + if (size(encoded_notification) < 3) + print("Error: emesary.IncomingBridge.ProcessIncoming bad msg ",encoded_val); + else + { + var msg_idx = encoded_notification[0]; + var msg_type_id = encoded_notification[1]; + if (msg_type_id >= size(me.NotificationsToBridge)) + { + print("Error: emesary.IncomingBridge.ProcessIncoming invalid type_id ",msg_type_id); + } + else + { + var msg = me.NotificationsToBridge[msg_type_id]; + var msg_notify = encoded_notification[2]; + print("received idx=",msg_idx," ",msg_type_id,":",msg.Type); + if(msg_idx <= me.IncomingMessageIndex) + print(" **Already processed"); + else + { + # raise notification + var bridged_notification = msg; #emesary.Notification.new(msg.Type,"BridgedMessage"); + # populate fields + var bridgedProperties = msg.bridgeProperties(); + var encvals=split(";", msg_notify); + + for (var bpi = 0; bpi < size(encvals); bpi += 1) { + if (bpi < size(bridgedProperties)) { + var bp = bridgedProperties[bpi]; + print("encval ",bpi,"=",encvals[bpi]); + if (encvals[bpi] != ";" and encvals[bpi] != "") { + var bp = bridgedProperties[bpi]; + bp.setValue(encvals[bpi]); + } + #else + #print("EMPTY encoded ",bpi," empty"); + } else + print("Error: emesary.IncomingBridge.ProcessIncoming: supplementary encoded value at",bpi); + } + if (bridged_notification.Ident == "none") + bridged_notification.Ident = "mp-bridge"; + bridged_notification.FromIncomingBridge = 1; + bridged_notification.Callsign = me.Callsign; + me.Transmitter.NotifyAll(bridged_notification); + me.IncomingMessageIndex = msg_idx; + } + } + } + } + } + foreach(var n; new_class.NotificationsToBridge) + { + print("IncomingBridge: ",n.Type); + } + return new_class; + }, + startMPBridge : func(notification_list) + { + var incomingBridgeList = {}; + setlistener("/ai/models/model-added", func(v) + { +#Model added /ai[0]/models[0]/multiplayer[0] + var path = v.getValue(); + print("Model added ",path); + if (find("/multiplayer",path) > 0) + { + var callsign = getprop(path~"/callsign"); + print("Creating Emesary MPBridge for ",callsign); + if (callsign == "" or callsign == nil) + callsign = path; + var incomingBridge = emesary_mp_bridge.IncomingMPBridge.new(callsign~"mp", notification_list, 19); + incomingBridge.Connect(path~"/"); + incomingBridgeList[path] = incomingBridge; + } + }); + + setlistener("/ai/models/model-removed", func(v){ + print("Model removed ",v.getValue()); + var path = v.getValue(); + var bridge = incomingBridgeList[path]; + if (bridge != nil) + { + bridge.Remove(); + incomingBridgeList[path]=nil; + } + }); + }, +}; +#to setup bridges; +#io.load_nasal(getprop("/sim/fg-root") ~ "/Nasal/emesary_mp_bridge.nas"); +#var outgoingBridge = emesary_mp_bridge.OutgoingMPBridge.new("F-15mp"); +#outgoingBridge.AddMessage(an_spn_46.ANSPN46ActiveNotification.new("template")); +#incomingBridge = emesary_mp_bridge.IncomingMPBridge.new("F-15mp"); +#incomingBridge.AddMessage(an_spn_46.ANSPN46ActiveNotification.new("template")); +#var outgoingBridge = emesary_mp_bridge.OutgoingMPBridge.new("F-15mp"); +#outgoingBridge.AddMessage(an_spn_46.ANSPN46ActiveNotification.new("template")); +# var m = emesary.notifications.TacticalNotification("rr",2); +# emesary.GlobalTransmitter.NotifyAll(m); +#var outgoingBridge = emesary_mp_bridge.OutgoingMPBridge.new("F-15mp"); +#outgoingBridge.AddMessage(notifications.TacticalNotification.new(nil)); +# var m = emesary.notifications.TacticalNotification("rr",2); +# emesary.GlobalTransmitter.NotifyAll(m); diff --git a/Nasal/notifications.nas b/Nasal/notifications.nas new file mode 100644 index 000000000..57c787d69 --- /dev/null +++ b/Nasal/notifications.nas @@ -0,0 +1,49 @@ + #--------------------------------------------------------------------------- + # + # Title : EMESARY flightgear standardised notifications + # + # File Type : Implementation File + # + # Description : Messages that are applicable across all models and do not specifically relate to a single sysmte + # : - mostly needed when using the mutiplayer bridge + # + # Author : Richard Harrison (richard@zaretto.com) + # + # Creation Date : 06 April 2016 + # + # Version : 4.8 + # + # Copyright © 2016 Richard Harrison Released under GPL V2 + # + #---------------------------------------------------------------------------*/ + +var TacticalNotification = +{ + new: func(_ident=nil, _kind=0) + { + if(_ident==nil) + _ident="none"; + + var new_class = emesary.Notification.new("TacticalNotification", _ident); + + new_class.Kind = _kind; + new_class.Position=geo.aircraft_position(); + new_class.IsDistinct = 0; + + new_class.bridgeProperties = func + { + return + [ + { + getValue:func{return emesary.TransferCoord.encode(new_class.Position);}, + setValue:func(v){new_class.Position=emesary.TransferCoord.decode(v);}, + }, + { + getValue:func{return emesary.TransferByte.encode(new_class.Kind);}, + setValue:func(v){new_class.Kind=emesary.TransferByte.decode(v);}, + }, + ]; + }; + return new_class; + }, +};