diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CallStatusMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CallStatusMessenger.cs new file mode 100644 index 00000000..cd7edeb3 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CallStatusMessenger.cs @@ -0,0 +1,221 @@ +using System; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices that implement call status interfaces + /// without requiring VideoCodecBase inheritance + /// + public class CallStatusMessenger : MessengerBase + { + /// + /// Device with dialer capabilities + /// + protected IHasDialer Dialer { get; private set; } + + /// + /// Device with content sharing capabilities (optional) + /// + protected IHasContentSharing ContentSharing { get; private set; } + + /// + /// Constructor + /// + /// + /// + /// + public CallStatusMessenger(string key, IHasDialer dialer, string messagePath) + : base(key, messagePath, dialer as IKeyName) + { + Dialer = dialer ?? throw new ArgumentNullException(nameof(dialer)); + dialer.CallStatusChange += Dialer_CallStatusChange; + + // Check for optional content sharing interface + if (dialer is IHasContentSharing contentSharing) + { + ContentSharing = contentSharing; + contentSharing.SharingContentIsOnFeedback.OutputChange += SharingContentIsOnFeedback_OutputChange; + contentSharing.SharingSourceFeedback.OutputChange += SharingSourceFeedback_OutputChange; + } + } + + /// + /// Handles call status changes + /// + /// + /// + private void Dialer_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e) + { + try + { + SendFullStatus(); + } + catch (Exception ex) + { + this.LogError(ex, "Error handling call status change: {error}", ex.Message); + } + } + + /// + /// Handles content sharing status changes + /// + /// + /// + private void SharingContentIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + PostStatusMessage(JToken.FromObject(new + { + sharingContentIsOn = e.BoolValue + })); + } + + /// + /// Handles sharing source changes + /// + /// + /// + private void SharingSourceFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + PostStatusMessage(JToken.FromObject(new + { + sharingSource = e.StringValue + })); + } + + /// + /// Gets active calls from the dialer + /// + /// + private object GetActiveCalls() + { + // Try to get active calls if the dialer has an ActiveCalls property + var dialerType = Dialer.GetType(); + var activeCallsProperty = dialerType.GetProperty("ActiveCalls"); + + if (activeCallsProperty != null && activeCallsProperty.PropertyType == typeof(System.Collections.Generic.List)) + { + var activeCalls = activeCallsProperty.GetValue(Dialer) as System.Collections.Generic.List; + return activeCalls ?? new System.Collections.Generic.List(); + } + + // Return basic call status if no ActiveCalls property + return new { isInCall = Dialer.IsInCall }; + } + + /// + /// Sends full status message + /// + public void SendFullStatus() + { + var status = new + { + isInCall = Dialer.IsInCall, + calls = GetActiveCalls() + }; + + // Add content sharing status if available + if (ContentSharing != null) + { + var statusWithSharing = new + { + isInCall = Dialer.IsInCall, + calls = GetActiveCalls(), + sharingContentIsOn = ContentSharing.SharingContentIsOnFeedback.BoolValue, + sharingSource = ContentSharing.SharingSourceFeedback.StringValue + }; + PostStatusMessage(JToken.FromObject(statusWithSharing)); + } + else + { + PostStatusMessage(JToken.FromObject(status)); + } + } + + /// + /// Registers actions for call control + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus()); + + // Basic call control actions + AddAction("/dial", (id, content) => + { + var msg = content.ToObject>(); + Dialer.Dial(msg.Value); + }); + + AddAction("/endAllCalls", (id, content) => Dialer.EndAllCalls()); + + AddAction("/dtmf", (id, content) => + { + var msg = content.ToObject>(); + Dialer.SendDtmf(msg.Value); + }); + + // Call-specific actions (if active calls are available) + AddAction("/endCallById", (id, content) => + { + var msg = content.ToObject>(); + var call = GetCallWithId(msg.Value); + if (call != null) + Dialer.EndCall(call); + }); + + AddAction("/acceptById", (id, content) => + { + var msg = content.ToObject>(); + var call = GetCallWithId(msg.Value); + if (call != null) + Dialer.AcceptCall(call); + }); + + AddAction("/rejectById", (id, content) => + { + var msg = content.ToObject>(); + var call = GetCallWithId(msg.Value); + if (call != null) + Dialer.RejectCall(call); + }); + + // Content sharing actions if available + if (ContentSharing != null) + { + AddAction("/startSharing", (id, content) => ContentSharing.StartSharing()); + AddAction("/stopSharing", (id, content) => ContentSharing.StopSharing()); + } + } + + /// + /// Finds a call by ID + /// + /// + /// + private CodecActiveCallItem GetCallWithId(string id) + { + // Try to get call using reflection for ActiveCalls property + var dialerType = Dialer.GetType(); + var activeCallsProperty = dialerType.GetProperty("ActiveCalls"); + + if (activeCallsProperty != null && activeCallsProperty.PropertyType == typeof(System.Collections.Generic.List)) + { + var activeCalls = activeCallsProperty.GetValue(Dialer) as System.Collections.Generic.List; + if (activeCalls != null) + { + return activeCalls.FirstOrDefault(c => c.Id.Equals(id)); + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Tests/MockCallStatusDevice.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Tests/MockCallStatusDevice.cs new file mode 100644 index 00000000..a510e5bd --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Tests/MockCallStatusDevice.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.AppServer.Messengers.Tests +{ + /// + /// Mock device for testing CallStatusMessenger that implements IHasDialer without VideoCodecBase + /// + public class MockCallStatusDevice : EssentialsDevice, IHasDialer, IHasContentSharing + { + public event EventHandler CallStatusChange; + + private List _activeCalls = new List(); + private bool _isInCall; + private bool _sharingContentIsOn; + private string _sharingSource = ""; + + public MockCallStatusDevice(string key, string name) : base(key, name) + { + SharingContentIsOnFeedback = new BoolFeedback(key + "-SharingContentIsOnFeedback", () => _sharingContentIsOn); + SharingSourceFeedback = new StringFeedback(key + "-SharingSourceFeedback", () => _sharingSource); + AutoShareContentWhileInCall = false; + } + + public bool IsInCall + { + get => _isInCall; + private set + { + if (_isInCall != value) + { + _isInCall = value; + OnCallStatusChange(); + } + } + } + + public List ActiveCalls => _activeCalls; + + public BoolFeedback SharingContentIsOnFeedback { get; private set; } + public StringFeedback SharingSourceFeedback { get; private set; } + public bool AutoShareContentWhileInCall { get; private set; } + + public void Dial(string number) + { + // Mock implementation + var call = new CodecActiveCallItem + { + Id = Guid.NewGuid().ToString(), + Name = $"Call to {number}", + Number = number, + Status = eCodecCallStatus.Dialing, + Direction = eCodecCallDirection.Outgoing + }; + + _activeCalls.Add(call); + IsInCall = true; + } + + public void EndCall(CodecActiveCallItem activeCall) + { + if (activeCall != null && _activeCalls.Contains(activeCall)) + { + _activeCalls.Remove(activeCall); + IsInCall = _activeCalls.Count > 0; + } + } + + public void EndAllCalls() + { + _activeCalls.Clear(); + IsInCall = false; + } + + public void AcceptCall(CodecActiveCallItem item) + { + if (item != null) + { + item.Status = eCodecCallStatus.Connected; + IsInCall = true; + } + } + + public void RejectCall(CodecActiveCallItem item) + { + if (item != null && _activeCalls.Contains(item)) + { + _activeCalls.Remove(item); + IsInCall = _activeCalls.Count > 0; + } + } + + public void SendDtmf(string digit) + { + // Mock implementation - nothing to do + } + + public void StartSharing() + { + _sharingContentIsOn = true; + _sharingSource = "Local"; + SharingContentIsOnFeedback.FireUpdate(); + SharingSourceFeedback.FireUpdate(); + } + + public void StopSharing() + { + _sharingContentIsOn = false; + _sharingSource = ""; + SharingContentIsOnFeedback.FireUpdate(); + SharingSourceFeedback.FireUpdate(); + } + + private void OnCallStatusChange() + { + CallStatusChange?.Invoke(this, new CodecCallStatusItemChangeEventArgs( + _activeCalls.Count > 0 ? _activeCalls[0] : null)); + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Tests/Program.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Tests/Program.cs new file mode 100644 index 00000000..6f61cd23 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Tests/Program.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.AppServer.Messengers.Tests; + +namespace PepperDash.Essentials.MobileControl.Tests +{ + /// + /// Simple test program to verify CallStatusMessenger functionality + /// + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Testing CallStatusMessenger with IHasDialer device..."); + + try + { + // Create a mock device that implements IHasDialer but not VideoCodecBase + var mockDevice = new MockCallStatusDevice("mock-codec-1", "Mock Call Device"); + + // Create the new CallStatusMessenger + var messenger = new CallStatusMessenger( + "test-messenger-1", + mockDevice, + "/device/mock-codec-1" + ); + + Console.WriteLine("āœ“ Successfully created CallStatusMessenger with IHasDialer device"); + + // Test basic call functionality + Console.WriteLine("\nTesting call functionality:"); + + Console.WriteLine("- Initial call status: " + (mockDevice.IsInCall ? "In Call" : "Not In Call")); + + // Test dialing a number + mockDevice.Dial("1234567890"); + Console.WriteLine("- After dialing: " + (mockDevice.IsInCall ? "In Call" : "Not In Call")); + + // Test ending all calls + mockDevice.EndAllCalls(); + Console.WriteLine("- After ending all calls: " + (mockDevice.IsInCall ? "In Call" : "Not In Call")); + + // Test content sharing if supported + if (mockDevice is IHasContentSharing sharingDevice) + { + Console.WriteLine("\nTesting content sharing:"); + Console.WriteLine("- Initial sharing status: " + sharingDevice.SharingContentIsOnFeedback.BoolValue); + + sharingDevice.StartSharing(); + Console.WriteLine("- After start sharing: " + sharingDevice.SharingContentIsOnFeedback.BoolValue); + + sharingDevice.StopSharing(); + Console.WriteLine("- After stop sharing: " + sharingDevice.SharingContentIsOnFeedback.BoolValue); + } + + Console.WriteLine("\nāœ“ All tests passed! CallStatusMessenger works with interface-based devices."); + + } + catch (Exception ex) + { + Console.WriteLine("āœ— Test failed: " + ex.Message); + Console.WriteLine("Stack trace: " + ex.StackTrace); + } + + Console.WriteLine("\nPress any key to exit..."); + Console.ReadKey(); + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index f5854516..a18d4e40 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -27,6 +27,7 @@ using PepperDash.Essentials.Core.Shades; using PepperDash.Essentials.Core.Web; using PepperDash.Essentials.Devices.Common.AudioCodec; using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Codec; using PepperDash.Essentials.Devices.Common.Displays; using PepperDash.Essentials.Devices.Common.Lighting; using PepperDash.Essentials.Devices.Common.SoftCodec; @@ -560,6 +561,21 @@ namespace PepperDash.Essentials messengerAdded = true; } + else if (device is IHasDialer dialer && !messengerAdded) + { + this.LogVerbose( + "Adding CallStatusMessenger for {deviceKey}", device.Key); + + var messenger = new CallStatusMessenger( + $"{device.Key}-callStatus-{Key}", + dialer, + $"/device/{device.Key}" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } if (device is AudioCodecBase audioCodec) {