diff --git a/FSMosquitoTopic.cs b/FSMosquitoTopic.cs index 8a614c6..2e8bb97 100644 --- a/FSMosquitoTopic.cs +++ b/FSMosquitoTopic.cs @@ -20,6 +20,9 @@ public static class FSMosquitoTopic // SimConnect Status Messages (Egress) public const string SimConnectStatus = "fsm/client/{0}/simconnect/status"; + // Set Simconnect topic value (Ingress) + public const string SetSimConnectTopicValue = "fsm/client/{0}/v/set/"; //Wildcard added by the subscription + // Simconnect topic value (Egress) public const string SimConnectTopicValue = "fsm/client/{0}/v/{1}"; } diff --git a/Forms/MainForm.cs b/Forms/MainForm.cs index f820835..4f1971e 100644 --- a/Forms/MainForm.cs +++ b/Forms/MainForm.cs @@ -34,6 +34,7 @@ public MainForm(IFsMqtt fsMqtt, IFsSimConnect fsSimConnect, ILogger lo FsMqtt.MqttConnectionClosed += FsMqtt_MqttConnectionClosed; FsMqtt.ReportSimConnectStatusRequestRecieved += FsMqtt_ReportSimConnectStatusRequestRecieved; FsMqtt.SubscribeRequestRecieved += FsMqtt_SubscribeRequestRecieved; + FsMqtt.SetSimVarRequestRecieved += FsMqtt_SetSimVarRequestRecieved; FsMqtt.MqttMessageRecieved += FsMqtt_MqttMessageRecieved; FsMqtt.MqttMessageTransmitted += FsMqtt_MqttMessageTransmitted; @@ -47,6 +48,7 @@ public MainForm(IFsMqtt fsMqtt, IFsSimConnect fsSimConnect, ILogger lo _pulseSimConnectStatusTimer.Elapsed += _pulseSimConnectStatusTimer_Elapsed; InitializeControls(); } + public IFsMqtt FsMqtt { get; @@ -139,11 +141,11 @@ private void SimConnect_SimConnectClosed(object sender, EventArgs e) _pulseSimConnectStatusTimer.Stop(); } - private void SimConnect_TopicValueChanged(object sender, (SimConnectTopic topic, object value) topicValue) + private void SimConnect_TopicValueChanged(object sender, (SimConnectTopic topic, uint objectId, object value) topicValue) { if (FsMqtt.IsConnected) { - FsMqtt.PublishTopicValue(topicValue.topic, topicValue.value); + FsMqtt.PublishTopicValue(topicValue.topic, topicValue.objectId, topicValue.value); } } @@ -200,6 +202,14 @@ private void FsMqtt_SubscribeRequestRecieved(object _, SimConnectTopic[] topics) } } } + + private void FsMqtt_SetSimVarRequestRecieved(object sender, (string datumName, uint? objectId, object value) request) + { + if (FsSimConnect.IsConnected) + { + FsSimConnect.Set(request.datumName, request.objectId, request.value); + } + } #endregion #region Initialize Controls @@ -243,7 +253,7 @@ private void InitializeControls() Dock = DockStyle.Top, }; - var label = new Label + var lblMosquito = new Label { Height = 140, Text = "FSMosquito", @@ -252,7 +262,7 @@ private void InitializeControls() Dock = DockStyle.Top, }; - Controls.Add(label); + Controls.Add(lblMosquito); Controls.Add(pb1); Controls.Add(statusPanel); } diff --git a/FsMqtt.cs b/FsMqtt.cs index b7320db..9b42289 100644 --- a/FsMqtt.cs +++ b/FsMqtt.cs @@ -30,6 +30,7 @@ public class FsMqtt : IFsMqtt public event EventHandler MqttConnectionClosed; public event EventHandler ReportSimConnectStatusRequestRecieved; public event EventHandler SubscribeRequestRecieved; + public event EventHandler<(string datumName, uint? objectId, object value)> SetSimVarRequestRecieved; public event EventHandler MqttMessageRecieved; public event EventHandler MqttMessageTransmitted; @@ -114,8 +115,9 @@ public async Task PublishSimConnectStatus(string simConnectStatus) await Publish(FSMosquitoTopic.SimConnectStatus, simConnectStatus); } - public async Task PublishTopicValue(SimConnectTopic topic, object value) + public async Task PublishTopicValue(SimConnectTopic topic, uint objectId, object value) { + //TODO: Include objectId in topic. var normalizedTopicName = Regex.Replace(topic.DatumName.ToLower(), "\\s", "_"); await Publish(FSMosquitoTopic.SimConnectTopicValue, value, true, new string[] { _clientId, normalizedTopicName }); } @@ -176,7 +178,8 @@ await MqttClient.SubscribeAsync( // SimConnect Events Subscription new MqttTopicFilterBuilder().WithTopic(string.Format(FSMosquitoTopic.ReportSimConnectStatus, _clientId)).Build(), new MqttTopicFilterBuilder().WithTopic(string.Format(FSMosquitoTopic.SubscribeToSimConnect, _clientId)).Build(), - new MqttTopicFilterBuilder().WithTopic(string.Format(FSMosquitoTopic.InvokeSimConnectFunction, _clientId)).Build() + new MqttTopicFilterBuilder().WithTopic(string.Format(FSMosquitoTopic.InvokeSimConnectFunction, _clientId)).Build(), + new MqttTopicFilterBuilder().WithTopic(string.Format(FSMosquitoTopic.SetSimConnectTopicValue, _clientId) + "+").Build() ); // Report that we've connected. @@ -214,7 +217,7 @@ private Task OnApplicationMessageReceived(MqttApplicationMessageReceivedEventArg try { var payload = string.Empty; - + if (e.ApplicationMessage.Payload != null && e.ApplicationMessage.Payload.Length > 0) payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); @@ -229,11 +232,17 @@ private Task OnApplicationMessageReceived(MqttApplicationMessageReceivedEventArg var typedPayload = JsonConvert.DeserializeObject(payload); OnSubscribeRequestRecieved(typedPayload); break; + case var setSimVar when setSimVar.StartsWith(string.Format(FSMosquitoTopic.SetSimConnectTopicValue, _clientId)): + var datumName = setSimVar.Substring(setSimVar.LastIndexOf('/') + 1, setSimVar.Length - setSimVar.LastIndexOf('/') - 1); + var deNormalizedTopicName = Regex.Replace(datumName.ToUpper(), "_", " "); + var setSimConnectVarRequestPayload = JsonConvert.DeserializeObject(payload); + OnSetSimVarRequestRecieved(deNormalizedTopicName, setSimConnectVarRequestPayload.ObjectId, setSimConnectVarRequestPayload.Value); + break; } OnMqttMessageRecieved(); } - catch(Exception ex) + catch (Exception ex) { _logger.LogError($"Error deserializing Application Message for topic {e.ApplicationMessage.Topic}: {ex.Message}", ex); } @@ -251,7 +260,7 @@ private async Task ProcessMessageQueue() return; } - while(_mqttMessageQueue.Count > 0) + while (_mqttMessageQueue.Count > 0) { if (_mqttMessageQueue.TryDequeue(out MqttApplicationMessage message)) { @@ -303,6 +312,14 @@ private void OnSubscribeRequestRecieved(SimConnectTopic[] topics) } } + private void OnSetSimVarRequestRecieved(string datumName, uint? objectId, object value) + { + if (SetSimVarRequestRecieved != null) + { + SetSimVarRequestRecieved.Invoke(this, (datumName, objectId, value)); + } + } + private void OnMqttMessageRecieved() { if (MqttMessageRecieved != null) diff --git a/FsSimConnect.cs b/FsSimConnect.cs index 2cc8a1c..f87b3f1 100644 --- a/FsSimConnect.cs +++ b/FsSimConnect.cs @@ -30,7 +30,7 @@ public sealed class FsSimConnect : IFsSimConnect public event EventHandler SimConnectOpened; public event EventHandler SimConnectClosed; - public event EventHandler<(SimConnectTopic, object)> TopicValueChanged; + public event EventHandler<(SimConnectTopic, uint, object)> TopicValueChanged; public event EventHandler SimConnectDataReceived; public event EventHandler SimConnectDataRequested; @@ -145,6 +145,27 @@ public void SignalReceiveSimConnectMessage() } } + public void Set(string datumName, uint? objectId, object value) + { + if (objectId.HasValue == false) + { + objectId = 0; + } + + if (!_subscriptions.ContainsKey(datumName)) + { + _logger.LogInformation($"Skipping setting value of {datumName} to {value} on object {objectId} as the topic has not been previously registered."); + } + + var subscription = _subscriptions[datumName]; + + var def = (Definition)Enum.ToObject(typeof(Definition), subscription.Id); + _simConnect.SetDataOnSimObject(def, objectId.Value, SIMCONNECT_DATA_SET_FLAG.DEFAULT, value); + + // Cause the updated value to be re-transmitted next pulse. + subscription.LastValue = null; + } + public void Subscribe(SimConnectTopic topic) { if (_subscriptions.ContainsKey(topic.DatumName)) @@ -283,38 +304,33 @@ private void SimConnect_OnRecvSimObjectDataByType(SimConnect sender, SIMCONNECT_ uint objectId = data.dwObjectID; // ObjectID == 1 is the user object data (I think) - switch(objectId) + object currentValue; + if (_pendingSubscriptions.TryRemove((int)requestId, out SimConnectSubscription subscription)) { - case 1: - object currentValue; - if (_pendingSubscriptions.TryRemove((int)requestId, out SimConnectSubscription subscription)) + switch (subscription.Topic.Units) + { + case Consts.SimConnectBool: + currentValue = (bool)data.dwData[0]; + break; + case Consts.SimConnectStringV: + currentValue = ((StringStruct)data.dwData[0]).value; + break; + default: + currentValue = (double)data.dwData[0]; + break; + } + + subscription.PendingRequestId = null; + subscription.PendingRequestStartTimeStamp = null; + + if (subscription.LastValue == null || subscription.LastValue.Equals(currentValue) == false) + { + if (subscription.LastValue != currentValue) { - switch (subscription.Topic.Units) - { - case Consts.SimConnectBool: - currentValue = (bool)data.dwData[0]; - break; - case Consts.SimConnectStringV: - currentValue = ((StringStruct)data.dwData[0]).value; - break; - default: - currentValue = (double)data.dwData[0]; - break; - } - - subscription.PendingRequestId = null; - subscription.PendingRequestStartTimeStamp = null; - - if (subscription.LastValue == null || subscription.LastValue.Equals(currentValue) == false) - { - if (subscription.LastValue != currentValue) - { - subscription.LastValue = currentValue; - OnTopicValue_Changed(subscription.Topic, currentValue); - } - } + subscription.LastValue = currentValue; + OnTopicValue_Changed(subscription.Topic, objectId, currentValue); } - break; + } } OnSimConnectDataRecieved(); @@ -351,11 +367,11 @@ private void OnSimConnect_Closed() } } - private void OnTopicValue_Changed(SimConnectTopic topic, object value) + private void OnTopicValue_Changed(SimConnectTopic topic, uint objectId, object value) { if (TopicValueChanged != null) { - TopicValueChanged.Invoke(this, (topic, value)); + TopicValueChanged.Invoke(this, (topic, objectId, value)); } } diff --git a/Helpers/SettingsHelpers.cs b/Helpers/SettingsHelpers.cs new file mode 100644 index 0000000..de73d53 --- /dev/null +++ b/Helpers/SettingsHelpers.cs @@ -0,0 +1,36 @@ +namespace FSMosquitoClient.Helpers +{ + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using System; + using System.IO; + + public static class SettingsHelpers + { + public static bool AddOrUpdateAppSetting(string sectionPathKey, T value) + { + try + { + var filePath = Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + var json = File.ReadAllText(filePath); + var obj = JObject.Parse(json); + + var token = obj.SelectToken(sectionPathKey); + if (token == null) + { + return false; + } + token.Replace(JToken.FromObject(value)); + + var output = obj.ToString(Formatting.Indented); + File.WriteAllText(filePath, output); + return true; + } + catch (Exception ex) + { + Console.WriteLine("Error writing app settings | {0}", ex.Message); + } + return false; + } + } +} diff --git a/IFsMqtt.cs b/IFsMqtt.cs index 8a304e4..acff8bb 100644 --- a/IFsMqtt.cs +++ b/IFsMqtt.cs @@ -26,15 +26,20 @@ public interface IFsMqtt : IDisposable public event EventHandler MqttMessageTransmitted; /// - /// Event that is raised when a report SimConnect status Request is received. + /// Event that is raised when a report SimConnect status request is received. /// public event EventHandler ReportSimConnectStatusRequestRecieved; /// - /// Event that is raised when a SimConnect Topic Subscription Request is received. + /// Event that is raised when a SimConnect Topic Subscription request is received. /// public event EventHandler SubscribeRequestRecieved; + /// + /// Event that is raised when a SimConnect Set SimVar request is recieved. + /// + public event EventHandler<(string datumName, uint? objectId, object value)> SetSimVarRequestRecieved; + /// /// Gets a value that indicates if the current instance is connected to MQTT /// @@ -66,8 +71,9 @@ public interface IFsMqtt : IDisposable /// Publishes the specified topic value to the MQTT broker /// /// + /// /// /// - Task PublishTopicValue(SimConnectTopic topic, object value); + Task PublishTopicValue(SimConnectTopic topic, uint objectId, object value); } } diff --git a/IFsSimConnect.cs b/IFsSimConnect.cs index 13f4e7c..ca4d240 100644 --- a/IFsSimConnect.cs +++ b/IFsSimConnect.cs @@ -20,7 +20,7 @@ public interface IFsSimConnect : IDisposable /// /// Event that is raised when a previously subscribed topic value has changed. /// - public event EventHandler<(SimConnectTopic, object)> TopicValueChanged; + public event EventHandler<(SimConnectTopic, uint, object)> TopicValueChanged; /// /// Event that is raised when a SimConnect data object is received. @@ -53,6 +53,14 @@ public interface IFsSimConnect : IDisposable /// void Disconnect(); + /// + /// Sets a SimConnect Datum value on the specified object id with the indicated value + /// + /// + /// + /// + void Set(string datumName, uint? objectId, object value); + /// /// Subscribes to a SimConnect Datum Topic /// diff --git a/Models/SetSimConnectVar.cs b/Models/SetSimConnectVar.cs new file mode 100644 index 0000000..9ac1321 --- /dev/null +++ b/Models/SetSimConnectVar.cs @@ -0,0 +1,21 @@ +namespace FSMosquitoClient +{ + using Newtonsoft.Json; + + class SetSimConnectVarRequest + { + [JsonProperty("objectId")] + public uint? ObjectId + { + get; + set; + } + + [JsonProperty("value")] + public object Value + { + get; + set; + } + } +} diff --git a/README.md b/README.md index 1e13a1f..2b89ac3 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,11 @@ There are three main classes involved: --- - TODO: Right now just the 'SimVars' and 'System Events' described in the FS2020 SimConnect SDK are registered and only in a one-way fashion. - This should be expanded to allow values to be set from the FSMosquito server, as well as SimConnect functions to be called. + The client currently also accepts SimVars to be set via MQTT messages as well, this opens up some interesting possibilities for automation. + TODO: System variables, invoking various SimConnect functions + TODO: Add ability to change MQTT broker in UI + TODO: Add ability to specify PAT (Pilot Access Token) and change topics accordingly ### Logs Currently the FSMosquitoClient creates log files located in the ./logs folder relative to where the FSMosquitoClient.exe is located.