Pocket PC 电视遥控器






4.83/5 (90投票s)
一篇介绍如何使用 Pocket PC 上的红外端口控制电视的文章。
引言
您是否曾经想过能够使用 Pocket PC 上的红外端口来控制您的电视、Hi-Fi 或录像机?以下是实现的方法。
背景
我最近丢失了我旧 Sony 电视的遥控器。这本身不成问题,因为我买了一个替代遥控器,它也能工作。然而,当电视失去了色彩设置时,我就遇到了麻烦,因为它只能显示黑白图像,而替代遥控器上没有色彩调节按钮。我决定在我的旧 Jornada 525 Pocket PC 上编写一个程序,通过红外端口向电视发送正确的代码。
似乎有三种主要的协议用于向设备发送红外代码。Sony 使用“脉冲编码”方法,该方法发送包含头位、'1' 位和 '0' 位的数据流,它们之间用空格分隔。这些位以 40KHz 的载波进行调制,并且长度不同,头位为 2200 us,1 位为 110 us,0 位为 550 us。空格是 550 us 的静默。大多数 Sony 设备使用 12 位数据,这些数据分为 6 位地址(设备类型)和 6 位命令。因此,数据看起来像这样:hxxxxxxyyyyyy,其中 h 是头位,xxxxxx 是 6 位命令(MSB 首先),yyyyyy 是 6 位地址。我将不再详述,因为互联网上有很多资源描述了该协议并列出了不同设备的编码。一些较新的 Sony 设备使用 19 位编码,我相信其他制造商也使用我描述的相同格式。对于使用“空格编码”或“移位编码”协议的设备,也可以编写类似的类。
我使用 Embedded C++ 编写了一个名为 CIrPulse
的类,该类封装了从运行 Windows CE 3.0 的 Jornada 525 PC 控制 Sony 和兼容设备的功能。它应该适用于其他设备和操作系统,但您需要自行尝试!
使用代码
CIrPulse
类公开了许多函数,使得发送红外代码尽可能容易。在声明 CIrPulse
类时,您应该调用一次 FindIrPort()
。它通过查找注册表来返回一个 UINT,表示 IrDA 端口的端口号。此端口号用于后续打开 IrDA 端口以进行串行通信的所有调用。
UINT CIrPulse::FindIrPort() { // Look into the registry for the IR port number HKEY hKey = NULL; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T("Comm\\IrDA"), 0, 0, &hKey) == ERROR_SUCCESS) { DWORD dwType = 0; DWORD dwData = 0; DWORD dwSize = sizeof(dwData); if (RegQueryValueEx(hKey, _T("Port"), NULL, &dwType, (LPBYTE) &dwData, &dwSize) == ERROR_SUCCESS) { if (dwType == REG_DWORD && dwSize == sizeof(dwData)) { RegCloseKey(hKey); return (UINT) dwData; } } RegCloseKey(hKey); } return 0; }
获得端口号后,您可以调用 Open(UINT)
函数,将从 FindIrPort()
调用中收到的端口号传递进去。这将打开端口并设置串行参数,成功则返回 true。端口设置为 115200 波特率、8 数据位、2 停止位和偶校验。关于载波如何产生以及为什么我使用这些设置的讨论将在文章后面出现。
BOOL CIrPulse::Open(UINT uiPort) { ASSERT(uiPort > 0 && uiPort <= 255); Close(); // Open the IRDA port CString strPort; strPort.Format(_T("COM%d:"), uiPort); m_irPort = CreateFile((LPCTSTR) strPort, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (m_irPort == INVALID_HANDLE_VALUE) { return FALSE; } // Set the size of input and output buffers VERIFY(SetupComm(m_irPort, 2048, 2048)); // clear the read and write buffers VERIFY(PurgeComm(m_irPort, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR)); // Reinitializes all IRDA port settings DCB dcb; dcb.DCBlength = sizeof(DCB); VERIFY(GetCommState(m_irPort, &dcb)); dcb.BaudRate = CBR_115200; dcb.fBinary = TRUE; dcb.fParity = TRUE; dcb.fOutxCtsFlow = FALSE; dcb.fOutxDsrFlow = FALSE; dcb.fDtrControl = DTR_CONTROL_DISABLE; dcb.fDsrSensitivity = FALSE; dcb.fTXContinueOnXoff = FALSE; dcb.fOutX = FALSE; dcb.fInX = FALSE; dcb.fErrorChar = FALSE; dcb.fNull = FALSE; dcb.fRtsControl = RTS_CONTROL_DISABLE; dcb.fAbortOnError = FALSE; dcb.ByteSize = 8; dcb.Parity = EVENPARITY; dcb.StopBits = TWOSTOPBITS; VERIFY(SetCommState(m_irPort, &dcb)); // Set the timeouts for all read and write operations COMMTIMEOUTS timeouts; VERIFY(GetCommTimeouts(m_irPort, &timeouts)); timeouts.ReadIntervalTimeout = MAXDWORD; timeouts.ReadTotalTimeoutMultiplier = 0; timeouts.ReadTotalTimeoutConstant = 0; timeouts.WriteTotalTimeoutMultiplier = 0; timeouts.WriteTotalTimeoutConstant = 0; VERIFY(SetCommTimeouts(m_irPort, &timeouts)); DWORD dwEvent=EV_TXEMPTY; SetCommMask(m_irPort,dwEvent); return TRUE; }
调用 SetCodeSize(DWORD)
函数设置要传输的位数(例如 12)。这可以随时进行,并且只需要执行一次。它将一直生效,直到后续调用更改它。
最后调用 SendCode(long)
函数,传递要发送的实际代码。
BOOL CIrPulse::SendCode(DWORD lValue) { DWORD dwCount; int i=0; ASSERT(iDataLength>0); //purge the transmit buffer VERIFY(PurgeComm(m_irPort, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR)); // send the code 6 times for each button press for(int x=0;x<6;x++) { MakeStream(lValue); //send the code dwCount=GetTickCount(); while(GetTickCount()<dwCount+26) //delay for 26 ms i++; } return true; }
请注意,此函数调用另一个函数 MakeStream(long)
6 次,每次调用之间暂停 26 毫秒。我发现代码必须发送多次才能让接收设备响应,这可能是为了防止误激活。26 毫秒的延迟对于接收设备在下一个代码出现之前识别代码是必要的。
MakeStream(long)
函数将字节流写入 IrPort,并确保根据要发送的起始位、'1' 位或 '0' 位来发送正确长度的数据包。包含数据字节 (0xdb) 的缓冲区是 ByteArray 形式的。
Close()
函数自然会在使用后关闭 IrPort。
该函数在我的 Jornada 上运行正常,但请参阅下面的讨论,了解您可能需要进行的更改。
BOOL CIrPulse::MakeStream(DWORD lValue) { DWORD dwStreamLength; //make the start pulse dwStreamLength=iHPulse/charWidth; ASSERT(Write((const char *)bPulseStream.GetData(), dwStreamLength)==dwStreamLength); // ******************************************** // ***** The Delay before the next pulse goes here // ******************************************** //loop through the bits in the Code sending the pulse for(int i=0;i<iDataLength;i++) { if(lValue & 1) { //make the 1 pulse dwStreamLength=i1Pulse/charWidth; ASSERT(Write((const char *)bPulseStream.GetData(), dwStreamLength)==dwStreamLength); // ******************************************** // ***** The Delay before the next pulse goes here // ******************************************** } else { //make the 0 pulse dwStreamLength=i0Pulse/charWidth; ASSERT(Write((const char *)bPulseStream.GetData(), dwStreamLength)==dwStreamLength); // ******************************************** // ***** The Delay before the next pulse goes here // ******************************************** } lValue >>= 1; } return TRUE; }
我包含了一个简单的应用程序,它使用 CIrPulse
来创建一个 Sony 电视的遥控器。它具有频道选择、音量控制和开关的基本功能。
关注点
由于 CIrPort
类使用串行通信接口到红外端口,因此必须通过从串行端口发送适当的字符来生成 40KHz 载波。幸运的是,如果我们以 115200 波特率、8 数据位、2 停止位和偶校验发送字符 0xdb,其效果是产生一个 38.4 KHz 的载波,这已经足够接近了。我所有的 Sony 设备都能毫无问题地接受它。
最大的问题是如何实现分隔每个脉冲的静默周期。无法从串行端口产生纯粹的静默,因为即使发送 0x0 字符,由于起始位和停止位,红外线上仍然会有脉冲。我尝试发送不同的字符,假设如果可以发送 40KHz 以外频率的载波,这可能会欺骗设备接受它作为静默。这有一个优点,即您可以生成一个包含整个代码数据的 ByteArray,从而确保计时准确。然而,结果并不一致,我因此放弃了这种方法,转而选择在发送完 0xdb 字符组之间暂停。由于所需的延迟在 550 us 的数量级,我(发现)没有办法始终如一地实现独立于处理器速度的延迟。在我的 Jornada 上,我甚至不需要创建延迟,因为每次调用 Write
函数似乎都花费了正确的时间。总之,我不得不说,您可能需要仔细调整以创建适合您的 Pocket PC 的延迟。如果有人能找到一种万无一失的方法来为任何设备实现正确的延迟,请告诉我!