Implement CallStatusMessenger for interface-based devices and update MobileControlSystemController

Co-authored-by: ngenovese11 <23391587+ngenovese11@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-08-13 21:14:29 +00:00
parent db80b7b501
commit 9946e9a9ae
4 changed files with 433 additions and 0 deletions

View File

@@ -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
{
/// <summary>
/// Provides a messaging bridge for devices that implement call status interfaces
/// without requiring VideoCodecBase inheritance
/// </summary>
public class CallStatusMessenger : MessengerBase
{
/// <summary>
/// Device with dialer capabilities
/// </summary>
protected IHasDialer Dialer { get; private set; }
/// <summary>
/// Device with content sharing capabilities (optional)
/// </summary>
protected IHasContentSharing ContentSharing { get; private set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="dialer"></param>
/// <param name="messagePath"></param>
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;
}
}
/// <summary>
/// Handles call status changes
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Dialer_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e)
{
try
{
SendFullStatus();
}
catch (Exception ex)
{
this.LogError(ex, "Error handling call status change: {error}", ex.Message);
}
}
/// <summary>
/// Handles content sharing status changes
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SharingContentIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e)
{
PostStatusMessage(JToken.FromObject(new
{
sharingContentIsOn = e.BoolValue
}));
}
/// <summary>
/// Handles sharing source changes
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SharingSourceFeedback_OutputChange(object sender, FeedbackEventArgs e)
{
PostStatusMessage(JToken.FromObject(new
{
sharingSource = e.StringValue
}));
}
/// <summary>
/// Gets active calls from the dialer
/// </summary>
/// <returns></returns>
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<CodecActiveCallItem>))
{
var activeCalls = activeCallsProperty.GetValue(Dialer) as System.Collections.Generic.List<CodecActiveCallItem>;
return activeCalls ?? new System.Collections.Generic.List<CodecActiveCallItem>();
}
// Return basic call status if no ActiveCalls property
return new { isInCall = Dialer.IsInCall };
}
/// <summary>
/// Sends full status message
/// </summary>
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));
}
}
/// <summary>
/// Registers actions for call control
/// </summary>
protected override void RegisterActions()
{
base.RegisterActions();
AddAction("/fullStatus", (id, content) => SendFullStatus());
// Basic call control actions
AddAction("/dial", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
Dialer.Dial(msg.Value);
});
AddAction("/endAllCalls", (id, content) => Dialer.EndAllCalls());
AddAction("/dtmf", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
Dialer.SendDtmf(msg.Value);
});
// Call-specific actions (if active calls are available)
AddAction("/endCallById", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
var call = GetCallWithId(msg.Value);
if (call != null)
Dialer.EndCall(call);
});
AddAction("/acceptById", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
var call = GetCallWithId(msg.Value);
if (call != null)
Dialer.AcceptCall(call);
});
AddAction("/rejectById", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
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());
}
}
/// <summary>
/// Finds a call by ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
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<CodecActiveCallItem>))
{
var activeCalls = activeCallsProperty.GetValue(Dialer) as System.Collections.Generic.List<CodecActiveCallItem>;
if (activeCalls != null)
{
return activeCalls.FirstOrDefault(c => c.Id.Equals(id));
}
}
return null;
}
}
}

View File

@@ -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
{
/// <summary>
/// Mock device for testing CallStatusMessenger that implements IHasDialer without VideoCodecBase
/// </summary>
public class MockCallStatusDevice : EssentialsDevice, IHasDialer, IHasContentSharing
{
public event EventHandler<CodecCallStatusItemChangeEventArgs> CallStatusChange;
private List<CodecActiveCallItem> _activeCalls = new List<CodecActiveCallItem>();
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<CodecActiveCallItem> 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));
}
}
}

View File

@@ -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
{
/// <summary>
/// Simple test program to verify CallStatusMessenger functionality
/// </summary>
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();
}
}
}

View File

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