From 61f3c2ad37b222442f9f9d81cd9f7694ec2089f5 Mon Sep 17 00:00:00 2001 From: Lars Op den Kamp Date: Sun, 13 Nov 2011 20:47:14 +0100 Subject: [PATCH] cec: added a C++ CLR wrapper for libCEC, so libCEC can be used by any .NET language and ported the basics of cec-client over to C#. --- .gitignore | 18 +- README | 6 + project/CecSharpClient.sln | 20 + project/LibCecSharp.vcxproj | 101 +++++ project/LibCecSharp.vcxproj.filters | 42 ++ project/libCEC.nsi | 2 + project/libcec.sln | 39 ++ src/CecSharpTester/AssemblyInfo.cs | 36 ++ src/CecSharpTester/CecSharpClient.cs | 381 ++++++++++++++++ src/CecSharpTester/CecSharpClient.csproj | 61 +++ src/LibCecSharp/AssemblyInfo.cpp | 40 ++ src/LibCecSharp/LibCecSharp.cpp | 528 +++++++++++++++++++++++ src/LibCecSharp/LibCecSharp.suo | Bin 0 -> 11264 bytes src/LibCecSharp/Stdafx.cpp | 5 + src/LibCecSharp/Stdafx.h | 7 + src/LibCecSharp/resource.h | 3 + 16 files changed, 1286 insertions(+), 3 deletions(-) create mode 100644 project/CecSharpClient.sln create mode 100644 project/LibCecSharp.vcxproj create mode 100644 project/LibCecSharp.vcxproj.filters create mode 100644 src/CecSharpTester/AssemblyInfo.cs create mode 100644 src/CecSharpTester/CecSharpClient.cs create mode 100644 src/CecSharpTester/CecSharpClient.csproj create mode 100644 src/LibCecSharp/AssemblyInfo.cpp create mode 100644 src/LibCecSharp/LibCecSharp.cpp create mode 100644 src/LibCecSharp/LibCecSharp.suo create mode 100644 src/LibCecSharp/Stdafx.cpp create mode 100644 src/LibCecSharp/Stdafx.h create mode 100644 src/LibCecSharp/resource.h diff --git a/.gitignore b/.gitignore index e196aa9..2ad44d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .project .cproject +*.manifest aclocal.m4 autom4te.cache @@ -25,18 +26,28 @@ libcec.pdb cec-client.exe cec-client.ilk cec-client.pdb +CecSharpClient.exe +CecSharpClient.pdb +CecSharpClient.vshost.exe +CecSharpClient.vshost.exe.manifest + +LibCecSharp.dll +LibCecSharp.ilk +LibCecSharp.pdb build include/boost -project/boost-1_46_1-xbmc-win32 +project/bin project/Debug/ +project/Release/ project/ipch/ project/libcec.sdf project/libcec.suo -project/libcec.vcxproj.user -project/testclient.vcxproj.user +project/obj +project/Properties +project/*.user src/lib/.deps src/lib/.libs @@ -59,6 +70,7 @@ src/testclient/.libs src/testclient/cec-client src/testclient/*.o +src/CecSharpTester/obj /dpinst-x86.exe /dpinst-amd64.exe diff --git a/README b/README index 6c23712..5ef90e3 100644 --- a/README +++ b/README @@ -22,6 +22,12 @@ Test the device: For developers: * see /include/cec.h for the C++ API and /include/cecc.h for the C version. +* see src/testclient/main.cpp for an example + +For .NET developers: +* build project/libcec.sln first +* add a reference to LibCecSharp.dll +* see src\CecSharpTester\CecSharpClient.cs for an example If you wish to contribute to this project, you must first sign our contributors agreement Please see http://www.pulse-eight.net/contributors for more information \ No newline at end of file diff --git a/project/CecSharpClient.sln b/project/CecSharpClient.sln new file mode 100644 index 0000000..0a4d822 --- /dev/null +++ b/project/CecSharpClient.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CecSharpClient", "..\src\CecSharpTester\CecSharpClient.csproj", "{47EF8EFE-5758-4E82-B9BA-F9B002F9C0D4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {47EF8EFE-5758-4E82-B9BA-F9B002F9C0D4}.Debug|x86.ActiveCfg = Debug|x86 + {47EF8EFE-5758-4E82-B9BA-F9B002F9C0D4}.Debug|x86.Build.0 = Debug|x86 + {47EF8EFE-5758-4E82-B9BA-F9B002F9C0D4}.Release|x86.ActiveCfg = Release|x86 + {47EF8EFE-5758-4E82-B9BA-F9B002F9C0D4}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/project/LibCecSharp.vcxproj b/project/LibCecSharp.vcxproj new file mode 100644 index 0000000..2ced898 --- /dev/null +++ b/project/LibCecSharp.vcxproj @@ -0,0 +1,101 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7} + v4.0 + ManagedCProj + LibCecSharp + + + + DynamicLibrary + true + true + Unicode + + + DynamicLibrary + false + true + Unicode + + + + + + + + + + + + + true + $(SolutionDir)\..\include;$(IncludePath) + $(ProjectName) + $(SolutionDir)..\ + + + false + $(SolutionDir)\..\include;$(IncludePath) + + + + Level3 + Disabled + WIN32;_DEBUG;%(PreprocessorDefinitions) + NotUsing + + + true + + + + + + + Level3 + WIN32;NDEBUG;%(PreprocessorDefinitions) + NotUsing + + + true + + + + + + + + + + + + + + + + + + + + + + + {c04b0fb1-667d-4f1c-bdae-a07cdffaaaa0} + + + + + + \ No newline at end of file diff --git a/project/LibCecSharp.vcxproj.filters b/project/LibCecSharp.vcxproj.filters new file mode 100644 index 0000000..cdfa851 --- /dev/null +++ b/project/LibCecSharp.vcxproj.filters @@ -0,0 +1,42 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/project/libCEC.nsi b/project/libCEC.nsi index eaf1093..ee6df6e 100644 --- a/project/libCEC.nsi +++ b/project/libCEC.nsi @@ -52,6 +52,7 @@ Section "libCEC" SecLibCEC File "..\COPYING" File "..\libcec.dll" File "..\libcec.lib" + File "..\LibCecSharp.dll" File "..\pthreadVC2.dll" File "..\README" @@ -130,6 +131,7 @@ Section "Uninstall" Delete "$INSTDIR\libcec.dll" Delete "$INSTDIR\libcec.lib" Delete "$INSTDIR\libcec.pdb" + Delete "$INSTDIR\LibCecSharp.dll" Delete "$INSTDIR\pthreadVC2.dll" Delete "$INSTDIR\README" Delete "$INSTDIR\driver\OEM001.inf" diff --git a/project/libcec.sln b/project/libcec.sln index 2255a1b..facfece 100644 --- a/project/libcec.sln +++ b/project/libcec.sln @@ -8,20 +8,59 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "testclient", "testclient.vc {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0} = {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0} EndProjectSection EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LibCecSharp", "LibCecSharp.vcxproj", "{1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}" + ProjectSection(ProjectDependencies) = postProject + {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0} = {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms Debug|Win32 = Debug|Win32 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms Release|Win32 = Release|Win32 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Debug|Mixed Platforms.Build.0 = Debug|Win32 {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Debug|Win32.ActiveCfg = Debug|Win32 {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Debug|Win32.Build.0 = Debug|Win32 + {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Debug|x86.ActiveCfg = Debug|Win32 + {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Release|Any CPU.ActiveCfg = Release|Win32 + {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Release|Mixed Platforms.Build.0 = Release|Win32 {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Release|Win32.ActiveCfg = Release|Win32 {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Release|Win32.Build.0 = Release|Win32 + {C04B0FB1-667D-4F1C-BDAE-A07CDFFAAAA0}.Release|x86.ActiveCfg = Release|Win32 + {F01222BF-6B3D-43BD-B254-434031CB9887}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {F01222BF-6B3D-43BD-B254-434031CB9887}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {F01222BF-6B3D-43BD-B254-434031CB9887}.Debug|Mixed Platforms.Build.0 = Debug|Win32 {F01222BF-6B3D-43BD-B254-434031CB9887}.Debug|Win32.ActiveCfg = Debug|Win32 {F01222BF-6B3D-43BD-B254-434031CB9887}.Debug|Win32.Build.0 = Debug|Win32 + {F01222BF-6B3D-43BD-B254-434031CB9887}.Debug|x86.ActiveCfg = Debug|Win32 + {F01222BF-6B3D-43BD-B254-434031CB9887}.Release|Any CPU.ActiveCfg = Release|Win32 + {F01222BF-6B3D-43BD-B254-434031CB9887}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {F01222BF-6B3D-43BD-B254-434031CB9887}.Release|Mixed Platforms.Build.0 = Release|Win32 {F01222BF-6B3D-43BD-B254-434031CB9887}.Release|Win32.ActiveCfg = Release|Win32 {F01222BF-6B3D-43BD-B254-434031CB9887}.Release|Win32.Build.0 = Release|Win32 + {F01222BF-6B3D-43BD-B254-434031CB9887}.Release|x86.ActiveCfg = Release|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Debug|Win32.ActiveCfg = Debug|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Debug|Win32.Build.0 = Debug|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Debug|x86.ActiveCfg = Debug|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Release|Any CPU.ActiveCfg = Release|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Release|Mixed Platforms.Build.0 = Release|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Release|Win32.ActiveCfg = Release|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Release|Win32.Build.0 = Release|Win32 + {1AC27FBD-653A-4F5F-ADBC-2A8FD074EEB7}.Release|x86.ActiveCfg = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/CecSharpTester/AssemblyInfo.cs b/src/CecSharpTester/AssemblyInfo.cs new file mode 100644 index 0000000..058bbb9 --- /dev/null +++ b/src/CecSharpTester/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CecSharpClient")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Pulse-Eight Ltd.")] +[assembly: AssemblyProduct("CecSharpClient")] +[assembly: AssemblyCopyright("Copyright © Pulse-Eight Ltd. 2011")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fb8d5961-9ba9-4e56-8706-ac150183706f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/CecSharpTester/CecSharpClient.cs b/src/CecSharpTester/CecSharpClient.cs new file mode 100644 index 0000000..0cd6aeb --- /dev/null +++ b/src/CecSharpTester/CecSharpClient.cs @@ -0,0 +1,381 @@ +/* + * This file is part of the libCEC(R) library. + * + * libCEC(R) is Copyright (C) 2011 Pulse-Eight Limited. All rights reserved. + * libCEC(R) is an original work, containing original code. + * + * libCEC(R) is a trademark of Pulse-Eight Limited. + * + * This program is dual-licensed; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * + * Alternatively, you can license this library under a commercial license, + * please contact Pulse-Eight Licensing for more information. + * + * For more information contact: + * Pulse-Eight Licensing + * http://www.pulse-eight.com/ + * http://www.pulse-eight.net/ + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CecSharpClient +{ + class CecSharpClient + { + public CecSharpClient() + { + CecDeviceTypeList types = new CecDeviceTypeList(); + types.Types[0] = CecDeviceType.PlaybackDevice; + + Lib = new LibCecSharp("CEC Tester", types); + LogLevel = (int) CecLogLevel.All; + + Console.WriteLine("CEC Parser created - libcec version " + Lib.GetLibVersionMajor() + "." + Lib.GetLibVersionMinor()); + } + + void FlushLog() + { + CecLogMessage message = Lib.GetNextLogMessage(); + bool bGotMessage = !message.Empty; + while (bGotMessage) + { + if (((int)message.Level & LogLevel) == (int)message.Level) + { + string strLevel = ""; + switch (message.Level) + { + case CecLogLevel.Error: + strLevel = "ERROR: "; + break; + case CecLogLevel.Warning: + strLevel = "WARNING: "; + break; + case CecLogLevel.Notice: + strLevel = "NOTICE: "; + break; + case CecLogLevel.Traffic: + strLevel = "TRAFFIC: "; + break; + case CecLogLevel.Debug: + strLevel = "DEBUG: "; + break; + default: + break; + } + string strLog = string.Format("{0} {1,16} {2}", strLevel, message.Time, message.Message); + Console.WriteLine(strLog); + } + + message = Lib.GetNextLogMessage(); + bGotMessage = !message.Empty; + } + } + + public bool Connect(int timeout) + { + CecAdapter[] adapters = Lib.FindAdapters(string.Empty); + if (adapters.Length > 0) + return Connect(adapters[0].ComPort, timeout); + else + { + Console.WriteLine("Did not find any CEC adapters"); + return false; + } + } + + public bool Connect(string port, int timeout) + { + return Lib.Open(port, timeout); + } + + public void Close() + { + Lib.Close(); + } + + public void ListDevices() + { + int iAdapter = 0; + foreach (CecAdapter adapter in Lib.FindAdapters(string.Empty)) + { + Console.WriteLine("Adapter: " + iAdapter++); + Console.WriteLine("Path: " + adapter.Path); + Console.WriteLine("Com port: " + adapter.ComPort); + } + } + + void ShowConsoleHelp() + { + Console.WriteLine( + "================================================================================" + System.Environment.NewLine + + "Available commands:" + System.Environment.NewLine + + System.Environment.NewLine + + "tx {bytes} transfer bytes over the CEC line." + System.Environment.NewLine + + "txn {bytes} transfer bytes but don't wait for transmission ACK." + System.Environment.NewLine + + "[tx 40 00 FF 11 22 33] sends bytes 0x40 0x00 0xFF 0x11 0x22 0x33" + System.Environment.NewLine + + System.Environment.NewLine + + "on {address} power on the device with the given logical address." + System.Environment.NewLine + + "[on 5] power on a connected audio system" + System.Environment.NewLine + + System.Environment.NewLine + + "standby {address} put the device with the given address in standby mode." + System.Environment.NewLine + + "[standby 0] powers off the TV" + System.Environment.NewLine + + System.Environment.NewLine + + "la {logical_address} change the logical address of the CEC adapter." + System.Environment.NewLine + + "[la 4] logical address 4" + System.Environment.NewLine + + System.Environment.NewLine + + "pa {physical_address} change the physical address of the CEC adapter." + System.Environment.NewLine + + "[pa 1000] physical address 1.0.0.0" + System.Environment.NewLine + + System.Environment.NewLine + + "osd {addr} {string} set OSD message on the specified device." + System.Environment.NewLine + + "[osd 0 Test Message] displays 'Test Message' on the TV" + System.Environment.NewLine + + System.Environment.NewLine + + "ver {addr} get the CEC version of the specified device." + System.Environment.NewLine + + "[ver 0] get the CEC version of the TV" + System.Environment.NewLine + + System.Environment.NewLine + + "ven {addr} get the vendor ID of the specified device." + System.Environment.NewLine + + "[ven 0] get the vendor ID of the TV" + System.Environment.NewLine + + System.Environment.NewLine + + "lang {addr} get the menu language of the specified device." + System.Environment.NewLine + + "[lang 0] get the menu language of the TV" + System.Environment.NewLine + + System.Environment.NewLine + + "pow {addr} get the power status of the specified device." + System.Environment.NewLine + + "[pow 0] get the power status of the TV" + System.Environment.NewLine + + System.Environment.NewLine + + "poll {addr} poll the specified device." + System.Environment.NewLine + + "[poll 0] sends a poll message to the TV" + System.Environment.NewLine + + System.Environment.NewLine + + "[mon] {1|0} enable or disable CEC bus monitoring." + System.Environment.NewLine + + "[log] {1 - 31} change the log level. see cectypes.h for values." + System.Environment.NewLine + + System.Environment.NewLine + + "[ping] send a ping command to the CEC adapter." + System.Environment.NewLine + + "[bl] to let the adapter enter the bootloader, to upgrade" + System.Environment.NewLine + + " the flash rom." + System.Environment.NewLine + + "[r] reconnect to the CEC adapter." + System.Environment.NewLine + + "[h] or [help] show this help." + System.Environment.NewLine + + "[q] or [quit] to quit the CEC test client and switch off all" + System.Environment.NewLine + + " connected CEC devices." + System.Environment.NewLine + + "================================================================================"); + } + + public void MainLoop() + { + Lib.PowerOnDevices(CecLogicalAddress.Tv); + FlushLog(); + + Lib.SetActiveSource(CecDeviceType.PlaybackDevice); + FlushLog(); + + bool bContinue = true; + string command; + while (bContinue) + { + Console.WriteLine("waiting for input"); + + command = Console.ReadLine(); + if (command.Length == 0) + continue; + string[] splitCommand = command.Split(' '); + if (splitCommand[0] == "tx" || splitCommand[0] == "txn") + { + CecCommand bytes = new CecCommand(); + for (int iPtr = 1; iPtr < splitCommand.Length; iPtr++) + { + bytes.PushBack(byte.Parse(splitCommand[iPtr], System.Globalization.NumberStyles.HexNumber)); + } + + if (command == "txn") + bytes.TransmitTimeout = 0; + + Lib.Transmit(bytes); + } + else if (splitCommand[0] == "on") + { + if (splitCommand.Length > 1) + Lib.PowerOnDevices((CecLogicalAddress)byte.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber)); + else + Lib.PowerOnDevices(CecLogicalAddress.Broadcast); + } + else if (splitCommand[0] == "standby") + { + if (splitCommand.Length > 1) + Lib.StandbyDevices((CecLogicalAddress)byte.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber)); + else + Lib.StandbyDevices(CecLogicalAddress.Broadcast); + } + else if (splitCommand[0] == "poll") + { + bool bSent = false; + if (splitCommand.Length > 1) + bSent = Lib.PollDevice((CecLogicalAddress)byte.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber)); + else + bSent = Lib.PollDevice(CecLogicalAddress.Broadcast); + if (bSent) + Console.WriteLine("POLL message sent"); + else + Console.WriteLine("POLL message not sent"); + } + else if (splitCommand[0] == "la") + { + if (splitCommand.Length > 1) + Lib.SetLogicalAddress((CecLogicalAddress)byte.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber)); + } + else if (splitCommand[0] == "pa") + { + if (splitCommand.Length > 1) + Lib.SetPhysicalAddress(short.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber)); + } + else if (splitCommand[0] == "osd") + { + if (splitCommand.Length > 2) + { + StringBuilder osdString = new StringBuilder(); + for (int iPtr = 1; iPtr < splitCommand.Length; iPtr++) + { + osdString.Append(splitCommand[iPtr]); + if (iPtr != splitCommand.Length - 1) + osdString.Append(" "); + } + Lib.SetOSDString((CecLogicalAddress)byte.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber), CecDisplayControl.DisplayForDefaultTime, osdString.ToString()); + } + } + else if (splitCommand[0] == "ping") + { + Lib.PingAdapter(); + } + else if (splitCommand[0] == "mon") + { + bool enable = splitCommand.Length > 1 ? splitCommand[1] == "1" : false; + Lib.SwitchMonitoring(enable); + } + else if (splitCommand[0] == "bl") + { + Lib.StartBootloader(); + } + else if (splitCommand[0] == "lang") + { + if (splitCommand.Length > 1) + { + string language = Lib.GetDeviceMenuLanguage((CecLogicalAddress)byte.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber)); + Console.WriteLine("Menu language: " + language); + } + } + else if (splitCommand[0] == "ven") + { + if (splitCommand.Length > 1) + { + ulong vendor = Lib.GetDeviceVendorId((CecLogicalAddress)byte.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber)); + Console.WriteLine("Vendor ID: " + vendor); + } + } + else if (splitCommand[0] == "ver") + { + if (splitCommand.Length > 1) + { + CecVersion version = Lib.GetDeviceCecVersion((CecLogicalAddress)byte.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber)); + switch (version) + { + case CecVersion.V1_2: + Console.WriteLine("CEC version 1.2"); + break; + case CecVersion.V1_2A: + Console.WriteLine("CEC version 1.2a"); + break; + case CecVersion.V1_3: + Console.WriteLine("CEC version 1.3"); + break; + case CecVersion.V1_3A: + Console.WriteLine("CEC version 1.3a"); + break; + case CecVersion.V1_4: + Console.WriteLine("CEC version 1.4"); + break; + default: + Console.WriteLine("unknown CEC version"); + break; + } + } + } + else if (splitCommand[0] == "pow") + { + if (splitCommand.Length > 1) + { + CecPowerStatus power = Lib.GetDevicePowerStatus((CecLogicalAddress)byte.Parse(splitCommand[1], System.Globalization.NumberStyles.HexNumber)); + switch (power) + { + case CecPowerStatus.On: + Console.WriteLine("powered on"); + break; + case CecPowerStatus.InTransitionOnToStandby: + Console.WriteLine("on -> standby"); + break; + case CecPowerStatus.InTransitionStandbyToOn: + Console.WriteLine("standby -> on"); + break; + case CecPowerStatus.Standby: + Console.WriteLine("standby"); + break; + default: + Console.WriteLine("unknown power status"); + break; + } + } + } + else if (splitCommand[0] == "r") + { + Console.WriteLine("closing the connection"); + Lib.Close(); + FlushLog(); + + Console.WriteLine("opening a new connection"); + Connect(10000); + FlushLog(); + + Console.WriteLine("setting active source"); + Lib.SetActiveSource(CecDeviceType.PlaybackDevice); + } + else if (splitCommand[0] == "h" || splitCommand[0] == "help") + ShowConsoleHelp(); + else if (splitCommand[0] == "q" || splitCommand[0] == "quit") + bContinue = false; + else if (splitCommand[0] == "log" && splitCommand.Length > 1) + LogLevel = int.Parse(splitCommand[1]); + + FlushLog(); + } + } + + static void Main(string[] args) + { + CecSharpClient p = new CecSharpClient(); + if (p.Connect(10000)) + { + p.MainLoop(); + } + else + { + Console.WriteLine("Could not open a connection to the CEC adapter"); + } + p.FlushLog(); + } + + private int LogLevel; + private LibCecSharp Lib; + } +} diff --git a/src/CecSharpTester/CecSharpClient.csproj b/src/CecSharpTester/CecSharpClient.csproj new file mode 100644 index 0000000..eff98ef --- /dev/null +++ b/src/CecSharpTester/CecSharpClient.csproj @@ -0,0 +1,61 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {47EF8EFE-5758-4E82-B9BA-F9B002F9C0D4} + Exe + Properties + CecSharpClient + CecSharpClient + v4.0 + Client + 512 + + + x86 + true + full + false + ..\..\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\LibCecSharp.dll + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/LibCecSharp/AssemblyInfo.cpp b/src/LibCecSharp/AssemblyInfo.cpp new file mode 100644 index 0000000..459ae9f --- /dev/null +++ b/src/LibCecSharp/AssemblyInfo.cpp @@ -0,0 +1,40 @@ +#include "stdafx.h" + +using namespace System; +using namespace System::Reflection; +using namespace System::Runtime::CompilerServices; +using namespace System::Runtime::InteropServices; +using namespace System::Security::Permissions; + +// +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +// +[assembly:AssemblyTitleAttribute("LibCecSharp")]; +[assembly:AssemblyDescriptionAttribute("")]; +[assembly:AssemblyConfigurationAttribute("")]; +[assembly:AssemblyCompanyAttribute("Pulse-Eight Ltd.")]; +[assembly:AssemblyProductAttribute("LibCecSharp")]; +[assembly:AssemblyCopyrightAttribute("Copyright (c) Pulse-Eight Ltd. 2011")]; +[assembly:AssemblyTrademarkAttribute("")]; +[assembly:AssemblyCultureAttribute("")]; + +// +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the value or you can default the Revision and Build Numbers +// by using the '*' as shown below: + +[assembly:AssemblyVersionAttribute("1.0.*")]; + +[assembly:ComVisible(false)]; + +[assembly:CLSCompliantAttribute(true)]; + +[assembly:SecurityPermission(SecurityAction::RequestMinimum, UnmanagedCode = true)]; diff --git a/src/LibCecSharp/LibCecSharp.cpp b/src/LibCecSharp/LibCecSharp.cpp new file mode 100644 index 0000000..c72e0dd --- /dev/null +++ b/src/LibCecSharp/LibCecSharp.cpp @@ -0,0 +1,528 @@ +/* + * This file is part of the libCEC(R) library. + * + * libCEC(R) is Copyright (C) 2011 Pulse-Eight Limited. All rights reserved. + * libCEC(R) is an original work, containing original code. + * + * libCEC(R) is a trademark of Pulse-Eight Limited. + * + * This program is dual-licensed; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * + * Alternatively, you can license this library under a commercial license, + * please contact Pulse-Eight Licensing for more information. + * + * For more information contact: + * Pulse-Eight Licensing + * http://www.pulse-eight.com/ + * http://www.pulse-eight.net/ + */ + +#include "stdafx.h" +#include +#include +#include +#include +#using + +using namespace System; +using namespace CEC; +using namespace msclr::interop; + +public enum class CecDeviceType +{ + Tv = 0, + RecordingDevice = 1, + Reserved = 2, + Tuner = 3, + PlaybackDevice = 4, + AudioSystem = 5 +}; + +public enum class CecLogLevel +{ + None = 0, + Error = 1, + Warning = 2, + Notice = 4, + Traffic = 8, + Debug = 16, + All = 31 +}; + +public enum class CecLogicalAddress +{ + Unknown = -1, //not a valid logical address + Tv = 0, + RecordingDevice1 = 1, + RecordingDevice2 = 2, + Tuner1 = 3, + PlaybackDevice1 = 4, + AudioSystem = 5, + Tuner2 = 6, + Tuner3 = 7, + PlaybackDevice2 = 8, + RecordingDevice3 = 9, + Tuner4 = 10, + PlaybackDevice3 = 11, + Reserved1 = 12, + Reserved2 = 13, + FreeUse = 14, + Unregistered = 15, + Broadcast = 15 +}; + +public enum class CecPowerStatus +{ + On = 0x00, + Standby = 0x01, + InTransitionStandbyToOn = 0x02, + InTransitionOnToStandby = 0x03, + Unknown = 0x99 +}; + +public enum class CecVersion +{ + Unknown = 0x00, + V1_2 = 0x01, + V1_2A = 0x02, + V1_3 = 0x03, + V1_3A = 0x04, + V1_4 = 0x05 +}; + +public enum class CecDisplayControl +{ + DisplayForDefaultTime = 0x00, + DisplayUntilCleared = 0x40, + ClearPreviousMessage = 0x80, + ReservedForFutureUse = 0xC0 +}; + +public enum class CecMenuState +{ + Activated = 0, + Deactivated = 1 +}; + +public enum class CecDeckControlMode +{ + SkipForwardWind = 1, + SkipReverseRewind = 2, + Stop = 3, + Eject = 4 +}; + +public enum class CecDeckInfo +{ + Play = 0x11, + Record = 0x12, + Reverse = 0x13, + Still = 0x14, + Slow = 0x15, + SlowReverse = 0x16, + FastForward = 0x17, + FastReverse = 0x18, + NoMedia = 0x19, + Stop = 0x1A, + SkipForwardWind = 0x1B, + SkipReverseRewind = 0x1C, + IndexSearchForward = 0x1D, + IndexSearchReverse = 0x1E, + OtherStatus = 0x1F +}; + +public ref class CecAdapter +{ +public: + CecAdapter(String ^ strPath, String ^ strComPort) + { + Path = strPath; + ComPort = strComPort; + } + + property String ^ Path; + property String ^ ComPort; +}; + +public ref class CecDeviceTypeList +{ +public: + CecDeviceTypeList(void) + { + Types = gcnew array(5); + for (unsigned int iPtr = 0; iPtr < 5; iPtr++) + Types[iPtr] = CecDeviceType::Reserved; + } + + property array ^ Types; +}; + +public ref class CecDatapacket +{ +public: + CecDatapacket(void) + { + Data = gcnew array(100); + Size = 0; + } + + void PushBack(uint8_t data) + { + if (Size < 100) + { + Data[Size] = data; + Size++; + } + } + + property array ^ Data; + property uint8_t Size; +}; + +public ref class CecCommand +{ +public: + CecCommand(CecLogicalAddress iInitiator, CecLogicalAddress iDestination, bool bAck, bool bEom, int8_t iOpcode, int32_t iTransmitTimeout) + { + Initiator = iInitiator; + Destination = iDestination; + Ack = bAck; + Eom = bEom; + Opcode = iOpcode; + OpcodeSet = true; + TransmitTimeout = iTransmitTimeout; + Parameters = gcnew CecDatapacket; + Empty = false; + } + + CecCommand(void) + { + Initiator = CecLogicalAddress::Unknown; + Destination = CecLogicalAddress::Unknown; + Ack = false; + Eom = false; + Opcode = 0; + OpcodeSet = false; + TransmitTimeout = 0; + Parameters = gcnew CecDatapacket; + Empty = true; + } + + void PushBack(uint8_t data) + { + if (Initiator == CecLogicalAddress::Unknown && Destination == CecLogicalAddress::Unknown) + { + Initiator = (CecLogicalAddress) (data >> 4); + Destination = (CecLogicalAddress) (data & 0xF); + } + else if (!OpcodeSet) + { + OpcodeSet = true; + Opcode = data; + } + else + { + Parameters->PushBack(data); + } + } + + property bool Empty; + property CecLogicalAddress Initiator; + property CecLogicalAddress Destination; + property bool Ack; + property bool Eom; + property int8_t Opcode; + property CecDatapacket ^ Parameters; + property bool OpcodeSet; + property int32_t TransmitTimeout; +}; + +public ref class CecKeypress +{ +public: + CecKeypress(int iKeycode, unsigned int iDuration) + { + Keycode = iKeycode; + Duration = iDuration; + Empty = false; + } + + CecKeypress(void) + { + Keycode = 0; + Duration = 0; + Empty = true; + } + + property bool Empty; + property int Keycode; + property unsigned int Duration; +}; + +public ref class CecLogMessage +{ +public: + CecLogMessage(String ^ strMessage, CecLogLevel iLevel, int64_t iTime) + { + Message = strMessage; + Level = iLevel; + Time = iTime; + Empty = false; + } + + CecLogMessage(void) + { + Message = ""; + Level = CecLogLevel::None; + Time = 0; + Empty = true; + } + + property bool Empty; + property String ^ Message; + property CecLogLevel Level; + property int64_t Time; +}; + +public ref class LibCecSharp +{ +public: + LibCecSharp(String ^ strDeviceName, CecDeviceTypeList ^ deviceTypes) + { + marshal_context ^ context = gcnew marshal_context(); + + const char* strDeviceNameC = context->marshal_as(strDeviceName); + + cec_device_type_list types; + for (unsigned int iPtr = 0; iPtr < 5; iPtr++) + types.types[iPtr] = (cec_device_type)deviceTypes->Types[iPtr]; + m_libCec = (ICECAdapter *) CECInit(strDeviceNameC, types); + delete context; + } + + ~LibCecSharp(void) + { + CECDestroy(m_libCec); + m_libCec = NULL; + } + +protected: + !LibCecSharp(void) + { + CECDestroy(m_libCec); + m_libCec = NULL; + } + +public: + array ^ FindAdapters(String ^ path) + { + cec_adapter *devices = new cec_adapter[10]; + + marshal_context ^ context = gcnew marshal_context(); + const char* strPathC = path->Length > 0 ? context->marshal_as(path) : NULL; + + uint8_t iDevicesFound = m_libCec->FindAdapters(devices, 10, NULL); + + array ^ adapters = gcnew array(iDevicesFound); + for (unsigned int iPtr = 0; iPtr < iDevicesFound; iPtr++) + adapters[iPtr] = gcnew CecAdapter(gcnew String(devices[iPtr].path), gcnew String(devices[iPtr].comm)); + + delete devices; + delete context; + return adapters; + } + + bool Open(String ^ strPort, int iTimeoutMs) + { + marshal_context ^ context = gcnew marshal_context(); + const char* strPortC = context->marshal_as(strPort); + bool bReturn = m_libCec->Open(strPortC, iTimeoutMs); + delete context; + return bReturn; + } + + void Close(void) + { + m_libCec->Close(); + } + + bool PingAdapter(void) + { + return m_libCec->PingAdapter(); + } + + bool StartBootloader(void) + { + return m_libCec->StartBootloader(); + } + + int GetMinLibVersion(void) + { + return m_libCec->GetMinLibVersion(); + } + + int GetLibVersionMajor(void) + { + return m_libCec->GetLibVersionMajor(); + } + + int GetLibVersionMinor(void) + { + return m_libCec->GetLibVersionMinor(); + } + + CecLogMessage ^ GetNextLogMessage(void) + { + cec_log_message msg; + if (m_libCec->GetNextLogMessage(&msg)) + { + return gcnew CecLogMessage(gcnew String(msg.message), (CecLogLevel)msg.level, msg.time); + } + + return gcnew CecLogMessage(); + } + + CecKeypress ^ GetNextKeypress(void) + { + cec_keypress key; + if (m_libCec->GetNextKeypress(&key)) + { + return gcnew CecKeypress(key.keycode, key.duration); + } + + return gcnew CecKeypress(); + } + + CecCommand ^ GetNextCommand(void) + { + cec_command command; + if (m_libCec->GetNextCommand(&command)) + { + // TODO parameters + return gcnew CecCommand((CecLogicalAddress)command.initiator, (CecLogicalAddress)command.destination, command.ack == 1 ? true : false, command.eom == 1 ? true : false, command.opcode, command.transmit_timeout); + } + + return gcnew CecCommand(); + } + + bool Transmit(CecCommand ^ command) + { + cec_command ccommand; + cec_command::format(ccommand, (cec_logical_address)command->Initiator, (cec_logical_address)command->Destination, (cec_opcode)command->Opcode); + ccommand.transmit_timeout = command->TransmitTimeout; + ccommand.eom = command->Eom; + ccommand.ack = command->Ack; + for (unsigned int iPtr = 0; iPtr < command->Parameters->Size; iPtr++) + ccommand.parameters.push_back(command->Parameters->Data[iPtr]); + + return m_libCec->Transmit(ccommand); + } + + bool SetLogicalAddress(CecLogicalAddress logicalAddress) + { + return m_libCec->SetLogicalAddress((cec_logical_address) logicalAddress); + } + + bool SetPhysicalAddress(int16_t physicalAddress) + { + return m_libCec->SetPhysicalAddress(physicalAddress); + } + + bool PowerOnDevices(CecLogicalAddress logicalAddress) + { + return m_libCec->PowerOnDevices((cec_logical_address) logicalAddress); + } + + bool StandbyDevices(CecLogicalAddress logicalAddress) + { + return m_libCec->StandbyDevices((cec_logical_address) logicalAddress); + } + + bool PollDevice(CecLogicalAddress logicalAddress) + { + return m_libCec->PollDevice((cec_logical_address) logicalAddress); + } + + bool SetActiveSource(CecDeviceType type) + { + return m_libCec->SetActiveSource((cec_device_type) type); + } + + bool SetDeckControlMode(CecDeckControlMode mode, bool sendUpdate) + { + return m_libCec->SetDeckControlMode((cec_deck_control_mode) mode, sendUpdate); + } + + bool SetDeckInfo(CecDeckInfo info, bool sendUpdate) + { + return m_libCec->SetDeckInfo((cec_deck_info) info, sendUpdate); + } + + bool SetInactiveView(void) + { + return m_libCec->SetInactiveView(); + } + + bool SetMenuState(CecMenuState state, bool sendUpdate) + { + return m_libCec->SetMenuState((cec_menu_state) state, sendUpdate); + } + + bool SetOSDString(CecLogicalAddress logicalAddress, CecDisplayControl duration, String ^ message) + { + marshal_context ^ context = gcnew marshal_context(); + const char* strMessageC = context->marshal_as(message); + + bool bReturn = m_libCec->SetOSDString((cec_logical_address) logicalAddress, (cec_display_control) duration, strMessageC); + + delete context; + return bReturn; + } + + bool SwitchMonitoring(bool enable) + { + return m_libCec->SwitchMonitoring(enable); + } + + CecVersion GetDeviceCecVersion(CecLogicalAddress logicalAddress) + { + return (CecVersion) m_libCec->GetDeviceCecVersion((cec_logical_address) logicalAddress); + } + + String ^ GetDeviceMenuLanguage(CecLogicalAddress logicalAddress) + { + cec_menu_language lang; + if (m_libCec->GetDeviceMenuLanguage((cec_logical_address) logicalAddress, &lang)) + { + return gcnew String(lang.language); + } + + return gcnew String(""); + } + + uint64_t GetDeviceVendorId(CecLogicalAddress logicalAddress) + { + return m_libCec->GetDeviceVendorId((cec_logical_address) logicalAddress); + } + + CecPowerStatus GetDevicePowerStatus(CecLogicalAddress logicalAddress) + { + return (CecPowerStatus) m_libCec->GetDevicePowerStatus((cec_logical_address) logicalAddress); + } + +private: + ICECAdapter * m_libCec; +}; diff --git a/src/LibCecSharp/LibCecSharp.suo b/src/LibCecSharp/LibCecSharp.suo new file mode 100644 index 0000000000000000000000000000000000000000..0b75e65e407f9d8ecf08b2303911128d31aca738 GIT binary patch literal 11264 zcmeHNU2G%O6}}0(uvvB~6j)$c7DKmm3$bzRILT(4E&dBol8qrIiz;jBcszDy!OrN`2wy9S{uoo}@*ax^4upe+8;2QuezqkSLO~8!+ z>cCA-eFd@ql_&KvQN_;^0g)CJ)OE3pRrFsDh}~;@o_f@-{OtAf!{XKV`>w*E^Hdg; z3@}#38SyZ%7tuj79H_agdyP3kYlW`!miVnIv^6SXBC$JGE$s=v`;!Xu); zkNZH;&|$cIOqf6A%F01-KjVEx-{# z3UCkLJAk8r?*i@x+y@v01OXv{qG1T_2p|d=cKS!0dd#WEQJ-+y>b-|?{{SHF^e3G< zjd~LBAmAaV?+2(K0ZcjfC!IQj`cbD1)~?8+e=txQ`%C2H3#&i=$)ERs`X{inrSh*N zHKd2a%|OYqX4yx@JU!P)8Tn=@Q#Tq$#nLRZTFGeof>tsH19L{bf%}P}a7g|J1IcFD zYSxX3iqW*{T6r*#Y39qO{)n-htu7dqiF`O(j1@WGjQUz5TdkH)nUzBI=|(7}Sz6YtH9{G)Qql5dL*As9EThse z^Jdw!mO~j*+CYzP+&3i~MxF1?TGhHiKChMW)0fTuZgO7PKBir4DwZ9AB&+4Jp>y?! zau-D;4bK40&Go#nFt+Hns737MeeU<97#BHFh6T<`+pELU6kvlF(OYwTlz}G}?&&M? zc!T$|Vjdo(jxo8eNTAK#j>37)18hEQuUZ0Knm8*0uxKWDGvHwYu&PU9OpL&?=EOAY ztpQ$|qJ$rP&?&rOfv}!)asQj}CqRP%8WteYDyU{m(#{7T z1^(NClrgGb{2cq3Luf1ij6djmYrt!Qf=XNL^i?+h^p}3|Glo|FzZE}MsshcM1@0zf z(J2m4_&J|n{EXjE()^&9z`zKs0Vsm!fS5yH1GqJ4OaOFPXcwd`0%Ayn;eEr(U6pK2fPT_st)!>1NF5GEiZzabxPFzeG(uQez z>Dw?-hnv0*&pSQdD@t%f{osqrv==Z50R$mnDu6<!i#c||_2pb6-R+QGtF5e@ zONG-z@mO*sn25%X1&7naL&0N-k#H~?9f=)_M3RwsB(nO89i+|M{w!lpe+XmGI>BEGIK1$yvv^9zBpTk@TtYqaPd`gbsCcyH zTm;u8^fG=@Pd4Lb5A?5(Cm^1S{qp~9j6LHR$QkUhGl;_&tF^B29OAT2o zc5X|E31rh*j>{ayImA(Mtae0V3{2udte(jIj|0q4AAFP2EdVYFCTU+Vx+85v# z%qh~R`^6u|*g?m5Jq~|LteT943u}AU-WTmoo;F>uzY;HTv)H}uuWtSyf)*4c?h1G= zgPKX{kqS}@JCFa-fB@F*$bMWpD-wc^>vC*jq*lTZY7V`*tIb!1x@e=y^~ z+{2W!AKJxTiaq8Z7)x9Y@T>nZjAbsJMeTokzhyy16Sl}&`yNzBWZEtUL;Ts({$m~^ z&_abxnZGN(Kgl?V-#YyAKZ>z+XZjiN%lrk;w54hF&dkz?~QiIAe+pz=+7=J8+o=&+}vcU5fqwB5gKSrX| zW|nsQpD|H;?d3Pqe)%n%`E=4CC+%=yRQ{YA!w(l;u6thG{OiNNzh~(mkqi4@ZXP-C z_xE^=QLL(3&D2%>M34Fl(~y%UQ*DS4mW>AE^lx8!>)lVL|C^b=vv&L7^4f1^LX8!<2&hJ#HQWzO8sjQspnYrAc4lJbJGcbRK4U; zbN@4+#QzjHOX@Lly?Yu>^)HuB`1zlLcBLEsXE5RO?Izvgv#rYCJ-u3owZ6^z%THUL zMQt}bqr_;c_wT4~{}zHj5Hfr6qivbn+tj)K+vv&d<_FoGV0hnuo;~&1Q~!SFjhoW7 zxA(3NFjjLfvP;qP+kxLpD7Gd2_