Compare commits

..

30 Commits

Author SHA1 Message Date
Neil Dorin
8999097100 Adds additional debug to help with room on/off events 2021-01-06 16:12:12 -07:00
Andrew Welker
3b0a5285ab fix order for comm monitor 2020-12-21 17:00:37 -07:00
Andrew Welker
ae03b8cd7e fix PresetsList saving to file 2020-12-21 16:19:45 -07:00
Andrew Welker
cc159e306e update Essentials to use PepperDash Core 1.0.43 2020-12-21 14:40:19 -07:00
Andrew Welker
0a43f43f66 add ICommunicationMonitor to EiscApiAdvanced 2020-12-21 12:31:00 -07:00
Andrew Welker
0f924360c1 fix issues in LinkToApi 2020-12-21 10:51:12 -07:00
Neil Dorin
870f2f8fa6 properly defines the IsWarming/Cooling FeedbackFuncs and fires feedbacks 2020-12-18 14:34:15 -07:00
Andrew Welker
522c107ce6 Merge pull request #532 from PepperDash/feature/room-updates
Feature/room updates
2020-12-17 16:31:37 -07:00
Andrew Welker
cb29775004 Merge branch 'development' into feature/room-updates 2020-12-17 16:13:45 -07:00
Neil Dorin
fccbb55344 Merge pull request #528 from PepperDash/feature/update-plugin-dependency-check
Update Plugin Dependency Check
2020-12-17 16:13:30 -07:00
Andrew Welker
b2402402d9 remove dummy device add 2020-12-17 14:48:01 -07:00
Neil Dorin
f0a3b27e3b Adds dummy source and room power on implementation 2020-12-17 14:28:27 -07:00
Neil Dorin
57ebd2b608 Adds IRunDirectAction to EssentialsTechRoom 2020-12-17 14:07:48 -07:00
Andrew Welker
695ff5487f actually fire the PresetsSaved event 2020-12-17 10:38:31 -07:00
Andrew Welker
66cd39c013 changed event names and added saved event 2020-12-16 16:21:10 -07:00
Andrew Welker
91eec8c258 fix scheduled event saving 2020-12-15 16:43:09 -07:00
Andrew Welker
d2c308c009 Add methods & logic to make sure...
...day & time is in the list of recurrence days
2020-12-15 09:47:35 -07:00
Andrew Welker
a4a99f4a9b remove JsonConverter Attribute 2020-12-15 09:47:08 -07:00
Andrew Welker
eb114b4a95 make Days enum serialize to string 2020-12-15 08:44:10 -07:00
Andrew Welker
93e8d50e55 Merge branch 'development' into feature/update-plugin-dependency-check 2020-12-11 15:51:10 -07:00
Andrew Welker
3c6fc978d4 Merge pull request #530 from PepperDash/hotfix/displayBase-fixes
Hotfix/display base fixes
2020-12-11 15:50:55 -07:00
Andrew Welker
01ddf1721c add method to get scheduled events 2020-12-11 15:42:38 -07:00
Andrew Welker
1ee87c0499 add some debug statements and fix presets file loading 2020-12-09 16:37:14 -07:00
Andrew Welker
f8ae6264f7 add some debug statements and fix presets file loading 2020-12-09 16:18:21 -07:00
Andrew Welker
945db8a233 Merge branch 'development' into hotfix/displayBase-fixes 2020-12-09 13:02:59 -07:00
Andrew Welker
6e4fa48b9d rearrange message formatting 2020-12-08 16:34:00 -07:00
Andrew Welker
93a5f2e3b2 fix slases 2020-12-04 12:18:08 -07:00
Andrew Welker
0e4edca08a update plugin dependency check message 2020-12-04 11:05:46 -07:00
Andrew Welker
f9925f9ec9 Add PowerIsOnFeedback back to DisplayBase and marked it as Obsolete 2020-12-03 16:46:10 -07:00
Andrew Welker
25c4d94366 Merge pull request #517 from PepperDash/hotfix/zoom-auto-layout
Fix some issues with Zoom Rooms
2020-11-30 14:05:35 -07:00
14 changed files with 1583 additions and 1382 deletions

View File

@@ -5,6 +5,9 @@ namespace PepperDash.Essentials.Room.Config
{
public class EssentialsTechRoomConfig
{
[JsonProperty("dummySourceKey")]
public string DummySourceKey { get; set; }
[JsonProperty("displays")]
public List<string> Displays;
@@ -17,7 +20,7 @@ namespace PepperDash.Essentials.Room.Config
[JsonProperty("techPin")]
public string TechPin;
[JsonProperty("presetFileName")]
[JsonProperty("presetsFileName")]
public string PresetsFileName;
[JsonProperty("scheduledEvents")]

View File

@@ -17,7 +17,7 @@ using PepperDash.Essentials.Room.Config;
namespace PepperDash.Essentials
{
public class EssentialsTechRoom : EssentialsRoomBase, ITvPresetsProvider, IBridgeAdvanced
public class EssentialsTechRoom : EssentialsRoomBase, ITvPresetsProvider, IBridgeAdvanced, IRunDirectRouteAction
{
private readonly EssentialsTechRoomConfig _config;
private readonly Dictionary<string, TwoWayDisplayBase> _displays;
@@ -28,15 +28,42 @@ namespace PepperDash.Essentials
private Dictionary<string, string> _currentPresets;
private ScheduledEventGroup _roomScheduledEventGroup;
/// <summary>
///
/// </summary>
protected override Func<bool> IsWarmingFeedbackFunc
{
get
{
return () =>
{
return _displays.All(kv => kv.Value.IsWarmingUpFeedback.BoolValue);
};
}
}
/// <summary>
///
/// </summary>
protected override Func<bool> IsCoolingFeedbackFunc
{
get
{
return () =>
{
return _displays.All(kv => kv.Value.IsCoolingDownFeedback.BoolValue);
};
}
}
public EssentialsTechRoom(DeviceConfig config) : base(config)
{
_config = config.Properties.ToObject<EssentialsTechRoomConfig>();
_tunerPresets = new DevicePresetsModel(String.Format("{0}-presets", config.Key), _config.PresetsFileName);
_tunerPresets.LoadChannels();
_tunerPresets.SetFileName(_config.PresetsFileName);
_tunerPresets.PresetChanged += TunerPresetsOnPresetChanged;
_tunerPresets.PresetRecalled += TunerPresetsOnPresetRecalled;
_tuners = GetDevices<IRSetTopBoxBase>(_config.Tuners);
_displays = GetDevices<TwoWayDisplayBase>(_config.Displays);
@@ -78,7 +105,7 @@ namespace PepperDash.Essentials
#endregion
private void TunerPresetsOnPresetChanged(ISetTopBoxNumericKeypad device, string channel)
private void TunerPresetsOnPresetRecalled(ISetTopBoxNumericKeypad device, string channel)
{
if (!_currentPresets.ContainsKey(device.Key))
{
@@ -111,7 +138,12 @@ namespace PepperDash.Essentials
foreach (var display in _displays)
{
display.Value.PowerIsOnFeedback.OutputChange +=
(sender, args) => RoomPowerIsOnFeedback.InvokeFireUpdate();
(sender, args) =>
{
RoomPowerIsOnFeedback.InvokeFireUpdate();
IsWarmingUpFeedback.InvokeFireUpdate();
IsCoolingDownFeedback.InvokeFireUpdate();
};
}
}
@@ -170,12 +202,16 @@ namespace PepperDash.Essentials
{
//update config based on key of scheduleEvent
GetOrCreateScheduleGroup();
var existingEvent = _config.ScheduledEvents.FirstOrDefault(e => e.Key == scheduledEvent.Key);
var existingEventIndex = _config.ScheduledEvents.FindIndex((e) => e.Key == scheduledEvent.Key);
if (existingEvent == null)
if (existingEventIndex < 0)
{
_config.ScheduledEvents.Add(scheduledEvent);
}
else
{
_config.ScheduledEvents[existingEventIndex] = scheduledEvent;
}
//create or update event based on config
CreateOrUpdateSingleEvent(scheduledEvent);
@@ -187,6 +223,11 @@ namespace PepperDash.Essentials
OnScheduledEventUpdate();
}
public List<ScheduledEventConfig> GetScheduledEvents()
{
return _config.ScheduledEvents ?? new List<ScheduledEventConfig>();
}
private void OnScheduledEventUpdate()
{
var handler = ScheduledEventsChanged;
@@ -220,8 +261,16 @@ namespace PepperDash.Essentials
CrestronInvoke.BeginInvoke((o) =>
{
Debug.Console(2, this, "There are {0} actions to execute for this event.", eventConfig.Actions.Count);
foreach (var a in eventConfig.Actions)
{
Debug.Console(2, this,
@"Attempting to run action:
DeviceKey: {0}
MethodName: {1}
Params: {2}"
, a.DeviceKey, a.MethodName, a.Params);
DeviceJsonApi.DoDeviceAction(a);
}
});
@@ -230,14 +279,26 @@ namespace PepperDash.Essentials
public void RoomPowerOn()
{
Debug.Console(2, this, "Room Powering On");
var dummySource = DeviceManager.GetDeviceForKey(_config.DummySourceKey) as IRoutingOutputs;
if (dummySource == null)
{
Debug.Console(1, this, "Unable to get source with key: {0}", _config.DummySourceKey);
return;
}
foreach (var display in _displays)
{
display.Value.PowerOn();
RunDirectRoute(dummySource, display.Value);
}
}
public void RoomPowerOff()
{
Debug.Console(2, this, "Room Powering Off");
foreach (var display in _displays)
{
display.Value.PowerOff();
@@ -264,16 +325,6 @@ namespace PepperDash.Essentials
#region Overrides of EssentialsRoomBase
protected override Func<bool> IsWarmingFeedbackFunc
{
get { return () => false; }
}
protected override Func<bool> IsCoolingFeedbackFunc
{
get { return () => false; }
}
protected override Func<bool> OnFeedbackFunc
{
get { return () => RoomPowerIsOn; }
@@ -341,6 +392,8 @@ namespace PepperDash.Essentials
feedback.Value.FireUpdate();
}
};
return;
}
i = 0;
@@ -349,6 +402,8 @@ namespace PepperDash.Essentials
var tuner = setTopBox;
trilist.SetStringSigAction(joinMap.CurrentPreset.JoinNumber + i, s => _tunerPresets.Dial(s, tuner.Value));
i++;
}
}
@@ -364,6 +419,51 @@ namespace PepperDash.Essentials
{
}
}
#region IRunDirectRouteAction Members
private void RunDirectRoute(IRoutingOutputs source, IRoutingSink dest)
{
if (dest == null)
{
Debug.Console(1, this, "Cannot route, unknown destination '{0}'", dest.Key);
return;
}
if (source == null)
{
dest.ReleaseRoute();
if (dest is IHasPowerControl)
(dest as IHasPowerControl).PowerOff();
}
else
{
dest.ReleaseAndMakeRoute(source, eRoutingSignalType.Video);
}
}
/// <summary>
/// Attempts to route directly between a source and destination
/// </summary>
/// <param name="sourceKey"></param>
/// <param name="destinationKey"></param>
public void RunDirectRoute(string sourceKey, string destinationKey)
{
IRoutingSink dest = null;
dest = DeviceManager.GetDeviceForKey(destinationKey) as IRoutingSink;
var source = DeviceManager.GetDeviceForKey(sourceKey) as IRoutingOutputs;
if (source == null || dest == null)
{
Debug.Console(1, this, "Cannot route unknown source or destination '{0}' to {1}", sourceKey, destinationKey);
return;
}
RunDirectRoute(source, dest);
}
#endregion
}
public class ScheduledEventEventArgs : EventArgs

View File

@@ -78,7 +78,7 @@ namespace PepperDash.Essentials.Core.Bridges
/// <summary>
/// Bridge API using EISC
/// </summary>
public class EiscApiAdvanced : BridgeApi
public class EiscApiAdvanced : BridgeApi, ICommunicationMonitor
{
public EiscApiPropertiesConfig PropertiesConfig { get; private set; }
@@ -98,8 +98,23 @@ namespace PepperDash.Essentials.Core.Bridges
Eisc.SigChange += Eisc_SigChange;
CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, Eisc, 120000, 300000);
AddPostActivationAction(LinkDevices);
AddPostActivationAction(LinkRooms);
AddPostActivationAction(RegisterEisc);
}
public override bool CustomActivate()
{
CommunicationMonitor.Start();
return base.CustomActivate();
}
public override bool Deactivate()
{
CommunicationMonitor.Stop();
return base.Deactivate();
}
private void LinkDevices()
@@ -137,8 +152,6 @@ namespace PepperDash.Essentials.Core.Bridges
bridge.LinkToApi(Eisc, d.JoinStart, d.JoinMapKey, this);
}
}
RegisterEisc();
}
private void RegisterEisc()
@@ -182,8 +195,6 @@ namespace PepperDash.Essentials.Core.Bridges
rm.LinkToApi(Eisc, room.JoinStart, room.JoinMapKey, this);
}
RegisterEisc();
}
/// <summary>
@@ -324,6 +335,12 @@ namespace PepperDash.Essentials.Core.Bridges
Debug.Console(2, this, "Error in Eisc_SigChange handler: {0}", e);
}
}
#region Implementation of ICommunicationMonitor
public StatusMonitorBase CommunicationMonitor { get; private set; }
#endregion
}
public class EiscApiPropertiesConfig

View File

@@ -20,7 +20,8 @@ namespace PepperDash.Essentials.Core
public IrOutputPortController IrPort { get; private set; }
public ushort IrPulseTime { get; set; }
public BoolFeedback PowerIsOnFeedback { get; private set; }
[Obsolete("This property will be removed in version 2.0.0")]
public override BoolFeedback PowerIsOnFeedback { get; protected set; }
protected Func<bool> PowerIsOnFeedbackFunc
{

View File

@@ -18,7 +18,7 @@ namespace PepperDash.Essentials.Core
/// <summary>
///
/// </summary>
public abstract class DisplayBase : EssentialsDevice, IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking
public abstract class DisplayBase : EssentialsDevice, IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking, IPower
{
public event SourceInfoChangeHandler CurrentSourceChange;
@@ -49,6 +49,9 @@ namespace PepperDash.Essentials.Core
public BoolFeedback IsCoolingDownFeedback { get; protected set; }
public BoolFeedback IsWarmingUpFeedback { get; private set; }
[Obsolete("This property will be removed in version 2.0.0")]
public abstract BoolFeedback PowerIsOnFeedback { get; protected set; }
public UsageTracking UsageTracker { get; set; }
public uint WarmupTime { get; set; }
@@ -81,8 +84,6 @@ namespace PepperDash.Essentials.Core
}
public abstract void PowerOn();
public abstract void PowerOff();
public abstract void PowerToggle();
@@ -99,7 +100,7 @@ namespace PepperDash.Essentials.Core
}
}
public abstract void ExecuteSwitch(object selector);
public abstract void ExecuteSwitch(object selector);
protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, uint joinStart, string joinMapKey,
EiscApiAdvanced bridge)
@@ -261,7 +262,8 @@ namespace PepperDash.Essentials.Core
abstract protected Func<string> CurrentInputFeedbackFunc { get; }
public BoolFeedback PowerIsOnFeedback { get; protected set; }
public override BoolFeedback PowerIsOnFeedback { get; protected set; }
abstract protected Func<bool> PowerIsOnFeedbackFunc { get; }
@@ -315,7 +317,5 @@ namespace PepperDash.Essentials.Core
var newEvent = NumericSwitchChange;
if (newEvent != null) newEvent(this, e);
}
}
}

View File

@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharp.Reflection;
using Crestron.SimplSharp.Scheduler;
using PepperDash.Core;
using PepperDash.Essentials.Core.Fusion;
using PepperDash.Essentials.Room.Config;
using Activator = System.Activator;
namespace PepperDash.Essentials.Core
{
@@ -170,7 +174,24 @@ namespace PepperDash.Essentials.Core
var eventTime = DateTime.Parse(config.Time);
if (DateTime.Now > eventTime) eventTime = eventTime.AddDays(1);
if (DateTime.Now > eventTime)
{
eventTime = eventTime.AddDays(1);
}
Debug.Console(2, "[Scheduler] Current Date day of week: {0} recurrence days: {1}", eventTime.DayOfWeek,
config.Days);
var dayOfWeekConverted = ConvertDayOfWeek(eventTime);
Debug.Console(1, "[Scheduler] eventTime Day: {0}", dayOfWeekConverted);
while (!dayOfWeekConverted.IsFlagSet(config.Days))
{
eventTime = eventTime.AddDays(1);
dayOfWeekConverted = ConvertDayOfWeek(eventTime);
}
scheduledEvent.DateAndTime.SetAbsoluteEventTime(eventTime);
@@ -185,5 +206,28 @@ namespace PepperDash.Essentials.Core
scheduledEvent.Disable();
}
}
private static ScheduledEventCommon.eWeekDays ConvertDayOfWeek(DateTime eventTime)
{
return (ScheduledEventCommon.eWeekDays) Enum.Parse(typeof(ScheduledEventCommon.eWeekDays), eventTime.DayOfWeek.ToString(), true);
}
private static bool IsFlagSet<T>(this T value, T flag) where T : struct
{
CheckIsEnum<T>(true);
var lValue = Convert.ToInt64(value);
var lFlag = Convert.ToInt64(flag);
return (lValue & lFlag) != 0;
}
private static void CheckIsEnum<T>(bool withFlags)
{
if (!typeof(T).IsEnum)
throw new ArgumentException(string.Format("Type '{0}' is not an enum", typeof(T).FullName));
if (withFlags && !Attribute.IsDefined(typeof(T), typeof(FlagsAttribute)))
throw new ArgumentException(string.Format("Type '{0}' doesn't have the 'Flags' attribute", typeof(T).FullName));
}
}
}

View File

@@ -14,7 +14,7 @@
<tags>crestron 3series 4series</tags>
<repository type="git" url="https://github.com/PepperDash/Essentials"/>
<dependencies>
<dependency id="PepperDashCore" version="[1.0.43, 1.1.0)"/>
<dependency id="PepperDashCore" version="[1.0.44, 1.1.0)"/>
</dependencies>
</metadata>
<files>

View File

@@ -402,13 +402,16 @@ namespace PepperDash.Essentials
/// Loads a
/// </summary>
/// <param name="plugin"></param>
/// <param name="loadedAssembly"></param>
static void LoadCustomPlugin(IPluginDeviceFactory plugin, LoadedAssembly loadedAssembly)
{
var passed = Global.IsRunningMinimumVersionOrHigher(plugin.MinimumEssentialsFrameworkVersion);
if (!passed)
{
Debug.Console(0, Debug.ErrorLogLevel.Error, "Plugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin", plugin.MinimumEssentialsFrameworkVersion);
Debug.Console(0, Debug.ErrorLogLevel.Error,
"\r\n********************\r\n\tPlugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin {1}\r\n********************",
plugin.MinimumEssentialsFrameworkVersion, loadedAssembly.Name);
return;
}
else

View File

@@ -16,12 +16,14 @@ namespace PepperDash.Essentials.Core.Presets
/// </summary>
public class DevicePresetsModel : Device
{
public delegate void PresetChangedCallback(ISetTopBoxNumericKeypad device, string channel);
public delegate void PresetRecalledCallback(ISetTopBoxNumericKeypad device, string channel);
public delegate void PresetsSavedCallback(List<PresetChannel> presets);
private readonly CCriticalSection _fileOps = new CCriticalSection();
private readonly bool _initSuccess;
private ISetTopBoxNumericKeypad _setTopBox;
private readonly ISetTopBoxNumericKeypad _setTopBox;
/// <summary>
/// The methods on the STB device to call when dialing
@@ -83,7 +85,8 @@ namespace PepperDash.Essentials.Core.Presets
_initSuccess = true;
}
public event PresetChangedCallback PresetChanged;
public event PresetRecalledCallback PresetRecalled;
public event PresetsSavedCallback PresetsSaved;
public int PulseTime { get; set; }
public int DigitSpacingMs { get; set; }
@@ -106,6 +109,8 @@ namespace PepperDash.Essentials.Core.Presets
public void SetFileName(string path)
{
_filePath = ListPathPrefix + path;
Debug.Console(2, this, "Setting presets file path to {0}", _filePath);
LoadChannels();
}
@@ -114,6 +119,8 @@ namespace PepperDash.Essentials.Core.Presets
try
{
_fileOps.Enter();
Debug.Console(2, this, "Loading presets from {0}", _filePath);
PresetsAreLoaded = false;
try
{
@@ -184,7 +191,7 @@ namespace PepperDash.Essentials.Core.Presets
if (_setTopBox == null) return;
OnPresetChanged(_setTopBox, chanNum);
OnPresetRecalled(_setTopBox, chanNum);
}
public void Dial(int presetNum, ISetTopBoxNumericKeypad setTopBox)
@@ -214,14 +221,14 @@ namespace PepperDash.Essentials.Core.Presets
_enterFunction = setTopBox.KeypadEnter;
OnPresetChanged(setTopBox, chanNum);
OnPresetRecalled(setTopBox, chanNum);
Dial(chanNum);
}
private void OnPresetChanged(ISetTopBoxNumericKeypad setTopBox, string channel)
private void OnPresetRecalled(ISetTopBoxNumericKeypad setTopBox, string channel)
{
var handler = PresetChanged;
var handler = PresetRecalled;
if (handler == null)
{
@@ -241,6 +248,8 @@ namespace PepperDash.Essentials.Core.Presets
PresetsList[index] = preset;
SavePresets();
OnPresetsSaved();
}
public void UpdatePresets(List<PresetChannel> presets)
@@ -248,6 +257,8 @@ namespace PepperDash.Essentials.Core.Presets
PresetsList = presets;
SavePresets();
OnPresetsSaved();
}
private void SavePresets()
@@ -255,7 +266,8 @@ namespace PepperDash.Essentials.Core.Presets
try
{
_fileOps.Enter();
var json = JsonConvert.SerializeObject(PresetsList);
var pl = new PresetsList {Channels = PresetsList, Name = Name};
var json = JsonConvert.SerializeObject(pl, Formatting.Indented);
using (var file = File.Open(_filePath, FileMode.Truncate))
{
@@ -269,6 +281,15 @@ namespace PepperDash.Essentials.Core.Presets
}
private void OnPresetsSaved()
{
var handler = PresetsSaved;
if (handler == null) return;
handler(PresetsList);
}
private void Pulse(Action<bool> act)
{
act(true);

View File

@@ -10,19 +10,22 @@ namespace PepperDash.Essentials.Core.Presets
public class PresetChannel
{
[JsonProperty(Required = Required.Always)]
[JsonProperty(Required = Required.Always,PropertyName = "name")]
public string Name { get; set; }
[JsonProperty(Required = Required.Always)]
[JsonProperty(Required = Required.Always, PropertyName = "iconUrl")]
public string IconUrl { get; set; }
[JsonProperty(Required = Required.Always)]
[JsonProperty(Required = Required.Always, PropertyName = "channel")]
public string Channel { get; set; }
}
public class PresetsList
{
[JsonProperty(Required=Required.Always)]
[JsonProperty(Required=Required.Always,PropertyName = "name")]
public string Name { get; set; }
[JsonProperty(Required = Required.Always)]
[JsonProperty(Required = Required.Always, PropertyName = "channels")]
public List<PresetChannel> Channels { get; set; }
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Crestron.SimplSharp.Scheduler;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using PepperDash.Essentials.Core;
namespace PepperDash.Essentials.Room.Config

View File

@@ -41,6 +41,14 @@ namespace PepperDash.Essentials.Core
}
/// <summary>
/// Simplified routing direct from source to destination
/// </summary>
public interface IRunDirectRouteAction
{
void RunDirectRoute(string sourceKey, string destinationKey);
}
/// <summary>
/// For rooms that default presentation only routing
/// </summary>

View File

@@ -1,3 +1,3 @@
<packages>
<package id="PepperDashCore" version="1.0.43" targetFramework="net35" allowedVersions="[1.0,1.1)"/>
<package id="PepperDashCore" version="1.0.44" targetFramework="net35" allowedVersions="[1.0,1.1)"/>
</packages>