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] 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 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)
{