From db80b7b501f085b45503924165b8f64b9304560a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:58:29 +0000 Subject: [PATCH 1/3] Initial plan From 9946e9a9ae921426d5cc8114e43d9e65cc7dff77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:14:29 +0000 Subject: [PATCH 2/3] Implement CallStatusMessenger for interface-based devices and update MobileControlSystemController Co-authored-by: ngenovese11 <23391587+ngenovese11@users.noreply.github.com> --- .../Messengers/CallStatusMessenger.cs | 221 ++++++++++++++++++ .../Tests/MockCallStatusDevice.cs | 123 ++++++++++ .../Tests/Program.cs | 73 ++++++ .../MobileControlSystemController.cs | 16 ++ 4 files changed, 433 insertions(+) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CallStatusMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Tests/MockCallStatusDevice.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Tests/Program.cs 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 000000000..cd7edeb3f --- /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 000000000..a510e5bdd --- /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 000000000..6f61cd230 --- /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 f58545168..a18d4e40b 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -27,6 +27,7 @@ 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 @@ device as DisplayBase 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) { From 94909d2c7c8913f6a2c9cca2ab83ff5def4b83a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:16:35 +0000 Subject: [PATCH 3/3] Finalize CallStatusMessenger implementation - remove test files and verify clean implementation Co-authored-by: ngenovese11 <23391587+ngenovese11@users.noreply.github.com> --- .../Tests/MockCallStatusDevice.cs | 123 ------------------ .../Tests/Program.cs | 73 ----------- 2 files changed, 196 deletions(-) delete mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Tests/MockCallStatusDevice.cs delete mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Tests/Program.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Tests/MockCallStatusDevice.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Tests/MockCallStatusDevice.cs deleted file mode 100644 index a510e5bdd..000000000 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Tests/MockCallStatusDevice.cs +++ /dev/null @@ -1,123 +0,0 @@ -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 deleted file mode 100644 index 6f61cd230..000000000 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Tests/Program.cs +++ /dev/null @@ -1,73 +0,0 @@ -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